/** * @class Ext.layout.container.boxOverflow.Scroller * @extends Ext.layout.container.boxOverflow.None * @private */ Ext.define('Ext.layout.container.boxOverflow.Scroller', { /* Begin Definitions */ extend: 'Ext.layout.container.boxOverflow.None', requires: ['Ext.util.ClickRepeater', 'Ext.core.Element'], alternateClassName: 'Ext.layout.boxOverflow.Scroller', mixins: { observable: 'Ext.util.Observable' }, /* End Definitions */ /** * @cfg {Boolean} animateScroll * True to animate the scrolling of items within the layout (defaults to true, ignored if enableScroll is false) */ animateScroll: false, /** * @cfg {Number} scrollIncrement * The number of pixels to scroll by on scroller click (defaults to 24) */ scrollIncrement: 20, /** * @cfg {Number} wheelIncrement * The number of pixels to increment on mouse wheel scrolling (defaults to <tt>3</tt>). */ wheelIncrement: 10, /** * @cfg {Number} scrollRepeatInterval * Number of milliseconds between each scroll while a scroller button is held down (defaults to 20) */ scrollRepeatInterval: 60, /** * @cfg {Number} scrollDuration * Number of milliseconds that each scroll animation lasts (defaults to 400) */ scrollDuration: 400, /** * @cfg {String} beforeCtCls * CSS class added to the beforeCt element. This is the element that holds any special items such as scrollers, * which must always be present at the leftmost edge of the Container */ /** * @cfg {String} afterCtCls * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers, * which must always be present at the rightmost edge of the Container */ /** * @cfg {String} scrollerCls * CSS class added to both scroller elements if enableScroll is used */ scrollerCls: Ext.baseCSSPrefix + 'box-scroller', /** * @cfg {String} beforeScrollerCls * CSS class added to the left scroller element if enableScroll is used */ /** * @cfg {String} afterScrollerCls * CSS class added to the right scroller element if enableScroll is used */ constructor: function(layout, config) { this.layout = layout; Ext.apply(this, config || {}); this.addEvents( /** * @event scroll * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller * @param {Number} newPosition The new position of the scroller * @param {Boolean/Object} animate If animating or not. If true, it will be a animation configuration, else it will be false */ 'scroll' ); }, initCSSClasses: function() { var me = this, layout = me.layout; if (!me.CSSinitialized) { me.beforeCtCls = me.beforeCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelBefore; me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelAfter; me.beforeScrollerCls = me.beforeScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelBefore; me.afterScrollerCls = me.afterScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelAfter; me.CSSinitializes = true; } }, handleOverflow: function(calculations, targetSize) { var me = this, layout = me.layout, methodName = 'get' + layout.parallelPrefixCap, newSize = {}; me.initCSSClasses(); me.callParent(arguments); this.createInnerElements(); this.showScrollers(); newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix]; newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - (me.beforeCt[methodName]() + me.afterCt[methodName]()); return { targetSize: newSize }; }, /** * @private * Creates the beforeCt and afterCt elements if they have not already been created */ createInnerElements: function() { var me = this, target = me.layout.getRenderTarget(); //normal items will be rendered to the innerCt. beforeCt and afterCt allow for fixed positioning of //special items such as scrollers or dropdown menu triggers if (!me.beforeCt) { target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body'); me.beforeCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.beforeCtCls}, 'before'); me.afterCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.afterCtCls}, 'after'); me.createWheelListener(); } }, /** * @private * Sets up an listener to scroll on the layout's innerCt mousewheel event */ createWheelListener: function() { this.layout.innerCt.on({ scope : this, mousewheel: function(e) { e.stopEvent(); this.scrollBy(e.getWheelDelta() * this.wheelIncrement * -1, false); } }); }, /** * @private */ clearOverflow: function() { this.hideScrollers(); }, /** * @private * Shows the scroller elements in the beforeCt and afterCt. Creates the scrollers first if they are not already * present. */ showScrollers: function() { this.createScrollers(); this.beforeScroller.show(); this.afterScroller.show(); this.updateScrollButtons(); this.layout.owner.addClsWithUI('scroller'); }, /** * @private * Hides the scroller elements in the beforeCt and afterCt */ hideScrollers: function() { if (this.beforeScroller != undefined) { this.beforeScroller.hide(); this.afterScroller.hide(); this.layout.owner.removeClsWithUI('scroller'); } }, /** * @private * Creates the clickable scroller elements and places them into the beforeCt and afterCt */ createScrollers: function() { if (!this.beforeScroller && !this.afterScroller) { var before = this.beforeCt.createChild({ cls: Ext.String.format("{0} {1} ", this.scrollerCls, this.beforeScrollerCls) }); var after = this.afterCt.createChild({ cls: Ext.String.format("{0} {1}", this.scrollerCls, this.afterScrollerCls) }); before.addClsOnOver(this.beforeScrollerCls + '-hover'); after.addClsOnOver(this.afterScrollerCls + '-hover'); before.setVisibilityMode(Ext.core.Element.DISPLAY); after.setVisibilityMode(Ext.core.Element.DISPLAY); this.beforeRepeater = Ext.create('Ext.util.ClickRepeater', before, { interval: this.scrollRepeatInterval, handler : this.scrollLeft, scope : this }); this.afterRepeater = Ext.create('Ext.util.ClickRepeater', after, { interval: this.scrollRepeatInterval, handler : this.scrollRight, scope : this }); /** * @property beforeScroller * @type Ext.core.Element * The left scroller element. Only created when needed. */ this.beforeScroller = before; /** * @property afterScroller * @type Ext.core.Element * The left scroller element. Only created when needed. */ this.afterScroller = after; } }, /** * @private */ destroy: function() { Ext.destroy(this.beforeRepeater, this.afterRepeater, this.beforeScroller, this.afterScroller, this.beforeCt, this.afterCt); }, /** * @private * Scrolls left or right by the number of pixels specified * @param {Number} delta Number of pixels to scroll to the right by. Use a negative number to scroll left */ scrollBy: function(delta, animate) { this.scrollTo(this.getScrollPosition() + delta, animate); }, /** * @private * @return {Object} Object passed to scrollTo when scrolling */ getScrollAnim: function() { return { duration: this.scrollDuration, callback: this.updateScrollButtons, scope : this }; }, /** * @private * Enables or disables each scroller button based on the current scroll position */ updateScrollButtons: function() { if (this.beforeScroller == undefined || this.afterScroller == undefined) { return; } var beforeMeth = this.atExtremeBefore() ? 'addCls' : 'removeCls', afterMeth = this.atExtremeAfter() ? 'addCls' : 'removeCls', beforeCls = this.beforeScrollerCls + '-disabled', afterCls = this.afterScrollerCls + '-disabled'; this.beforeScroller[beforeMeth](beforeCls); this.afterScroller[afterMeth](afterCls); this.scrolling = false; }, /** * @private * Returns true if the innerCt scroll is already at its left-most point * @return {Boolean} True if already at furthest left point */ atExtremeBefore: function() { return this.getScrollPosition() === 0; }, /** * @private * Scrolls to the left by the configured amount */ scrollLeft: function() { this.scrollBy(-this.scrollIncrement, false); }, /** * @private * Scrolls to the right by the configured amount */ scrollRight: function() { this.scrollBy(this.scrollIncrement, false); }, /** * Returns the current scroll position of the innerCt element * @return {Number} The current scroll position */ getScrollPosition: function(){ var layout = this.layout; return parseInt(layout.innerCt.dom['scroll' + layout.parallelBeforeCap], 10) || 0; }, /** * @private * Returns the maximum value we can scrollTo * @return {Number} The max scroll value */ getMaxScrollPosition: function() { var layout = this.layout; return layout.innerCt.dom['scroll' + layout.parallelPrefixCap] - this.layout.innerCt['get' + layout.parallelPrefixCap](); }, /** * @private * Returns true if the innerCt scroll is already at its right-most point * @return {Boolean} True if already at furthest right point */ atExtremeAfter: function() { return this.getScrollPosition() >= this.getMaxScrollPosition(); }, /** * @private * Scrolls to the given position. Performs bounds checking. * @param {Number} position The position to scroll to. This is constrained. * @param {Boolean} animate True to animate. If undefined, falls back to value of this.animateScroll */ scrollTo: function(position, animate) { var me = this, layout = me.layout, oldPosition = me.getScrollPosition(), newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition()); if (newPosition != oldPosition && !me.scrolling) { if (animate == undefined) { animate = me.animateScroll; } layout.innerCt.scrollTo(layout.parallelBefore, newPosition, animate ? me.getScrollAnim() : false); if (animate) { me.scrolling = true; } else { me.scrolling = false; me.updateScrollButtons(); } me.fireEvent('scroll', me, newPosition, animate ? me.getScrollAnim() : false); } }, /** * Scrolls to the given component. * @param {String|Number|Ext.Component} item The item to scroll to. Can be a numerical index, component id * or a reference to the component itself. * @param {Boolean} animate True to animate the scrolling */ scrollToItem: function(item, animate) { var me = this, layout = me.layout, visibility, box, newPos; item = me.getItem(item); if (item != undefined) { visibility = this.getItemVisibility(item); if (!visibility.fullyVisible) { box = item.getBox(true, true); newPos = box[layout.parallelPosition]; if (visibility.hiddenEnd) { newPos -= (this.layout.innerCt['get' + layout.parallelPrefixCap]() - box[layout.parallelPrefix]); } this.scrollTo(newPos, animate); } } }, /** * @private * For a given item in the container, return an object with information on whether the item is visible * with the current innerCt scroll value. * @param {Ext.Component} item The item * @return {Object} Values for fullyVisible, hiddenStart and hiddenEnd */ getItemVisibility: function(item) { var me = this, box = me.getItem(item).getBox(true, true), layout = me.layout, itemStart = box[layout.parallelPosition], itemEnd = itemStart + box[layout.parallelPrefix], scrollStart = me.getScrollPosition(), scrollEnd = scrollStart + layout.innerCt['get' + layout.parallelPrefixCap](); return { hiddenStart : itemStart < scrollStart, hiddenEnd : itemEnd > scrollEnd, fullyVisible: itemStart > scrollStart && itemEnd < scrollEnd }; } });