Chromium Code Reviews| Index: chrome/browser/resources/ntp_search/tile_page.js |
| diff --git a/chrome/browser/resources/ntp_search/tile_page.js b/chrome/browser/resources/ntp_search/tile_page.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..ec6bcaaff75c26e85ca290d15afd3f4c0af05177 |
| --- /dev/null |
| +++ b/chrome/browser/resources/ntp_search/tile_page.js |
| @@ -0,0 +1,1476 @@ |
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +cr.define('ntp', function() { |
| + 'use strict'; |
| + |
| + |
| + function Tile(gridValues) { |
| + throw 'Tile is a virtual class and is not supposed to be instantiated'; |
| + } |
| + |
| + Tile.subclass = function(Subclass) { |
| + var Base = Tile.prototype |
| + for (var name in Base) { |
| + if (!Subclass.hasOwnProperty(name)) { |
| + Subclass[name] = Base[name]; |
| + } |
| + } |
| + for (var name in TileGetters) { |
| + if (!Subclass.hasOwnProperty(name)) { |
| + Subclass.__defineGetter__(name, TileGetters[name]); |
| + } |
| + } |
| + return Subclass; |
| + }; |
| + |
| + Tile.prototype = { |
| + initialize: function(gridValues) { |
| + // TODO: rename to tile |
| + this.className = 'tile-content ' + gridValues.tileClassName; |
| + |
| + // TODO set using CSS? |
| + if (gridValues.reinforceStyles) { |
| + var style = this.style; |
| + style.width = gridValues.tileWidth + 'px'; |
| + style.height = gridValues.tileHeight + 'px'; |
| + style.borderWidth = gridValues.tileBorderWidth + 'px'; |
| + } |
| + }, |
| + |
| + tearDown_: function () { |
| + }, |
| + }; |
| + |
| + var TileGetters = { |
| + 'tile': function() { |
| + return findAncestorByClass(this, 'tile-cell'); |
| + }, |
| + 'index': function() { |
| + return this.tile.index; |
| + } |
| + }; |
| + |
| + |
| + |
| + // We can't pass the currently dragging tile via dataTransfer because of |
| + // http://crbug.com/31037 |
| + // TODO(xci) drag |
| + var currentlyDraggingTile = null; |
| + function getCurrentlyDraggingTile() { |
| + return currentlyDraggingTile; |
| + } |
| + function setCurrentlyDraggingTile(tile) { |
| + currentlyDraggingTile = tile; |
| + if (tile) |
| + ntp.enterRearrangeMode(); |
| + else |
| + ntp.leaveRearrangeMode(); |
| + } |
| + |
| + /** |
| + * Changes the current dropEffect of a drag. This modifies the native cursor |
| + * and serves as an indicator of what we should do at the end of the drag as |
| + * well as give indication to the user if a drop would succeed if they let go. |
| + * @param {DataTransfer} dataTransfer A dataTransfer object from a drag event. |
| + * @param {string} effect A drop effect to change to (i.e. copy, move, none). |
| + */ |
| + // TODO(xci) drag |
| + function setCurrentDropEffect(dataTransfer, effect) { |
| + dataTransfer.dropEffect = effect; |
| + if (currentlyDraggingTile) |
| + currentlyDraggingTile.lastDropEffect = dataTransfer.dropEffect; |
| + } |
| + |
| + /** |
| + * Creates a new TileCell object. Tiles wrap content on a TilePage, providing |
| + * some styling and drag functionality. |
| + * @constructor |
| + * @extends {HTMLDivElement} |
| + */ |
| + function TileCell(tile, gridValues) { |
| + var tileCell = cr.doc.createElement('div'); |
| + tileCell.__proto__ = TileCell.prototype; |
| + tileCell.initialize(tile, gridValues); |
| + |
| + return tileCell; |
| + } |
| + |
| + TileCell.prototype = { |
| + __proto__: HTMLDivElement.prototype, |
| + |
| + initialize: function(tile, gridValues) { |
| + // 'real' as opposed to doppleganger. |
| + // TODO: rename to tile-cell |
| + this.className = 'tile-cell real'; |
| + |
| + if (gridValues.reinforceStyles) { |
| + var style = this.style; |
| + var borderWidth = 2 * gridValues.tileBorderWidth; |
| + style.width = gridValues.tileWidth + borderWidth + 'px'; |
| + style.height = gridValues.tileHeight + borderWidth + 'px'; |
| + style.marginLeft = gridValues.tileHorMargin + 'px'; |
| + style.marginBottom = gridValues.tileVerMargin + 'px'; |
| + } |
| + |
| + this.assign_(tile); |
| + }, |
| + |
| + get index() { |
| + return Array.prototype.indexOf.call(this.tilePage.tileElements_, this.firstChild); |
| + }, |
| + |
| + get tilePage() { |
| + return findAncestorByClass(this, 'tile-page'); |
| + }, |
| + |
| + assign_: function(tile) { |
| + if (this.firstChild) { |
| + this.replaceChild(tile, this.firstChild); |
| + } else { |
| + this.appendChild(tile); |
| + } |
| + }, |
| + |
| + tearDown_: function () { |
| + }, |
| + |
| + /** |
| + * The handler for dragstart events fired on |this|. |
| + * @param {Event} e The event for the drag. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + onDragStart_: function(e) { |
| + // The user may start dragging again during a previous drag's finishing |
| + // animation. |
| + if (this.classList.contains('dragging')) |
| + this.finalizeDrag_(); |
| + |
| + setCurrentlyDraggingTile(this); |
| + |
| + e.dataTransfer.effectAllowed = 'copyMove'; |
| + this.firstChild.setDragData(e.dataTransfer); |
| + |
| + // The drag clone is the node we use as a representation during the drag. |
| + // It's attached to the top level document element so that it floats above |
| + // image masks. |
| + this.dragClone = this.cloneNode(true); |
| + this.dragClone.style.right = ''; |
| + this.dragClone.classList.add('drag-representation'); |
| + $('card-slider-frame').appendChild(this.dragClone); |
| + this.eventTracker.add(this.dragClone, 'webkitTransitionEnd', |
| + this.onDragCloneTransitionEnd_.bind(this)); |
| + |
| + this.classList.add('dragging'); |
| + // offsetLeft is mirrored in RTL. Un-mirror it. |
| + var offsetLeft = isRTL() ? |
| + this.parentNode.clientWidth - this.offsetLeft : |
| + this.offsetLeft; |
| + this.dragOffsetX = e.x - offsetLeft - this.parentNode.offsetLeft; |
| + this.dragOffsetY = e.y - this.offsetTop - |
| + // Unlike offsetTop, this value takes scroll position into account. |
| + this.parentNode.getBoundingClientRect().top; |
| + |
| + this.onDragMove_(e); |
| + }, |
| + |
| + /** |
| + * The handler for drag events fired on |this|. |
| + * @param {Event} e The event for the drag. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + onDragMove_: function(e) { |
| + if (e.view != window || (e.x == 0 && e.y == 0)) { |
| + this.dragClone.hidden = true; |
| + return; |
| + } |
| + |
| + this.dragClone.hidden = false; |
| + this.dragClone.style.left = toCssPx(e.x - this.dragOffsetX); |
| + this.dragClone.style.top = toCssPx(e.y - this.dragOffsetY); |
| + }, |
| + |
| + /** |
| + * The handler for dragend events fired on |this|. |
| + * @param {Event} e The event for the drag. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + onDragEnd_: function(e) { |
| + this.dragClone.hidden = false; |
| + this.dragClone.classList.add('placing'); |
| + |
| + setCurrentlyDraggingTile(null); |
| + |
| + // tilePage will be null if we've already been removed. |
| + var tilePage = this.tilePage; |
| + if (tilePage) |
| + //tilePage.positionTile_(this.index); // TODO(xci) function was deleted! |
| + |
| + // Take an appropriate action with the drag clone. |
| + if (this.landedOnTrash) { |
| + this.dragClone.classList.add('deleting'); |
| + } else if (tilePage) { |
| + // TODO(dbeam): Until we fix dropEffect to the correct behavior it will |
| + // differ on windows - crbug.com/39399. That's why we use the custom |
| + // this.lastDropEffect instead of e.dataTransfer.dropEffect. |
| + if (tilePage.selected && this.lastDropEffect != 'copy') { |
| + // The drag clone can still be hidden from the last drag move event. |
| + this.dragClone.hidden = false; |
| + // The tile's contents may have moved following the respositioning; |
| + // adjust for that. |
| + var contentDiffX = this.dragClone.firstChild.offsetLeft - |
| + this.firstChild.offsetLeft; |
| + var contentDiffY = this.dragClone.firstChild.offsetTop - |
| + this.firstChild.offsetTop; |
| + this.dragClone.style.left = |
| + toCssPx(this.gridX + this.parentNode.offsetLeft - |
| + contentDiffX); |
| + this.dragClone.style.top = |
| + toCssPx(this.gridY + |
| + this.parentNode.getBoundingClientRect().top - |
| + contentDiffY); |
| + } else if (this.dragClone.hidden) { |
| + this.finalizeDrag_(); |
| + } else { |
| + // The CSS3 transitions spec intentionally leaves it up to individual |
| + // user agents to determine when styles should be applied. On some |
| + // platforms (at the moment, Windows), when you apply both classes |
| + // immediately a transition may not occur correctly. That's why we're |
| + // using a setTimeout here to queue adding the class until the |
| + // previous class (currently: .placing) sets up a transition. |
| + // http://dev.w3.org/csswg/css3-transitions/#starting |
| + window.setTimeout(function() { |
| + if (this.dragClone) |
| + this.dragClone.classList.add('dropped-on-other-page'); |
| + }.bind(this), 0); |
| + } |
| + } |
| + |
| + delete this.lastDropEffect; |
| + this.landedOnTrash = false; |
| + }, |
| + |
| + /** |
| + * Creates a clone of this node offset by the coordinates. Used for the |
| + * dragging effect where a tile appears to float off one side of the grid |
| + * and re-appear on the other. |
| + * @param {number} x x-axis offset, in pixels. |
| + * @param {number} y y-axis offset, in pixels. |
| + */ |
| + // TODO(xci) drag |
| + showDoppleganger: function(x, y) { |
| + // We always have to clear the previous doppleganger to make sure we get |
| + // style updates for the contents of this tile. |
| + this.clearDoppleganger(); |
| + |
| + var clone = this.cloneNode(true); |
| + clone.classList.remove('real'); |
| + clone.classList.add('doppleganger'); |
| + var clonelets = clone.querySelectorAll('.real'); |
| + for (var i = 0; i < clonelets.length; i++) { |
| + clonelets[i].classList.remove('real'); |
| + } |
| + |
| + this.appendChild(clone); |
| + this.doppleganger_ = clone; |
| + |
| + if (isRTL()) |
| + x *= -1; |
| + |
| + this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' + |
| + y + 'px)'; |
| + }, |
| + |
| + /** |
| + * Destroys the current doppleganger. |
| + */ |
| + // TODO(xci) drag |
| + clearDoppleganger: function() { |
| + if (this.doppleganger_) { |
| + this.removeChild(this.doppleganger_); |
| + this.doppleganger_ = null; |
| + } |
| + }, |
| + |
| + /** |
| + * Returns status of doppleganger. |
| + * @return {boolean} True if there is a doppleganger showing for |this|. |
| + */ |
| + // TODO(xci) drag |
| + hasDoppleganger: function() { |
| + return !!this.doppleganger_; |
| + }, |
| + |
| + /** |
| + * Cleans up after the drag is over. This is either called when the |
| + * drag representation finishes animating to the final position, or when |
| + * the next drag starts (if the user starts a 2nd drag very quickly). |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + finalizeDrag_: function() { |
| + assert(this.classList.contains('dragging')); |
| + |
| + var clone = this.dragClone; |
| + this.dragClone = null; |
| + |
| + clone.parentNode.removeChild(clone); |
| + this.eventTracker.remove(clone, 'webkitTransitionEnd'); |
| + this.classList.remove('dragging'); |
| + if (this.firstChild.finalizeDrag) |
| + this.firstChild.finalizeDrag(); |
| + }, |
| + |
| + /** |
| + * Called when the drag representation node is done migrating to its final |
| + * resting spot. |
| + * @param {Event} e The transition end event. |
| + */ |
| + // TODO(xci) drag |
| + onDragCloneTransitionEnd_: function(e) { |
| + if (this.classList.contains('dragging') && |
| + (e.propertyName == 'left' || e.propertyName == 'top' || |
| + e.propertyName == '-webkit-transform')) { |
| + this.finalizeDrag_(); |
| + } |
| + }, |
| + |
| + /** |
| + * Called when an app is removed from Chrome. Animates its disappearance. |
| + * @param {boolean=} opt_animate Whether the animation should be animated. |
| + */ |
| + doRemove: function(opt_animate) { |
| + if (opt_animate) |
| + this.firstChild.classList.add('removing-tile-contents'); |
| + else |
| + this.tilePage.removeTile(this, false); |
| + }, |
| + |
| + /** |
| + * Callback for the webkitAnimationEnd event on the tile's contents. |
| + * @param {Event} e The event object. |
| + */ |
| + onContentsAnimationEnd_: function(e) { |
| + if (this.firstChild.classList.contains('new-tile-contents')) |
| + this.firstChild.classList.remove('new-tile-contents'); |
| + if (this.firstChild.classList.contains('removing-tile-contents')) |
| + this.tilePage.removeTile(this, true); |
| + }, |
| + }; |
| + |
| + /** |
| + * Creates a new TilePage object. This object contains tiles and controls |
| + * their layout. |
| + * @param {Object} gridValues Pixel values that define the size and layout |
| + * of the tile grid. |
| + * @constructor |
| + * @extends {HTMLDivElement} |
| + */ |
| + function TilePage() { |
| + var el = cr.doc.createElement('div'); |
| + el.__proto__ = TilePage.prototype; |
| + el.initialize(); |
| + |
| + return el; |
| + } |
| + |
| + TilePage.prototype = { |
| + __proto__: HTMLDivElement.prototype, |
| + |
| + // The grid values should be defined by each TilePage subclass. |
| + gridValues_: { |
| + tileWidth: 0, |
| + tileHeight: 0, |
| + tileHorMargin: 0, // TODO margin with CSS / there's no margin in first col |
| + tileVerMargin: 0, |
| + tileBorderWidth: 0, |
| + bottomPanelHorMargin: 0, // TODO it doesn't make sense having this here. |
| + |
| + tileCount: 0, // TODO remove this dependency. rename it to maxTileCount. |
| + tileClassName: '', |
| + reinforceStyles: true, |
| + |
| + // debug |
| + slowFactor: 1, |
| + debug: false |
| + }, |
| + |
| + initialize: function() { |
| + this.className = 'tile-page'; |
| + |
| + // This contains everything but the scrollbar. |
| + this.content_ = this.ownerDocument.createElement('div'); |
| + this.content_.className = 'tile-page-content'; |
| + this.appendChild(this.content_); |
| + |
| + // Div that holds the tiles. |
| + this.tileGrid_ = this.ownerDocument.createElement('div'); |
| + this.tileGrid_.className = 'tile-grid'; |
| + //this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px'; // TODO(xci) |
| + this.content_.appendChild(this.tileGrid_); |
| + |
| + // TODO(xci) new! |
| + this.tileGridContent_ = this.ownerDocument.createElement('div'); |
| + this.tileGridContent_.className = 'tile-grid-content'; |
| + this.tileGrid_.appendChild(this.tileGridContent_); |
| + |
| + // TODO(xci) new! |
| + this.tileGrid_.addEventListener('webkitTransitionEnd', |
| + this.onTileGridAnimationEnd_.bind(this)); |
| + |
| + // Ordered list of our tiles. |
| + //this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real'); |
| + this.tileElements_ = []; |
| + |
| + // Ordered list of the elements which want to accept keyboard focus. These |
| + // elements will not be a part of the normal tab order; the tile grid |
| + // initially gets focused and then these elements can be focused via the |
| + // arrow keys. |
| + // TODO(xci) |
| + /* |
| + this.focusableElements_ = |
| + this.tileGrid_.getElementsByClassName('focusable'); |
| + */ |
| + |
| + this.eventTracker = new EventTracker(); |
| + this.eventTracker.add(window, 'resize', this.onResize_.bind(this)); |
| + this.eventTracker.add(window, 'keydown', this.onKeyDown_.bind(this)); // TODO(xci) new |
| + |
| + // TODO(xci) |
| + /* |
| + this.addEventListener('DOMNodeInsertedIntoDocument', |
| + this.onNodeInsertedIntoDocument_); |
| + |
| + this.dragWrapper_ = new cr.ui.DragWrapper(this.tileGrid_, this); |
| + |
| + this.addEventListener('cardselected', this.handleCardSelection_); |
| + this.addEventListener('carddeselected', this.handleCardDeselection_); |
| + this.addEventListener('focus', this.handleFocus_); |
| + this.addEventListener('keydown', this.handleKeyDown_); |
| + this.addEventListener('mousedown', this.handleMouseDown_); |
| + |
| + this.focusElementIndex_ = -1; |
| + */ |
| + }, |
| + |
| + get tiles() { |
| + return this.tileElements_; |
| + }, |
| + |
| + get tileCount() { |
| + return this.tileElements_.length; |
| + }, |
| + |
| + get selected() { |
| + return Array.prototype.indexOf.call(this.parentNode.children, this) == |
| + ntp.getCardSlider().currentCard; |
| + }, |
| + |
| + /** |
| + * The notification content of this tile (if any, otherwise null). |
| + * @type {!HTMLElement} |
| + */ |
| + get notification() { |
| + return this.topMargin_.nextElementSibling.id == 'notification-container' ? |
| + this.topMargin_.nextElementSibling : null; |
| + }, |
| + /** |
| + * The notification content of this tile (if any, otherwise null). |
| + * @type {!HTMLElement} |
| + */ |
| + set notification(node) { |
| + assert(node instanceof HTMLElement, '|node| isn\'t an HTMLElement!'); |
| + // NOTE: Implicitly removes from DOM if |node| is inside it. |
| + this.content_.insertBefore(node, this.topMargin_.nextElementSibling); |
| + this.positionNotification_(); |
| + }, |
| + |
| + /** |
| + * Removes the tilePage from the DOM and cleans up event handlers. |
| + */ |
| + remove: function() { |
| + // This checks arguments.length as most remove functions have a boolean |
| + // |opt_animate| argument, but that's not necesarilly applicable to |
| + // removing a tilePage. Selecting a different card in an animated way and |
| + // deleting the card afterward is probably a better choice. |
| + assert(typeof arguments[0] != 'boolean', |
| + 'This function takes no |opt_animate| argument.'); |
| + this.tearDown_(); |
| + this.parentNode.removeChild(this); |
| + }, |
| + |
| + /** |
| + * Cleans up resources that are no longer needed after this TilePage |
| + * instance is removed from the DOM. |
| + * @private |
| + */ |
| + tearDown_: function() { |
| + this.eventTracker.removeAll(); |
| + }, |
| + |
| + /** |
| + * Appends a tile to the end of the tile grid. |
| + * @param {HTMLElement} tileElement The contents of the tile. |
| + * @param {boolean} animate If true, the append will be animated. |
| + * @protected |
| + */ |
| + // TODO(xci) |
| + /* |
| + appendTile: function(tileElement, animate) { |
| + this.addTileAt(tileElement, this.tileElements_.length, animate); |
| + }, |
| + */ |
| + |
| + /** |
| + * Adds the given element to the tile grid. |
| + * @param {Node} tileElement The tile object/node to insert. |
| + * @param {number} index The location in the tile grid to insert it at. |
| + * @param {boolean} animate If true, the tile in question will be |
| + * animated (other tiles, if they must reposition, do not animate). |
| + * @protected |
| + */ |
| + // TODO(xci) |
| + /* |
| + addTileAt: function(tileElement, index, animate) { |
| + this.calculateLayoutValues_(); |
| + this.fireAddedEvent(wrapperDiv, index, animate); |
| + }, |
| + */ |
| + |
| + /** |
| + * Notify interested subscribers that a tile has been removed from this |
| + * page. |
| + * @param {TileCell} tile The newly added tile. |
| + * @param {number} index The index of the tile that was added. |
| + * @param {boolean} wasAnimated Whether the removal was animated. |
| + */ |
| + fireAddedEvent: function(tile, index, wasAnimated) { |
| + var e = document.createEvent('Event'); |
| + e.initEvent('tilePage:tile_added', true, true); |
| + e.addedIndex = index; |
| + e.addedTile = tile; |
| + e.wasAnimated = wasAnimated; |
| + this.dispatchEvent(e); |
| + }, |
| + |
| + /** |
| + * Removes the given tile and animates the repositioning of the other tiles. |
| + * @param {boolean=} opt_animate Whether the removal should be animated. |
| + * @param {boolean=} opt_dontNotify Whether a page should be removed if the |
| + * last tile is removed from it. |
| + */ |
| + removeTile: function(tile, opt_animate, opt_dontNotify) { |
| + if (opt_animate) |
| + this.classList.add('animating-tile-page'); |
| + var index = tile.index; |
| + tile.parentNode.removeChild(tile); |
| + this.calculateLayoutValues_(); |
| + this.cleanupDrag(); |
| + |
| + if (!opt_dontNotify) |
| + this.fireRemovedEvent(tile, index, !!opt_animate); |
| + }, |
| + |
| + /** |
| + * Notify interested subscribers that a tile has been removed from this |
| + * page. |
| + * @param {TileCell} tile The tile that was removed. |
| + * @param {number} oldIndex Where the tile was positioned before removal. |
| + * @param {boolean} wasAnimated Whether the removal was animated. |
| + */ |
| + fireRemovedEvent: function(tile, oldIndex, wasAnimated) { |
| + var e = document.createEvent('Event'); |
| + e.initEvent('tilePage:tile_removed', true, true); |
| + e.removedIndex = oldIndex; |
| + e.removedTile = tile; |
| + e.wasAnimated = wasAnimated; |
| + this.dispatchEvent(e); |
| + }, |
| + |
| + /** |
| + * Removes all tiles from the page. |
| + */ |
| + removeAllTiles: function() { |
| + // TODO(xci) dispatch individual tearDown functions |
| + this.tileGrid_.innerHTML = ''; |
| + }, |
| + |
| + /** |
| + * Called when the page is selected (in the card selector). |
| + * @param {Event} e A custom cardselected event. |
| + * @private |
| + */ |
| + 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
|
| + this.tabIndex = 1; |
| + |
| + // When we are selected, we re-calculate the layout values. (See comment |
| + // in doDrop.) |
| + this.calculateLayoutValues_(); |
| + }, |
| + |
| + /** |
| + * Called when the page loses selection (in the card selector). |
| + * @param {Event} e A custom carddeselected event. |
| + * @private |
| + */ |
| + handleCardDeselection_: function(e) { |
| + this.tabIndex = -1; |
| + if (this.currentFocusElement_) |
| + this.currentFocusElement_.tabIndex = -1; |
| + }, |
| + |
| + /** |
| + * When we get focus, pass it on to the focus element. |
| + * @param {Event} e The focus event. |
| + * @private |
| + */ |
| + handleFocus_: function(e) { |
| + if (this.focusableElements_.length == 0) |
| + return; |
| + |
| + this.updateFocusElement_(); |
| + }, |
| + |
| + /** |
| + * Since we are doing custom focus handling, we have to manually |
| + * set focusability on click (as well as keyboard nav above). |
| + * @param {Event} e The focus event. |
| + * @private |
| + */ |
| + handleMouseDown_: function(e) { |
| + var focusable = findAncestorByClass(e.target, 'focusable'); |
| + if (focusable) { |
| + this.focusElementIndex_ = |
| + Array.prototype.indexOf.call(this.focusableElements_, |
| + focusable); |
| + this.updateFocusElement_(); |
| + } else { |
| + // This prevents the tile page from getting focus when the user clicks |
| + // inside the grid but outside of any tile. |
| + e.preventDefault(); |
| + } |
| + }, |
| + |
| + /** |
| + * Handle arrow key focus nav. |
| + * @param {Event} e The focus event. |
| + * @private |
| + */ |
| + handleKeyDown_: function(e) { |
| + // We only handle up, down, left, right without control keys. |
| + if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey) |
| + return; |
| + |
| + // Wrap the given index to |this.focusableElements_|. |
| + var wrap = function(idx) { |
| + return (idx + this.focusableElements_.length) % |
| + this.focusableElements_.length; |
| + }.bind(this); |
| + |
| + switch (e.keyIdentifier) { |
| + case 'Right': |
| + case 'Left': |
| + var direction = e.keyIdentifier == 'Right' ? 1 : -1; |
| + this.focusElementIndex_ = wrap(this.focusElementIndex_ + direction); |
| + break; |
| + case 'Up': |
| + case 'Down': |
| + // Look through all focusable elements. Find the first one that is |
| + // in the same column. |
| + var direction = e.keyIdentifier == 'Up' ? -1 : 1; |
| + var currentIndex = |
| + Array.prototype.indexOf.call(this.focusableElements_, |
| + this.currentFocusElement_); |
| + var newFocusIdx = wrap(currentIndex + direction); |
| + var tile = this.currentFocusElement_.parentNode; |
| + for (;; newFocusIdx = wrap(newFocusIdx + direction)) { |
| + var newTile = this.focusableElements_[newFocusIdx].parentNode; |
| + var rowTiles = this.layoutValues_.numRowTiles; |
| + if ((newTile.index - tile.index) % rowTiles == 0) |
| + break; |
| + } |
| + |
| + this.focusElementIndex_ = newFocusIdx; |
| + break; |
| + |
| + default: |
| + return; |
| + } |
| + |
| + this.updateFocusElement_(); |
| + |
| + e.preventDefault(); |
| + e.stopPropagation(); |
| + }, |
| + |
| + /** |
| + * Focuses the element for |this.focusElementIndex_|. Makes the current |
| + * focus element, if any, no longer eligible for focus. |
| + * @private |
| + */ |
| + updateFocusElement_: function() { |
| + this.focusElementIndex_ = Math.min(this.focusableElements_.length - 1, |
| + this.focusElementIndex_); |
| + this.focusElementIndex_ = Math.max(0, this.focusElementIndex_); |
| + |
| + var newFocusElement = this.focusableElements_[this.focusElementIndex_]; |
| + var lastFocusElement = this.currentFocusElement_; |
| + if (lastFocusElement && lastFocusElement != newFocusElement) |
| + lastFocusElement.tabIndex = -1; |
| + |
| + newFocusElement.tabIndex = 1; |
| + newFocusElement.focus(); |
| + this.tabIndex = -1; |
| + }, |
| + |
| + /** |
| + * The current focus element is that element which is eligible for focus. |
| + * @type {HTMLElement} The node. |
| + * @private |
| + */ |
| + get currentFocusElement_() { |
| + return this.querySelector('.focusable[tabindex="1"]'); |
| + }, |
| + |
| + /** |
| + * Makes some calculations for tile layout. These change depending on |
| + * height, width, and the number of tiles. |
| + * TODO(estade): optimize calls to this function. Do nothing if the page is |
| + * hidden, but call before being shown. |
| + * @private |
| + */ |
| + calculateLayoutValues_: function() { |
| + |
| + // We need to update the top margin as well. |
| + this.updateTopMargin_(); |
| + |
| + // TODO(pedrosimonetti): when do we really need to send this message? |
| + this.firePageLayoutEvent_(); |
| + }, |
| + |
| + /** |
| + * Dispatches the custom pagelayout event. |
| + * @private |
| + */ |
| + firePageLayoutEvent_: function() { |
| + cr.dispatchSimpleEvent(this, 'pagelayout', true, true); |
| + }, |
| + |
| + |
| + /** |
| + * Gets the index of the tile that should occupy coordinate (x, y). Note |
| + * that this function doesn't care where the tiles actually are, and will |
| + * return an index even for the space between two tiles. This function is |
| + * effectively the inverse of |positionTile_|. |
| + * @param {number} x The x coordinate, in pixels, relative to the left of |
| + * |this|. |
| + * @param {number} y The y coordinate, in pixels, relative to the top of |
| + * |this|. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + getWouldBeIndexForPoint_: function(x, y) { |
| + var grid = this.gridValues_; |
| + var layout = this.layoutValues_; |
| + |
| + var gridClientRect = this.tileGrid_.getBoundingClientRect(); |
| + var col = Math.floor((x - gridClientRect.left - layout.leftMargin) / |
| + layout.colWidth); |
| + if (col < 0 || col >= layout.numRowTiles) |
| + return -1; |
| + |
| + if (isRTL()) |
| + col = layout.numRowTiles - 1 - col; |
| + |
| + var row = Math.floor((y - gridClientRect.top) / layout.rowHeight); |
| + return row * layout.numRowTiles + col; |
| + }, |
| + |
| + /** |
| + * The tile grid has an image mask which fades at the edges. We only show |
| + * the mask when there is an active drag; it obscures doppleganger tiles |
| + * as they enter or exit the grid. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + updateMask_: function() { |
| + if (!this.isCurrentDragTarget) { |
| + this.tileGrid_.style.WebkitMaskBoxImage = ''; |
| + return; |
| + } |
| + |
| + var leftMargin = this.layoutValues_.leftMargin; |
| + // The fade distance is the space between tiles. |
| + var fadeDistance = (this.gridValues_.tileSpacingFraction * |
| + this.layoutValues_.tileWidth); |
| + fadeDistance = Math.min(leftMargin, fadeDistance); |
| + // On Skia we don't use any fade because it works very poorly. See |
| + // http://crbug.com/99373 |
| + if (!cr.isMac) |
| + fadeDistance = 1; |
| + var gradient = |
| + '-webkit-linear-gradient(left,' + |
| + 'transparent, ' + |
| + 'transparent ' + (leftMargin - fadeDistance) + 'px, ' + |
| + 'black ' + leftMargin + 'px, ' + |
| + 'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' + |
| + 'transparent ' + (this.tileGrid_.clientWidth - leftMargin + |
| + fadeDistance) + 'px, ' + |
| + 'transparent)'; |
| + this.tileGrid_.style.WebkitMaskBoxImage = gradient; |
| + }, |
| + |
| + // TODO(xci) delete (used by drag and drop) |
| + updateTopMargin_: function() { |
| + return; |
| + }, |
| + |
| + /** |
| + * Position the notification if there's one showing. |
| + */ |
| + positionNotification_: function() { |
| + if (this.notification && !this.notification.hidden) { |
| + this.notification.style.margin = |
| + -this.notification.offsetHeight + 'px ' + |
| + this.layoutValues_.leftMargin + 'px 0'; |
| + } |
| + }, |
| + |
| + /** |
| + * Handles final setup that can only happen after |this| is inserted into |
| + * the page. |
| + * @private |
| + */ |
| + onNodeInsertedIntoDocument_: function(e) { |
| + this.calculateLayoutValues_(); |
| + }, |
| + |
| + /** |
| + * Places an element at the bottom of the content div. Used in bare-minimum |
| + * mode to hold #footer. |
| + * @param {HTMLElement} footerNode The node to append to content. |
| + */ |
| + appendFooter: function(footerNode) { |
| + this.footerNode_ = footerNode; |
| + this.content_.appendChild(footerNode); |
| + }, |
| + |
| + /** |
| + * Scrolls the page in response to an mousewheel event, although the event |
| + * may have been triggered on a different element. Return true if the |
| + * event triggered scrolling, and false otherwise. |
| + * This is called explicitly, which allows a consistent experience whether |
| + * the user scrolls on the page or on the page switcher, because this |
| + * function provides a common conversion factor between wheel delta and |
| + * scroll delta. |
| + * @param {Event} e The mousewheel event. |
| + */ |
| + handleMouseWheel: function(e) { |
| + if (e.wheelDeltaY == 0) |
| + return false; |
| + |
| + this.content_.scrollTop -= e.wheelDeltaY / 3; |
| + return true; |
| + }, |
| + |
| + /** Dragging **/ |
| + |
| + // TODO(xci) drag |
| + get isCurrentDragTarget() { |
| + return this.dragWrapper_.isCurrentDragTarget; |
| + }, |
| + |
| + /** |
| + * Thunk for dragleave events fired on |tileGrid_|. |
| + * @param {Event} e A MouseEvent for the drag. |
| + */ |
| + // TODO(xci) drag |
| + doDragLeave: function(e) { |
| + this.cleanupDrag(); |
| + }, |
| + |
| + /** |
| + * Performs all actions necessary when a drag enters the tile page. |
| + * @param {Event} e A mouseover event for the drag enter. |
| + */ |
| + // TODO(xci) drag |
| + doDragEnter: function(e) { |
| + // Applies the mask so doppleganger tiles disappear into the fog. |
| + this.updateMask_(); |
| + |
| + this.classList.add('animating-tile-page'); |
| + this.withinPageDrag_ = this.contains(currentlyDraggingTile); |
| + this.dragItemIndex_ = this.withinPageDrag_ ? |
| + currentlyDraggingTile.index : this.tileElements_.length; |
| + this.currentDropIndex_ = this.dragItemIndex_; |
| + |
| + // The new tile may change the number of rows, hence the top margin |
| + // will change. |
| + if (!this.withinPageDrag_) |
| + // TODO(xci) this function does nothing now! |
| + this.updateTopMargin_(); |
| + |
| + this.doDragOver(e); |
| + }, |
| + |
| + /** |
| + * Performs all actions necessary when the user moves the cursor during |
| + * a drag over the tile page. |
| + * @param {Event} e A mouseover event for the drag over. |
| + */ |
| + // TODO(xci) drag |
| + doDragOver: function(e) { |
| + e.preventDefault(); |
| + |
| + this.setDropEffect(e.dataTransfer); |
| + var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY); |
| + if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length) |
| + newDragIndex = this.dragItemIndex_; |
| + this.updateDropIndicator_(newDragIndex); |
| + }, |
| + |
| + /** |
| + * Performs all actions necessary when the user completes a drop. |
| + * @param {Event} e A mouseover event for the drag drop. |
| + */ |
| + // TODO(xci) drag |
| + doDrop: function(e) { |
| + e.stopPropagation(); |
| + e.preventDefault(); |
| + |
| + var index = this.currentDropIndex_; |
| + // Only change data if this was not a 'null drag'. |
| + if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) { |
| + var adjustedIndex = this.currentDropIndex_ + |
| + (index > this.dragItemIndex_ ? 1 : 0); |
| + if (this.withinPageDrag_) { |
| + this.tileGrid_.insertBefore( |
| + currentlyDraggingTile, |
| + this.tileElements_[adjustedIndex]); |
| + this.tileMoved(currentlyDraggingTile, this.dragItemIndex_); |
| + } else { |
| + var originalPage = currentlyDraggingTile ? |
| + currentlyDraggingTile.tilePage : null; |
| + this.addDragData(e.dataTransfer, adjustedIndex); |
| + if (originalPage) |
| + originalPage.cleanupDrag(); |
| + } |
| + |
| + // Dropping the icon may cause topMargin to change, but changing it |
| + // now would cause everything to move (annoying), so we leave it |
| + // alone. The top margin will be re-calculated next time the window is |
| + // resized or the page is selected. |
| + } |
| + |
| + this.classList.remove('animating-tile-page'); |
| + this.cleanupDrag(); |
| + }, |
| + |
| + /** |
| + * Appends the currently dragged tile to the end of the page. Called |
| + * from outside the page, e.g. when dropping on a nav dot. |
| + */ |
| + // TODO(xci) drag |
| + appendDraggingTile: function() { |
| + var originalPage = currentlyDraggingTile.tilePage; |
| + if (originalPage == this) |
| + return; |
| + |
| + this.addDragData(null, this.tileElements_.length); |
| + if (originalPage) |
| + originalPage.cleanupDrag(); |
| + }, |
| + |
| + /** |
| + * Makes sure all the tiles are in the right place after a drag is over. |
| + */ |
| + // TODO(xci) drag |
| + cleanupDrag: function() { |
| + this.repositionTiles_(currentlyDraggingTile); |
| + // Remove the drag mask. |
| + this.updateMask_(); |
| + }, |
| + |
| + /** |
| + * Reposition all the tiles (possibly ignoring one). |
| + * @param {?Node} ignoreNode An optional node to ignore. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + repositionTiles_: function(ignoreNode) { |
| + for (var i = 0; i < this.tileElements_.length; i++) { |
| + if (!ignoreNode || ignoreNode !== this.tileElements_[i]) |
| + ;//this.positionTile_(i); TODO(xci) this function was deleted! |
| + } |
| + }, |
| + |
| + /** |
| + * Updates the visual indicator for the drop location for the active drag. |
| + * @param {Event} e A MouseEvent for the drag. |
| + * @private |
| + */ |
| + // TODO(xci) drag |
| + updateDropIndicator_: function(newDragIndex) { |
| + var oldDragIndex = this.currentDropIndex_; |
| + if (newDragIndex == oldDragIndex) |
| + return; |
| + |
| + var repositionStart = Math.min(newDragIndex, oldDragIndex); |
| + var repositionEnd = Math.max(newDragIndex, oldDragIndex); |
| + |
| + for (var i = repositionStart; i <= repositionEnd; i++) { |
| + if (i == this.dragItemIndex_) |
| + continue; |
| + else if (i > this.dragItemIndex_) |
| + var adjustment = i <= newDragIndex ? -1 : 0; |
| + else |
| + var adjustment = i >= newDragIndex ? 1 : 0; |
| + |
| + //this.positionTile_(i, adjustment); TODO(xci) function was deleted! |
| + } |
| + this.currentDropIndex_ = newDragIndex; |
| + }, |
| + |
| + /** |
| + * Checks if a page can accept a drag with the given data. |
| + * @param {Event} e The drag event if the drag object. Implementations will |
| + * likely want to check |e.dataTransfer|. |
| + * @return {boolean} True if this page can handle the drag. |
| + */ |
| + // TODO(xci) drag |
| + shouldAcceptDrag: function(e) { |
| + return false; |
| + }, |
| + |
| + /** |
| + * Called to accept a drag drop. Will not be called for in-page drops. |
| + * @param {Object} dataTransfer The data transfer object that holds the drop |
| + * data. This should only be used if currentlyDraggingTile is null. |
| + * @param {number} index The tile index at which the drop occurred. |
| + */ |
| + // TODO(xci) drag |
| + addDragData: function(dataTransfer, index) { |
| + assert(false); |
| + }, |
| + |
| + /** |
| + * Called when a tile has been moved (via dragging). Override this to make |
| + * backend updates. |
| + * @param {Node} draggedTile The tile that was dropped. |
| + * @param {number} prevIndex The previous index of the tile. |
| + */ |
| + tileMoved: function(draggedTile, prevIndex) { |
| + }, |
| + |
| + /** |
| + * Sets the drop effect on |dataTransfer| to the desired value (e.g. |
| + * 'copy'). |
| + * @param {Object} dataTransfer The drag event dataTransfer object. |
| + */ |
| + // TODO(xci) drag |
| + setDropEffect: function(dataTransfer) { |
| + assert(false); |
| + }, |
| + |
| + |
| + // ######################################################################### |
| + // XCI - Extended Chrome Instant |
| + // ######################################################################### |
| + |
| + |
| + // properties |
| + // ------------------------------------------------------------------------- |
| + colCount: 5, |
| + rowCount: 2, |
| + |
| + numOfVisibleRows: 0, |
| + animatingColCount: 5, // TODO initialize |
| + |
| + // TODO move to layout? |
| + pageOffset: 0, |
| + |
| + appendTile: function(tile) { |
| + this.tileElements_.push(tile); |
| + this.renderGrid_(); |
| + }, |
| + |
| + addTileAt: function(tile) { |
| + this.appendTile(tile); |
| + }, |
| + |
| + // internal helpers |
| + // ------------------------------------------------------------------------- |
| + |
| + // TODO move to layout? |
| + getNumOfVisibleRows: function() { |
| + return this.numOfVisibleRows; |
| + }, |
| + |
| + getTileRequiredWidth_: function() { |
| + var grid = this.gridValues_; |
| + return grid.tileWidth + 2 * grid.tileBorderWidth + grid.tileHorMargin; |
| + }, |
| + |
| + getColCountForWidth_: function(width) { |
| + var availableWidth = width + this.gridValues_.tileHorMargin; |
| + var requiredWidth = this.getTileRequiredWidth_(); |
| + var colCount = Math.floor(availableWidth / requiredWidth); |
| + return colCount; |
| + }, |
| + |
| + getWidthForColCount_: function(colCount) { |
| + var requiredWidth = this.getTileRequiredWidth_(); |
| + var width = colCount * requiredWidth - this.gridValues_.tileHorMargin; |
| + return width; |
| + }, |
| + |
| + getBottomPanelWidth_: function() { |
| + var windowWidth = cr.doc.documentElement.clientWidth; |
| + var width; |
| + if (windowWidth >= 948) { |
| + width = 748; |
| + } else if (windowWidth >= 500) { |
| + width = windowWidth - 2 * this.gridValues_.bottomPanelHorMargin; |
| + } else if (windowWidth >= 300) { |
| + // TODO(pedrosimonetti): check math and document |
| + width = Math.floor(((windowWidth - 300) / 200) * 100 + 200); |
| + } else { |
| + width = 200; |
| + } |
| + return width; |
| + }, |
| + |
| + getAvailableColCount_: function() { |
| + return this.getColCountForWidth_(this.getBottomPanelWidth_()); |
| + }, |
| + |
| + // rendering |
| + // ------------------------------------------------------------------------- |
| + |
| + renderGrid_: function(colCount, tileElements) { |
| + //if (window.xc > 5) debugger; |
| + //window.xc = window.xc || 1, console.log('renderGrid' + window.xc++); |
| + colCount = colCount || this.colCount; |
| + |
| + var tileGridContent = this.tileGridContent_; |
| + |
| + tileElements = tileElements || this.tileElements_; |
| + |
| + var tileCount = tileElements.length; |
| + var rowCount = Math.ceil(tileCount / colCount); |
| + |
| + var tileRows = tileGridContent.getElementsByClassName('tile-row'); |
| + var tileRow; |
| + var tileRowTiles; |
| + var tileCell; |
| + var tileElement; |
| + var maxColCount; |
| + |
| + var numOfVisibleRows = this.getNumOfVisibleRows(); |
| + var pageOffset = this.pageOffset; |
| + |
| + for (var tile = 0, row = 0; row < rowCount; row++) { |
| + // Get the current tile row. |
| + tileRow = tileRows[row]; |
| + |
| + // Create tile row if there's no one yet. |
| + if (!tileRow) { |
| + tileRow = cr.doc.createElement('div'); |
| + tileRow.className = 'tile-row tile-row-' + row;// TODO do we need id? |
| + tileGridContent.appendChild(tileRow); |
| + } |
| + |
| + // Adjust row visibility. |
| + var rowVisible = (row >= pageOffset && |
| + row <= (pageOffset + numOfVisibleRows - 1)); |
| + tileRow.classList[rowVisible ? 'remove' : 'add']('hide-row'); |
| + |
| + // The tiles inside the current row. |
| + tileRowTiles = tileRow.childNodes; |
| + |
| + // Remove excessive columns from a particular tile row. |
| + maxColCount = Math.min(colCount, tileCount - tile); |
| + while (tileRowTiles.length > maxColCount) { |
| + tileRow.removeChild(tileRow.lastElementChild); |
| + } |
| + |
| + // For each column in the current row. |
| + for (var col = 0; /*tile < tileCount &&*/ col < colCount; col++, tile++) { |
| + |
| + if (tileRowTiles[col]) { |
| + tileCell = tileRowTiles[col]; |
| + } else { |
| + var span = cr.doc.createElement('span'); |
| + tileCell = new TileCell(span, this.gridValues_); |
| + } |
| + |
| + // Reset column class. |
| + this.resetTileCol_(tileCell, col); |
| + |
| + // Render Tiles. |
| + if (tile < tileCount) { |
| + tileCell.classList.remove('filler'); |
| + tileElement = tileElements[tile]; |
| + if (tileCell.firstChild) { |
| + if (tileElement != tileCell.firstChild) { |
| + tileCell.replaceChild(tileElement, tileCell.firstChild); |
| + } |
| + } else { |
| + tileCell.appendChild(tileElement); |
| + } |
| + } else { |
| + // TODO create filler and method to |
| + if (!tileCell.classList.contains('filler')) { |
| + tileCell.classList.add('filler'); |
| + tileElement = cr.doc.createElement('span'); |
| + if (tileCell.firstChild) |
| + tileCell.replaceChild(tileElement, tileCell.firstChild); |
| + else |
| + tileCell.appendChild(tileElement); |
| + } |
| + } |
| + |
| + if (!tileRowTiles[col]) { |
| + tileRow.appendChild(tileCell); |
| + } |
| + } |
| + } |
| + |
| + // Remove excessive tile rows from the tile grid. |
| + while (tileRows.length > rowCount) { |
| + tileGridContent.removeChild(tileGridContent.lastElementChild); |
| + } |
| + |
| + this.colCount = colCount; |
| + this.rowCount = rowCount; |
| + }, |
| + |
| + layout_: function() { |
| + var pageList = $('page-list'); |
| + var panel = this.content_; |
| + var menu = $('page-list-menu'); |
| + var tileGrid = this.tileGrid_; |
| + var tileGridContent = this.tileGridContent_; |
| + var tileRows = tileGridContent.getElementsByClassName('tile-row'); |
| + |
| + tileGridContent.classList.add('animate-tile'); |
| + |
| + var bottomPanelWidth = this.getBottomPanelWidth_(); |
| + var colCount = this.getColCountForWidth_(bottomPanelWidth); |
| + var lastColCount = this.colCount; |
| + var animatingColCount = this.animatingColCount; |
| + |
| + var windowHeight = cr.doc.documentElement.clientHeight; |
| + |
| + // TODO better handling of height state |
| + // changeVisibleRows |
| + // TODO need to call paginate when height changes |
| + var numOfVisibleRows = this.numOfVisibleRows; |
| + if (windowHeight > 500) { |
| + numOfVisibleRows = 2; |
| + } else { |
| + numOfVisibleRows = 1; |
| + } |
| + |
| + if (numOfVisibleRows != this.numOfVisibleRows) { |
| + this.numOfVisibleRows = numOfVisibleRows; |
| + this.paginate(null, true); |
| + pageList.style.height = (107 * numOfVisibleRows) + 'px'; |
| + //tileGrid.style.height = (107 * numOfVisibleRows) + 'px'; |
| + } |
| + |
| + // changeVisibleCols |
| + if (colCount != animatingColCount) { |
| + |
| + var newWidth = this.getWidthForColCount_(colCount); |
| + if (colCount > animatingColCount) { |
| + |
| + // TODO actual size check |
| + if (colCount != lastColCount) { |
| + this.renderGrid_(colCount); |
| + //this.renderTiles_(colCount); |
| + } |
| + |
| + this.showTileCols_(animatingColCount, false); |
| + |
| + var self = this; |
| + setTimeout((function(animatingColCount){ |
| + return function(){ |
| + self.showTileCols_(animatingColCount, true); |
| + } |
| + })(animatingColCount), 0); |
| + |
| + } else { |
| + this.showTileCols_(colCount, false); |
| + } |
| + |
| + tileGrid.style.width = newWidth + 'px'; |
| + menu.style.width = newWidth + 'px'; |
| + |
| + // TODO: listen animateEnd |
| + var self = this; |
| + this.onTileGridAnimationEndHandler_ = function(){ |
| + if (colCount < lastColCount) { |
| + self.renderGrid_(colCount); |
| + //self.renderTiles_(colCount); |
| + } else { |
| + self.showTileCols_(0, true); |
| + } |
| + }; |
| + |
| + this.paginate(); |
| + |
| + } |
| + |
| + panel.style.width = bottomPanelWidth + 'px'; |
| + |
| + this.animatingColCount = colCount; |
| + }, |
| + |
| + // animation helpers |
| + // ------------------------------------------------------------------------- |
| + |
| + // TODO make it local? |
| + showTileCols_: function(col, show) { |
| + var prop = show ? 'remove' : 'add'; |
| + var max = 10; // TODO(pedrosimonetti) const? |
| + var tileGridContent = this.tileGridContent_; |
| + for (var i = col; i < max; i++) { |
| + tileGridContent.classList[prop]('hide-col-' + i); |
| + } |
| + }, |
| + |
| + // TODO make it local? |
| + resetTileCol_: function(tileCell, col) { |
| + var max = 10; |
| + for (var i = 0; i < max; i++) { |
| + if (i != col) { |
| + tileCell.classList.remove('tile-col-' + i); |
| + } |
| + } |
| + tileCell.classList.add('tile-col-' + col); |
| + }, |
| + |
| + // pagination |
| + // ------------------------------------------------------------------------- |
| + |
| + paginate: function(pageOffset, force) { |
| + var numOfVisibleRows = this.getNumOfVisibleRows(); |
| + var pageOffset = typeof pageOffset == 'number' ? |
| + pageOffset : this.pageOffset; |
| + |
| + pageOffset = Math.max(0, pageOffset); |
| + pageOffset = Math.min(this.rowCount - numOfVisibleRows, pageOffset); |
| + |
| + if (pageOffset != this.pageOffset || force) { |
| + var rows = this.tileGridContent_.getElementsByClassName('tile-row'); |
| + for (var i = 0, l = rows.length; i < l; i++) { |
| + var row = rows[i]; |
| + if (i >= pageOffset && i <= (pageOffset + numOfVisibleRows - 1)) { |
| + row.classList.remove('hide-row'); |
| + } else { |
| + row.classList.add('hide-row'); |
| + } |
| + } |
| + |
| + this.pageOffset = pageOffset; |
| + this.tileGridContent_.style.webkitTransform = 'translate3d(0, ' + |
| + (-pageOffset * 106)+ 'px, 0)'; |
| + } |
| + }, |
| + |
| + // event handlers |
| + // ------------------------------------------------------------------------- |
| + |
| + onResize_: function() { |
| + this.layout_(); |
| + }, |
| + |
| + onTileGridAnimationEnd_: function() { |
| + // TODO figure out how to cleanup each kind animation properly |
| + //console.log('AnimationEnd', event.target.className, event.target, event); |
| + //console.log('-----------------------------------------------------------'); |
| + if (event.target == this.tileGrid_ && |
| + this.onTileGridAnimationEndHandler_) { |
| + if (this.tileGridContent_.classList.contains('animate-tile')) { |
| + this.onTileGridAnimationEndHandler_(); |
| + //this.tileGridContent_.classList.remove('animate-tile') |
| + } |
| + } |
| + }, |
| + |
| + onKeyDown_: function(e) { |
| + var pageOffset = this.pageOffset; |
| + |
| + var keyCode = e.keyCode; |
| + if (keyCode == 40 /* down */ ) { |
| + pageOffset++; |
| + } else if (keyCode == 38 /* up */ ) { |
| + pageOffset--; |
| + } |
| + |
| + if (pageOffset != this.pageOffset) { |
| + this.paginate(pageOffset); |
| + } |
| + } |
| + |
| + }; |
| + |
| + |
| + var duration = 200; |
| + var defaultDuration = 200; |
| + var animatedProperties = [ |
| + '#card-slider-frame .tile-grid', |
| + '#card-slider-frame .tile-grid-content', |
| + '#card-slider-frame .tile-row', |
| + '.animate-tile .tile-cell', |
| + '.debug .animate-tile .tile-cell' |
| + ]; |
| + |
| + function adjustAnimationTiming(slownessFactor, selectors) { |
| + slownessFactor = slownessFactor || 1; |
| + duration = defaultDuration * slownessFactor; |
| + for (var i = 0, l = selectors.length; i < l; i++) { |
| + var selector = selectors[i]; |
| + var rule = findCSSRule(selector); |
| + if (rule) { |
| + rule.style.webkitTransitionDuration = duration + 'ms'; |
| + } else { |
| + throw 'Could not find the CSS rule "' + selector + '"'; |
| + } |
| + } |
| + } |
| + |
| + function findCSSRule(selectorText){ |
| + var rules = document.styleSheets[0].rules; |
| + for (var i = 0, l = rules.length; i < l; i++) { |
| + var rule = rules[i]; |
| + if (rule.selectorText == selectorText) |
| + { |
| + return rule; |
| + } |
| + } |
| + } |
| + |
| + function changeSlowFactor(el) { |
| + if (el.value) |
| + adjustAnimationTiming(el.value-0, animatedProperties); |
| + } |
| + |
| + function changeDebugMode(el) { |
| + var prop = el.checked ? 'add' : 'remove'; |
| + document.body.classList[prop]('debug'); |
| + } |
| + |
| + return { |
| + // TODO(xci) drag |
| + //getCurrentlyDraggingTile2: getCurrentlyDraggingTile, |
| + //setCurrentDropEffect2: setCurrentDropEffect, |
| + Tile2: Tile, |
| + TilePage2: TilePage, |
| + }; |
| +}); |