]> git.proxmox.com Git - sencha-touch.git/blob - src/src/viewport/Default.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / viewport / Default.js
1 /**
2 * @private
3 * Base class for iOS and Android viewports.
4 */
5 Ext.define('Ext.viewport.Default', {
6 extend: 'Ext.Container',
7
8 xtype: 'viewport',
9
10 PORTRAIT: 'portrait',
11
12 LANDSCAPE: 'landscape',
13
14 requires: [
15 'Ext.LoadMask',
16 'Ext.layout.Card',
17 'Ext.util.InputBlocker'
18 ],
19
20 /**
21 * @event ready
22 * Fires when the Viewport is in the DOM and ready.
23 * @param {Ext.Viewport} this
24 */
25
26 /**
27 * @event maximize
28 * Fires when the Viewport is maximized.
29 * @param {Ext.Viewport} this
30 */
31
32 /**
33 * @event orientationchange
34 * Fires when the Viewport orientation has changed.
35 * @param {Ext.Viewport} this
36 * @param {String} newOrientation The new orientation.
37 * @param {Number} width The width of the Viewport.
38 * @param {Number} height The height of the Viewport.
39 */
40
41 config: {
42 /**
43 * @private
44 */
45 autoMaximize: false,
46
47 /**
48 * @private
49 */
50 autoBlurInput: true,
51
52 /**
53 * @cfg {Boolean} preventPanning
54 * Whether or not to always prevent default panning behavior of the
55 * browser's viewport.
56 * @accessor
57 */
58 preventPanning: true,
59
60 /**
61 * @cfg {Boolean} preventZooming
62 * `true` to attempt to stop zooming when you double tap on the screen on mobile devices,
63 * typically HTC devices with HTC Sense UI.
64 * @accessor
65 */
66 preventZooming: false,
67
68 /**
69 * @cfg
70 * @private
71 */
72 autoRender: true,
73
74 /**
75 * @cfg {Object/String} layout Configuration for this Container's layout. Example:
76 *
77 * Ext.create('Ext.Container', {
78 * layout: {
79 * type: 'hbox',
80 * align: 'middle'
81 * },
82 * items: [
83 * {
84 * xtype: 'panel',
85 * flex: 1,
86 * style: 'background-color: red;'
87 * },
88 * {
89 * xtype: 'panel',
90 * flex: 2,
91 * style: 'background-color: green'
92 * }
93 * ]
94 * });
95 *
96 * See the [layouts guide](../../../core_concepts/layouts.html) for more information.
97 *
98 * @accessor
99 */
100 layout: 'card',
101
102 /**
103 * @cfg
104 * @private
105 */
106 width: '100%',
107
108 /**
109 * @cfg
110 * @private
111 */
112 height: '100%',
113
114 useBodyElement: true,
115
116 /**
117 * An object of all the menus on this viewport.
118 * @private
119 */
120 menus: {},
121
122 /**
123 * @private
124 */
125 orientation: null
126 },
127
128 getElementConfig: function() {
129 var cfg = this.callParent(arguments);
130
131 // Used in legacy browser that do not support matchMedia. Hidden element is used for checking of orientation
132 if (!Ext.feature.has.MatchMedia) {
133 cfg.children.unshift({reference: 'orientationElement', className: 'x-orientation-inspector'});
134 }
135 return cfg;
136 },
137
138 /**
139 * @property {Boolean} isReady
140 * `true` if the DOM is ready.
141 */
142 isReady: false,
143
144 isViewport: true,
145
146 isMaximizing: false,
147
148 id: 'ext-viewport',
149
150 isInputRegex: /^(input|textarea|select|a)$/i,
151
152 isInteractiveWebComponentRegEx: /^(audio|video)$/i,
153
154 focusedElement: null,
155
156 /**
157 * @private
158 */
159 fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',
160
161 constructor: function(config) {
162 var bind = Ext.Function.bind;
163
164 this.doPreventPanning = bind(this.doPreventPanning, this);
165 this.doPreventZooming = bind(this.doPreventZooming, this);
166 this.doBlurInput = bind(this.doBlurInput, this);
167
168 this.maximizeOnEvents = [
169 'ready',
170 'orientationchange'
171 ];
172
173 // set default devicePixelRatio if it is not explicitly defined
174 window.devicePixelRatio = window.devicePixelRatio || 1;
175
176 this.callSuper([config]);
177
178 this.windowWidth = this.getWindowWidth();
179 this.windowHeight = this.getWindowHeight();
180 this.windowOuterHeight = this.getWindowOuterHeight();
181
182 if (!this.stretchHeights) {
183 this.stretchHeights = {};
184 }
185
186 if(Ext.feature.has.OrientationChange) {
187 this.addWindowListener('orientationchange', bind(this.onOrientationChange, this));
188 } else {
189 this.addWindowListener('resize', bind(this.onResize, this));
190 }
191
192 document.addEventListener('focus', bind(this.onElementFocus, this), true);
193 document.addEventListener('blur', bind(this.onElementBlur, this), true);
194
195 Ext.onDocumentReady(this.onDomReady, this);
196
197 this.on('ready', this.onReady, this, {single: true});
198 this.getEventDispatcher().addListener('component', '*', 'fullscreen', 'onItemFullscreenChange', this);
199 return this;
200 },
201
202 onDomReady: function() {
203 this.isReady = true;
204 this.updateSize();
205 this.fireEvent('ready', this);
206 },
207
208 onReady: function() {
209 if (this.getAutoRender()) {
210 this.render();
211 }
212 if (Ext.browser.name == 'ChromeiOS') {
213 this.setHeight('-webkit-calc(100% - ' + ((window.outerHeight - window.innerHeight) / 2) + 'px)');
214 }
215 },
216
217 onElementFocus: function(e) {
218 this.focusedElement = e.target;
219 },
220
221 onElementBlur: function() {
222 this.focusedElement = null;
223 },
224
225 render: function() {
226 if (!this.rendered) {
227 var body = Ext.getBody(),
228 clsPrefix = Ext.baseCSSPrefix,
229 classList = [],
230 osEnv = Ext.os,
231 osName = osEnv.name.toLowerCase(),
232 browserName = Ext.browser.name.toLowerCase(),
233 osMajorVersion = osEnv.version.getMajor();
234
235 this.renderTo(body);
236
237 classList.push(clsPrefix + osEnv.deviceType.toLowerCase());
238
239 if (osEnv.is.iPad) {
240 classList.push(clsPrefix + 'ipad');
241 }
242
243 classList.push(clsPrefix + osName);
244 classList.push(clsPrefix + browserName);
245
246 if (osMajorVersion) {
247 classList.push(clsPrefix + osName + '-' + osMajorVersion);
248 }
249
250 if (osEnv.is.BlackBerry) {
251 classList.push(clsPrefix + 'bb');
252 if (Ext.browser.userAgent.match(/Kbd/gi)) {
253 classList.push(clsPrefix + 'bb-keyboard');
254 }
255 }
256
257 if (Ext.browser.is.WebKit) {
258 classList.push(clsPrefix + 'webkit');
259 }
260
261 if (Ext.browser.is.Standalone) {
262 classList.push(clsPrefix + 'standalone');
263 }
264
265 if (Ext.browser.is.AndroidStock) {
266 classList.push(clsPrefix + 'android-stock');
267 }
268
269 if (Ext.browser.is.GoogleGlass) {
270 classList.push(clsPrefix + 'google-glass');
271 }
272
273 this.setOrientation(this.determineOrientation());
274 classList.push(clsPrefix + this.getOrientation());
275 body.addCls(classList);
276 }
277 },
278
279 applyAutoBlurInput: function(autoBlurInput) {
280 var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
281
282 if (autoBlurInput) {
283 this.addWindowListener(touchstart, this.doBlurInput, false);
284 }
285 else {
286 this.removeWindowListener(touchstart, this.doBlurInput, false);
287 }
288
289 return autoBlurInput;
290 },
291
292 applyAutoMaximize: function(autoMaximize) {
293 if (Ext.browser.is.WebView) {
294 autoMaximize = false;
295 }
296 if (autoMaximize) {
297 this.on('ready', 'doAutoMaximizeOnReady', this, { single: true });
298 this.on('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
299 }
300 else {
301 this.un('ready', 'doAutoMaximizeOnReady', this);
302 this.un('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
303 }
304
305 return autoMaximize;
306 },
307
308 applyPreventPanning: function(preventPanning) {
309 if (preventPanning) {
310 this.addWindowListener('touchmove', this.doPreventPanning, false);
311 }
312 else {
313 this.removeWindowListener('touchmove', this.doPreventPanning, false);
314 }
315
316 return preventPanning;
317 },
318
319 applyPreventZooming: function(preventZooming) {
320 var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
321
322 if (preventZooming) {
323 this.addWindowListener(touchstart, this.doPreventZooming, false);
324 }
325 else {
326 this.removeWindowListener(touchstart, this.doPreventZooming, false);
327 }
328
329 return preventZooming;
330 },
331
332 doAutoMaximizeOnReady: function() {
333 var controller = arguments[arguments.length - 1];
334
335 controller.pause();
336
337 this.isMaximizing = true;
338
339 this.on('maximize', function() {
340 this.isMaximizing = false;
341
342 this.updateSize();
343
344 controller.resume();
345
346 this.fireEvent('ready', this);
347 }, this, { single: true });
348
349 this.maximize();
350 },
351
352 doAutoMaximizeOnOrientationChange: function() {
353 var controller = arguments[arguments.length - 1],
354 firingArguments = controller.firingArguments;
355
356 controller.pause();
357
358 this.isMaximizing = true;
359
360 this.on('maximize', function() {
361 this.isMaximizing = false;
362
363 this.updateSize();
364
365 firingArguments[2] = this.windowWidth;
366 firingArguments[3] = this.windowHeight;
367
368 controller.resume();
369 }, this, { single: true });
370
371 this.maximize();
372 },
373
374 doBlurInput: function(e) {
375 var target = e.target,
376 focusedElement = this.focusedElement;
377 //In IE9/10 browser window loses focus and becomes inactive if focused element is <body>. So we shouldn't call blur for <body>
378 if (focusedElement && focusedElement.nodeName.toUpperCase() != 'BODY' && !this.isInputRegex.test(target.tagName)) {
379 delete this.focusedElement;
380 focusedElement.blur();
381 }
382 },
383
384 doPreventPanning: function(e) {
385 var target = e.target, touch;
386
387 // If we have an interaction on a WebComponent we need to check the actual shadow dom element selected
388 // to determine if it is an input before preventing default behavior
389 // Side effect to this is if the shadow input does not do anything with 'touchmove' the user could pan
390 // the screen.
391 if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
392 touch = e.touches[0];
393 if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
394 return;
395 }
396 }
397
398 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
399 e.preventDefault();
400 }
401 },
402
403 doPreventZooming: function(e) {
404 // Don't prevent right mouse event
405 if ('button' in e && e.button !== 0) {
406 return;
407 }
408
409 var target = e.target, touch;
410 if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
411 touch = e.touches[0];
412 if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
413 return;
414 }
415 }
416
417 if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
418 e.preventDefault();
419 }
420 },
421
422 addWindowListener: function(eventName, fn, capturing) {
423 window.addEventListener(eventName, fn, Boolean(capturing));
424 },
425
426 removeWindowListener: function(eventName, fn, capturing) {
427 window.removeEventListener(eventName, fn, Boolean(capturing));
428 },
429
430 doAddListener: function(eventName, fn, scope, options) {
431 if (eventName === 'ready' && this.isReady && !this.isMaximizing) {
432 fn.call(scope);
433 return this;
434 }
435
436 return this.callSuper(arguments);
437 },
438
439 determineOrientation: function() {
440 // First attempt will be to use Native Orientation information
441 if (Ext.feature.has.Orientation) {
442 var nativeOrientation= this.getWindowOrientation();
443 // 90 || -90 || 270 is landscape
444 if (Math.abs(nativeOrientation) === 90 || nativeOrientation === 270) {
445 return this.LANDSCAPE;
446 } else {
447 return this.PORTRAIT;
448 }
449 // Second attempt will be to use MatchMedia and a media query
450 } else if (Ext.feature.has.MatchMedia) {
451 return window.matchMedia('(orientation : landscape)').matches ? this.LANDSCAPE : this.PORTRAIT;
452 // Fall back on hidden element with media query attached to it (media query in Base Theme)
453 } else if (this.orientationElement) {
454 return this.orientationElement.getStyle('content');
455 }
456 },
457
458 updateOrientation: function(newValue, oldValue) {
459 if (oldValue) {
460 this.fireOrientationChangeEvent(newValue, oldValue);
461 }
462 },
463
464 /**
465 * Listener for Orientation Change in environments that support orientationchange events
466 * @private
467 */
468 onOrientationChange: function() {
469 this.setOrientation(this.determineOrientation());
470 },
471
472 /**
473 * Listener for Orientation Change in environments that do no support orientationchange events
474 * @private
475 */
476 onResize: function() {
477 this.updateSize();
478 this.setOrientation(this.determineOrientation());
479 },
480
481 fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
482 var clsPrefix = Ext.baseCSSPrefix;
483 Ext.getBody().replaceCls(clsPrefix + oldOrientation, clsPrefix + newOrientation);
484
485 this.updateSize();
486 this.fireEvent('orientationchange', this, newOrientation, this.windowWidth, this.windowHeight);
487 },
488
489 updateSize: function(width, height) {
490 this.windowWidth = width !== undefined ? width : this.getWindowWidth();
491 this.windowHeight = height !== undefined ? height : this.getWindowHeight();
492
493 return this;
494 },
495
496 maximize: function() {
497 this.fireMaximizeEvent();
498 },
499
500 fireMaximizeEvent: function() {
501 this.updateSize();
502 this.fireEvent('maximize', this);
503 },
504
505 doSetHeight: function(height) {
506 Ext.getBody().setHeight(height);
507
508 this.callParent(arguments);
509 },
510
511 doSetWidth: function(width) {
512 Ext.getBody().setWidth(width);
513
514 this.callParent(arguments);
515 },
516
517 scrollToTop: function() {
518 window.scrollTo(0, -1);
519 },
520
521 /**
522 * Retrieves the document width.
523 * @return {Number} width in pixels.
524 */
525 getWindowWidth: function() {
526 return window.innerWidth;
527 },
528
529 /**
530 * Retrieves the document height.
531 * @return {Number} height in pixels.
532 */
533 getWindowHeight: function() {
534 return window.innerHeight;
535 },
536
537 getWindowOuterHeight: function() {
538 return window.outerHeight;
539 },
540
541 getWindowOrientation: function() {
542 return window.orientation;
543 },
544
545 getSize: function() {
546 return {
547 width: this.windowWidth,
548 height: this.windowHeight
549 };
550 },
551
552 onItemFullscreenChange: function(item) {
553 item.addCls(this.fullscreenItemCls);
554 this.add(item);
555 },
556 waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
557 if (!delay) {
558 delay = 50;
559 }
560
561 if (!timeoutDuration) {
562 timeoutDuration = 2000;
563 }
564
565 var scope = this,
566 elapse = 0;
567
568 setTimeout(function repeat() {
569 elapse += delay;
570
571 if (condition.call(scope) === true) {
572 if (onSatisfied) {
573 onSatisfied.call(scope);
574 }
575 }
576 else {
577 if (elapse >= timeoutDuration) {
578 if (onTimeout) {
579 onTimeout.call(scope);
580 }
581 }
582 else {
583 setTimeout(repeat, delay);
584 }
585 }
586 }, delay);
587 },
588
589 /**
590 * Sets a menu for a given side of the Viewport.
591 *
592 * Adds functionality to show the menu by swiping from the side of the screen from the given side.
593 *
594 * If a menu is already set for a given side, it will be removed.
595 *
596 * Available sides are: `left`, `right`, `top`, and `bottom`.
597 *
598 * @param {Ext.Menu} menu The menu to assign to the viewport
599 * @param {Object} config The configuration for the menu.
600 * @param {String} config.side The side to put the menu on.
601 * @param {Boolean} config.cover True to cover the viewport content. Defaults to `true`.
602 */
603 setMenu: function(menu, config) {
604 var me = this;
605 config = config || {};
606
607 // Temporary workaround for body shifting issue
608 if (Ext.os.is.iOS && !this.hasiOSOrientationFix) {
609 this.hasiOSOrientationFix = true;
610 this.on('orientationchange', function() {
611 window.scrollTo(0, 0);
612 }, this);
613 }
614
615 if (!menu) {
616 //<debug error>
617 Ext.Logger.error("You must specify a side to dock the menu.");
618 //</debug>
619 return;
620 }
621
622 if (!config.side) {
623 //<debug error>
624 Ext.Logger.error("You must specify a side to dock the menu.");
625 //</debug>
626 return;
627 }
628
629 if (['left', 'right', 'top', 'bottom'].indexOf(config.side) == -1) {
630 //<debug error>
631 Ext.Logger.error("You must specify a valid side (left, right, top or botom) to dock the menu.");
632 //</debug>
633 return;
634 }
635
636 var menus = me.getMenus();
637
638 if (!menus) {
639 menus = {};
640 }
641
642 // Add a listener to show this menu on swipe
643 if (!me.addedSwipeListener) {
644 me.addedSwipeListener = true;
645
646 me.element.on({
647 tap: me.onTap,
648 swipestart: me.onSwipeStart,
649 edgeswipestart: me.onEdgeSwipeStart,
650 edgeswipe: me.onEdgeSwipe,
651 edgeswipeend: me.onEdgeSwipeEnd,
652 scope: me
653 });
654
655 // Add BB10 webworks API for swipe down.
656 if (window.blackberry) {
657 var toggleMenu = function() {
658 var menus = me.getMenus(),
659 menu = menus['top'];
660
661 if (!menu) {
662 return;
663 }
664
665 if (menu.isHidden()) {
666 me.showMenu('top');
667 } else {
668 me.hideMenu('top');
669 }
670 };
671
672 if (blackberry.app && blackberry.app.event && blackberry.app.event.onSwipeDown) {
673 blackberry.app.event.onSwipeDown(toggleMenu); // PlayBook
674 }
675 else if (blackberry.event && blackberry.event.addEventListener) {
676 blackberry.event.addEventListener("swipedown", toggleMenu); // BB10
677 }
678 }
679 }
680
681 menus[config.side] = menu;
682 menu.$reveal = Boolean(config.reveal);
683 menu.$cover = config.cover !== false && !menu.$reveal;
684 menu.$side = config.side;
685
686 me.fixMenuSize(menu, config.side);
687
688 if (config.side == 'left') {
689 menu.setLeft(0);
690 menu.setRight(null);
691 menu.setTop(0);
692 menu.setBottom(0);
693 }
694 else if (config.side == 'right') {
695 menu.setLeft(null);
696 menu.setRight(0);
697 menu.setTop(0);
698 menu.setBottom(0);
699 }
700 else if (config.side == 'top') {
701 menu.setLeft(0);
702 menu.setRight(0);
703 menu.setTop(0);
704 menu.setBottom(null);
705 }
706 else if (config.side == 'bottom') {
707 menu.setLeft(0);
708 menu.setRight(0);
709 menu.setTop(null);
710 menu.setBottom(0);
711 }
712
713 me.setMenus(menus);
714 },
715
716 /**
717 * Removes a menu from a specified side.
718 * @param {String} side The side to remove the menu from
719 */
720 removeMenu: function(side) {
721 var menus = this.getMenus() || {},
722 menu = menus[side];
723
724 if(menu) this.hideMenu(side);
725 delete menus[side];
726 this.setMenus(menus);
727 },
728
729 /**
730 * @private
731 * Changes the sizing of the specified menu so that it displays correctly when shown.
732 */
733 fixMenuSize: function(menu, side) {
734 if (side == 'top' || side == 'bottom') {
735 menu.setWidth('100%');
736 }
737 else if (side == 'left' || side == 'right') {
738 menu.setHeight('100%');
739 }
740 },
741
742 /**
743 * Shows a menu specified by the menu's side.
744 * @param {String} side The side which the menu is placed.
745 */
746 showMenu: function(side) {
747 var menus = this.getMenus(),
748 menu = menus[side],
749 before, after,
750 viewportBefore, viewportAfter;
751
752 if (!menu || menu.isAnimating) {
753 return;
754 }
755
756 this.hideOtherMenus(side);
757
758 before = {
759 translateX: 0,
760 translateY: 0
761 };
762
763 after = {
764 translateX: 0,
765 translateY: 0
766 };
767
768 viewportBefore = {
769 translateX: 0,
770 translateY: 0
771 };
772
773 viewportAfter = {
774 translateX: 0,
775 translateY: 0
776 };
777
778 if (menu.$reveal) {
779 Ext.getBody().insertFirst(menu.element);
780 }
781 else {
782 Ext.Viewport.add(menu);
783 }
784
785 menu.show();
786 menu.addCls('x-' + side);
787
788 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
789
790 if (side == 'left') {
791 before.translateX = -size;
792 viewportAfter.translateX = size;
793 }
794 else if (side == 'right') {
795 before.translateX = size;
796 viewportAfter.translateX = -size;
797 }
798 else if (side == 'top') {
799 before.translateY = -size;
800 viewportAfter.translateY = size;
801 }
802 else if (side == 'bottom') {
803 before.translateY = size;
804 viewportAfter.translateY = -size;
805 }
806
807 if (menu.$reveal) {
808 if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
809 menu.translate(0, 0);
810 }
811 }
812 else {
813 menu.translate(before.translateX, before.translateY);
814 }
815
816 if (menu.$cover) {
817 menu.getTranslatable().on('animationend', function() {
818 menu.isAnimating = false;
819 }, this, {
820 single: true
821 });
822
823 menu.translate(after.translateX, after.translateY, {
824 preserveEndState: true,
825 duration: 200
826 });
827
828 }
829 else {
830 this.translate(viewportBefore.translateX, viewportBefore.translateY);
831
832
833 this.getTranslatable().on('animationend', function() {
834 menu.isAnimating = false;
835 }, this, {
836 single: true
837 });
838
839 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
840 preserveEndState: true,
841 duration: 200
842 });
843 }
844
845 // Make the menu as animating
846 menu.isAnimating = true;
847 },
848
849 /**
850 * Hides a menu specified by the menu's side.
851 * @param {String} side The side which the menu is placed.
852 */
853 hideMenu: function(side, animate) {
854 var menus = this.getMenus(),
855 menu = menus[side],
856 after, viewportAfter,
857 size;
858
859 animate = (animate === false) ? false : true;
860
861 if (!menu || (menu.isHidden() || menu.isAnimating)) {
862 return;
863 }
864
865 after = {
866 translateX: 0,
867 translateY: 0
868 };
869
870 viewportAfter = {
871 translateX: 0,
872 translateY: 0
873 };
874
875 size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
876
877 if (side == 'left') {
878 after.translateX = -size;
879 }
880 else if (side == 'right') {
881 after.translateX = size;
882 }
883 else if (side == 'top') {
884 after.translateY = -size;
885 }
886 else if (side == 'bottom') {
887 after.translateY = size;
888 }
889
890 if (menu.$cover) {
891 if (animate) {
892 menu.getTranslatable().on('animationend', function() {
893 menu.isAnimating = false;
894 menu.hide();
895 }, this, {
896 single: true
897 });
898
899 menu.translate(after.translateX, after.translateY, {
900 preserveEndState: true,
901 duration: 200
902 });
903 }
904 else {
905 menu.translate(after.translateX, after.translateY);
906 menu.hide()
907 }
908 }
909 else {
910 if (animate) {
911 this.getTranslatable().on('animationend', function() {
912 menu.isAnimating = false;
913 menu.hide();
914 }, this, {
915 single: true
916 });
917
918 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
919 preserveEndState: true,
920 duration: 200
921 });
922 }
923 else {
924 this.translate(viewportAfter.translateX, viewportAfter.translateY);
925 menu.hide();
926 }
927 }
928 },
929
930 /**
931 * Hides all visible menus.
932 */
933 hideAllMenus: function(animation) {
934 var menus = this.getMenus();
935
936 for (var side in menus) {
937 this.hideMenu(side, animation);
938 }
939 },
940
941 /**
942 * Hides all menus except for the side specified
943 * @param {String} side Side(s) not to hide
944 * @param {String} animation Animation to hide with
945 */
946 hideOtherMenus: function(side, animation){
947 var menus = this.getMenus();
948
949 for (var menu in menus) {
950 if (side != menu) {
951 this.hideMenu(menu, animation);
952 }
953 }
954 },
955
956 /**
957 * Toggles the menu specified by side
958 * @param {String} side The side which the menu is placed.
959 */
960 toggleMenu: function(side) {
961 var menus = this.getMenus(), menu;
962 if (menus[side]) {
963 menu = menus[side];
964 if (menu.isHidden()) {
965 this.showMenu(side);
966 } else {
967 this.hideMenu(side);
968 }
969 }
970 },
971
972 /**
973 * @private
974 */
975 sideForDirection: function(direction) {
976 if (direction == 'left') {
977 return 'right';
978 }
979 else if (direction == 'right') {
980 return 'left';
981 }
982 else if (direction == 'up') {
983 return 'bottom';
984 }
985 else if (direction == 'down') {
986 return 'top';
987 }
988 },
989
990 /**
991 * @private
992 */
993 sideForSwipeDirection: function(direction) {
994 if (direction == "up") {
995 return "top";
996 }
997 else if (direction == "down") {
998 return "bottom";
999 }
1000 return direction;
1001 },
1002
1003 /**
1004 * @private
1005 */
1006 onTap: function(e) {
1007 // this.hideAllMenus();
1008 },
1009
1010 /**
1011 * @private
1012 */
1013 onSwipeStart: function(e) {
1014 var side = this.sideForSwipeDirection(e.direction);
1015 this.hideMenu(side);
1016 },
1017
1018 /**
1019 * @private
1020 */
1021 onEdgeSwipeStart: function(e) {
1022 var side = this.sideForDirection(e.direction),
1023 menus = this.getMenus(),
1024 menu = menus[side],
1025 menuSide, checkMenu;
1026
1027 if (!menu || !menu.isHidden()) {
1028 return;
1029 }
1030
1031 for (menuSide in menus) {
1032 checkMenu = menus[menuSide];
1033 if (checkMenu.isHidden() !== false) {
1034 return;
1035 }
1036 }
1037
1038 this.$swiping = true;
1039
1040 this.hideAllMenus(false);
1041
1042 // show the menu first so we can calculate the size
1043 if (menu.$reveal) {
1044 Ext.getBody().insertFirst(menu.element);
1045 }
1046 else {
1047 Ext.Viewport.add(menu);
1048 }
1049 menu.show();
1050
1051 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
1052 after, viewportAfter;
1053
1054 after = {
1055 translateX: 0,
1056 translateY: 0
1057 };
1058
1059 viewportAfter = {
1060 translateX: 0,
1061 translateY: 0
1062 };
1063
1064 if (side == 'left') {
1065 after.translateX = -size;
1066 }
1067 else if (side == 'right') {
1068 after.translateX = size;
1069 }
1070 else if (side == 'top') {
1071 after.translateY = -size;
1072 }
1073 else if (side == 'bottom') {
1074 after.translateY = size;
1075 }
1076
1077 var transformStyleName = 'webkitTransform' in document.createElement('div').style ? 'webkitTransform' : 'transform',
1078 setTransform = menu.element.dom.style[transformStyleName];
1079
1080 if (setTransform) {
1081 menu.element.dom.style[transformStyleName] = '';
1082 }
1083
1084 if (menu.$reveal) {
1085 if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
1086 menu.translate(0, 0);
1087 }
1088 }
1089 else {
1090 menu.translate(after.translateX, after.translateY);
1091 }
1092
1093 if (!menu.$cover) {
1094 if (setTransform) {
1095 this.innerElement.dom.style[transformStyleName] = '';
1096 }
1097
1098 this.translate(viewportAfter.translateX, viewportAfter.translateY);
1099 }
1100 },
1101
1102 /**
1103 * @private
1104 */
1105 onEdgeSwipe: function(e) {
1106 var side = this.sideForDirection(e.direction),
1107 menu = this.getMenus()[side];
1108
1109 if (!menu || !this.$swiping) {
1110 return;
1111 }
1112
1113 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
1114 after, viewportAfter,
1115 movement = Math.min(e.distance - size, 0),
1116 viewportMovement = Math.min(e.distance, size);
1117
1118 after = {
1119 translateX: 0,
1120 translateY: 0
1121 };
1122
1123 viewportAfter = {
1124 translateX: 0,
1125 translateY: 0
1126 };
1127
1128 if (side == 'left') {
1129 after.translateX = movement;
1130 viewportAfter.translateX = viewportMovement;
1131 }
1132 else if (side == 'right') {
1133 after.translateX = -movement;
1134 viewportAfter.translateX = -viewportMovement;
1135 }
1136 else if (side == 'top') {
1137 after.translateY = movement;
1138 viewportAfter.translateY = viewportMovement;
1139 }
1140 else if (side == 'bottom') {
1141 after.translateY = -movement;
1142 viewportAfter.translateY = -viewportMovement;
1143 }
1144
1145 if (menu.$cover) {
1146 menu.translate(after.translateX, after.translateY);
1147 }
1148 else {
1149 this.translate(viewportAfter.translateX, viewportAfter.translateY);
1150 }
1151 },
1152
1153 /**
1154 * @private
1155 */
1156 onEdgeSwipeEnd: function(e) {
1157 var side = this.sideForDirection(e.direction),
1158 menu = this.getMenus()[side],
1159 shouldRevert = false;
1160
1161 if (!menu) {
1162 return;
1163 }
1164
1165 var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
1166 velocity = (e.flick) ? e.flick.velocity : 0;
1167
1168 // check if continuing in the right direction
1169 if (side == 'right') {
1170 if (velocity.x > 0) {
1171 shouldRevert = true;
1172 }
1173 }
1174 else if (side == 'left') {
1175 if (velocity.x < 0) {
1176 shouldRevert = true;
1177 }
1178 }
1179 else if (side == 'top') {
1180 if (velocity.y < 0) {
1181 shouldRevert = true;
1182 }
1183 }
1184 else if (side == 'bottom') {
1185 if (velocity.y > 0) {
1186 shouldRevert = true;
1187 }
1188 }
1189
1190 var movement = (shouldRevert) ? size : 0,
1191 viewportMovement = (shouldRevert) ? 0 : -size,
1192 after, viewportAfter;
1193
1194 after = {
1195 translateX: 0,
1196 translateY: 0
1197 };
1198
1199 viewportAfter = {
1200 translateX: 0,
1201 translateY: 0
1202 };
1203
1204 if (side == 'left') {
1205 after.translateX = -movement;
1206 viewportAfter.translateX = -viewportMovement;
1207 }
1208 else if (side == 'right') {
1209 after.translateX = movement;
1210 viewportAfter.translateX = viewportMovement;
1211 }
1212 else if (side == 'top') {
1213 after.translateY = -movement;
1214 viewportAfter.translateY = -viewportMovement;
1215 }
1216 else if (side == 'bottom') {
1217 after.translateY = movement;
1218 viewportAfter.translateY = viewportMovement;
1219 }
1220
1221 // Move the viewport if cover is not enabled
1222 if (menu.$cover) {
1223 menu.getTranslatable().on('animationend', function() {
1224 if (shouldRevert) {
1225 menu.hide();
1226 }
1227 }, this, {
1228 single: true
1229 });
1230
1231 menu.translate(after.translateX, after.translateY, {
1232 preserveEndState: true,
1233 duration: 200
1234 });
1235
1236 }
1237 else {
1238 this.getTranslatable().on('animationend', function() {
1239 if (shouldRevert) {
1240 menu.hide();
1241 }
1242 }, this, {
1243 single: true
1244 });
1245
1246 this.translate(viewportAfter.translateX, viewportAfter.translateY, {
1247 preserveEndState: true,
1248 duration: 200
1249 });
1250 }
1251
1252 this.$swiping = false;
1253 }
1254 });