Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1446 lines
42 KiB

/**
* @class Ext.scroll.TouchScroller
* @private
* Momentum scrolling is one of the most important parts of the user experience on touch-screen
* devices. Depending on the device and browser, Ext JS will select one of several different
* scrolling implementations for best performance.
*
* Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable}
* configuration on {@link Ext.Component}. Here is a simple example of how to adjust the
* scroller settings when using a Component (or anything that extends it).
*
* @example
* Ext.create('Ext.Component', {
* renderTo: Ext.getBody(),
* height: 100,
* width: 100,
* // this component is scrollable vertically but not horizontally
* scrollable: 'y',
* html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque convallis lorem et magna tempus fermentum.'
* });
*/
Ext.define('Ext.scroll.TouchScroller', {
extend: 'Ext.scroll.Scroller',
alias: 'scroller.touch',
requires: [
'Ext.fx.easing.BoundMomentum',
'Ext.fx.easing.EaseOut',
'Ext.util.Translatable',
'Ext.scroll.Indicator',
'Ext.GlobalEvents'
],
isTouchScroller: true,
config: {
/**
* @cfg autoRefresh
* @private
*/
autoRefresh: true,
/**
* @cfg bounceEasing
* @private
*/
bounceEasing: {
duration: 400
},
/**
* @cfg
* @private
*/
elementSize: undefined,
indicators: true,
/**
* @cfg fps
* @private
*/
fps: 'auto',
/**
* @cfg maxAbsoluteVelocity
* @private
*/
maxAbsoluteVelocity: 6,
/**
* @cfg {Object} momentumEasing
* @inheritdoc
* The default value is:
*
* {
* momentum: {
* acceleration: 30,
* friction: 0.5
* },
* bounce: {
* acceleration: 30,
* springTension: 0.3
* }
* }
*
* Note that supplied object will be recursively merged with the default object. For example, you can simply
* pass this to change the momentum acceleration only:
*
* {
* momentum: {
* acceleration: 10
* }
* }
*/
momentumEasing: {
momentum: {
acceleration: 30,
friction: 0.5
},
bounce: {
acceleration: 30,
springTension: 0.3
},
minVelocity: 1
},
/**
* @cfg outOfBoundRestrictFactor
* @private
*/
outOfBoundRestrictFactor: 0.5,
/**
* @cfg {Ext.dom.Element}
* @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,
size: undefined,
/**
* @cfg
* @private
*/
slotSnapEasing: {
duration: 150
},
/**
* @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
* }
*
* @accessor
*/
slotSnapSize: {
x: 0,
y: 0
},
/**
* @cfg slotSnapOffset
* @private
*/
slotSnapOffset: {
x: 0,
y: 0
},
/**
* @cfg startMomentumResetTime
* @private
*/
startMomentumResetTime: 300,
/**
* @cfg translatable
* @private
*/
translatable: {
translationMethod: 'auto',
useWrapper: false
}
},
cls: Ext.baseCSSPrefix + 'scroll-container',
scrollerCls: Ext.baseCSSPrefix + 'scroll-scroller',
dragStartTime: 0,
dragEndTime: 0,
isDragging: false,
isAnimating: false,
isMouseEvent: {
mousedown: 1,
mousemove: 1,
mouseup: 1
},
listenerMap: {
touchstart: 'onTouchStart',
touchmove: 'onTouchMove',
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd'
},
refreshCounter: 0,
constructor: function(config) {
var me = this,
onEvent = 'onEvent';
me.elementListeners = {
touchstart: onEvent,
touchmove: onEvent,
dragstart: onEvent,
drag: onEvent,
dragend: onEvent,
scope: me
};
me.minPosition = { x: 0, y: 0 };
me.startPosition = { x: 0, y: 0 };
me.position = { 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};
Ext.GlobalEvents.on('idle', me.onIdle, me);
me.callParent([config]);
me.refreshAxes();
},
applyBounceEasing: function(easing) {
var defaultClass = Ext.fx.easing.EaseOut;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
applyElementSize: function(size) {
var el = this.getElement(),
dom, x, y;
if (!el) {
return null;
}
dom = el.dom;
if (!dom) {
return;
}
if (size == null) { // null or undefined
x = dom.clientWidth;
y = dom.clientHeight;
} else {
x = size.x;
y = size.y;
}
return {
x: x,
y: y
};
},
applyIndicators: function(indicators, oldIndicators) {
var me = this,
xIndicator, yIndicator, x, y;
if (indicators) {
if (indicators === true) {
xIndicator = yIndicator = {};
} else {
x = indicators.x;
y = indicators.y;
if (x || y) {
// handle an object with x/y keys for configuring the indicators
// individually. undfined/null/true are all the same, only false
// can prevent the indicator from being created
xIndicator = (x == null || x === true) ? {} : x;
yIndicator = (x == null || y === true) ? {} : y;
} else {
// not an object with x/y keys, handle as a single indicators config
// that applies to both axes
xIndicator = yIndicator = indicators;
}
}
if (oldIndicators) {
if (xIndicator) {
oldIndicators.x.setConfig(xIndicator);
} else {
oldIndicators.x.destroy();
oldIndicators.x = null;
}
if (yIndicator) {
oldIndicators.y.setConfig(yIndicator);
} else {
oldIndicators.y.destroy();
oldIndicators.y = null;
}
indicators = oldIndicators;
} else {
indicators = { x: null, y: null };
if (xIndicator) {
indicators.x = new Ext.scroll.Indicator(Ext.applyIf({
axis: 'x',
scroller: me
}, xIndicator));
}
if (yIndicator) {
indicators.y = new Ext.scroll.Indicator(Ext.applyIf({
axis: 'y',
scroller: me
}, yIndicator));
}
}
} else if (oldIndicators) {
oldIndicators.x.destroy();
oldIndicators.y.destroy();
oldIndicators.x = null;
oldIndicators.y = null;
}
return indicators;
},
applyMomentumEasing: function(easing) {
var defaultClass = Ext.fx.easing.BoundMomentum;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
applyInnerElement: function(innerElement) {
if (innerElement && !innerElement.isElement) {
innerElement = Ext.get(innerElement);
}
//<debug>
if (this.isConfiguring && !innerElement) {
Ext.Error.raise("Cannot create Ext.scroll.TouchScroller instance with null innerElement");
}
//</debug>
return innerElement;
},
applySize: function(size) {
var el, dom, scrollerDom, x, y;
if (size == null) { // null or undefined
el = this.getElement();
if (!el) {
return null;
}
dom = el.dom;
scrollerDom = this.getInnerElement().dom;
// using scrollWidth/scrollHeight instead of offsetWidth/offsetHeight ensures
// that the size includes any contained absolutely positioned items
x = Math.max(scrollerDom.scrollWidth, dom.clientWidth);
y = Math.max(scrollerDom.scrollHeight, dom.clientHeight);
} else if (typeof size === 'number') {
x = size;
y = size;
} else {
x = size.x;
y = size.y;
}
return {
x: x,
y: y
};
},
applySlotSnapOffset: function(snapOffset) {
if (typeof snapOffset === 'number') {
return {
x: snapOffset,
y: snapOffset
};
}
return snapOffset;
},
applySlotSnapSize: function(snapSize) {
if (typeof snapSize === 'number') {
return {
x: snapSize,
y: snapSize
};
}
return snapSize;
},
applySlotSnapEasing: function(easing) {
var defaultClass = Ext.fx.easing.EaseOut;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
applyTranslatable: function(config, translatable) {
return Ext.factory(config, Ext.util.Translatable, translatable);
},
destroy: function() {
var me = this,
element = me.getElement(),
innerElement = me.getInnerElement(),
sizeMonitors = me.sizeMonitors;
if (sizeMonitors) {
sizeMonitors.element.destroy();
sizeMonitors.container.destroy();
}
if (element && !element.isDestroyed) {
element.removeCls(me.cls);
}
if (innerElement && !innerElement.isDestroyed) {
innerElement.removeCls(me.scrollerCls);
}
if (me._isWrapped) {
if (!element.isDestroyed) {
me.unwrapContent();
}
innerElement.destroy();
if (me.FixedHBoxStretching) {
innerElement.parent().destroy();
}
}
me.setElement(null);
me.setInnerElement(null);
Ext.GlobalEvents.un('idle', me.onIdle, me);
Ext.destroy(me.getTranslatable());
me.callParent(arguments);
},
getPosition: function() {
return this.position;
},
refresh: function(immediate, /* private */ options) {
++this.refreshCounter;
if (immediate) {
this.doRefresh(options);
}
},
updateAutoRefresh: function(autoRefresh) {
this.toggleResizeListeners(autoRefresh);
},
updateBounceEasing: function(easing) {
this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
},
updateElementSize: function() {
if (!this.isConfiguring) {
// to avoid multiple calls to refreshAxes() during initialization we will
// call it once after initConfig has finished.
this.refreshAxes();
}
},
updateDisabled: function(disabled) {
// attachment of listeners is handled by updateElement during initial config
if (!this.isConfiguring) {
if (disabled) {
this.detachListeners();
} else {
this.attachListeners();
}
}
},
updateElement: function(element, oldElement) {
var me = this,
// first check if the user configured a innerElement
innerElement = me.getInnerElement(),
fixedHBoxStretching = this.FixedHBoxStretching,
listeners;
if (!innerElement) {
// if no configured scroller element, check if the first child has the
// scrollerCls, if so we can assume that the user already wrapped the content
// in a scrollerEl (this is true of both Ext and Touch Components).
innerElement = element.dom.firstChild;
if (fixedHBoxStretching && innerElement) {
innerElement = innerElement.dom.firstChild;
}
if (!innerElement || innerElement.nodeType !== 1 ||
!Ext.fly(innerElement).hasCls(me.scrollerCls)) {
// no scrollerEl found, generate one now
innerElement = me.wrapContent(element);
}
me.setInnerElement(innerElement);
}
if (!fixedHBoxStretching) {
element.addCls(me.cls);
}
if (me.isConfiguring) {
if (!me.getTranslatable().isScrollParent) {
// If using full virtual scrolling attach a mousewheel listener for moving
// the scroll position. Otherwise we use native scrolling when interacting
// using the mouse and so do not want to override the native behavior
listeners = me.elementListeners;
listeners.mousewheel = 'onMouseWheel';
listeners.scroll = {
fn: 'onElementScroll',
delegated: false,
scope: me
};
}
}
if (!me.getDisabled()) {
me.attachListeners();
}
if (!me.isConfiguring) {
// setting element after initial construction of Scroller
// sync up configs that depend on element
if (me.getAutoRefresh()) {
me.toggleResizeListeners(true);
}
// setting null size and elementSize will cause them to be updated from the DOM
me.setSize(null);
me.setElementSize(null);
}
me.callParent([element, oldElement]);
},
updateFps: function(fps) {
if (fps !== 'auto') {
this.getTranslatable().setFps(fps);
}
},
updateMaxUserPosition: function() {
this.snapToBoundary();
},
updateMinUserPosition: function() {
this.snapToBoundary();
},
updateInnerElement: function(innerElement) {
if (innerElement) {
innerElement.addCls(this.scrollerCls);
}
this.getTranslatable().setElement(innerElement);
},
updateSize: function() {
if (!this.isConfiguring) {
// to avoid multiple calls to refreshAxes() during initialization we will
// call it once after initConfig has finished.
this.refreshAxes();
}
},
updateTranslatable: function(translatable) {
translatable.setElement(this.getInnerElement());
translatable.on({
animationframe: 'onAnimationFrame',
animationend: 'onAnimationEnd',
scope: this
});
},
updateX: function() {
if (!this.isConfiguring) {
// to avoid multiple calls to refreshAxes() during initialization we will
// call it once after initConfig has finished.
this.refreshAxes();
}
},
updateY: function() {
if (!this.isConfiguring) {
// to avoid multiple calls to refreshAxes() during initialization we will
// call it once after initConfig has finished.
this.refreshAxes();
}
},
privates: {
attachListeners: function() {
this.getElement().on(this.elementListeners);
},
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(options) {
var me = this,
size, elementSize;
if (me.refreshCounter && me.getElement()) {
me.stopAnimation();
me.getTranslatable().refresh();
if (options) {
// refresh called due to resize handler on element or innerElement.
// Do not bother to read the DOM to determine sizing info, just set the size
// given by the resize handler
size = options.size;
elementSize = options.elementSize;
if (size) {
me.setSize(size);
}
if (elementSize) {
me.setElementSize(elementSize);
}
} else {
// calling doRefresh() without options will cause both size and
// elementSize to be measured from the DOM.
me.setSize(null);
me.setElementSize(null);
}
me.fireEvent('refresh', me);
me.refreshCounter = 0;
}
},
doScrollTo: function(x, y, animation, /* private */ allowOverscroll) {
var me = this,
isDragging = me.isDragging,
fireScrollCallback;
if (me.isDestroyed || !me.getElement()) {
return me;
}
// Normally the scroll position is constrained to the max scroll position, but
// during a drag operation or during reflectio the scroller is allowed to overscroll.
allowOverscroll = allowOverscroll || me.isDragging;
var translatable = me.getTranslatable(),
position = me.position,
positionChanged = false,
translationX, translationY;
if (!isDragging || me.isAxisEnabled('x')) {
if (isNaN(x) || typeof x !== 'number') {
x = position.x;
} else {
if (!allowOverscroll) {
x = me.constrainX(x);
}
if (position.x !== x) {
position.x = x;
positionChanged = true;
}
}
translationX = me.convertX(-x);
}
if (!isDragging || me.isAxisEnabled('y')) {
if (isNaN(y) || typeof y !== 'number') {
y = position.y;
} else {
if (!allowOverscroll) {
y = me.constrainY(y);
}
if (position.y !== y) {
position.y = y;
positionChanged = true;
}
}
translationY = -y;
}
if (positionChanged) {
if (animation) {
// We need a callback to fire it after the animation
fireScrollCallback = function() {
me.onScroll();
};
// If they passed a boolean, create an object to hold the callback.
if (animation === true) {
animation = {
callback: fireScrollCallback
};
}
// They want a callback, so we need to create a sequence on it.
else if (animation.callback) {
animation.callback = Ext.Function.createSequence(animation.callback, fireScrollCallback);
}
// We can just use the callback for our own purpose
else {
animation.callback = fireScrollCallback;
}
translatable.translateAnimated(translationX, translationY, animation);
}
else {
translatable.translate(translationX, translationY);
me.onScroll();
}
}
return me;
},
/**
* @private
*/
getAnimationEasing: function(axis, e) {
if (!this.isAxisEnabled(axis)) {
return null;
}
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;
}
if (Ext.browser.is.IE) {
velocity *= 2;
}
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;
},
hideIndicators: function() {
var me = this,
indicators = me.getIndicators(),
xIndicator, yIndicator;
if (indicators) {
if (me.isAxisEnabled('x')) {
xIndicator = indicators.x;
if (xIndicator) {
xIndicator.hide();
}
}
if (me.isAxisEnabled('y')) {
yIndicator = indicators.y;
if (yIndicator) {
yIndicator.hide();
}
}
}
},
/**
* 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.snapToBoundary();
this.onScrollEnd();
},
onAnimationFrame: function(translatable, x, y) {
var position = this.position;
position.x = this.convertX(-x);
position.y = -y;
this.onScroll();
},
onAxisDrag: function(axis, delta) {
if (!this.isAxisEnabled(axis)) {
return;
}
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;
},
// In "hybrid" touch scroll mode where the TouchScroller is used to control the
// scroll position of a naturally overflowing element, we need to sync the scroll
// position of the TouchScroller when the element is scrolled
onDomScroll: function() {
var me = this,
dom, position;
if (me.getTranslatable().isScrollParent) {
dom = me.getElement().dom;
position = me.position;
position.x = dom.scrollLeft;
position.y = dom.scrollTop;
}
me.callParent();
},
onDrag: function(e) {
var me = this,
lastDragPosition = me.lastDragPosition;
if (!me.isDragging) {
return;
}
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) {
return;
}
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,
direction = me.getDirection(),
absDeltaX = e.absDeltaX,
absDeltaY = e.absDeltaY,
directionLock = me.getDirectionLock(),
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();
me.isDragging = true;
if (directionLock && direction !== 'both') {
if ((direction === 'horizontal' && absDeltaX > absDeltaY) ||
(direction === 'vertical' && absDeltaY > absDeltaX)) {
e.stopPropagation();
}
else {
me.isDragging = false;
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;
me.onScrollStart();
},
onElementResize: function(element, info) {
this.refresh(true, {
elementSize: {
x: info.width,
y: info.height
}
});
},
onElementScroll: function(event, targetEl) {
targetEl.scrollTop = targetEl.scrollLeft = 0;
},
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,
browserEvent = e.browserEvent;
if ((!me.self.isTouching || me.isTouching) && // prevents nested scrolling
// prevents scrolling in response to mouse input on multi-input devices
// such as windows 8 laptops with touch screens.
// Don't bother checking the event type if we are on a device that uses
// full virtual scrolling (!isScrollParent)
// TODO: this should be handled by the event system once EXTJSIV-12840
// is implemented
((!me.getTranslatable().isScrollParent) || (!me.isMouseEvent[browserEvent.type] &&
browserEvent.pointerType !== 'mouse')) &&
(me.getY() || me.getX())) {
me[me.listenerMap[e.type]](e);
}
},
onIdle: function() {
this.doRefresh();
},
onInnerElementResize: function(element, info) {
this.refresh(true, {
size: {
x: info.width,
y: info.height
}
});
},
onMouseWheel: function(e) {
var me = this,
delta = e.getWheelDeltas(),
deltaX = -delta.x,
deltaY = -delta.y,
position = me.position,
maxPosition = me.getMaxUserPosition(),
minPosition = me.getMinUserPosition(),
max = Math.max,
min = Math.min,
positionX = max(min(position.x + deltaX, maxPosition.x), minPosition.x),
positionY = max(min(position.y + deltaY, maxPosition.y), minPosition.y);
deltaX = positionX - position.x;
deltaY = positionY - position.y;
if (!deltaX && !deltaY) {
return;
}
e.stopEvent();
me.onScrollStart();
me.scrollBy(deltaX, deltaY);
me.onScroll();
me.onScrollEnd();
},
onPartnerScrollEnd: function() {
this.hideIndicators();
},
onPartnerScrollStart: function() {
this.showIndicators();
},
onScroll: function() {
var me = this,
position = me.position,
x = position.x,
y = position.y,
indicators = me.getIndicators(),
xIndicator, yIndicator;
if (indicators) {
if (me.isAxisEnabled('x')) {
xIndicator = indicators.x;
if (xIndicator) {
xIndicator.setValue(x);
}
}
if (me.isAxisEnabled('y')) {
yIndicator = indicators.y;
if (yIndicator) {
yIndicator.setValue(y);
}
}
}
me.fireScroll(x, y);
},
onScrollEnd: function() {
var me = this,
position = me.position;
if (!me.isTouching && !me.snapToSlot()) {
me.hideIndicators();
Ext.isScrolling = false;
me.fireScrollEnd(position.x, position.y);
}
},
onScrollStart: function() {
var me = this,
position = me.position;
me.showIndicators();
Ext.isScrolling = true;
me.fireScrollStart(position.x, position.y);
},
onTouchEnd: function() {
var me = this;
me.isTouching = me.self.isTouching = false;
if (!me.isDragging && me.snapToSlot()) {
me.onScrollStart();
}
},
onTouchMove: function(e) {
// Prevents the page from scrolling while an element is being scrolled using
// the TouchScroller. Only needed when inside a page that does not use a
// Viewport, since the Viewport already prevents default behavior of touchmove
e.preventDefault();
},
onTouchStart: function() {
var me = this;
me.isTouching = me.self.isTouching = true;
Ext.getDoc().on({
touchend: 'onTouchEnd',
scope: me,
single: true
});
me.stopAnimation();
},
refreshAxes: function() {
var me = this,
flags = me.isAxisEnabledFlags,
size = me.getSize(),
elementSize = me.getElementSize(),
indicators = me.getIndicators(),
maxX, maxY, x, y, xIndicator, yIndicator;
if (!size || !elementSize) {
return;
}
maxX = Math.max(0, size.x - elementSize.x);
maxY = Math.max(0, size.y - elementSize.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;
xIndicator = indicators && indicators.x;
if (xIndicator) {
// hide the x indicator if the x axis is disabled, just in case we
// are refreshing during a scroll
xIndicator.hide();
}
} 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;
yIndicator = indicators && indicators.y;
if (yIndicator) {
// hide the y indicator if the y axis is disabled, just in case we
// are refreshing during a scroll
yIndicator.hide();
}
} else if (y === 'scroll') {
flags.y = true;
}
me.setMaxUserPosition({
x: flags.x ? maxX : 0,
y: flags.y ? maxY : 0
});
// If we are using regular DOM ovberflow scrolling, sync the element styles.
if (Ext.supports.touchScroll === 1) {
me.initXStyle();
me.initYStyle();
}
},
showIndicators: function() {
var me = this,
indicators = me.getIndicators(),
xIndicator, yIndicator;
if (indicators) {
if (me.isAxisEnabled('x')) {
xIndicator = indicators.x;
if (xIndicator) {
xIndicator.show();
}
}
if (me.isAxisEnabled('y')) {
yIndicator = indicators.y;
if (yIndicator) {
yIndicator.show();
}
}
}
},
snapToBoundary: function() {
if (this.isConfiguring) {
return;
}
var me = this,
position = me.position,
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();
},
toggleResizeListeners: function(on) {
var me = this,
element = me.getElement(),
method = on ? 'on' : 'un';
if (element) {
element[method]('resize', 'onElementResize', me);
me.getInnerElement()[method]('resize', 'onInnerElementResize', me);
}
},
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();
}
}
});