/** * Ext.scroll.Scroller allows any element to have scrollable content, both on desktop and * touch-screen devices, and defines a set of useful methods for manipulating the scroll * position and controlling the scrolling behavior. */Ext.define('Ext.scroll.Scroller', { extend: 'Ext.Evented', alias: 'scroller.scroller', mixins: [ 'Ext.mixin.Factoryable', 'Ext.mixin.Bufferable' ], requires: [ 'Ext.util.CSS', 'Ext.util.translatable.ScrollPosition', 'Ext.Deferred' ], factoryConfig: { defaultType: 'scroller' }, bufferableMethods: { onDomScrollEnd: 100 }, isScroller: true, /** * @event refresh * Fires whenever the Scroller is refreshed. * @param {Ext.scroll.Scroller} this */ /** * @event scrollstart * Fires whenever the scrolling is started. * @param {Ext.scroll.Scroller} this * @param {Number} x The current x position. * @param {Number} y The current y position. */ /** * @event scrollend * Fires whenever the scrolling is ended. * @param {Ext.scroll.Scroller} this * @param {Number} x The current x position. * @param {Number} y The current y position. * @param {Number} deltaX The change in x value. * @param {Number} deltaY The change in y value. */ /** * @event scroll * Fires whenever the Scroller is scrolled. * @param {Ext.scroll.Scroller} this * @param {Number} x The new x position. * @param {Number} y The new y position. * @param {Number} deltaX The change in x value. * @param {Number} deltaY The change in y value. */ config: { /** * @cfg {'auto'/'vertical'/'horizontal'/'both'} [direction='auto'] * @deprecated 5.1.0 use {@link #x} and {@link #y} instead */ direction: undefined, // undefined because we need the updater to always run /** * @cfg {String/HTMLElement/Ext.dom.Element} * The element to make scrollable. */ element: undefined, /** * @cfg {Boolean} [scrollbars=true] * `false` to hide scrollbars on browsers where it is possible via CSS, * Currently Webkit, Chrome, and IE10+ * @private */ scrollbars: null, /** * @cfg {String} A CSS selector that identifies items inside this scroller that * should be snapped into position when user scrolling ends. By default the items * top/left will be aligned with the top/left of the container. This alignment * can be changed using {@link #snapOffset} * * This api is highly experimental as it is based on bleeding-edge CSS implementations * that may change in the near future. Do not rely on it in your applications. * * @private */ snapSelector: null, /** * @cfg {Object} An object with x and y properties for offsetting the currently * snapped item from the top/left of the container. * * This api is highly experimental as it is based on bleeding-edge CSS implementations * that may change in the near future. Do not rely on it in your applications. * * @private */ snapOffset: null, /** * @cfg {Object} an object with x and y properties that specifies the size of the * snap points on the x and y axes. For IE10+/Edge only, since those browsers do * not support the newer CSS properties for snapping to element boundaries. * * This config is experimental and may be removed in a future version of the framework. * * @private */ msSnapInterval: null, /** * @cfg {Boolean/String} * - `true` or `'auto'` to enable horizontal auto-scrolling. In auto-scrolling mode * scrolling is only enabled when the {@link #element} has overflowing content. * - `false` to disable horizontal scrolling * - `'scroll'` to always enable horizontal scrolling regardless of content size. */ x: true, /** * @cfg {Boolean/String} * - `true` or `'auto'` to enable vertical auto-scrolling. In auto-scrolling mode * scrolling is only enabled when the {@link #element} has overflowing content. * - `false` to disable vertical scrolling * - `'scroll'` to always enable vertical scrolling regardless of content size. */ y: true, /** * @cfg {Ext.dom.Element} scrollElement * The element to read the scrollTop/scrollLeft from. This is used to * work around cross browser issues where WebKit/Blink require reading/writing * scrollTop/scrollLeft on the document.body, even if the documentElement is * the thing overflowing. In future this can be removed once document.scrollingElement * becomes a standard across all supported browsers. * * Note that scroll(Width/Height) and other dimensions can be read from the * documentElement without issue. * @private */ scrollElement: null, /** * @cfg {Object} * The size of the scrollable content expressed as an object with x and y properties * @private * @readonly */ size: null, spacerXY: null, /** * @cfg {Object} touchAction for the scroller's {@link #element}. * * For more details see {@link Ext.dom.Element#setTouchAction} */ touchAction: null }, snappableCls: Ext.baseCSSPrefix + 'scroller-snappable', elementCls: Ext.baseCSSPrefix + 'scroller', spacerCls: Ext.baseCSSPrefix + 'scroller-spacer', noScrollbarsCls: Ext.baseCSSPrefix + 'no-scrollbars', statics: { /** * Creates and returns an appropriate Scroller instance for the current device. * @param {Object} config Configuration options for the Scroller * @param type * @return {Ext.scroll.Scroller} */ create: function(config, type) { return Ext.Factory.scroller(config, type); }, /** * Get the scrolling element for the document based on feature detection. * See: https://dev.opera.com/articles/fixing-the-scrolltop-bug/ * * @return {HTMLElement} * * @private */ getScrollingElement: function() { var doc = document, standard = this.$standardScrollElement, el = doc.scrollingElement, iframe, frameDoc; // Normalize the scrollElement we need to read/write from // First attempt to detect the newer standard for viewport // scrolling if (el) { return el; } // The newer standard doesn't exist, let the scroller // decide via feature detection. if (standard === undefined) { iframe = document.createElement('iframe'); iframe.style.height = '1px'; document.body.appendChild(iframe); frameDoc = iframe.contentWindow.document; frameDoc.write('<!DOCTYPE html><div style="height:9999em">x</div>'); frameDoc.close(); standard = frameDoc.documentElement.scrollHeight > frameDoc.body.scrollHeight; iframe.parentNode.removeChild(iframe); this.$standardScrollElement = standard; } return standard ? doc.documentElement : doc.body; }, /** * @private */ initViewportScroller: function() { var scroller = Ext.getViewportScroller(); if (!scroller.getElement()) { // if the viewport component has already claimed the viewport scroller // it will have already set its overflow element as the scroller element, // otherwise, the element is always the body. scroller.setElement(Ext.getBody()); } } }, constructor: function(config) { var me = this; me.position = { x: 0, y: 0 }; me.callParent([config]); }, destroy: function() { var me = this, partners = me._partners, key; Ext.undefer(me.restoreTimer); // Clear any overflow styles me.setX(Ext.emptyString); me.setY(Ext.emptyString); if (me._spacer) { me._spacer.destroy(); } if (me.scrollListener) { me.scrollListener.destroy(); } if (partners) { for (key in partners) { me.removePartner(partners[key].scroller); } } // Remove element listeners, this will cause scrollElement to // be cleared also. me.setElement(null); me._partners = me.component = null; if (me.translatable) { me.translatable.destroy(); me.translatable = null; } me.removeSnapStylesheet(); me.callParent(); }, /** * Adds a "partner" scroller. Partner scrollers reflect each other's scroll position * at all times - if either scroller is scrolled, the scroll position of its partner * will be be automatically synchronized. * * A scroller may have multiple partners. * * @param {Ext.scroll.Scroller} partner * @param {String} [axis='both'] The axis to synchronize (`'x'`, '`y`', or '`both`') */ addPartner: function(partner, axis) { var me = this, partners = me._partners || (me._partners = {}), otherPartners = partner._partners || (partner._partners = {}); // Translate to boolean flags. {x:<boolean>,y:<boolean>} axis = me.axisConfigs[axis || 'both']; partners[partner.getId()] = { scroller: partner, axes: axis }; otherPartners[me.getId()] = { scroller: me, axes: axis }; }, applyElement: function(element, oldElement) { var me = this; if (oldElement && me.scrollListener) { me.scrollListener.destroy(); } if (element) { //<debug> if (typeof element === 'string' && !Ext.get(element)) { Ext.raise("Cannot create Ext.scroll.Scroller instance. " + "Element with id '" + element + "' not found."); } //</debug> element = Ext.get(element); } return element; }, applySize: function(size, oldSize) { var x, y; if (size === null || typeof size === 'number') { x = y = size; } else if (size) { x = size.x; y = size.y; } if (x === null) { x = 0; } else if (x === undefined) { x = (oldSize ? oldSize.x : 0); } if (y === null) { y = 0; } else if (y === undefined) { y = (oldSize ? oldSize.y : 0); } if (!oldSize || x !== oldSize.x || y !== oldSize.y) { return { x: x, y: y }; } }, /** * Gets the `clientWidth` and `clientHeight` of the {@link #element} for this scroller. * @return {Object} An object with `x` and `y` properties. */ getClientSize: function() { var dom = this.getElement().dom; return { x: dom.clientWidth, y: dom.clientHeight }; }, /** * Returns the amount of space consumed by scrollbars in the DOM * @return {Object} size An object containing the scrollbar sizes. * @return {Number} return.width The width of the vertical scrollbar. * @return {Number} return.height The height of the horizontal scrollbar. */ getScrollbarSize: function() { var me = this, width = 0, height = 0, element = me.getElement(), dom, x, y, hasXScroll, hasYScroll, scrollbarSize; if (element && !element.destroyed) { x = me.getX(); y = me.getY(); dom = element.dom; if (x || y) { scrollbarSize = Ext.getScrollbarSize(); } if (x === 'scroll') { hasXScroll = true; } else if (x) { hasXScroll = dom.scrollWidth > dom.clientWidth; } if (y === 'scroll') { hasYScroll = true; } else if (y) { hasYScroll = dom.scrollHeight > dom.clientHeight; } if (hasXScroll) { height = scrollbarSize.height; } if (hasYScroll) { width = scrollbarSize.width; } } return { width: width, height: height }; }, /** * @method getPosition * Returns the current scroll position * @return {Object} An object with `x` and `y` properties. */ getPosition: function() { var me = this; if (me.positionDirty) { me.updateDomScrollPosition(); } return me.position; }, /** * @method getSize * Returns the size of the scrollable content * @return {Object} size * @return {Number} return.x The width of the scrollable content * @return {Number} return.y The height of the scrollable content */ getSize: function() { var element = this.getElement(), size, dom; if (element && !element.destroyed) { dom = element.dom; size = { x: dom.scrollWidth, y: dom.scrollHeight }; } else { size = { x: 0, y: 0 }; } return size; }, /** * @method getMaxPosition * Returns the maximum scroll position for this scroller * @return {Object} position * @return {Number} return.x The maximum scroll position on the x axis * @return {Number} return.y The maximum scroll position on the y axis */ getMaxPosition: function() { var element = this.getElement(), x = 0, y = 0, dom; if (element && !element.destroyed) { dom = element.dom; x = dom.scrollWidth - dom.clientWidth; y = dom.scrollHeight - dom.clientHeight; } return { x: x, y: y }; }, /** * @method getMaxUserPosition * Returns the maximum scroll position for this scroller for scrolling that is initiated * by the user via mouse or touch. This differs from getMaxPosition in that getMaxPosition * returns the true maximum scroll position regardless of which axes are enabled for * user scrolling. * @return {Object} position * @return {Number} return.x The maximum scroll position on the x axis * @return {Number} return.y The maximum scroll position on the y axis */ getMaxUserPosition: function() { var me = this, element = me.getElement(), x = 0, y = 0, dom; if (element && !element.destroyed) { dom = element.dom; if (me.getX()) { x = dom.scrollWidth - dom.clientWidth; } if (me.getY()) { y = dom.scrollHeight - dom.clientHeight; } } return { x: x, y: y }; }, /** * Refreshes the scroller size and maxPosition. * @param {Boolean} immediate `true` to refresh immediately. By default refreshes * are deferred until the next {@link Ext.GlobalEvents#event-idle idle} event to * ensure any pending writes have been flushed to the dom and any reflows have * taken place. * @return {Ext.scroll.Scroller} this * @chainable */ refresh: function() { // Element size has changed. // Our position property may need refreshing from the DOM this.positionDirty = true; this.fireEvent('refresh', this); return this; }, /** * Removes a partnership that was created via {@link #addPartner} * @param {Ext.scroll.Scroller} partner * @private */ removePartner: function(partner) { var partners = this._partners, otherPartners = partner._partners; if (partners) { delete partners[partner.getId()]; } if (otherPartners) { delete(otherPartners[this.getId()]); } }, /** * Scrolls by the passed delta values, optionally animating. * * All of the following are equivalent: * * scroller.scrollBy(10, 10, true); * scroller.scrollBy([10, 10], true); * scroller.scrollBy({ x: 10, y: 10 }, true); * * A null value for either `x` or `y` will result in no scrolling on the given axis, * for example: * * scroller.scrollBy(null, 10); * * will scroll by 10 on the y axis and leave the x axis at its current scroll position * * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x * and y deltas or an object with "x" and "y" properties. * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag or * config object. * @param {Boolean/Object} animate Animate flag/config object if the delta values were * passed separately. * @return {Ext.Promise} A promise for when the scroll completes. */ scrollBy: function(deltaX, deltaY, animate) { var position = this.getPosition(); if (deltaX) { if (deltaX.length) { // array animate = deltaY; deltaY = deltaX[1]; deltaX = deltaX[0]; } else if (typeof deltaX !== 'number') { // object animate = deltaY; deltaY = deltaX.y; deltaX = deltaX.x; } } deltaX = (typeof deltaX === 'number') ? deltaX + position.x : null; deltaY = (typeof deltaY === 'number') ? deltaY + position.y : null; return this.doScrollTo(deltaX, deltaY, animate); }, /** * Ensures a descendant element of the scroller is visible by scrolling to it. * * @param {String/HTMLElement/Ext.dom.Element/Object} el * The descendant element to scroll into view. May also be the options object with * the `element` key defining the descendant element. * * @param {Object} [options] An object containing options to modify the operation. * * @param {Object} [options.align] The alignment for the scroll. * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not * specified, the minimum will be done to make the element visible. The behavior is `undefined` * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will * only take place if the item is not already in the visible area. * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not * specified, the minimum will be done to make the element visible. The behavior is `undefined` * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will * only take place if the item is not already in the visible area. * * @param {Boolean} [options.animation] Pass `true` to animate the row into view. * * @param {Boolean} [options.highlight=false] Pass `true` to highlight the row with a glow * animation when it is in view. Can also be a hex color to use for highlighting * (defaults to yellow = '#ffff9c'). * * @param {Boolean} [options.x=true] `false` to disable horizontal scroll. * @param {Boolean} [options.y=true] `false` to disable vertical scroll. * * @return {Ext.Promise} A promise for when the scroll completes. * @since 6.5.1 */ ensureVisible: function(el, options) { var me = this, position = me.getPosition(), highlight, newPosition, ret; // Might get called before Component#onBoxReady which is when the Scroller is set up with elements. if (el) { if (el && el.element && !el.isElement) { options = el; el = options.element; } options = options || {}; highlight = options.highlight; newPosition = me.getEnsureVisibleXY(el, options); // Only attempt to scroll if it's needed. if (newPosition.y !== position.y || newPosition.x !== position.x) { if (highlight) { me.on({ scrollend: 'doHighlight', scope: me, single: true, args: [el, highlight] }); } ret = me.doScrollTo(newPosition.x, newPosition.y, options.animation); } else { // No scrolling needed, but still honor highlight request if (highlight) { me.doHighlight(el, highlight); } // Resolve straight away ret = Ext.Deferred.getCachedResolved(); } } else { // Can't scroll ret = Ext.Deferred.getCachedRejected(); } return ret; }, /** * Scrolls a descendant element of the scroller into view. * @param {String/HTMLElement/Ext.dom.Element} el the descendant to scroll into view * @param {Boolean} [hscroll=true] False to disable horizontal scroll. * @param {Boolean/Object} [animate] true for the default animation or a standard Element * animation config object * @param {Boolean/String} [highlight=false] true to * {@link Ext.dom.Element#highlight} the element when it is in view. Can also be a * hex color to use for highlighting (defaults to yellow = '#ffff9c'). * * @deprecated 6.5.1 Use {@link #ensureVisible} instead. * @return {Ext.Promise} A promise for when the scroll completes. */ scrollIntoView: function(el, hscroll, animate, highlight) { return this.ensureVisible(el, { animation: animate, highlight: highlight, x: hscroll }); }, /** * Determines if the passed element is within the visible x and y scroll viewport. * @param {String/HTMLElement/Ext.dom.Element} el The dom node, Ext.dom.Element, or * id (string) of the dom element that is to be verified to be in view * @return {Object} Which ranges the element is in. * @return {Boolean} return.x `true` if the passed element is within the x visible range. * @return {Boolean} return.y `true` if the passed element is within the y visible range. */ isInView: function(el) { return this.doIsInView(el); }, /** * Scrolls to the given position. * * All of the following are equivalent: * * scroller.scrollTo(10, 10, true); * scroller.scrollTo([10, 10], true); * scroller.scrollTo({ x: 10, y: 10 }, true); * * A null value for either `x` or `y` will result in no scrolling on the given axis, * for example: * * scroller.scrollTo(null, 10); * * will scroll to 10 on the y axis and leave the x axis at its current scroll position * * A negative value for either `x` or `y` represents an offset from the maximum scroll * position on the given axis: * * // scrolls to 10px from the maximum x scroll position and 20px from maximum y * scroller.scrollTo(-10, -20); * * A value of Infinity on either axis will scroll to the maximum scroll position on * that axis: * * // scrolls to the maximum position on both axes * scroller.scrollTo(Infinity, Infinity); * * @param {Number} x The scroll position on the x axis. * @param {Number} y The scroll position on the y axis. * @param {Boolean/Object} [animation] Whether or not to animate the scrolling to the new position. * * @return {Ext.Promise} A promise for when the scroll completes. */ scrollTo: function(x, y, animation) { var maxPosition; if (x) { if (x.length) { // array animation = y; y = x[1]; x = x[0]; } else if (typeof x !== 'number') { // object animation = y; y = x.y; x = x.x; } } if (x < 0 || y < 0) { maxPosition = this.getMaxPosition(); if (x < 0) { x += maxPosition.x; } if (y < 0) { y += maxPosition.y; } } return this.doScrollTo(x, y, animation); }, updateDirection: function(direction) { var me = this, x, y; if (!direction) { // if no direction was configured we set its value based on the values of // x and y. This ensures getDirection() always returns something useful // for backward compatibility. x = me.getX(); y = me.getY(); if (x && y) { direction = (y === 'scroll' && x === 'scroll') ? 'both' : 'auto'; } else if (y) { direction = 'vertical'; } else if (x) { direction = 'horizontal'; } // set the _direction property directly to avoid the updater being called // and triggering setX/setY calls me._direction = direction; } else { if (direction === 'auto') { x = true; y = true; } else if (direction === 'vertical') { x = false; y = true; } else if (direction === 'horizontal') { x = true; y = false; } else if (direction === 'both') { x = 'scroll'; y = 'scroll'; } me.setX(x); me.setY(y); } }, updateScrollbars: function(scrollbars, oldScrollbars) { this.syncScrollbarCls(); }, updateSize: function(size) { var me = this, element = me.getElement(), x = size.x, y = size.y, spacer; if (element) { me.positionDirty = true; spacer = me.getSpacer(); // Typically a dom scroller simply assumes the scroll size dictated by its content. // In some cases, however, it is necessary to be able to manipulate this scroll size // (infinite lists for example). This method positions a 1x1 px spacer element // within the scroller element to set a specific scroll size. if (!x && !y) { spacer.hide(); } else { // Subtract spacer size from coordinates (spacer is always 1x1 px in size) if (x > 0) { x -= 1; } if (y > 0) { y -= 1; } me.setSpacerXY({ x: x, y: y }); spacer.show(); } } }, updateMsSnapInterval: function() { this.initMsSnapInterval(); }, updateSnapSelector: function() { this.initSnap(); }, updateSnapOffset: function() { this.initSnap(); }, updateTouchAction: function(touchAction) { var element = this.getElement(); if (element) { element.setTouchAction(touchAction); } }, updateElement: function(element, oldElement) { var me = this, touchAction = me.getTouchAction(), scrollListener = me.scrollListener, elementCls = me.elementCls, eventSource, scrollEl; // If we have a scrollListener, we also have a scrollElement if (scrollListener) { scrollListener.destroy(); me.scrollListener = null; me.setScrollElement(null); } if (oldElement && !oldElement.destroyed) { // TODO: might be nice to have x-scroller-foo classes to map overflow styling oldElement.setStyle('overflow', 'hidden'); oldElement.removeCls(elementCls); } if (element) { if (element.dom === document.documentElement || element.dom === document.body) { // When the documentElement or body is scrolled, its scroll events are // fired via the window object eventSource = Ext.getWin(); scrollEl = Ext.scroll.Scroller.getScrollingElement(); } else { scrollEl = eventSource = element; } me.setScrollElement(Ext.get(scrollEl)); me.scrollListener = eventSource.on({ scroll: me.onDomScroll, scope: me, destroyable: true }); if (touchAction) { element.setTouchAction(touchAction); } me.initXStyle(); me.initYStyle(); element.addCls(elementCls); me.initSnap(); me.initMsSnapInterval(); me.syncScrollbarCls(); } }, updateX: function(x) { this.initXStyle(); }, updateY: function(y) { this.initYStyle(); }, deprecated: { '5': { methods: { /** * @method getScroller * Returns this scroller. * * In Sencha Touch 2, access to a Component's Scroller was provided via * a Ext.scroll.View class that was returned from the Component's getScrollable() * method: * * component.getScrollable().getScroller(); * * in 5.0 all the functionality of Ext.scroll.View has been rolled into * Ext.scroll.Scroller, and Ext.scroll.View has been removed. Component's * getScrollable() method now returns a Ext.scroll.Scroller. This method is * provided for compatibility. * @deprecated 5.0 This method is deprecated. Please use Ext.scroll.Scroller's * getScrollable() method instead. */ getScroller: function() { return this; } } }, '5.1.0': { methods: { /** * @method scrollToTop * Scrolls to 0 on both axes * @param {Boolean/Object} animate * @private * @return {Ext.scroll.Scroller} this * @chainable * @deprecated 5.1.0 Use scrollTo instead */ scrollToTop: function(animate) { return this.scrollTo(0, 0, animate); }, /** * @method scrollToEnd * Scrolls to the maximum position on both axes * @param {Boolean/Object} animate * @private * @return {Ext.scroll.Scroller} this * @chainable * @deprecated 5.1.0 Use scrollTo instead */ scrollToEnd: function(animate) { return this.scrollTo(Infinity, Infinity, animate); } } } }, privates: { axisConfigs: { x: { x: true }, y: { y: true }, both: { x: true, y: true } }, /** * @private * Gets the x/y coordinates to ensure the element is scrolled into view. * * @param {String/HTMLElement/Ext.dom.Element/Object} el * The descendant element to scroll into view. May also be the options object with * the `element` key defining the descendant element. * * @param {Object} [options] An object containing options to modify the operation. * * @param {Object/String} [options.align] The alignment for the scroll. If a string, this value * will be used for both `x` and `y` alignments. * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not * specified, the minimum will be done to make the element visible. The behavior is `undefined` * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will * only take place if the item is not already in the visible area. * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not * specified, the minimum will be done to make the element visible. The behavior is `undefined` * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will * only take place if the item is not already in the visible area. * * @param {Boolean} [options.x=true] `false` to disable horizontal scroll and `x` align option. * @param {Boolean} [options.y=true] `false` to disable vertical scroll and `y` align option. * @return {Object} The new position that will be used to scroll the element into view. * @since 6.5.1 */ getEnsureVisibleXY: function(el, options) { var position = this.getPosition(), viewport = this.component ? this.component.getScrollableClientRegion() : this.getElement(), newPosition, align; if (el && el.element && !el.isElement) { options = el; el = options.element; } options = options || {}; align = options.align; if (align) { if (Ext.isString(align)) { align = { x: options.x === false ? null : align, y: options.y === false ? null : align }; } else if (Ext.isObject(align)) { if (align.x && options.x === false) { align.x = null; } if (align.y && options.y === false) { align.y = null; } } } newPosition = Ext.fly(el).getScrollIntoViewXY(viewport, position.x, position.y, align); newPosition.x = options.x === false ? position.x : newPosition.x; newPosition.y = options.y === false ? position.y : newPosition.y; return newPosition; }, getSpacer: function() { var me = this, spacer = me._spacer, element; // In some cases (e.g. infinite lists) we need to be able to tell the scroller // to have a specific size, regardless of its contents. This creates a spacer // element which can then be absolutely positioned to affect the element's // scroll size. Must be first element, so it is not translated due to being after // the element contrainer el. if (!spacer) { element = me.getElement(); spacer = me._spacer = element.createChild({ cls: me.spacerCls, role: 'presentation' }, element.dom.firstChild); spacer.setVisibilityMode(2); // 'display' visibilityMode spacer.hide(); // make sure the element is positioned if it is not already. This ensures // that the spacer's position will affect the element's scroll size element.position(); } return spacer; }, applySpacerXY: function(pos, oldPos) { // Opt out if we have the same value if (oldPos && pos.x === oldPos.x && pos.y === oldPos.y) { pos = undefined; } return pos; }, updateSpacerXY: function(pos) { var me = this, spacer = me.getSpacer(), sStyle = spacer.dom.style, scrollHeight = pos.y, shortfall; sStyle.marginTop = ''; me.translateSpacer(pos.x, me.constrainScrollRange(scrollHeight)); // Force a synchronous layout to update the scrollHeight. // This flip-flops between 0px and 1px sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px'; // See if we can get any more scrollHeight from a margin-top if (scrollHeight > 1000000) { shortfall = scrollHeight - me.getElement().dom.scrollHeight; if (shortfall > 0) { sStyle.marginTop = Math.min(shortfall, me.maxSpacerMargin || 0) + 'px'; } } }, // rtl hook - rtl version sets right style translateSpacer: function(x, y) { this.getSpacer().translate(x, y); }, doIsInView: function(el, skipCheck) { var me = this, c = me.component, result = { x: false, y: false }, elRegion, myEl = me.getElement(), myElRegion; if (el && (skipCheck || (myEl.contains(el) || (c && c.owns(el))))) { myElRegion = myEl.getRegion(); elRegion = Ext.fly(el).getRegion(); result.x = elRegion.right > myElRegion.left && elRegion.left < myElRegion.right; result.y = elRegion.bottom > myElRegion.top && elRegion.top < myElRegion.bottom; } return result; }, // Checks if the scroller contains a component by searching up the element hierarchy // using components. It uses component navigation as opposed to elements because we // want logical ownership. contains: function(component) { var el = this.getElement(), owner = component; while (owner && owner !== Ext.Viewport) { if (el.contains(owner.el)) { return true; } owner = owner.getRefOwner(); } return false; }, constrainScrollRange: function(scrollRange) { var maxScrollHeight, tooHigh, tooLow, scrollTest, stretcher, sStyle; // Only do the expensive search for the browser limit if they // want more than a million pixels. if (scrollRange < 1000000) { return scrollRange; } if (!this.maxSpacerTranslate) { // // Find max scroll height which transform: translateY(npx) will support. // IE11 appears to have 21,474,834 // Chrome and Safari have 16,777,216, but additional margin-top of 16777215px allows a scrollHeight of 33,554,431 // Firefox has 17,895,698 // IE9-10 1,534,000 // maxScrollHeight = Math.pow(2, 32); tooHigh = maxScrollHeight; tooLow = 500; scrollTest = Ext.getBody().createChild({ style: { position: 'absolute', left: '-10000px', top: '0', width: '500px', height: '500px' }, cn: { cls: this.spacerCls } }, null, true); stretcher = Ext.get(scrollTest.firstChild); sStyle = stretcher.dom.style; stretcher.translate(0, maxScrollHeight - 1); sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px'; // See what the max translateY is which still stretches the scrollHeight while (tooHigh !== tooLow + 1) { stretcher.translate(0, (maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2))); // Force a synchronous layout to update the scrollHeight. // This flip-flops between 0px and 1px sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px'; if (scrollTest.scrollHeight < maxScrollHeight) { tooHigh = maxScrollHeight; } else { tooLow = maxScrollHeight; } } stretcher.translate(0, Ext.scroll.Scroller.prototype.maxSpacerTranslate = tooLow); // Go through the same steps seeing how far we can push it with margin-top tooHigh = tooLow * 2; while (tooHigh !== tooLow + 1) { stretcher.dom.style.marginTop = ((maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2))) + 'px'; // Force a synchronous layout to update the scrollHeight. // This flip-flops between 0px and 1px sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px'; if (scrollTest.scrollHeight < maxScrollHeight) { tooHigh = maxScrollHeight; } else { tooLow = maxScrollHeight; } } Ext.fly(scrollTest).destroy(); Ext.scroll.Scroller.prototype.maxSpacerMargin = tooLow - Ext.scroll.Scroller.prototype.maxSpacerTranslate; } // The maximum a translateY transform can be pushed to stretch the scrollHeight before // it collapses back to offsetHeight return Math.min(scrollRange, this.maxSpacerTranslate); }, // hook for rtl mode to convert an x coordinate to RTL space. convertX: function(x) { return x; }, // highlights an element after it has been scrolled into view doHighlight: function(el, highlight) { if (highlight !== true) { // handle hex color Ext.fly(el).highlight(highlight); } else { Ext.fly(el).highlight(); } }, doScrollTo: function(x, y, animate) { // There is an IE8 override of this method; when making changes here // don't forget to update the override as well var me = this, element = me.getScrollElement(), maxPosition, dom, xInf, yInf, ret, translatable, deferred; if (element && !element.destroyed) { dom = element.dom; xInf = (x === Infinity); yInf = (y === Infinity); if (xInf || yInf) { maxPosition = me.getMaxPosition(); if (xInf) { x = maxPosition.x; } if (yInf) { y = maxPosition.y; } } if (x !== null) { x = me.convertX(x); } if (animate) { translatable = me.translatable; if (!translatable) { me.translatable = translatable = new Ext.util.translatable.ScrollPosition({ element: element }); } 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 }); translatable.translate(x, y, animate); ret = deferred.promise; } else { if (y != null) { dom.scrollTop = y; } if (x != null) { dom.scrollLeft = x; } ret = Ext.Deferred.getCachedResolved(); } // Our position object will need refreshing before returning. me.positionDirty = true; } else { ret = Ext.Deferred.getCachedRejected(); } return ret; }, fireScrollStart: function(x, y, xDelta, yDelta) { var me = this, component = me.component; me.invokePartners('onPartnerScrollStart', x, y, xDelta, yDelta); me.startX = x - xDelta; me.startY = y - yDelta; if (me.hasListeners.scrollstart) { me.fireEvent('scrollstart', me, x, y); } if (component && component.onScrollStart) { component.onScrollStart(x, y); } Ext.GlobalEvents.fireEvent('scrollstart', me, x, y); }, fireScroll: function(x, y, xDelta, yDelta) { var me = this, component = me.component; me.invokePartners('onPartnerScroll', x, y, xDelta, yDelta); if (me.hasListeners.scroll) { me.fireEvent('scroll', me, x, y, xDelta, yDelta); } if (component && component.onScrollMove) { component.onScrollMove(x, y); } Ext.GlobalEvents.fireEvent('scroll', me, x, y, xDelta, yDelta); }, fireScrollEnd: function(x, y, xDelta, yDelta) { var me = this, component = me.component, dx = x - me.startX, dy = y - me.startY; me.startX = me.startY = null; me.invokePartners('onPartnerScrollEnd', x, y, xDelta, yDelta); if (me.hasListeners.scrollend) { me.fireEvent('scrollend', me, x, y, dx, dy); } if (component && component.onScrollEnd) { component.onScrollEnd(x, y); } Ext.GlobalEvents.fireEvent('scrollend', me, x, y, dx, dy); }, // rtl hook getElementScroll: function(element) { return element.getScroll(); }, initSnap: function() { var me = this, snapOffset = me.getSnapOffset(), snapSelector = me.getSnapSelector(), element = me.getElement(), offsetX, offsetY, snapCoordinate; if (element && snapSelector) { element.addCls(me.snappableCls); me.removeSnapStylesheet(); if (snapOffset) { offsetX = snapOffset.x || 0; offsetY = snapOffset.y || 0; if (offsetX) { offsetX = -offsetX + 'px'; } if (offsetY) { offsetY = -offsetY + 'px'; } } snapCoordinate = offsetX + ' ' + offsetY + ';'; me.snapStylesheet = Ext.util.CSS.createStyleSheet( '#' + element.id + ' ' + snapSelector + '{-webkit-scroll-snap-coordinate:' + snapCoordinate + 'scroll-snap-coordinate:' + snapCoordinate + '}' ); } }, initMsSnapInterval: function() { var element = this.getElement(), interval, x, y, style; if (element) { interval = this.getMsSnapInterval(); if (interval) { x = interval.x; y = interval.y; style = element.dom.style; if (x) { style['-ms-scroll-snap-points-x'] = 'snapInterval(0px, ' + x + 'px)'; } if (y) { style['-ms-scroll-snap-points-y'] = 'snapInterval(0px, ' + y + 'px)'; } } } }, initXStyle: function() { var element = this.getElement(), x = this.getX(); // Check that element exists and is not destroyed if (element && element.dom) { if (!x) { x = 'hidden'; } else if (x === true) { x = 'auto'; } element.setStyle('overflow-x', x); } }, initYStyle: function() { var element = this.getElement(), y = this.getY(); // Check that element exists and is not destroyed if (element && element.dom) { if (!y) { y = 'hidden'; } else if (y === true) { y = 'auto'; } element.setStyle('overflow-y', y); } }, invokePartners: function(method, x, y, xDelta, yDelta) { var me = this, partners = me._partners, partner, id, axes; if (!me.suspendSync) { me.invokingPartners = true; for (id in partners) { axes = partners[id].axes; partner = partners[id].scroller; // Only pass the scroll on to partners if we are are configured to pass on the scrolled dimension if (!partner.invokingPartners && (xDelta && axes.x || yDelta && axes.y)) { partner[method](me, axes.x ? x : null, axes.y ? y : null, xDelta, yDelta); } } me.invokingPartners = false; } }, suspendPartnerSync: function() { this.suspendSync = (this.suspendSync || 0) + 1; }, resumePartnerSync: function(syncNow) { var me = this, position; if (me.suspendSync) { me.suspendSync--; } if (!me.suspendSync && syncNow) { position = me.getPosition(); me.invokePartners('onPartnerScroll', position.x, position.y); me.invokePartners('onPartnerScrollEnd', position.x, position.y); } }, readPosition: function(position) { var me = this, element = me.getScrollElement(), elScroll; position = position || {}; if (element && !element.destroyed) { elScroll = me.getElementScroll(element); position.x = elScroll.left; position.y = elScroll.top; } return position; }, updateDomScrollPosition: function(silent) { var me = this, position = me.position, oldX = position.x, oldY = position.y, x, y, xDelta, yDelta; me.readPosition(position); x = position.x; y = position.y; me.positionDirty = false; if (!silent) { xDelta = x - oldX; yDelta = y - oldY; // If we already know about the position. then we've been coerced there by a partner // and that will have been firing our event sequence synchronously, so they do not // not need to be fire in response to the ensuing scroll event. if (xDelta || yDelta) { if (!me.isScrolling) { me.isScrolling = Ext.isScrolling = true; me.fireScrollStart(x, y, xDelta, yDelta); } me.fireScroll(x, y, xDelta, yDelta); me.onDomScrollEnd(x, y, xDelta, yDelta); } } return position; }, /** * @private * May be called when a Component is rendererd AFTER some scrolling partner has begun its lifecycle to sync * this scroller with partners which may be scrolled anywhere by now. */ syncWithPartners: function() { var me = this, partners = me._partners, id, partner, position; me.suspendPartnerSync(); for (id in partners) { partner = partners[id].scroller; position = partner.getPosition(); me.onPartnerScroll(partner, position.x, position.y); } me.resumePartnerSync(); }, syncScrollbarCls: function() { var element = this.getElement(); if (element) { element.toggleCls(this.noScrollbarsCls, this.getScrollbars() === false); } }, onDomScroll: function() { var hasTimer = !!this.restoreTimer; this.updateDomScrollPosition(hasTimer); if (hasTimer) { Ext.undefer(this.onDomScrollEnd.timer); return; } }, doOnDomScrollEnd: function(x, y, xDelta, yDelta) { var me = this; // Could be destroyed by this time if (me.destroying || me.destroyed) { return; } me.isScrolling = Ext.isScrolling = false; // if this is being flushed we only need to set the scrolling status to false if (x === undefined) { return; } me.trackingScrollLeft = x; me.trackingScrollTop = y; me.fireScrollEnd(x, y, xDelta, yDelta); }, onPartnerScrollStart: function(partner, x, y, xDelta, yDelta) { // Pass the signal on immediately to all partners. this.isScrolling = true; this.fireScrollStart(x, y, xDelta, yDelta); }, onPartnerScroll: function(partner, x, y, xDelta, yDelta) { this.doScrollTo(x, y, false); // Update the known scroll position so that when it reacts to its DOM, // it will not register a change and so will not invoke partners. // All scroll intentions are propagated synchronously. // The ensuing multiple scroll events are then ignored. this.updateDomScrollPosition(true); // Pass the signal on immediately to all partners. this.fireScroll(x, y, xDelta, yDelta); }, onPartnerScrollEnd: function(partner, x, y, xDelta, yDelta) { // manually clearing the buffer queue before calling the method without buffering this.cancelOnDomScrollEnd(); // Pass the signal on immediately to all partners. // We are called by the onDomScrollEnd of our controller // so we must not add another delay and call doOnScrollEnd directly. this.doOnDomScrollEnd(x, y, xDelta, yDelta); }, removeSnapStylesheet: function() { var stylesheet = this.snapStylesheet; if (stylesheet) { Ext.util.CSS.removeStyleSheet(stylesheet); this.snapStylesheet = null; } }, restoreState: function() { var me = this, el = me.getScrollElement(); if (el) { // Only restore state if has been previously captured! For example, // floaters probably have not been hidden before initially shown. if (me.trackingScrollTop !== undefined) { // If we're restoring the scroll position, we don't want to publish // scroll events since the scroll position should not have changed // at all as far as the user is concerned, so just do it silently // while ensuring we maintain the correct internal state. 50ms is // enough to capture the async scroll events, anything after that // we re-enable. if (!me.restoreTimer) { me.restoreTimer = Ext.defer(function() { me.restoreTimer = null; }, 50); } me.doScrollTo(me.trackingScrollLeft, me.trackingScrollTop, false); // Do not discard the state. // It may need to be restored again. } } } }}, function(Scroller) { /** * @private * @return {Ext.scroll.Scroller} */ Ext.getViewportScroller = function() { // This method creates the global viewport scroller. This scroller instance must // always exist regardless of whether or not there is a Viewport component in use // so that global scroll events will still fire. Menus and some other floating // things use these scroll events to hide themselves. var scroller = Scroller.viewport; if (!scroller) { Scroller.viewport = scroller = new Scroller(); Scroller.initViewportScroller(); } return scroller; }; /** * @private * @param {Ext.scroll.Scroller} scroller */ Ext.setViewportScroller = function(scroller) { if (Scroller.viewport !== scroller) { Ext.destroy(Scroller.viewport); if (scroller && !scroller.isScroller) { scroller = new Scroller(scroller); } Scroller.viewport = scroller; } }; Ext.onReady(function() { // The viewport scroller must always exist, but it is deferred so that the // viewport component has a chance to call Ext.setViewportScroller() with // its own scroller first. // We assign the timer to a property to cancel the call while setting up // for unit tests. We will call initViewportScroller without waiting for the // Viewport to initialize. Scroller.initViewportScrollerTimer = Ext.defer(Scroller.initViewportScroller, 100); });});