diff --git a/ionic/components/menu/menu-controller.ts b/ionic/components/menu/menu-controller.ts index 3e793927763..67298f04da9 100644 --- a/ionic/components/menu/menu-controller.ts +++ b/ionic/components/menu/menu-controller.ts @@ -209,7 +209,7 @@ export class MenuController { */ isEnabled(menuId?: string): boolean { let menu = this.get(menuId); - return menu && menu.isEnabled || false; + return menu && menu.enabled || false; } /** @@ -235,6 +235,14 @@ export class MenuController { return (this._menus.length ? this._menus[0] : null); } + + /** + * @return {Array} Returns an array of all menu instances. + */ + getMenus(): Array { + return this._menus; + } + /** * @private */ diff --git a/ionic/components/menu/menu-gestures.ts b/ionic/components/menu/menu-gestures.ts index 6746d082a0d..cad46533bb2 100644 --- a/ionic/components/menu/menu-gestures.ts +++ b/ionic/components/menu/menu-gestures.ts @@ -17,15 +17,13 @@ export class MenuContentGesture extends SlideEdgeGesture { threshold: 0, maxEdgeStart: menu.maxEdgeStart || 75 }, options)); - - this.listen(); } canStart(ev) { let menu = this.menu; - if (!menu.isEnabled || !menu.isSwipeEnabled) { - console.debug('menu can not start, isEnabled:', menu.isEnabled, 'isSwipeEnabled:', menu.isSwipeEnabled, 'side:', menu.side); + if (!menu.enabled || !menu.swipeEnabled) { + console.debug('menu can not start, isEnabled:', menu.enabled, 'isSwipeEnabled:', menu.swipeEnabled, 'side:', menu.side); return false; } diff --git a/ionic/components/menu/menu.ts b/ionic/components/menu/menu.ts index 11c9f749dec..2875f6f76b0 100644 --- a/ionic/components/menu/menu.ts +++ b/ionic/components/menu/menu.ts @@ -8,7 +8,7 @@ import {MenuContentGesture, MenuTargetGesture} from './menu-gestures'; import {Gesture} from '../../gestures/gesture'; import {MenuController} from './menu-controller'; import {MenuType} from './menu-types'; -import {isFalseProperty} from '../../util/util'; +import {isTrueProperty} from '../../util/util'; /** @@ -17,12 +17,11 @@ import {isFalseProperty} from '../../util/util'; @Component({ selector: 'ion-menu', host: { - 'role': 'navigation', - '[attr.side]': 'side', - '[attr.type]': 'type', - '[attr.swipeEnabled]': 'swipeEnabled' + 'role': 'navigation' }, - template: '
', + template: + '' + + '
', directives: [forwardRef(() => MenuBackdrop)] }) export class Menu extends Ion { @@ -32,23 +31,16 @@ export class Menu extends Ion { private _menuGesture: Gesture; private _type: MenuType; private _resizeUnreg: Function; - + private _isEnabled: boolean = true; + private _isSwipeEnabled: boolean = true; + private _isListening: boolean = false; + private _init: boolean = false; /** * @private */ isOpen: boolean = false; - /** - * @private - */ - isEnabled: boolean = true; - - /** - * @private - */ - isSwipeEnabled: boolean = true; - /** * @private */ @@ -83,7 +75,28 @@ export class Menu extends Ion { /** * @private */ - @Input() swipeEnabled: any; + @Input() + get enabled(): boolean { + return this._isEnabled; + } + + set enabled(val: boolean) { + this._isEnabled = isTrueProperty(val); + this._setListeners(); + } + + /** + * @private + */ + @Input() + get swipeEnabled(): boolean { + return this._isSwipeEnabled; + } + + set swipeEnabled(val: boolean) { + this._isSwipeEnabled = isTrueProperty(val); + this._setListeners(); + } /** * @private @@ -112,6 +125,8 @@ export class Menu extends Ion { */ ngOnInit() { let self = this; + self._init = true; + let content = self.content; self._cntEle = (content instanceof Node) ? content : content && content.getNativeElement && content.getNativeElement(); @@ -132,23 +147,29 @@ export class Menu extends Ion { } self._renderer.setElementAttribute(self._elementRef.nativeElement, 'type', self.type); - // add the gesture listeners - self._zone.runOutsideAngular(function() { - self._cntGesture = new MenuContentGesture(self, self.getContentElement()); - self._menuGesture = new MenuTargetGesture(self, self.getNativeElement()); - - self.onContentClick = function(ev: UIEvent) { - if (self.isEnabled) { - ev.preventDefault(); - ev.stopPropagation(); - self.close(); - } - }; - }); + // add the gestures + self._cntGesture = new MenuContentGesture(self, self.getContentElement()); + self._menuGesture = new MenuTargetGesture(self, self.getNativeElement()); - if (isFalseProperty(self.swipeEnabled)) { - self.isSwipeEnabled = false; + // register listeners if this menu is enabled + // check if more than one menu is on the same side + let hasEnabledSameSideMenu = self._menuCtrl.getMenus().some(m => { + return m.side === self.side && m.enabled; + }); + if (hasEnabledSameSideMenu) { + // auto-disable if another menu on the same side is already enabled + self._isEnabled = false; } + self._setListeners(); + + // create a reusable click handler on this instance, but don't assign yet + self.onContentClick = function(ev: UIEvent) { + if (self._isEnabled) { + ev.preventDefault(); + ev.stopPropagation(); + self.close(); + } + }; self._cntEle.classList.add('menu-content'); self._cntEle.classList.add('menu-content-' + self.type); @@ -157,6 +178,34 @@ export class Menu extends Ion { self._menuCtrl.register(self); } + /** + * @private + */ + private _setListeners() { + let self = this; + + if (self._init) { + // only listen/unlisten if the menu has initialized + + if (self._isEnabled && self._isSwipeEnabled && !self._isListening) { + // should listen, but is not currently listening + console.debug('menu, gesture listen', self.side); + self._zone.runOutsideAngular(function() { + self._cntGesture.listen(); + self._menuGesture.listen(); + }); + self._isListening = true; + + } else if (self._isListening && (!self._isEnabled || !self._isSwipeEnabled)) { + // should not listen, but is currently listening + console.debug('menu, gesture unlisten', self.side); + self._cntGesture.unlisten(); + self._menuGesture.unlisten(); + self._isListening = false; + } + } + } + /** * @private */ @@ -176,7 +225,7 @@ export class Menu extends Ion { * @param {boolean} shouldOpen If the Menu is open or not. * @return {Promise} returns a promise once set */ - setOpen(shouldOpen): Promise { + setOpen(shouldOpen: boolean): Promise { // _isPrevented is used to prevent unwanted opening/closing after swiping open/close // or swiping open the menu while pressing down on the menuToggle button if ((shouldOpen && this.isOpen) || this._isPrevented()) { @@ -198,7 +247,7 @@ export class Menu extends Ion { */ setProgressStart() { // user started swiping the menu open/close - if (this._isPrevented() || !this.isEnabled || !this.isSwipeEnabled) return; + if (this._isPrevented() || !this._isEnabled || !this._isSwipeEnabled) return; this._before(); this._getType().setProgressStart(this.isOpen); @@ -209,7 +258,7 @@ export class Menu extends Ion { */ setProgessStep(stepValue: number) { // user actively dragging the menu - if (this.isEnabled && this.isSwipeEnabled) { + if (this._isEnabled && this._isSwipeEnabled) { this._prevent(); this._getType().setProgessStep(stepValue); this.opening.next(stepValue); @@ -221,7 +270,7 @@ export class Menu extends Ion { */ setProgressEnd(shouldComplete: boolean, currentStepValue: number) { // user has finished dragging the menu - if (this.isEnabled && this.isSwipeEnabled) { + if (this._isEnabled && this._isSwipeEnabled) { this._prevent(); this._getType().setProgressEnd(shouldComplete, currentStepValue, (isOpen) => { console.debug('menu, progress end', this.side); @@ -236,7 +285,7 @@ export class Menu extends Ion { private _before() { // this places the menu into the correct location before it animates in // this css class doesn't actually kick off any animations - if (this.isEnabled) { + if (this._isEnabled) { this.getNativeElement().classList.add('show-menu'); this.getBackdropElement().classList.add('show-backdrop'); @@ -252,7 +301,7 @@ export class Menu extends Ion { // keep opening/closing the menu disabled for a touch more yet // only add listeners/css if it's enabled and isOpen // and only remove listeners/css if it's not open - if ((this.isEnabled && isOpen) || !isOpen) { + if ((this._isEnabled && isOpen) || !isOpen) { this._prevent(); this.isOpen = isOpen; @@ -318,7 +367,7 @@ export class Menu extends Ion { * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ enable(shouldEnable: boolean): Menu { - this.isEnabled = shouldEnable; + this.enabled = shouldEnable; if (!shouldEnable && this.isOpen) { this.close(); } @@ -331,7 +380,7 @@ export class Menu extends Ion { * @return {Menu} Returns the instance of the menu, which is useful for chaining. */ swipeEnable(shouldEnable: boolean): Menu { - this.isSwipeEnabled = shouldEnable; + this.swipeEnabled = shouldEnable; return this; } diff --git a/ionic/components/menu/test/multiple/index.ts b/ionic/components/menu/test/multiple/index.ts index 5e212df941a..eaab81dc85d 100644 --- a/ionic/components/menu/test/multiple/index.ts +++ b/ionic/components/menu/test/multiple/index.ts @@ -5,8 +5,9 @@ import {App, Page, MenuController} from '../../../../../ionic/ionic'; templateUrl: 'page1.html' }) class Page1 { - constructor(menu: MenuController) { - this.menu = menu; + activeMenu: string; + + constructor(private menu: MenuController) { this.menu1Active(); } menu1Active() { @@ -26,6 +27,8 @@ class Page1 { templateUrl: 'main.html' }) class E2EApp { + rootPage; + constructor() { this.rootPage = Page1; } diff --git a/ionic/gestures/gesture.ts b/ionic/gestures/gesture.ts index 71440dcd8cc..aff40f4b705 100644 --- a/ionic/gestures/gesture.ts +++ b/ionic/gestures/gesture.ts @@ -33,7 +33,7 @@ export class Gesture { assign(this._options, opts); } - on(type, cb) { + on(type: string, cb: Function) { if(type == 'pinch' || type == 'rotate') { this._hammer.get('pinch').set({enable: true}); } @@ -41,7 +41,7 @@ export class Gesture { (this._callbacks[type] || (this._callbacks[type] = [])).push(cb); } - off(type, cb) { + off(type: string, cb: Function) { this._hammer.off(type, this._callbacks[type] ? cb : null); } @@ -50,19 +50,20 @@ export class Gesture { } unlisten() { + var type, i; if (this._hammer) { - for (let type in this._callbacks) { - for (let i = 0; i < this._callbacks[type].length; i++) { + for (type in this._callbacks) { + for (i = 0; i < this._callbacks[type].length; i++) { this._hammer.off(type, this._callbacks[type]); } } - this._hammer.destroy(); - this._hammer = null; this._callbacks = {}; + this._hammer.destroy(); } } destroy() { - this.unlisten() + this.unlisten(); + this._hammer = this.element = this._options = null; } } diff --git a/ionic/util/util.ts b/ionic/util/util.ts index a4a6a5cffda..d3b5f794244 100644 --- a/ionic/util/util.ts +++ b/ionic/util/util.ts @@ -1,5 +1,3 @@ -// Simple noop function -export function noop() {}; /** * Given a min and max, restrict the given number @@ -60,7 +58,7 @@ function _baseExtend(dst, objs, deep) { return dst; } -export function debounce(func: any, wait: number, immediate = false) { +export function debounce(fn: Function, wait: number, immediate: boolean = false): any { var timeout, args, context, timestamp: number, result; return function() { context = this; @@ -72,14 +70,14 @@ export function debounce(func: any, wait: number, immediate = false) { timeout = setTimeout(later, wait - last); } else { timeout = null; - if (!immediate) result = func.apply(context, args); + if (!immediate) result = fn.apply(context, args); } }; var callNow = immediate && !timeout; if (!timeout) { timeout = setTimeout(later, wait); } - if (callNow) result = func.apply(context, args); + if (callNow) result = fn.apply(context, args); return result; }; } @@ -112,7 +110,7 @@ export const isBlank = val => val === undefined || val === null; export const isObject = val => typeof val === 'object'; export const isArray = Array.isArray; -export const isTrueProperty = function(val) { +export const isTrueProperty = function(val: any): boolean { if (typeof val === 'string') { val = val.toLowerCase().trim(); return (val === 'true' || val === ''); @@ -120,24 +118,17 @@ export const isTrueProperty = function(val) { return !!val; }; -export const isFalseProperty = function(val) { - if (typeof val === 'string') { - return (val.toLowerCase().trim() === 'false'); - } - return !!val; -}; - /** * Convert a string in the format thisIsAString to a slug format this-is-a-string */ -export function pascalCaseToDashCase(str = '') { - return str.charAt(0).toLowerCase() + str.substring(1).replace(/[A-Z]/g, match => { +export function pascalCaseToDashCase(val: string = ''): string { + return val.charAt(0).toLowerCase() + val.substring(1).replace(/[A-Z]/g, match => { return '-' + match.toLowerCase(); }); } let uid = 0; -export function nextUid() { +export function nextUid(): number { return ++uid; } @@ -185,7 +176,7 @@ export function getQuerystring(url: string): any { * Throttle the given fun, only allowing it to be * called at most every `wait` ms. */ -export function throttle(func, wait, options) { +export function throttle(fn: Function, wait: number, options: any): any { var context, args, result; var timeout = null; var previous = 0; @@ -193,7 +184,7 @@ export function throttle(func, wait, options) { var later = function() { previous = options.leading === false ? 0 : Date.now(); timeout = null; - result = func.apply(context, args); + result = fn.apply(context, args); }; return function() { var now = Date.now(); @@ -205,7 +196,7 @@ export function throttle(func, wait, options) { clearTimeout(timeout); timeout = null; previous = now; - result = func.apply(context, args); + result = fn.apply(context, args); } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); }