]> git.proxmox.com Git - extjs.git/blob - extjs/packages/core/src/scroll/TouchScroller.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / scroll / TouchScroller.js
1 /**
2 * @class Ext.scroll.TouchScroller
3 * @private
4 * Momentum scrolling is one of the most important parts of the user experience on touch-screen
5 * devices. Depending on the device and browser, Ext JS will select one of several different
6 * scrolling implementations for best performance.
7 *
8 * Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable}
9 * configuration on {@link Ext.Component}. Here is a simple example of how to adjust the
10 * scroller settings when using a Component (or anything that extends it).
11 *
12 * @example
13 * Ext.create('Ext.Component', {
14 * renderTo: Ext.getBody(),
15 * height: 100,
16 * width: 100,
17 * // this component is scrollable vertically but not horizontally
18 * scrollable: 'y',
19 * html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque convallis lorem et magna tempus fermentum.'
20 * });
21 */
22 Ext.define('Ext.scroll.TouchScroller', {
23 extend: 'Ext.scroll.Scroller',
24 alias: 'scroller.touch',
25
26 requires: [
27 'Ext.fx.easing.BoundMomentum',
28 'Ext.fx.easing.EaseOut',
29 'Ext.util.Translatable',
30 'Ext.scroll.Indicator',
31 'Ext.GlobalEvents'
32 ],
33
34 isTouchScroller: true,
35
36 config: {
37 /**
38 * @cfg autoRefresh
39 * Accepted values:
40 *
41 * - `true`: monitors both element and innerElement for resize
42 * - `null`: only monitors element for resize
43 * - `false`: does not monitor resize of either element or innerElement
44 * @private
45 */
46 autoRefresh: true,
47
48 /**
49 * @cfg bounceEasing
50 * @private
51 */
52 bounceEasing: {
53 duration: 400
54 },
55
56 /**
57 * @cfg
58 * @private
59 */
60 elementSize: undefined,
61
62 indicators: true,
63
64 /**
65 * @cfg fps
66 * @private
67 */
68 fps: 'auto',
69
70 /**
71 * @cfg maxAbsoluteVelocity
72 * @private
73 */
74 maxAbsoluteVelocity: 6,
75
76 /**
77 * @cfg {Object} momentumEasing
78 * @inheritdoc
79 * The default value is:
80 *
81 * {
82 * momentum: {
83 * acceleration: 30,
84 * friction: 0.5
85 * },
86 * bounce: {
87 * acceleration: 30,
88 * springTension: 0.3
89 * }
90 * }
91 *
92 * Note that supplied object will be recursively merged with the default object. For example, you can simply
93 * pass this to change the momentum acceleration only:
94 *
95 * {
96 * momentum: {
97 * acceleration: 10
98 * }
99 * }
100 */
101 momentumEasing: {
102 momentum: {
103 acceleration: 30,
104 friction: 0.5
105 },
106
107 bounce: {
108 acceleration: 30,
109 springTension: 0.3
110 },
111
112 minVelocity: 1
113 },
114
115 /**
116 * @cfg outOfBoundRestrictFactor
117 * @private
118 */
119 outOfBoundRestrictFactor: 0.5,
120
121 /**
122 * @cfg {Ext.dom.Element}
123 * @private
124 * The element that wraps the content of {@link #element} and is translated in
125 * response to user interaction. If not configured, one will be automatically
126 * generated.
127 */
128 innerElement: null,
129
130 size: undefined,
131
132 /**
133 * @cfg
134 * @private
135 */
136 slotSnapEasing: {
137 duration: 150
138 },
139
140 /**
141 * @cfg slotSnapOffset
142 * @private
143 */
144 slotSnapOffset: {
145 x: 0,
146 y: 0
147 },
148
149 /**
150 * @cfg startMomentumResetTime
151 * @private
152 */
153 startMomentumResetTime: 300,
154
155 /**
156 * @cfg translatable
157 * @private
158 */
159 translatable: {
160 translationMethod: 'auto',
161 useWrapper: false
162 }
163 },
164
165 cls: Ext.baseCSSPrefix + 'scroll-container',
166 scrollerCls: Ext.baseCSSPrefix + 'scroll-scroller',
167
168 dragStartTime: 0,
169
170 dragEndTime: 0,
171
172 isDragging: false,
173
174 isAnimating: false,
175
176 isMouseEvent: {
177 mousedown: 1,
178 mousemove: 1,
179 mouseup: 1
180 },
181
182 listenerMap: {
183 touchstart: 'onTouchStart',
184 touchmove: 'onTouchMove',
185 dragstart: 'onDragStart',
186 drag: 'onDrag',
187 dragend: 'onDragEnd'
188 },
189
190 refreshCounter: 0,
191
192 constructor: function(config) {
193 var me = this,
194 onEvent = 'onEvent';
195
196 me.elementListeners = {
197 touchstart: onEvent,
198 touchmove: onEvent,
199 dragstart: onEvent,
200 drag: onEvent,
201 dragend: onEvent,
202 scope: me
203 };
204
205 me.minPosition = { x: 0, y: 0 };
206
207 me.startPosition = { x: 0, y: 0 };
208
209 me.velocity = { x: 0, y: 0 };
210
211 me.isAxisEnabledFlags = { x: false, y: false };
212
213 me.flickStartPosition = { x: 0, y: 0 };
214
215 me.flickStartTime = { x: 0, y: 0 };
216
217 me.lastDragPosition = { x: 0, y: 0 };
218
219 me.dragDirection = { x: 0, y: 0};
220
221 me.callParent([config]);
222
223 me.refreshAxes();
224
225 me.scheduleRefresh = {
226 idle: me.doRefresh,
227 scope: me,
228 single: true,
229 destroyable: true
230 }
231 },
232
233 applyBounceEasing: function(easing) {
234 var defaultClass = Ext.fx.easing.EaseOut;
235
236 return {
237 x: Ext.factory(easing, defaultClass),
238 y: Ext.factory(easing, defaultClass)
239 };
240 },
241
242 applyElementSize: function(size) {
243 var el = this.getElement(),
244 dom, x, y;
245
246 if (!el) {
247 return null;
248 }
249
250 dom = el.dom;
251
252 if (!dom) {
253 return;
254 }
255
256 if (size == null) { // null or undefined
257 x = dom.clientWidth;
258 y = dom.clientHeight;
259 } else {
260 x = size.x;
261 y = size.y;
262 }
263
264 return {
265 x: x,
266 y: y
267 };
268 },
269
270 applyIndicators: function(indicators, oldIndicators) {
271 var me = this,
272 xIndicator, yIndicator, x, y;
273
274 if (indicators) {
275 if (indicators === true) {
276 xIndicator = yIndicator = {};
277 } else {
278 x = indicators.x;
279 y = indicators.y;
280 if (x || y) {
281 // handle an object with x/y keys for configuring the indicators
282 // individually. undfined/null/true are all the same, only false
283 // can prevent the indicator from being created
284 xIndicator = (x == null || x === true) ? {} : x;
285 yIndicator = (x == null || y === true) ? {} : y;
286 } else {
287 // not an object with x/y keys, handle as a single indicators config
288 // that applies to both axes
289 xIndicator = yIndicator = indicators;
290 }
291 }
292
293 if (oldIndicators) {
294 if (xIndicator) {
295 oldIndicators.x.setConfig(xIndicator);
296 } else {
297 oldIndicators.x.destroy();
298 oldIndicators.x = null;
299 }
300 if (yIndicator) {
301 oldIndicators.y.setConfig(yIndicator);
302 } else {
303 oldIndicators.y.destroy();
304 oldIndicators.y = null;
305 }
306 indicators = oldIndicators;
307 } else {
308 indicators = { x: null, y: null };
309 if (xIndicator) {
310 indicators.x = new Ext.scroll.Indicator(Ext.applyIf({
311 axis: 'x',
312 scroller: me
313 }, xIndicator));
314 }
315 if (yIndicator) {
316 indicators.y = new Ext.scroll.Indicator(Ext.applyIf({
317 axis: 'y',
318 scroller: me
319 }, yIndicator));
320 }
321 }
322 } else if (oldIndicators) {
323 if (oldIndicators.x) {
324 oldIndicators.x.destroy();
325 }
326 if (oldIndicators.y) {
327 oldIndicators.y.destroy();
328 }
329 oldIndicators.x = oldIndicators.y = null;
330 }
331
332 return indicators;
333 },
334
335 applyMomentumEasing: function(easing) {
336 var defaultClass = Ext.fx.easing.BoundMomentum;
337
338 return {
339 x: Ext.factory(easing, defaultClass),
340 y: Ext.factory(easing, defaultClass)
341 };
342 },
343
344 applyInnerElement: function(innerElement) {
345 if (innerElement && !innerElement.isElement) {
346 innerElement = Ext.get(innerElement);
347 }
348
349 //<debug>
350 if (this.isConfiguring && !innerElement) {
351 Ext.raise("Cannot create Ext.scroll.TouchScroller instance with null innerElement");
352 }
353 //</debug>
354
355 return innerElement;
356 },
357
358 applyMaxPosition: function(maxPosition, oldMaxPosition) {
359 // If a no-op (generated setter tests object identity), return undefined to abort set.
360 if (oldMaxPosition && maxPosition.x === oldMaxPosition.x && maxPosition.y === oldMaxPosition.y) {
361 return;
362 }
363 var translatable = this.getTranslatable(),
364 yEasing;
365
366 // If an animation is in flight...
367 if (translatable.isAnimating) {
368
369 // Find its Y dimension easing object
370 yEasing = translatable.activeEasingY;
371
372 // If it's animating in the -ve direction (scrolling up), and we are
373 // shortening the scroll range, ensure the easing's min point complies
374 // with the new end position.
375 if (yEasing && yEasing.getStartVelocity &&
376 yEasing.getStartVelocity() < 0 && maxPosition.y < oldMaxPosition.y) {
377 yEasing.setMinMomentumValue(-maxPosition.y);
378 }
379 }
380
381 return maxPosition;
382 },
383
384 applyMaxUserPosition: function(maxUserPosition, oldMaxUserPosition) {
385 // If a no-op (generated setter tests object identity), return undefined to abort set.
386 if (oldMaxUserPosition && maxUserPosition.x === oldMaxUserPosition.x && maxUserPosition.y === oldMaxUserPosition.y) {
387 return;
388 }
389 return maxUserPosition;
390 },
391
392 applySize: function(size) {
393 var el = this.getElement(),
394 dom, scrollerDom, x, y;
395
396 if (typeof size === 'number') {
397 x = size;
398 y = size;
399 } else if (size) {
400 x = size.x;
401 y = size.y;
402 }
403
404 if (el && (x == null || y == null)) {
405 dom = el.dom;
406 scrollerDom = this.getInnerElement().dom;
407
408 // using scrollWidth/scrollHeight instead of offsetWidth/offsetHeight ensures
409 // that the size includes any contained absolutely positioned items
410 if (x == null) {
411 x = Math.max(scrollerDom.scrollWidth, dom.clientWidth);
412 }
413
414 if (y == null) {
415 y = Math.max(scrollerDom.scrollHeight, dom.clientHeight);
416 }
417 }
418
419 return {
420 x: x,
421 y: y
422 };
423 },
424
425 applySlotSnapOffset: function(snapOffset) {
426 if (typeof snapOffset === 'number') {
427 snapOffset = {
428 x: snapOffset,
429 y: snapOffset
430 };
431 }
432
433 return snapOffset;
434 },
435
436 applySlotSnapSize: function(snapSize) {
437 if (typeof snapSize === 'number') {
438 snapSize = {
439 x: snapSize,
440 y: snapSize
441 };
442 }
443
444 return snapSize;
445 },
446
447 applySlotSnapEasing: function(easing) {
448 var defaultClass = Ext.fx.easing.EaseOut;
449
450 return {
451 x: Ext.factory(easing, defaultClass),
452 y: Ext.factory(easing, defaultClass)
453 };
454 },
455
456 applyTranslatable: function(config, translatable) {
457 return Ext.factory(config, Ext.util.Translatable, translatable);
458 },
459
460 destroy: function() {
461 var me = this,
462 element = me.getElement(),
463 innerElement = me.getInnerElement(),
464 sizeMonitors = me.sizeMonitors;
465
466 if (sizeMonitors) {
467 sizeMonitors.element.destroy();
468 sizeMonitors.container.destroy();
469 }
470
471 if (element && !element.destroyed) {
472 element.removeCls(me.cls);
473 }
474
475 if (innerElement && !innerElement.destroyed) {
476 innerElement.removeCls(me.scrollerCls);
477 }
478
479 if (me._isWrapped) {
480 if (!element.destroyed) {
481 me.unwrapContent();
482 }
483
484 innerElement.destroy();
485 }
486
487 me.setElement(null);
488 me.setInnerElement(null);
489 me.setIndicators(null);
490
491 Ext.destroy(me.getTranslatable());
492
493 me.callParent();
494 },
495
496 refresh: function(immediate, /* private */ options) {
497 var me = this;
498
499 ++me.refreshCounter;
500 if (immediate) {
501 me.doRefresh(options);
502 }
503 // Schedule a refresh at the next transition to idle.
504 else if (!me.refreshScheduled) {
505 me.scheduleRefresh.args = [options];
506 me.refreshScheduled = Ext.on(me.scheduleRefresh);
507 }
508 },
509
510 updateAutoRefresh: function(autoRefresh) {
511 this.toggleResizeListeners(autoRefresh);
512 },
513
514 updateBounceEasing: function(easing) {
515 this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
516 },
517
518 updateElementSize: function() {
519 if (!this.isConfiguring) {
520 // to avoid multiple calls to refreshAxes() during initialization we will
521 // call it once after initConfig has finished.
522 this.refreshAxes();
523 }
524 },
525
526 updateDisabled: function(disabled) {
527 // attachment of listeners is handled by updateElement during initial config
528 if (!this.isConfiguring) {
529 if (disabled) {
530 this.detachListeners();
531 } else {
532 this.attachListeners();
533 }
534 }
535 },
536
537 updateElement: function(element, oldElement) {
538 var me = this,
539 // first check if the user configured a innerElement
540 innerElement = me.getInnerElement(),
541 listeners, autoRefresh;
542
543 if (!innerElement) {
544 // if no configured scroller element, check if the first child has the
545 // scrollerCls, if so we can assume that the user already wrapped the content
546 // in a scrollerEl (this is true of both Ext and Touch Components).
547 innerElement = element.dom.firstChild;
548
549 if (!innerElement || innerElement.nodeType !== 1 ||
550 !Ext.fly(innerElement).hasCls(me.scrollerCls)) {
551 // no scrollerEl found, generate one now
552 innerElement = me.wrapContent(element);
553 }
554
555 me.setInnerElement(innerElement);
556 }
557
558 element.addCls(me.cls);
559
560 if (me.isConfiguring) {
561 if (!me.getTranslatable().isScrollParent) {
562 // Not using DOM scrolling, clear overflow styles.
563 element.dom.style.overflowX = element.dom.style.overflowY = '';
564
565 // If using full virtual scrolling attach a mousewheel listener for moving
566 // the scroll position. Otherwise we use native scrolling when interacting
567 // using the mouse and so do not want to override the native behavior
568 listeners = me.elementListeners;
569 listeners.mousewheel = 'onMouseWheel';
570 listeners.scroll = {
571 fn: 'onElementScroll',
572 delegated: false,
573 scope: me
574 };
575 }
576 }
577
578 if (!me.getDisabled()) {
579 me.attachListeners();
580 }
581
582 if (!me.isConfiguring) {
583 // setting element after initial construction of Scroller
584 // sync up configs that depend on element
585 autoRefresh = me.getAutoRefresh();
586
587 if (autoRefresh !== false) {
588 me.toggleResizeListeners(autoRefresh);
589
590 if (autoRefresh) {
591 me.refresh();
592 } else if (autoRefresh === null) {
593 // setting elementSize to null will cause it to be updated from the dom
594 me.setElementSize(null);
595 }
596 }
597 }
598 },
599
600 updateFps: function(fps) {
601 if (fps !== 'auto') {
602 this.getTranslatable().setFps(fps);
603 }
604 },
605
606 updateMaxUserPosition: function() {
607 this.snapToBoundary();
608 },
609
610 updateMinUserPosition: function() {
611 this.snapToBoundary();
612 },
613
614 updateInnerElement: function(innerElement) {
615 if (innerElement) {
616 innerElement.addCls(this.scrollerCls);
617 }
618
619 this.getTranslatable().setElement(innerElement);
620 },
621
622 updateSize: function(size) {
623 if (!this.isConfiguring) {
624
625 // Base class keeps the spacer el sized to "stretch" a DOM scroll range.
626 if (Ext.supports.touchScroll === 1) {
627 this.callParent([size]);
628 }
629 // to avoid multiple calls to refreshAxes() during initialization we will
630 // call it once after initConfig has finished.
631 this.refreshAxes();
632 }
633 },
634
635 updateTranslatable: function(translatable) {
636 translatable.setElement(this.getInnerElement());
637
638 // We only need to process scroll in each frame, and scroll end on animation end
639 // if we are using CSS transform.
640 //
641 // If we are using DOM scrollTop/scrollLeft, then
642 // Scroller#onDomScroll will perform these duties.
643 if (!translatable.isScrollParent) {
644 translatable.on({
645 animationframe: 'onAnimationFrame',
646 animationend: 'onAnimationEnd',
647 scope: this
648 });
649 }
650 },
651
652 updateX: function() {
653 if (!this.isConfiguring) {
654 // to avoid multiple calls to refreshAxes() during initialization we will
655 // call it once after initConfig has finished.
656 this.refreshAxes();
657 }
658 },
659
660 updateY: function() {
661 if (!this.isConfiguring) {
662 // to avoid multiple calls to refreshAxes() during initialization we will
663 // call it once after initConfig has finished.
664 this.refreshAxes();
665 }
666 },
667
668 privates: {
669 attachListeners: function() {
670 this.getElement().on(this.elementListeners);
671 },
672
673 constrainX: function(x) {
674 return Math.min(this.getMaxPosition().x, Math.max(x, 0));
675 },
676
677 constrainY: function(y) {
678 return Math.min(this.getMaxPosition().y, Math.max(y, 0));
679 },
680
681 // overridden in RTL mode to swap min/max momentum values
682 convertEasingConfig: function(config) {
683 return config;
684 },
685
686 detachListeners: function() {
687 this.getElement().un(this.elementListeners);
688 },
689
690 /**
691 * @private
692 */
693 doRefresh: function(options) {
694 var me = this,
695 size, elementSize;
696
697 if (me.refreshScheduled) {
698 me.refreshScheduled = me.refreshScheduled.destroy();
699 }
700
701 if (me.refreshCounter && me.getElement()) {
702 me.stopAnimation();
703
704 me.getTranslatable().refresh();
705
706 if (options) {
707 // If either size or elementSize were provided in options, do not bother
708 // to read the DOM to determine sizing info, just set the size given.
709 size = options.size;
710 elementSize = options.elementSize;
711 }
712
713 // If size or elementSize are null, they will be read from the DOM
714 me.setSize(size);
715 me.setElementSize(elementSize);
716
717 me.fireEvent('refresh', me);
718 me.refreshCounter = 0;
719 }
720 },
721
722 doScrollTo: function(x, y, animation, /* private */ allowOverscroll) {
723 var me = this,
724 isDragging = me.isDragging,
725 // We only call onScroll if we are programatically CSS translating the scrollable.
726 // If we are using DOM scrollTop/scrollLeft, then
727 // Scroller#onDomScroll will perform this duty.
728 DOMScrolling = me.getTranslatable().isScrollParent,
729 // We only fire scrollstart, and scrollend if we are not reflecting,
730 // (and not having this handled by Scroller#onDomScroll)
731 // If we are reflecting, then onPartnerScrollStart and onPartnerScrollEnd
732 // will perform these duties.
733 fireStartEnd = !me.isReflecting && !DOMScrolling;
734
735 if (me.destroyed || !me.getElement()) {
736 return me;
737 }
738
739 // Normally the scroll position is constrained to the max scroll position, but
740 // during a drag operation or during reflection the scroller is allowed to overscroll.
741 allowOverscroll = allowOverscroll || me.isDragging;
742
743 var translatable = me.getTranslatable(),
744 position = me.position,
745 positionChanged = false,
746 translationX, translationY;
747
748 if (!isDragging || me.isAxisEnabled('x')) {
749 if (isNaN(x) || typeof x !== 'number') {
750 x = position.x;
751 } else {
752
753 if (!allowOverscroll) {
754 x = me.constrainX(x);
755 }
756
757 if (position.x !== x) {
758 position.x = x;
759 positionChanged = true;
760 }
761 }
762
763 translationX = me.convertX(-x);
764 }
765
766 if (!isDragging || me.isAxisEnabled('y')) {
767 if (isNaN(y) || typeof y !== 'number') {
768 y = position.y;
769 } else {
770 if (!allowOverscroll) {
771 y = me.constrainY(y);
772 }
773
774 if (position.y !== y) {
775 position.y = y;
776 positionChanged = true;
777 }
778 }
779
780 translationY = -y;
781 }
782
783 if (positionChanged) {
784 // If we are using scrollTop/scrollLeft, then DOM events will fire
785 // and Scroller#onDomScroll will perform scroll start processing
786 if (fireStartEnd) {
787 // Invoke scroll start handlers.
788 // If we are already scrolling, and this movement
789 // is in response to a subsequent touchmove, the this.isScrolling
790 // flag will already be set, and this will do nothing.
791 me.onScrollStart();
792 }
793 if (animation) {
794 // onAnimationEnd calls onScrollEnd
795 translatable.translateAnimated(translationX, translationY, animation);
796 } else {
797 // If we are using scrollTop/scrollLeft, then DOM events will fire
798 // and Scroller#onDomScroll will perform scroll processing
799 if (!DOMScrolling) {
800 me.onScroll();
801 }
802 translatable.translate(translationX, translationY);
803 // If we are using scrollTop/scrollLeft, then DOM events will fire
804 // and Scroller#onDomScroll will schedule scroll end processing
805 if (fireStartEnd) {
806 me.onScrollEnd();
807 }
808 }
809 } else if (animation && animation.callback) {
810 animation.callback();
811 }
812
813 return me;
814 },
815
816 /**
817 * @private
818 */
819 getAnimationEasing: function(axis, e) {
820 if (!this.isAxisEnabled(axis)) {
821 return null;
822 }
823
824 var me = this,
825 currentPosition = me.position[axis],
826 minPosition = me.getMinUserPosition()[axis],
827 maxPosition = me.getMaxUserPosition()[axis],
828 maxAbsVelocity = me.getMaxAbsoluteVelocity(),
829 boundValue = null,
830 dragEndTime = me.dragEndTime,
831 velocity = e.flick.velocity[axis],
832 isX = axis === 'x',
833 easingConfig, easing;
834
835 if (currentPosition < minPosition) {
836 boundValue = minPosition;
837 }
838 else if (currentPosition > maxPosition) {
839 boundValue = maxPosition;
840 }
841
842 if (isX) {
843 currentPosition = me.convertX(currentPosition);
844 boundValue = me.convertX(boundValue);
845 }
846
847 // Out of bound, to be pulled back
848 if (boundValue !== null) {
849 easing = me.getBounceEasing()[axis];
850 easing.setConfig({
851 startTime: dragEndTime,
852 startValue: -currentPosition,
853 endValue: -boundValue
854 });
855
856 return easing;
857 }
858
859 if (velocity === 0) {
860 return null;
861 }
862
863 if (velocity < -maxAbsVelocity) {
864 velocity = -maxAbsVelocity;
865 }
866 else if (velocity > maxAbsVelocity) {
867 velocity = maxAbsVelocity;
868 }
869
870 easing = me.getMomentumEasing()[axis];
871 easingConfig = {
872 startTime: dragEndTime,
873 startValue: -currentPosition,
874 startVelocity: velocity * 1.5,
875 minMomentumValue: -maxPosition,
876 maxMomentumValue: 0
877 };
878
879 if (isX) {
880 me.convertEasingConfig(easingConfig);
881 }
882
883 easing.setConfig(easingConfig);
884
885 return easing;
886 },
887
888 /**
889 * @private
890 * @return {Number/null}
891 */
892 getSnapPosition: function(axis) {
893 var me = this,
894 snapSize = me.getSlotSnapSize()[axis],
895 snapPosition = null,
896 position, snapOffset, maxPosition, mod;
897
898 if (snapSize !== 0 && me.isAxisEnabled(axis)) {
899 position = me.position[axis];
900 snapOffset = me.getSlotSnapOffset()[axis];
901 maxPosition = me.getMaxUserPosition()[axis];
902
903 mod = Math.floor((position - snapOffset) % snapSize);
904
905 if (mod !== 0) {
906 if (position !== maxPosition) {
907 if (Math.abs(mod) > snapSize / 2) {
908 snapPosition = Math.min(maxPosition, position + ((mod > 0) ? snapSize - mod : mod - snapSize));
909 }
910 else {
911 snapPosition = position - mod;
912 }
913 }
914 else {
915 snapPosition = position - mod;
916 }
917 }
918 }
919
920 return snapPosition;
921 },
922
923 hideIndicators: function() {
924 var me = this,
925 indicators = me.getIndicators(),
926 xIndicator, yIndicator;
927
928 if (indicators) {
929 if (me.isAxisEnabled('x')) {
930 xIndicator = indicators.x;
931 if (xIndicator) {
932 xIndicator.hide();
933 }
934 }
935
936 if (me.isAxisEnabled('y')) {
937 yIndicator = indicators.y;
938 if (yIndicator) {
939 yIndicator.hide();
940 }
941 }
942 }
943 },
944
945 /**
946 * Returns `true` if a specified axis is enabled.
947 * @private
948 * @param {String} axis The axis to check (`x` or `y`).
949 * @return {Boolean} `true` if the axis is enabled.
950 */
951 isAxisEnabled: function(axis) {
952 this.getX();
953 this.getY();
954
955 return this.isAxisEnabledFlags[axis];
956 },
957
958 onAnimationEnd: function() {
959 this.snapToBoundary();
960 this.onScrollEnd();
961 },
962
963 onAnimationFrame: function(translatable, x, y) {
964 var position = this.position;
965
966 position.x = this.convertX(-x);
967 position.y = -y;
968
969 this.onScroll();
970 },
971
972 onAxisDrag: function(axis, delta) {
973 // Nothing to do if no delta, or it's on a disabled axis
974 if (delta && this.isAxisEnabled(axis)) {
975 var me = this,
976 flickStartPosition = me.flickStartPosition,
977 flickStartTime = me.flickStartTime,
978 lastDragPosition = me.lastDragPosition,
979 dragDirection = me.dragDirection,
980 old = me.position[axis],
981 min = me.getMinUserPosition()[axis],
982 max = me.getMaxUserPosition()[axis],
983 start = me.startPosition[axis],
984 last = lastDragPosition[axis],
985 current = start - delta,
986 lastDirection = dragDirection[axis],
987 restrictFactor = me.getOutOfBoundRestrictFactor(),
988 startMomentumResetTime = me.getStartMomentumResetTime(),
989 now = Ext.Date.now(),
990 distance;
991
992 if (current < min) {
993 current *= restrictFactor;
994 }
995 else if (current > max) {
996 distance = current - max;
997 current = max + distance * restrictFactor;
998 }
999
1000 if (current > last) {
1001 dragDirection[axis] = 1;
1002 }
1003 else if (current < last) {
1004 dragDirection[axis] = -1;
1005 }
1006
1007 if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection)) ||
1008 (now - flickStartTime[axis]) > startMomentumResetTime) {
1009 flickStartPosition[axis] = old;
1010 flickStartTime[axis] = now;
1011 }
1012
1013 lastDragPosition[axis] = current;
1014 return true;
1015 }
1016 },
1017
1018 // In "hybrid" touch scroll mode where the TouchScroller is used to control the
1019 // scroll position of a naturally overflowing element, we need to sync the scroll
1020 // position of the TouchScroller when the element is scrolled
1021 onDomScroll: function() {
1022 var me = this,
1023 dom, position;
1024
1025 if (me.getTranslatable().isScrollParent) {
1026 dom = me.getElement().dom;
1027 position = me.position;
1028
1029 position.x = dom.scrollLeft;
1030 position.y = dom.scrollTop;
1031 }
1032 me.callParent();
1033 },
1034
1035 onDrag: function(e) {
1036 var me = this,
1037 lastDragPosition = me.lastDragPosition;
1038
1039 if (!me.isDragging) {
1040 return;
1041 }
1042
1043 // If there's any moving to do, then move the content.
1044 // Boolean or operator avoids shortcutting the second function call if
1045 // first returns true.
1046 if (me.onAxisDrag('x', me.convertX(e.deltaX)) | me.onAxisDrag('y', e.deltaY)) {
1047 me.doScrollTo(lastDragPosition.x, lastDragPosition.y);
1048 }
1049 },
1050
1051 onDragEnd: function(e) {
1052 var me = this,
1053 easingX, easingY;
1054
1055 if (!me.isDragging) {
1056 return;
1057 }
1058
1059 me.dragEndTime = Ext.Date.now();
1060
1061 me.onDrag(e);
1062
1063 me.isDragging = false;
1064
1065 easingX = me.getAnimationEasing('x', e);
1066 easingY = me.getAnimationEasing('y', e);
1067
1068 if (easingX || easingY) {
1069 me.getTranslatable().animate(easingX, easingY);
1070 } else {
1071 me.onScrollEnd();
1072 }
1073 },
1074
1075 onDragStart: function(e) {
1076 var me = this,
1077 direction = me.getDirection(),
1078 absDeltaX = e.absDeltaX,
1079 absDeltaY = e.absDeltaY,
1080 directionLock = me.getDirectionLock(),
1081 startPosition = me.startPosition,
1082 flickStartPosition = me.flickStartPosition,
1083 flickStartTime = me.flickStartTime,
1084 lastDragPosition = me.lastDragPosition,
1085 currentPosition = me.position,
1086 dragDirection = me.dragDirection,
1087 x = currentPosition.x,
1088 y = currentPosition.y,
1089 now = Ext.Date.now();
1090
1091 if (directionLock && direction !== 'both') {
1092 if ((direction === 'horizontal' && absDeltaX > absDeltaY) ||
1093 (direction === 'vertical' && absDeltaY > absDeltaX)) {
1094 e.stopPropagation();
1095 }
1096 else {
1097 return;
1098 }
1099 }
1100
1101 lastDragPosition.x = x;
1102 lastDragPosition.y = y;
1103
1104 flickStartPosition.x = x;
1105 flickStartPosition.y = y;
1106
1107 startPosition.x = x;
1108 startPosition.y = y;
1109
1110 flickStartTime.x = now;
1111 flickStartTime.y = now;
1112
1113 dragDirection.x = 0;
1114 dragDirection.y = 0;
1115
1116 me.dragStartTime = now;
1117
1118 me.isDragging = true;
1119
1120 // Only signal a scroll start if we are not already scrolling.
1121 // If the drag is just the user giving another impulse, it is NOT
1122 // the start of a drag.
1123 if (!me.isScrolling) {
1124 me.onScrollStart();
1125 }
1126 },
1127
1128 onElementResize: function(element, info) {
1129 this.refresh(true, {
1130 elementSize: {
1131 x: info.contentWidth,
1132 y: info.contentHeight
1133 },
1134 size: this.getAutoRefresh() ? null : this.getSize()
1135 });
1136 },
1137
1138 onElementScroll: function(event, targetEl) {
1139 targetEl.scrollTop = targetEl.scrollLeft = 0;
1140 },
1141
1142 onEvent: function(e) {
1143 // use browserEvent to get the "real" type of DOM event that was fired, not a
1144 // potentially translated (or recognized) type
1145 var me = this,
1146 browserEvent = e.browserEvent;
1147
1148 if ((!me.self.isTouching || me.isTouching) && // prevents nested scrolling
1149 // prevents scrolling in response to mouse input on multi-input devices
1150 // such as windows 8 laptops with touch screens.
1151 // Don't bother checking the event type if we are on a device that uses
1152 // full virtual scrolling (!isScrollParent)
1153 // TODO: this should be handled by the event system once EXTJSIV-12840
1154 // is implemented
1155 ((!me.getTranslatable().isScrollParent) || (!me.isMouseEvent[browserEvent.type] &&
1156 browserEvent.pointerType !== 'mouse')) &&
1157 (me.getY() || me.getX())) {
1158 me[me.listenerMap[e.type]](e);
1159 }
1160 },
1161
1162 onInnerElementResize: function(element, info) {
1163 this.refresh(true, {
1164 size: {
1165 x: info.width,
1166 y: info.height
1167 }
1168 });
1169 },
1170
1171 onMouseWheel: function(e) {
1172 var me = this,
1173 delta = e.getWheelDeltas(),
1174 deltaX = -delta.x,
1175 deltaY = -delta.y,
1176 position = me.position,
1177 maxPosition = me.getMaxUserPosition(),
1178 minPosition = me.getMinUserPosition(),
1179 max = Math.max,
1180 min = Math.min,
1181 positionX = max(min(position.x + deltaX, maxPosition.x), minPosition.x),
1182 positionY = max(min(position.y + deltaY, maxPosition.y), minPosition.y);
1183
1184 deltaX = positionX - position.x;
1185 deltaY = positionY - position.y;
1186
1187 if (!deltaX && !deltaY) {
1188 return;
1189 }
1190 e.stopEvent();
1191
1192 me.onScrollStart();
1193 me.scrollBy(deltaX, deltaY);
1194 me.onScroll();
1195 me.onScrollEnd();
1196 },
1197
1198 onPartnerScrollEnd: function(x, y) {
1199 var me = this;
1200
1201 // In "hybrid" touch scroll mode where the TouchScroller is used to control the
1202 // scroll position of a naturally overflowing element, we do NOT need to call
1203 // fireScrollEnd because we will have been recieving DOM scroll events and will
1204 // begin and end scrolling in response to those events.
1205 //
1206 // If scrollers programatically animate the scroll, then we must take the correct
1207 // scroll end action when our partner ends its scroll.
1208 if (!me.getTranslatable().isScrollParent) {
1209 me.fireScrollEnd(x, y);
1210 }
1211 me.callParent([x, y]);
1212 me.isScrolling = false;
1213 me.hideIndicators();
1214 },
1215
1216 onPartnerScrollStart: function(x, y) {
1217 var me = this;
1218
1219 me.isScrolling = true;
1220
1221 // In "hybrid" touch scroll mode where the TouchScroller is used to control the
1222 // scroll position of a naturally overflowing element, we do NOT need to call
1223 // fireScrollStart because we will soon start recieving DOM scroll events and will
1224 // begin and end scrolling in response to those events.
1225 //
1226 // If scrollers programatically animate the scroll, then we must take the correct
1227 // scroll end action when our partner starts its scroll.
1228 if (!me.getTranslatable().isScrollParent) {
1229 me.fireScrollStart(x, y);
1230 }
1231 me.showIndicators();
1232 },
1233
1234 onScroll: function() {
1235 var me = this,
1236 position = me.position,
1237 x = position.x,
1238 y = position.y,
1239 indicators = me.getIndicators(),
1240 xIndicator, yIndicator;
1241
1242 if (indicators) {
1243 if (me.isAxisEnabled('x')) {
1244 xIndicator = indicators.x;
1245 if (xIndicator) {
1246 xIndicator.setValue(x);
1247 }
1248 }
1249 if (me.isAxisEnabled('y')) {
1250 yIndicator = indicators.y;
1251 if (yIndicator) {
1252 yIndicator.setValue(y);
1253 }
1254 }
1255 }
1256
1257 me.fireScroll(x, y);
1258 },
1259
1260 onScrollEnd: function() {
1261 var me = this,
1262 position = me.position;
1263
1264 if (me.isScrolling && !me.isTouching && !me.snapToSlot()) {
1265 me.hideIndicators();
1266 me.isScrolling = Ext.isScrolling = false;
1267 me.fireScrollEnd(position.x, position.y);
1268 }
1269 },
1270
1271 onScrollStart: function() {
1272 var me = this,
1273 position = me.position;
1274
1275 if (!me.isScrolling) {
1276 me.showIndicators();
1277 me.isScrolling = Ext.isScrolling = true;
1278 me.fireScrollStart(position.x, position.y);
1279 }
1280 },
1281
1282 onTouchEnd: function() {
1283 var me = this;
1284
1285 me.isTouching = me.self.isTouching = false;
1286
1287 if (!me.isDragging && me.snapToSlot()) {
1288 me.onScrollStart();
1289 }
1290 },
1291
1292 onTouchMove: function(e) {
1293 // Prevents the page from scrolling while an element is being scrolled using
1294 // the TouchScroller. Only needed when inside a page that does not use a
1295 // Viewport, since the Viewport already prevents default behavior of touchmove
1296 e.preventDefault();
1297 },
1298
1299 onTouchStart: function() {
1300 var me = this;
1301
1302 me.isTouching = me.self.isTouching = true;
1303
1304 Ext.getDoc().on({
1305 touchend: 'onTouchEnd',
1306 scope: me,
1307 single: true
1308 });
1309
1310 me.stopAnimation();
1311 },
1312
1313 refreshAxes: function() {
1314 var me = this,
1315 flags = me.isAxisEnabledFlags,
1316 size = me.getSize(),
1317 elementSize = me.getElementSize(),
1318 indicators = me.getIndicators(),
1319 maxX, maxY, x, y, xIndicator, yIndicator;
1320
1321 if (!size || !elementSize) {
1322 return;
1323 }
1324
1325 maxX = Math.max(0, size.x - elementSize.x);
1326 maxY = Math.max(0, size.y - elementSize.y);
1327 x = me.getX();
1328 y = me.getY();
1329
1330 me.setMaxPosition({
1331 x: maxX,
1332 y: maxY
1333 });
1334
1335 if (x === true || x === 'auto') {
1336 // auto scroll - axis is only enabled if the content is overflowing in the
1337 // same direction
1338 flags.x = !!maxX;
1339 } else if (x === false) {
1340 flags.x = false;
1341 xIndicator = indicators && indicators.x;
1342 if (xIndicator) {
1343 // hide the x indicator if the x axis is disabled, just in case we
1344 // are refreshing during a scroll
1345 xIndicator.hide();
1346 }
1347 } else if (x === 'scroll') {
1348 flags.x = true;
1349 }
1350
1351 if (y === true || y === 'auto') {
1352 // auto scroll - axis is only enabled if the content is overflowing in the
1353 // same direction
1354 flags.y = !!maxY;
1355 } else if (y === false) {
1356 flags.y = false;
1357 yIndicator = indicators && indicators.y;
1358 if (yIndicator) {
1359 // hide the y indicator if the y axis is disabled, just in case we
1360 // are refreshing during a scroll
1361 yIndicator.hide();
1362 }
1363 } else if (y === 'scroll') {
1364 flags.y = true;
1365 }
1366
1367 me.setMaxUserPosition({
1368 x: flags.x ? maxX : 0,
1369 y: flags.y ? maxY : 0
1370 });
1371
1372 // If we are using regular DOM overflow scrolling, sync the element styles.
1373 if (Ext.supports.touchScroll === 1) {
1374 me.initXStyle();
1375 me.initYStyle();
1376 }
1377 },
1378
1379 showIndicators: function() {
1380 var me = this,
1381 indicators = me.getIndicators(),
1382 xIndicator, yIndicator;
1383
1384 if (indicators) {
1385 if (me.isAxisEnabled('x')) {
1386 xIndicator = indicators.x;
1387 if (xIndicator) {
1388 xIndicator.show();
1389 }
1390 }
1391
1392 if (me.isAxisEnabled('y')) {
1393 yIndicator = indicators.y;
1394 if (yIndicator) {
1395 yIndicator.show();
1396 }
1397 }
1398 }
1399 },
1400
1401 snapToBoundary: function() {
1402 var me = this,
1403 position = me.getPosition();
1404
1405 // If we haven't scrolled anywhere, we're done.
1406 if (me.isConfiguring || !(position.x || position.y)) {
1407 return;
1408 }
1409
1410 var minPosition = me.getMinUserPosition(),
1411 maxPosition = me.getMaxUserPosition(),
1412 minX = minPosition.x,
1413 minY = minPosition.y,
1414 maxX = maxPosition.x,
1415 maxY = maxPosition.y,
1416 x = Math.round(position.x),
1417 y = Math.round(position.y);
1418
1419 if (x < minX) {
1420 x = minX;
1421 }
1422 else if (x > maxX) {
1423 x = maxX;
1424 }
1425
1426 if (y < minY) {
1427 y = minY;
1428 }
1429 else if (y > maxY) {
1430 y = maxY;
1431 }
1432
1433 me.doScrollTo(x, y);
1434 },
1435
1436 /**
1437 * @private
1438 * @return {Boolean}
1439 */
1440 snapToSlot: function() {
1441 var me = this,
1442 snapX = me.getSnapPosition('x'),
1443 snapY = me.getSnapPosition('y'),
1444 easing = me.getSlotSnapEasing();
1445
1446 if (snapX !== null || snapY !== null) {
1447 me.doScrollTo(snapX, snapY, {
1448 easingX: easing.x,
1449 easingY: easing.y
1450 });
1451
1452 return true;
1453 }
1454
1455 return false;
1456 },
1457
1458 /**
1459 * @private
1460 * Stops the animation of the scroller at any time.
1461 */
1462 stopAnimation: function() {
1463 this.getTranslatable().stopAnimation();
1464 },
1465
1466 toggleResizeListeners: function(autoRefresh) {
1467 var me = this,
1468 element = me.getElement(),
1469 method, innerMethod,
1470 innerElement;
1471
1472 if (element) {
1473 innerElement = me.getInnerElement();
1474 if (autoRefresh) {
1475 method = innerMethod = 'on';
1476 } else if (autoRefresh === null) {
1477 method = 'on';
1478 innerMethod = 'un';
1479 } else {
1480 method = innerMethod = 'un';
1481 }
1482
1483 element[method]('resize', 'onElementResize', me);
1484 innerElement[innerMethod]('resize', 'onInnerElementResize', me);
1485 }
1486 },
1487
1488 unwrapContent: function() {
1489 var innerDom = this.getInnerElement().dom,
1490 dom = this.getElement().dom,
1491 child;
1492
1493 while ((child = innerDom.firstChild)) {
1494 dom.insertBefore(child, innerDom);
1495 }
1496 },
1497
1498 /**
1499 * Wraps the element's content in a innerElement
1500 * @param {Ext.dom.Element} element
1501 * @return {Ext.dom.Element} the innerElement
1502 * @private
1503 */
1504 wrapContent: function(element) {
1505 var wrap = document.createElement('div'),
1506 dom = element.dom,
1507 child;
1508
1509 while (child = dom.lastChild) { // jshint ignore:line
1510 wrap.insertBefore(child, wrap.firstChild);
1511 }
1512
1513 dom.appendChild(wrap);
1514
1515 this.setInnerElement(wrap);
1516
1517 // Set a flag that indiacates the element's content was not already pre-wrapped
1518 // when the scroller was instanced. This means we had to wrap the content
1519 // and so must unwrap when we destroy the scroller.
1520 this._isWrapped = true;
1521
1522 return this.getInnerElement();
1523 }
1524 }
1525 });