/** * @private */Ext.define('Ext.layout.container.boxOverflow.Scroller', { /* Begin Definitions */ extend: 'Ext.layout.container.boxOverflow.None', requires: ['Ext.util.ClickRepeater', 'Ext.Element'], alternateClassName: 'Ext.layout.boxOverflow.Scroller', alias: [ 'box.overflow.scroller', 'box.overflow.Scroller' // capitalized for 4.x compat ], mixins: { observable: 'Ext.mixin.Observable' }, /* End Definitions */ /** * @cfg {Boolean} animateScroll * True to animate the scrolling of items within the layout (ignored if enableScroll is false) */ animateScroll: false, /** * @cfg {Number} scrollIncrement * The number of pixels to scroll by on scroller click */ scrollIncrement: 20, /** * @cfg {Number} wheelIncrement * The number of pixels to increment on mouse wheel scrolling. */ wheelIncrement: 10, /** * @cfg {Number} scrollRepeatInterval * Number of milliseconds between each scroll while a scroller button is held down */ scrollRepeatInterval: 60, /** * @cfg {Number} scrollDuration * Number of milliseconds that each scroll animation lasts */ scrollDuration: 400, /** * @private */ scrollerCls: Ext.baseCSSPrefix + 'box-scroller', beforeSuffix: '-before-scroller', afterSuffix: '-after-scroller', /** * @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 */ constructor: function(config) { var me = this; me.mixins.observable.constructor.call(me, config); me.scrollPosition = 0; me.scrollSize = 0; }, getPrefixConfig: function() { return { role: 'presentation', id: this.layout.owner.id + this.beforeSuffix, cls: this.createScrollerCls('beforeX'), style: 'display:none' }; }, getSuffixConfig: function() { return { role: 'presentation', id : this.layout.owner.id + this.afterSuffix, cls: this.createScrollerCls('afterX'), style: 'display:none' }; }, createScrollerCls: function(xName) { var me = this, layout = me.layout, owner = layout.owner, type = me.getOwnerType(owner), scrollerCls = me.scrollerCls, cls = scrollerCls + ' ' + scrollerCls + '-' + layout.names[xName] + ' ' + scrollerCls + '-' + type + ' ' + scrollerCls + '-' + type + '-' + owner.ui; if (owner.plain) { // Add plain class for components that need separate "plain" styling (e.g. tab bar) cls += ' ' + scrollerCls + '-plain'; } return cls; }, getOverflowCls: function(direction) { return this.scrollerCls + '-body-' + direction; }, beginLayout: function (ownerContext) { ownerContext.innerCtScrollPos = this.getScrollPosition(); this.callParent(arguments); }, finishedLayout: function(ownerContext) { var me = this, plan = ownerContext.state.boxPlan, layout = me.layout, names = layout.names, scrollPos = Math.min(me.getMaxScrollPosition(), ownerContext.innerCtScrollPos), lastProps; // If there is overflow... if (plan && plan.tooNarrow) { lastProps = ownerContext.childItems[ownerContext.childItems.length - 1].props; // capture this before callParent since it calls handle/clearOverflow: me.scrollSize = lastProps[names.x] + lastProps[names.width]; me.updateScrollButtons(); // Restore pre layout scroll position layout.innerCt[names.setScrollLeft](scrollPos); } me.callParent([ownerContext]); }, handleOverflow: function(ownerContext) { var me = this, names = me.layout.names, getWidth = names.getWidth, parallelMargins = names.parallelMargins, scrollerWidth, targetPaddingWidth, beforeScroller, afterScroller; me.showScrollers(); beforeScroller = me.getBeforeScroller(); afterScroller = me.getAfterScroller(); scrollerWidth = beforeScroller[getWidth]() + afterScroller[getWidth]() + beforeScroller.getMargin(parallelMargins) + afterScroller.getMargin(parallelMargins); targetPaddingWidth = ownerContext.targetContext.getPaddingInfo()[names.width]; return { reservedSpace: Math.max(scrollerWidth - targetPaddingWidth, 0) }; }, /** * @private * Returns a reference to the "before" scroller element. Creates click handlers on * the first call. */ getBeforeScroller: function() { var me = this; return me._beforeScroller || (me._beforeScroller = me.createScroller(me.beforeSuffix, 'beforeRepeater', 'scrollLeft')); }, /** * @private * Returns a reference to the "after" scroller element. Creates click handlers on * the first call. */ getAfterScroller: function() { var me = this; return me._afterScroller || (me._afterScroller = me.createScroller(me.afterSuffix, 'afterRepeater', 'scrollRight')); }, createScroller: function(suffix, repeaterName, scrollHandler) { var me = this, owner = me.layout.owner, scrollerCls = me.scrollerCls, scrollerEl; scrollerEl = owner.el.getById(owner.id + suffix); scrollerEl.addClsOnOver(scrollerCls + '-hover'); scrollerEl.addClsOnClick(scrollerCls + '-pressed'); scrollerEl.setVisibilityMode(Ext.Element.DISPLAY); me[repeaterName] = new Ext.util.ClickRepeater(scrollerEl, { interval: me.scrollRepeatInterval, handler: scrollHandler, scope: me }); return scrollerEl; }, /** * @private * Sets up an listener to scroll on the layout's innerCt mousewheel event */ createWheelListener: function() { var me = this; me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true}); }, onMouseWheel: function(e) { e.stopEvent(); this.scrollBy(this.getWheelDelta(e) * this.wheelIncrement * -1, false); }, getWheelDelta: function (e) { return e.getWheelDelta(); }, /** * @private */ clearOverflow: function () { this.hideScrollers(); }, /** * @private * Shows the scroller elements. Creates the scrollers first if they are not already present. */ showScrollers: function() { var me = this; if (!me.wheelListener) { me.createWheelListener(); } me.getBeforeScroller().show(); me.getAfterScroller().show(); me.layout.owner.addClsWithUI(me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller'); // TODO - this may invalidates data in the ContextItem's styleCache }, /** * @private * Hides the scroller elements. */ hideScrollers: function() { var me = this, beforeScroller = me.getBeforeScroller(), afterScroller = me.getAfterScroller(); if (beforeScroller) { beforeScroller.hide(); afterScroller.hide(); me.layout.owner.removeClsWithUI(me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller'); // TODO - this may invalidates data in the ContextItem's styleCache } }, destroy: function() { Ext.destroyMembers(this, 'beforeRepeater', 'afterRepeater', '_beforeScroller', '_afterScroller', 'wheelListener'); this.callParent(); }, /** * @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() { var me = this, beforeScroller = me.getBeforeScroller(), afterScroller = me.getAfterScroller(), disabledCls; if (!beforeScroller || !afterScroller) { return; } disabledCls = me.scrollerCls + '-disabled'; beforeScroller[me.atExtremeBefore() ? 'addCls' : 'removeCls'](disabledCls); afterScroller[me.atExtremeAfter() ? 'addCls' : 'removeCls'](disabledCls); me.scrolling = false; }, /** * @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 me = this, layout = me.layout, result; // Until we actually scroll, the scroll[Top|Left] is stored as zero to avoid DOM // hits, after that it's NaN. if (isNaN(me.scrollPosition)) { result = layout.innerCt[layout.names.getScrollLeft](); } else { result = me.scrollPosition; } return result; }, /** * @private * Returns the maximum value we can scrollTo * @return {Number} The max scroll value */ getMaxScrollPosition: function() { var me = this, layout = me.layout, maxScrollPos = me.scrollSize - layout.innerCt.lastBox[layout.names.width]; return (maxScrollPos < 0) ? 0 : maxScrollPos; }, /** * @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(); }, /** * @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 */ setVertical: function() { var me = this, beforeScroller = me.getBeforeScroller(), afterScroller = me.getAfterScroller(), names = me.layout.names, scrollerCls = me.scrollerCls; beforeScroller.removeCls(scrollerCls + '-' + names.beforeY); afterScroller.removeCls(scrollerCls + '-' + names.afterY); beforeScroller.addCls(scrollerCls + '-' + names.beforeX); afterScroller.addCls(scrollerCls + '-' + names.afterX); this.callParent(); }, /** * @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, names = layout.names, oldPosition = me.getScrollPosition(), newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition()); if (newPosition !== oldPosition && !me.scrolling) { me.scrollPosition = NaN; if (animate === undefined) { animate = me.animateScroll; } layout.innerCt[names.scrollTo](names.beforeScrollX, newPosition, animate ? me.getScrollAnim() : false); if (animate) { me.scrolling = true; } else { 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, owner = layout.owner, names = layout.names, innerCt = layout.innerCt, visibility, box, newPos; item = me.getItem(item); if (item !== undefined) { if (item === owner.items.first()) { newPos = 0; } else if (item === owner.items.last()) { newPos = me.getMaxScrollPosition(); } else { visibility = me.getItemVisibility(item); if (!visibility.fullyVisible) { box = item.getBox(false, true); newPos = box[names.x]; if (visibility.hiddenEnd) { newPos -= (innerCt[names.getWidth]() - box[names.width]); } } } if (newPos !== undefined) { me.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, names = layout.names, itemStart = box[names.x], itemEnd = itemStart + box[names.width], scrollStart = me.getScrollPosition(), scrollEnd = scrollStart + layout.innerCt[names.getWidth](); return { hiddenStart : itemStart < scrollStart, hiddenEnd : itemEnd > scrollEnd, fullyVisible: itemStart >= scrollStart && itemEnd <= scrollEnd }; }});