outlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplace
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
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(); |
|
} |
|
} |
|
});
|
|
|