Ext.define('Ext.scroll.VirtualScroller', { extend: 'Ext.scroll.Scroller', alias: 'scroller.virtual', isVirtualScroller: true, requires: [ 'Ext.scroll.indicator.*' ], config: { /** * @cfg {Boolean} autoRefresh * `true` to refresh the scroller automatically when the element size or content * size changes * @private */ autoRefresh: false, /** * @cfg {Ext.scroll.indicator.Bar} barIndicator * Default configuration object to use for constructing * {@link Ext.scroll.indicator.Bar bar} {@link #indicators}. */ barIndicator: { lazy: true, $value: { type: 'bar' } }, /** * @cfg {Ext.fx.easing.Bounce} bounceEasing * @private */ bounceEasing: { duration: 400 }, /** * @cfg {Boolean} directionLock * `true` to lock the direction of the scroller when the user starts scrolling. * Only applicable when interacting with the Scroller via touch-screen. * @accessor */ directionLock: false, /** * @cfg {Boolean} disabled * `true` to disable this scroller. */ disabled: null, /** * @cfg {Object} clientSize * @private */ clientSize: undefined, /** * @cfg {Object} scrollbarSize * @private */ scrollbarSize: { width: 0, height: 0 }, /** * @cfg {Boolean/Object/'overlay'/'bar'} [indicators=true] * `false` to hide scroll indicators while scrolling, `true` to show scroll indicators, * or a config object for {@link Ext.scroll.indicator.Indicator} to configure the * scroll indicators. * * May also be an object with 'x' and 'y' properties for configuring the vertical * and horizontal indicators separately. For example, to show only the vertical * indicator, but not the horizontal indicator: * * { * x: false, * y: true * } * * The Virtual Scroller will automatically choose an appropriate Indicator type * for the current platform: * * - {@link Ext.scroll.indicator.Bar} on devices that display native scrollbars. * - {@link Ext.scroll.indicator.Overlay} on devices with no scrollbars (iOS and * android devices, and MacOS when configured to not display scrollbars via "settings") * * The default selection of indicator type can be overridden by specifying the type * of indicator as a string, for example: * * indicators: 'overlay' * * Overlay indicators can be used on any device, but "bar" indicators are only available * on devices that have scrollbars natively. */ indicators: { x: true, y: true }, /** * @cfg {Boolean} infinite * `true` to enable "infinite" scrolling. In infinite scrolling mode when * {@link #pageSize} is reached the translation position of the innerElement will * be reset to 0, 0. The user of this scroller must adjust the positioning * of the content by the {@link #offsetY} and {@link #offsetX} amounts. */ infinite: true, /** * @cfg {Ext.dom.Element} innerElement * @private * The element that wraps the content of {@link #element} and is translated in * response to user interaction. If not configured, one will be automatically * generated. */ innerElement: null, /** * @cfg {Number} maxAbsoluteVelocity * @private */ maxAbsoluteVelocity: 6, /** * @cfg {Object} maxPosition * The max scroll position * @private */ maxPosition: { x: 0, y: 0 }, /** * @cfg {Object} maxUserPosition * The max scroll position that can be achieved via user interaction. * @private */ maxUserPosition: { x: 0, y: 0 }, /** * @cfg {Object} minPosition * The min scroll position. * @private */ minPosition: { x: 0, y: 0 }, /** * @cfg {Object} minUserPosition * The min scroll position that can be achieved via user interaction. * @private */ minUserPosition: { x: 0, y: 0 }, /** * @cfg {Ext.fx.easing.BoundMomentum} momentumEasing * * Momentum easing for touch-screen scrolling * @private */ momentumEasing: { momentum: { acceleration: 30, friction: 0.5 }, bounce: { acceleration: 30, springTension: 0.3 }, minVelocity: 1 }, /** * @cfg outOfBoundRestrictFactor * @private */ outOfBoundRestrictFactor: 0.5, /** * @cfg {Ext.scroll.indicator.Overlay} overlayIndicator * Default configuration object to use for constructing * {@link Ext.scroll.indicator.Overlay overlay} {@link #indicators}. */ overlayIndicator: { lazy: true, $value: { type: 'overlay' } }, /** * @cfg {Object} pageSize * @private * The size of each "virtual page" when {@link #infinite} * A page size of 0 means no infinite scrolling in that dimension. */ pageSize: { // These numbers ought to be less than 1 million since that's the point at // which CSS transforms lose precision. x: 500000, y: 500000 }, /** * @cfg {Object} slotSnapEasing * @private */ slotSnapEasing: { duration: 150 }, /** * @cfg {Object} slotSnapOffset * @private */ slotSnapOffset: { x: 0, y: 0 }, /** * @cfg {Number/Object} slotSnapSize * The size of each slot to snap to in 'px', can be either an object with `x` and `y` * values, i.e: * * { * x: 50, * y: 100 * } * * or a number value to be used for both directions. For example, a value of `50` * will be treated as: * * { * x: 50, * y: 50 * } */ slotSnapSize: { x: 0, y: 0 }, /** * @cfg {Number} startMomentumResetTime * @private */ startMomentumResetTime: 300 }, eventedConfig: { /** * @cfg {Number} offsetX * @private * The x offset for {@link #infinite} scrolling */ offsetX: 0, /** * @cfg {Number} offsetY * @private * The y offset for {@link #infinite} scrolling */ offsetY: 0 }, translatable: 'csstransform', dragStartTime: 0, dragEndTime: 0, isDragging: false, isWheeling: false, isAnimating: false, /** * This propery is used when touch Scrolling is in process to notify the last scroll at * scroll end so that the scroll start and scroll end is in order.For each scroll start * we increment the value to count number of scrolls and at each subsequent scroll end * the value is decremented */ touchScrollCount: 0, isMouseEvent: { mousedown: 1, mousemove: 1, mouseup: 1 }, listenerMap: { touchstart: 'onTouchStart', touchmove: 'onTouchMove', dragstart: 'onDragStart', drag: 'onDrag', dragend: 'onDragEnd' }, constructor: function(config) { var me = this, onEvent = 'onEvent', autoRefresh; me.elementListeners = { touchstart: onEvent, touchmove: onEvent, dragstart: onEvent, drag: onEvent, dragend: onEvent, wheel: 'onWheel', scope: me }; me.minPosition = { x: 0, y: 0 }; me.startPosition = { x: 0, y: 0 }; me.velocity = { x: 0, y: 0 }; me.isAxisEnabledFlags = { x: false, y: false }; me.flickStartPosition = { x: 0, y: 0 }; me.flickStartTime = { x: 0, y: 0 }; me.lastDragPosition = { x: 0, y: 0 }; me.dragDirection = { x: 0, y: 0 }; // This object contains the potential height and width of scrollbars for size/clientSize // calculations. To get the actual scrollbarSize after calculations use getScrollbarSize me.scrollbarSize = { width: 0, height: 0 }; me.callParent([config]); autoRefresh = me.getAutoRefresh(); me.refreshScrollbarSize(); if (autoRefresh) { me.refresh(true); } else { me.refreshAxes(); me.callIndicators('onRefresh'); } me.scheduleRefresh = { idle: me.doRefresh, scope: me, single: true, destroyable: true }; }, doDestroy: function() { var me = this, element = me.getElement(), innerElement = me.getInnerElement(), sizeMonitors = me.sizeMonitors; if (sizeMonitors) { sizeMonitors.element.destroy(); sizeMonitors.container.destroy(); } if (element && !element.destroyed) { element.removeCls(me.cls); } if (innerElement && !innerElement.destroyed) { innerElement.removeCls(me.innerCls); } if (me._isWrapped) { if (!element.destroyed) { me.unwrapContent(); } innerElement.destroy(); } me.setElement(null); me.setInnerElement(null); me.setIndicators(null); Ext.destroyMembers(me, 'scrollbarCorner'); me.callParent(); }, refresh: function(immediate) { var me = this; if (immediate) { me.doRefresh(); } // Schedule a refresh at the next transition to idle. else if (!me.refreshScheduled) { me.refreshScheduled = Ext.on(me.scheduleRefresh); } return me; }, //-------------------------------------------------------- // Public Config Properties // disabled updateDisabled: function(disabled) { // attachment of listeners is handled by updateElement during initial config if (!this.isConfiguring) { if (disabled) { this.detachListeners(); } else { this.attachListeners(); } } }, // element updateElement: function(element, oldElement) { var me = this, virtualScrollerCls = me.virtualScrollerCls, innerElement = me.getInnerElement(), autoRefresh; me.callParent([element, oldElement]); if (oldElement && !oldElement.destroyed) { oldElement.removeCls(virtualScrollerCls); } if (element) { element.addCls(virtualScrollerCls); if (!innerElement) { innerElement = me.wrapContent(element); me.setInnerElement(innerElement); } if (!me.getDisabled()) { me.attachListeners(); } if (!me.isConfiguring) { // setting element after initial construction of Scroller // sync up configs that depend on element autoRefresh = me.getAutoRefresh(); if (autoRefresh) { me.toggleResizeListeners(autoRefresh); me.refresh(); } } } }, // indicators applyIndicators: function(indicators, oldIndicators) { var me = this, type, defaultsConfig, xIndicator, yIndicator, oldXIndicator, oldYIndicator; if (indicators) { if (indicators === true || typeof indicators === 'string') { xIndicator = yIndicator = indicators; } else { type = indicators.type; xIndicator = indicators.x; yIndicator = indicators.y; } } if (oldIndicators) { indicators = oldIndicators; // reuse the old object } else { indicators = { x: null, y: null }; } if (xIndicator || yIndicator) { defaultsConfig = Ext.scrollbar.width() && type !== 'overlay' ? 'barIndicator' : 'overlayIndicator'; } oldXIndicator = indicators.x; oldYIndicator = indicators.y; if (xIndicator) { indicators.x = Ext.Factory.scrollindicator.update(oldXIndicator, xIndicator, me, 'createXIndicator', defaultsConfig); } else if (oldXIndicator) { oldXIndicator.destroy(); indicators.x = null; } if (yIndicator) { indicators.y = Ext.Factory.scrollindicator.update(oldYIndicator, yIndicator, me, 'createYIndicator', defaultsConfig); } else if (oldYIndicator) { oldYIndicator.destroy(); indicators.y = null; } return indicators; }, updateIndicators: function(indicators) { if (!this.isConfiguring) { this.refreshScrollbarSize(); } }, createXIndicator: function(defaults) { return Ext.apply({ axis: 'x', scroller: this }, defaults); }, createYIndicator: function(defaults) { return Ext.apply({ axis: 'y', scroller: this }, defaults); }, // slotSnapSize applySlotSnapSize: function(snapSize) { if (typeof snapSize === 'number') { snapSize = { x: snapSize, y: snapSize }; } return snapSize; }, // x updateX: function() { if (!this.isConfiguring) { this.refreshAxes(); this.refreshScrollbarSize(); } }, // y updateY: function() { if (!this.isConfiguring) { this.refreshAxes(); this.refreshScrollbarSize(); } }, privates: { virtualScrollerCls: Ext.baseCSSPrefix + 'virtualscroller', innerCls: Ext.baseCSSPrefix + 'scroller-inner', scrollbarCornerCls: Ext.baseCSSPrefix + 'scrollbar-corner', attachListeners: function() { this.getElement().on(this.elementListeners); }, callIndicators: function(methodName, force) { var me = this, indicators = me.getIndicators(), xIndicator, yIndicator; if (indicators) { if (force || me.isAxisEnabled('x')) { xIndicator = indicators.x; if (xIndicator && xIndicator[methodName]) { xIndicator[methodName](); } } if (force || me.isAxisEnabled('y')) { yIndicator = indicators.y; if (yIndicator && yIndicator[methodName]) { yIndicator[methodName](); } } } }, constrainX: function(x) { return Math.min(this.getMaxPosition().x, Math.max(x, 0)); }, constrainY: function(y) { return Math.min(this.getMaxPosition().y, Math.max(y, 0)); }, // overridden in RTL mode to swap min/max momentum values convertEasingConfig: function(config) { return config; }, detachListeners: function() { this.getElement().un(this.elementListeners); }, /** * @private */ doRefresh: function(size) { var me = this, configuredSize = me.configuredSize; if (me.refreshScheduled) { me.refreshScheduled = me.refreshScheduled.destroy(); } if (me.getElement()) { me.stopAnimation(); me.getTranslatable().refresh(); me.setSize({ x: configuredSize.x ? undefined : null, y: configuredSize.y ? undefined : null }); me.fireEvent('refresh', me); } }, doScrollTo: function(x, y, animate) { var me = this, isDragging = me.isDragging, isPrimary = me.isPrimary, ret, deferred, position, positionChanged, translatable, translationX, translationY; if (me.destroyed || !me.getElement()) { return me; } translatable = me.getTranslatable(); position = me.position; if (!isDragging || me.isAxisEnabled('x')) { if (isNaN(x) || typeof x !== 'number') { x = position.x; } else { if (isPrimary !== false && !isDragging) { x = me.constrainX(x); } if (position.x !== x) { positionChanged = true; } } } if (!isDragging || me.isAxisEnabled('y')) { if (isNaN(y) || typeof y !== 'number') { y = position.y; } else { if (isPrimary !== false && !isDragging) { y = me.constrainY(y); } if (position.y !== y) { positionChanged = true; } } } if (positionChanged) { if (!me.isScrolling) { me.onScrollStart(); } translationX = me.convertX(-x); translationY = -y; if (animate) { deferred = new Ext.Deferred(); // Use onFrame here to let the scroll complete and animations to fire. translatable.on('animationend', function() { // Check destroyed vs destroying since we're onFrame here if (me.destroyed) { deferred.reject(); } else { deferred.resolve(); } }, Ext.global, { single: true, onFrame: true }); // onAnimationEnd calls onScrollEnd translatable.translateAnimated(translationX, translationY, animate); ret = deferred.promise; } else { translatable.translate(translationX, translationY); me.onScroll(x, y); if (me.isWheeling || me.isScrollbarScrolling) { me.onScrollEnd(); } else if (!isDragging) { me.doOnScrollEnd(); } } } else if (animate && animate.callback) { animate.callback(); } return ret || Ext.Deferred.getCachedResolved(); }, /** * @private */ getAnimationEasing: function(axis, e) { if (!this.isAxisEnabled(axis)) { return null; } // eslint-disable-next-line vars-on-top var me = this, currentPosition = me.position[axis], minPosition = me.getMinUserPosition()[axis], maxPosition = me.getMaxUserPosition()[axis], maxAbsVelocity = me.getMaxAbsoluteVelocity(), boundValue = null, dragEndTime = me.dragEndTime, velocity = e.flick.velocity[axis], isX = axis === 'x', easingConfig, easing; if (currentPosition < minPosition) { boundValue = minPosition; } else if (currentPosition > maxPosition) { boundValue = maxPosition; } if (isX) { currentPosition = me.convertX(currentPosition); boundValue = me.convertX(boundValue); } // Out of bound, to be pulled back if (boundValue !== null) { easing = me.getBounceEasing()[axis]; easing.setConfig({ startTime: dragEndTime, startValue: -currentPosition, endValue: -boundValue }); return easing; } if (velocity === 0) { return null; } if (velocity < -maxAbsVelocity) { velocity = -maxAbsVelocity; } else if (velocity > maxAbsVelocity) { velocity = maxAbsVelocity; } easing = me.getMomentumEasing()[axis]; easingConfig = { startTime: dragEndTime, startValue: -currentPosition, startVelocity: velocity * 1.5, minMomentumValue: -maxPosition, maxMomentumValue: 0 }; if (isX) { me.convertEasingConfig(easingConfig); } easing.setConfig(easingConfig); return easing; }, /** * @private * @return {Number/null} */ getSnapPosition: function(axis) { var me = this, snapSize = me.getSlotSnapSize()[axis], snapPosition = null, position, snapOffset, maxPosition, mod; if (snapSize !== 0 && me.isAxisEnabled(axis)) { position = me.position[axis]; snapOffset = me.getSlotSnapOffset()[axis]; maxPosition = me.getMaxUserPosition()[axis]; mod = Math.floor((position - snapOffset) % snapSize); if (mod !== 0) { if (position !== maxPosition) { if (Math.abs(mod) > snapSize / 2) { snapPosition = Math.min(maxPosition, position + ( (mod > 0) ? snapSize - mod : mod - snapSize) ); } else { snapPosition = position - mod; } } else { snapPosition = position - mod; } } } return snapPosition; }, /** * Returns `true` if a specified axis is enabled. * @private * @param {String} axis The axis to check (`x` or `y`). * @return {Boolean} `true` if the axis is enabled. */ isAxisEnabled: function(axis) { this.getX(); this.getY(); return this.isAxisEnabledFlags[axis]; }, onAnimationEnd: function() { this.isAnimating = false; this.snapToBoundary(); this.onScrollEnd(); }, onAnimationFrame: function(translatable, x, y) { this.onScroll(this.convertX(-x), -y); }, onAnimationStart: function() { this.isAnimating = true; }, onAxisDrag: function(axis, delta) { // Nothing to do if no delta, or it's on a disabled axis if (delta && this.isAxisEnabled(axis)) { // eslint-disable-next-line vars-on-top var me = this, flickStartPosition = me.flickStartPosition, flickStartTime = me.flickStartTime, lastDragPosition = me.lastDragPosition, dragDirection = me.dragDirection, old = me.position[axis], min = me.getMinUserPosition()[axis], max = me.getMaxUserPosition()[axis], start = me.startPosition[axis], last = lastDragPosition[axis], current = start - delta, lastDirection = dragDirection[axis], restrictFactor = me.getOutOfBoundRestrictFactor(), startMomentumResetTime = me.getStartMomentumResetTime(), now = Ext.Date.now(), distance; if (current < min) { current *= restrictFactor; } else if (current > max) { distance = current - max; current = max + distance * restrictFactor; } if (current > last) { dragDirection[axis] = 1; } else if (current < last) { dragDirection[axis] = -1; } if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection)) || (now - flickStartTime[axis]) > startMomentumResetTime) { flickStartPosition[axis] = old; flickStartTime[axis] = now; } lastDragPosition[axis] = current; return true; } }, onDrag: function(e) { var me = this, lastDragPosition = me.lastDragPosition; if (!me.isDragging) { return; } // If there's any moving to do, then move the content. // Boolean or operator avoids shortcutting the second function call if // first returns true. if (me.onAxisDrag('x', me.convertX(e.deltaX)) | me.onAxisDrag('y', e.deltaY)) { me.doScrollTo(lastDragPosition.x, lastDragPosition.y); } }, onDragEnd: function(e) { var me = this, easingX, easingY; if (me.isDragging) { me.dragEndTime = Ext.Date.now(); me.onDrag(e); me.isDragging = false; easingX = me.getAnimationEasing('x', e); easingY = me.getAnimationEasing('y', e); if (easingX || easingY) { me.getTranslatable().animate(easingX, easingY); } else { me.onScrollEnd(); } } }, onDragStart: function(e) { var me = this, absDeltaX = e.absDeltaX, absDeltaY = e.absDeltaY, startPosition = me.startPosition, flickStartPosition = me.flickStartPosition, flickStartTime = me.flickStartTime, lastDragPosition = me.lastDragPosition, currentPosition = me.position, dragDirection = me.dragDirection, x = currentPosition.x, y = currentPosition.y, now = Ext.Date.now(), horizontal = me.getX(), vertical = me.getY(); Ext.undefer(me.scrollEndTimer); if (me.getDirectionLock() && !(horizontal && vertical)) { if ((horizontal && absDeltaX > absDeltaY) || (vertical && absDeltaY > absDeltaX)) { e.stopPropagation(); } else { return; } } lastDragPosition.x = x; lastDragPosition.y = y; flickStartPosition.x = x; flickStartPosition.y = y; startPosition.x = x; startPosition.y = y; flickStartTime.x = now; flickStartTime.y = now; dragDirection.x = 0; dragDirection.y = 0; me.dragStartTime = now; me.isDragging = true; // Only signal a scroll start if we are not already scrolling. // If the drag is just the user giving another impulse, it is NOT // the start of a drag. if (!me.isScrolling) { me.onScrollStart(); } }, onElementResize: function() { this.refresh(true); }, onEvent: function(e) { // use browserEvent to get the "real" type of DOM event that was fired, not a // potentially translated (or recognized) type var me = this, self = me.self; if ((!self.isTouching || me.isTouching) && // prevents nested scrolling !self.isWheeling && // ignore touch input while scrolling with mouse e.pointerType !== 'mouse' && (me.getY() || me.getX())) { return me[me.listenerMap[e.type]](e); } }, onInnerElementResize: function() { this.refresh(true); }, onPartnerScroll: function(x, y) { this.callParent([x, y]); this.callIndicators('onScroll'); }, onPartnerScrollStart: function(x, y, deltaX, deltaY) { this.callParent([x, y, deltaX, deltaY]); this.callIndicators('onScrollStart'); }, onPartnerScrollEnd: function() { this.callParent(); this.callIndicators('onScrollEnd', true); }, onScroll: function(logicalX, logicalY) { var me = this, position = me.position, deltaX = logicalX - position.x, deltaY = logicalY - position.y, pageSize, pageSizeX, pageSizeY, offsetX, offsetY, physicalX, physicalY; if (deltaX || deltaY) { position.x = logicalX; position.y = logicalY; if (me.getInfinite()) { pageSize = me.getPageSize(); pageSizeX = pageSize.x; if (pageSizeX) { offsetX = me.getOffsetX(); physicalX = logicalX - offsetX; if (physicalX > pageSizeX) { me.setOffsetX(Math.floor(logicalX / pageSizeX) * pageSizeX); } else if (physicalX < -pageSizeX) { me.setOffsetX(Math.ceil(logicalX / pageSizeX) * pageSizeX); } } pageSizeY = pageSize.y; if (pageSizeY) { offsetY = me.getOffsetY(); physicalY = logicalY - offsetY; if (physicalY > pageSizeY) { me.setOffsetY(Math.floor(logicalY / pageSizeY) * pageSizeY); } else if (physicalY < -pageSizeY) { me.setOffsetY(Math.ceil(logicalY / pageSizeY) * pageSizeY); } } } if (me.isPrimary) { me.callIndicators('onScroll'); me.callPartners('onPartnerScroll', logicalX, logicalY); me.fireScroll(logicalX, logicalY, deltaX, deltaY); me.callPartners('fireScroll', logicalX, logicalY); } } }, doOnScrollEnd: function() { var me = this, position = me.position, x = position.x, y = position.y; if (!me.destroying && !me.destroyed && me.isScrolling && me.isPrimary && !me.isTouching && !me.snapToSlot() && me.touchScrollCount <= 1) { me.isScrolling = Ext.isScrolling = me.isWheeling = me.self.isWheeling = me.isScrollbarScrolling = false; me.callIndicators('onScrollEnd'); me.callPartners('onPartnerScrollEnd'); me.fireScrollEnd(x, y); me.callPartners('fireScrollEnd', x, y); if (!me.isScrolling) { // if scrollend event handler did not initiate another scroll me.setPrimary(null); } } if (me.touchScrollCount > 0) { me.touchScrollCount--; } }, onScrollStart: function() { var me = this, position = me.position, x = position.x, y = position.y; if (!me.isScrolling) { me.setPrimary(true); me.callIndicators('onScrollStart'); me.callPartners('onPartnerScrollStart', x, y); me.fireScrollStart(x, y, 0, 0); me.callPartners('fireScrollStart', x, y); } }, onTouchEnd: function() { var me = this; me.isTouching = me.self.isTouching = false; if (!me.isAnimating) { me.onScrollEnd(); } }, onTouchMove: function(e) { // Prevents the page from scrolling during scroll of a VirtualScroller. e.preventDefault(); }, onTouchStart: function(e) { var me = this; Ext.getDoc().on({ touchend: 'onTouchEnd', scope: me, single: true }); me.isTouching = me.self.isTouching = true; me.stopAnimation(); me.touchScrollCount++; }, onWheel: function(e) { var me = this, self = me.self, deltaX = e.deltaX, deltaY = e.deltaY, position = me.position, oldX = position.x, oldY = position.y, x = me.constrainX(position.x + deltaX), y = me.constrainY(position.y + deltaY); if (x !== oldX || y !== oldY) { me.isWheeling = self.isWheeling = true; me.doScrollTo(x, y); e.preventDefault(); } else if (me.isScrolling) { me.onScrollEnd(); } }, refreshAxes: function() { var me = this, flags = me.isAxisEnabledFlags, size = me.getSize(), clientSize = me.getClientSize(), maxX, maxY, x, y; if (!size || !clientSize) { return; } maxX = Math.max(0, size.x - clientSize.x); maxY = Math.max(0, size.y - clientSize.y); x = me.getX(); y = me.getY(); me.setMaxPosition({ x: maxX, y: maxY }); if (x === true || x === 'auto') { // auto scroll - axis is only enabled if the content is overflowing in the // same direction flags.x = !!maxX; } else if (x === false) { flags.x = false; } else if (x === 'scroll') { flags.x = true; } if (y === true || y === 'auto') { // auto scroll - axis is only enabled if the content is overflowing in the // same direction flags.y = !!maxY; } else if (y === false) { flags.y = false; } else if (y === 'scroll') { flags.y = true; } me.setMaxUserPosition({ x: flags.x ? maxX : 0, y: flags.y ? maxY : 0 }); if (!me.isConfiguring) { me.callIndicators('onRefresh', true); } }, refreshScrollbarSize: function() { var me = this, indicators = me.getIndicators(), barSize = me.scrollbarSize, scrollbarSize = Ext.scrollbar.size(), xIndicator, yIndicator; if (indicators) { xIndicator = indicators.x; yIndicator = indicators.y; if (xIndicator && xIndicator.isScrollbar && me.getX()) { barSize.height = scrollbarSize.height; } if (yIndicator && yIndicator.isScrollbar && me.getY()) { barSize.width = scrollbarSize.width; } } }, refreshSize: function(size, oldSize) { var me = this, el = me.getElement(), extraPadding = Ext.isIE ? 1 : 0, scrollbarSize, scrollbarWidth, scrollbarHeight, globalScrollbarSize, hasVerticalScrollbar, hasHorizontalScrollbar, clientSizeX, clientSizeY, sizeX, sizeY, dom, innerDom, flags, measuredX, measuredY, indicators, xIndicator, yIndicator, recalcX, recalcY, scrollbarCorner; if (el && !el.destroyed) { dom = el.dom; innerDom = me.getInnerElement().dom; sizeX = size.x; sizeY = size.y; scrollbarSize = me.scrollbarSize; scrollbarWidth = scrollbarSize.width; scrollbarHeight = scrollbarSize.height; globalScrollbarSize = Ext.scrollbar.size(); hasVerticalScrollbar = false; hasHorizontalScrollbar = false; indicators = me.getIndicators(); xIndicator = indicators.x; yIndicator = indicators.y; scrollbarCorner = me.scrollbarCorner; if (sizeX === undefined) { sizeX = oldSize && oldSize.x; } if (sizeY === undefined) { sizeY = oldSize && oldSize.y; } // using scrollWidth/scrollHeight instead of offsetWidth/offsetHeight ensures // that the size includes any contained absolutely positioned items if (sizeX == null) { // could be undefined if oldSize has not yet been set sizeX = innerDom.scrollWidth; measuredX = true; } if (sizeY == null) { // could be undefined if oldSize has not yet been set sizeY = innerDom.scrollHeight; measuredY = true; } clientSizeX = dom.clientWidth; clientSizeY = dom.clientHeight; flags = me.getScrollbarFlags(sizeX, sizeY, clientSizeX, clientSizeY); hasVerticalScrollbar = !!(flags & 1); hasHorizontalScrollbar = !!(flags & 2); if (flags && (flags !== me.scrollbarFlags)) { if (xIndicator) { xIndicator.setEnabled(me.getX() === 'scroll'); // make indicator invisible to avoid potential flicker if we have to // perform multiple measurements xIndicator.setStyle('visibility', 'hidden'); } if (yIndicator) { yIndicator.setEnabled(me.getY() === 'scroll'); // make indicator invisible to avoid potential flicker if we have to // perform multiple measurements yIndicator.setStyle('visibility', 'hidden'); } if (measuredX && hasVerticalScrollbar) { yIndicator.setEnabled(true); recalcX = true; } if (measuredY && hasHorizontalScrollbar) { xIndicator.setEnabled(true); recalcY = true; } if (recalcX) { sizeX = innerDom.scrollWidth; } if (recalcY) { sizeY = innerDom.scrollHeight; } if (recalcX || recalcY) { flags = me.getScrollbarFlags(sizeX, sizeY, clientSizeX, clientSizeY); hasVerticalScrollbar = !!(flags & 1); hasHorizontalScrollbar = !!(flags & 2); } if (hasHorizontalScrollbar && hasVerticalScrollbar) { if (!scrollbarCorner) { me.scrollbarCorner = scrollbarCorner = me.getElement().createChild({ cls: me.scrollbarCornerCls }); scrollbarCorner.setVisibilityMode(2); // windows displays scroll buttons by default and in some browsers // this can cause scrollbarCorner to grow in size, so we manually // set height/width of this container. if (Ext.isWindows) { scrollbarCorner.setStyle({ width: (scrollbarWidth + extraPadding) + 'px', height: (scrollbarHeight + extraPadding) + 'px' }); } } scrollbarCorner.show(); } else if (scrollbarCorner) { scrollbarCorner.hide(); } if (xIndicator) { xIndicator.setStyle('visibility', ''); } if (yIndicator) { yIndicator.setStyle('visibility', ''); } me.scrollbarFlags = flags; me.setScrollbarSize({ width: hasVerticalScrollbar ? scrollbarWidth : 0, height: hasHorizontalScrollbar ? scrollbarHeight : 0, reservedWidth: hasVerticalScrollbar ? globalScrollbarSize.reservedWidth : '', reservedHeight: hasHorizontalScrollbar ? globalScrollbarSize.reservedHeight : '' }); } clientSizeX = hasVerticalScrollbar ? (clientSizeX - scrollbarWidth) : clientSizeX; clientSizeY = hasHorizontalScrollbar ? (clientSizeY - scrollbarHeight) : clientSizeY; me.setClientSize({ x: clientSizeX, y: clientSizeY }); return { x: Math.max(sizeX, clientSizeX), y: Math.max(sizeY, clientSizeY) }; } return null; }, /** * Returns the following bit flags * * vertical scrollbar 1 '00000001' * horizontal scrollbar 2 '00000010' * both scrollbars 3 '00000011' * * @private * @param {Number} sizeX * @param {Number} sizeY * @param {Number} clientSizeX * @param {Number} clientSizeY */ getScrollbarFlags: function(sizeX, sizeY, clientSizeX, clientSizeY) { var me = this, flags = 0, scrollbarSize = me.scrollbarSize, scrollbarWidth = scrollbarSize.width, scrollbarHeight = scrollbarSize.height; if (scrollbarWidth && (me.getY() === 'scroll' || sizeY > clientSizeY)) { flags |= 1; } if (scrollbarHeight && (me.getX() === 'scroll' || sizeX > clientSizeX)) { flags |= 2; } if ((flags & 1) && (scrollbarHeight && (sizeX > clientSizeX - scrollbarWidth))) { flags |= 2; } if ((flags & 2) && (scrollbarWidth && (sizeY > clientSizeY - scrollbarHeight))) { flags |= 1; } return flags; }, snapToBoundary: function() { var me = this, position = me.getPosition(); // If we haven't scrolled anywhere, we're done. if (me.isConfiguring || !(position.x || position.y)) { return; } // eslint-disable-next-line vars-on-top var minPosition = me.getMinUserPosition(), maxPosition = me.getMaxUserPosition(), minX = minPosition.x, minY = minPosition.y, maxX = maxPosition.x, maxY = maxPosition.y, x = Math.round(position.x), y = Math.round(position.y); if (x < minX) { x = minX; } else if (x > maxX) { x = maxX; } if (y < minY) { y = minY; } else if (y > maxY) { y = maxY; } me.doScrollTo(x, y); }, /** * @private * @return {Boolean} */ snapToSlot: function() { var me = this, snapX = me.getSnapPosition('x'), snapY = me.getSnapPosition('y'), easing = me.getSlotSnapEasing(); if (snapX !== null || snapY !== null) { me.doScrollTo(snapX, snapY, { easingX: easing.x, easingY: easing.y }); return true; } return false; }, /** * @private * Stops the animation of the scroller at any time. */ stopAnimation: function() { this.getTranslatable().stopAnimation(); }, syncOffsets: function(translatable) { translatable.setOffsetX(-this.getOffsetX()); translatable.setOffsetY(-this.getOffsetY()); }, toggleResizeListeners: function(autoRefresh) { var me = this, element = me.getElement(), method, innerElement; if (element) { innerElement = me.getInnerElement(); method = autoRefresh ? 'on' : 'un'; element[method]('resize', 'onElementResize', me); innerElement[method]('resize', 'onInnerElementResize', me); } }, /** * Removes the wrapper created by {@link #wrapContent}. * @private */ unwrapContent: function() { var innerDom = this.getInnerElement().dom, dom = this.getElement().dom, child; while ((child = innerDom.firstChild)) { dom.insertBefore(child, innerDom); } }, /** * Wraps the element's content in a innerElement * @param {Ext.dom.Element} element * @return {Ext.dom.Element} the innerElement * @private */ wrapContent: function(element) { var wrap = document.createElement('div'), dom = element.dom, child; while ((child = dom.lastChild)) { // jshint ignore:line wrap.insertBefore(child, wrap.firstChild); } dom.appendChild(wrap); this.setInnerElement(wrap); // Set a flag that indiacates the element's content was not already pre-wrapped // when the scroller was instanced. This means we had to wrap the content // and so must unwrap when we destroy the scroller. this._isWrapped = true; return this.getInnerElement(); }, //-------------------------------------------------------- // Private Config Properties // autoRefresh updateAutoRefresh: function(autoRefresh) { this.toggleResizeListeners(autoRefresh); }, // bounceEasing applyBounceEasing: function(easing) { var defaultClass = Ext.fx.easing.EaseOut; return { x: Ext.factory(easing, defaultClass), y: Ext.factory(easing, defaultClass) }; }, updateBounceEasing: function(easing) { this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y); }, // clientSize updateClientSize: function() { if (!this.isConfiguring) { // to avoid multiple calls to refreshAxes() during initialization we will // call it once after initConfig has finished. this.refreshAxes(); } }, // innerElement applyInnerElement: function(innerElement) { if (innerElement) { if (!innerElement.isElement) { innerElement = Ext.get(innerElement); } } //<debug> if (this.isConfiguring && !innerElement) { Ext.raise("Cannot create Ext.scroll.VirtualScroller with null innerElement"); } //</debug> return innerElement; }, updateInnerElement: function(innerElement) { if (innerElement) { innerElement.addCls(this.innerCls); } this.getTranslatable().setElement(innerElement); }, // maxPosition applyMaxPosition: function(maxPosition, oldMaxPosition) { // If a no-op (generated setter tests object identity), return undefined to abort set. if (oldMaxPosition && maxPosition.x === oldMaxPosition.x && maxPosition.y === oldMaxPosition.y) { return; } // eslint-disable-next-line vars-on-top var translatable = this.getTranslatable(), yEasing; // If an animation is in flight... if (translatable.isAnimating) { // Find its Y dimension easing object yEasing = translatable.activeEasingY; // If it's animating in the -ve direction (scrolling up), and we are // shortening the scroll range, ensure the easing's min point complies // with the new end position. if (yEasing && yEasing.getStartVelocity && yEasing.getStartVelocity() < 0 && maxPosition.y < oldMaxPosition.y) { yEasing.setMinMomentumValue(-maxPosition.y); } } return maxPosition; }, // maxUserPosition applyMaxUserPosition: function(maxUserPosition, oldMaxUserPosition) { // If a no-op (generated setter tests object identity), return undefined to abort set. if (!oldMaxUserPosition || maxUserPosition.x !== oldMaxUserPosition.x || maxUserPosition.y !== oldMaxUserPosition.y) { return maxUserPosition; } }, updateMaxUserPosition: function() { this.snapToBoundary(); }, // minUserPosition updateMinUserPosition: function() { this.snapToBoundary(); }, // momentumEasing applyMomentumEasing: function(easing) { var defaultClass = Ext.fx.easing.BoundMomentum; return { x: Ext.factory(easing, defaultClass), y: Ext.factory(easing, defaultClass) }; }, // offsetX updateOffsetX: function(offsetX) { if (!this.isConfiguring) { this.getTranslatable().setOffsetX(offsetX); } }, // offsetY updateOffsetY: function(offsetY) { if (!this.isConfiguring) { this.getTranslatable().setOffsetY(offsetY); } }, // size applySize: function(size, oldSize) { size = this.callParent([size, oldSize]); return this.refreshSize(size, oldSize); }, updateSize: function(size, oldSize) { if (!this.isConfiguring) { // to avoid multiple calls to refreshAxes() during initialization we will // call it once after initConfig has finished. this.refreshAxes(); } }, // slotSnapEasing applySlotSnapEasing: function(easing) { var defaultClass = Ext.fx.easing.EaseOut; return { x: Ext.factory(easing, defaultClass), y: Ext.factory(easing, defaultClass) }; }, // slotSnapOffset applySlotSnapOffset: function(snapOffset) { if (typeof snapOffset === 'number') { snapOffset = { x: snapOffset, y: snapOffset }; } return snapOffset; }, // translatable updateTranslatable: function(translatable) { if (translatable) { translatable.on({ animationframe: 'onAnimationFrame', animationstart: 'onAnimationStart', animationend: 'onAnimationEnd', scope: this }); } }, createTranslatable: function(defaults) { return Ext.apply({ element: this.getInnerElement() }, defaults); } }});