feat(menu): adds ion-menu and ion-menu-controller

This commit is contained in:
Manuel Mtz-Almeida
2017-09-15 11:05:58 -05:00
parent a1fcc61d3b
commit 16d13e03c9
18 changed files with 742 additions and 629 deletions

View File

@ -1501,14 +1501,6 @@
}
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
@ -1519,6 +1511,14 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
@ -3136,15 +3136,6 @@
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
"dev": true
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@ -3162,6 +3153,15 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",

View File

@ -5,6 +5,7 @@ import { transitionEnd } from './transition-end';
export class Animator {
private _afterAddClasses: string[];
private _afterRemoveClasses: string[];
private _afterStyles: { [property: string]: any; };
@ -639,7 +640,7 @@ export class Animator {
// flip the number if we're going in reverse
if (this._isReverse) {
stepValue = ((stepValue * -1) + 1);
stepValue = 1 - stepValue;
}
var i = 0;
var j = 0;
@ -1023,12 +1024,6 @@ export class Animator {
children[i].progressStep(stepValue);
}
if (this._isReverse) {
// if the animation is going in reverse then
// flip the step value: 0 becomes 1, 1 becomes 0
stepValue = ((stepValue * -1) + 1);
}
// ******** DOM WRITE ****************
this._progress(stepValue);
}

View File

@ -1,13 +1,13 @@
import { applyStyles, getElementReference, pointerCoordX, pointerCoordY } from '../../utils/helpers';
import { applyStyles, getElementReference, ElementRef, updateDetail, assert } from '../../utils/helpers';
import { BlockerDelegate, GestureController, GestureDelegate, BLOCK_ALL } from '../gesture-controller/gesture-controller';
import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core';
import { PanRecognizer } from './recognizers';
@Component({
tag: 'ion-gesture'
})
export class Gesture {
@Element() private el: HTMLElement;
private detail: GestureDetail = {};
private positions: number[] = [];
@ -18,9 +18,10 @@ export class Gesture {
private hasCapturedPan = false;
private hasPress = false;
private hasStartedPan = false;
private requiresMove = false;
private hasFiredStart = true;
private isMoveQueued = false;
private blocker: BlockerDelegate;
private fireOnMoveFunc: any;
@Event() private ionGestureMove: EventEmitter;
@Event() private ionGestureStart: EventEmitter;
@ -28,7 +29,8 @@ export class Gesture {
@Event() private ionGestureNotCaptured: EventEmitter;
@Event() private ionPress: EventEmitter;
@Prop() attachTo: string = 'child';
@Prop() enabled: boolean = true;
@Prop() attachTo: ElementRef = 'child';
@Prop() autoBlockAll: boolean = false;
@Prop() block: string = null;
@Prop() disableScroll: boolean = false;
@ -40,12 +42,16 @@ export class Gesture {
@Prop() type: string = 'pan';
@Prop() canStart: GestureCallback;
@Prop() onWillStart: (_: GestureDetail) => Promise<void>;
@Prop() onStart: GestureCallback;
@Prop() onMove: GestureCallback;
@Prop() onEnd: GestureCallback;
@Prop() onPress: GestureCallback;
@Prop() notCaptured: GestureCallback;
constructor() {
this.fireOnMoveFunc = this.fireOnMove.bind(this);
}
ionViewDidLoad() {
// in this case, we already know the GestureController and Gesture are already
@ -55,17 +61,13 @@ export class Gesture {
this.gesture = this.ctrl.createGesture(this.gestureName, this.gesturePriority, this.disableScroll);
const types = this.type.replace(/\s/g, '').toLowerCase().split(',');
if (types.indexOf('pan') > -1) {
this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle);
this.requiresMove = true;
}
this.hasPress = (types.indexOf('press') > -1);
this.enabledChange(true);
if (this.pan || this.hasPress) {
Context.enableListener(this, 'touchstart', true, this.attachTo);
Context.enableListener(this, 'mousedown', true, this.attachTo);
Context.dom.write(() => {
applyStyles(getElementReference(this.el, this.attachTo), GESTURE_INLINE_STYLES);
});
@ -77,6 +79,19 @@ export class Gesture {
}
}
@PropDidChange('enabled')
enabledChange(isEnabled: boolean) {
if (!this.gesture) {
return;
}
if (this.pan || this.hasPress) {
Context.enableListener(this, 'touchstart', isEnabled, this.attachTo);
Context.enableListener(this, 'mousedown', isEnabled, this.attachTo);
if (!isEnabled) {
this.abortGesture();
}
}
}
@PropDidChange('block')
blockChange(block: string) {
@ -94,10 +109,12 @@ export class Gesture {
onTouchStart(ev: TouchEvent) {
this.lastTouch = now(ev);
if (this.pointerDown(ev, this.lastTouch)) {
this.enableMouse(false);
this.enableTouch(true);
this.pointerDown(ev, this.lastTouch);
} else {
this.abortGesture();
}
}
@ -106,29 +123,36 @@ export class Gesture {
const timeStamp = now(ev);
if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) {
if (this.pointerDown(ev, timeStamp)) {
this.enableMouse(true);
this.enableTouch(false);
this.pointerDown(ev, timeStamp);
} else {
this.abortGesture();
}
}
}
private pointerDown(ev: UIEvent, timeStamp: number): boolean {
if (!this.gesture || this.hasStartedPan) {
if (!this.gesture || this.hasStartedPan || !this.hasFiredStart) {
return false;
}
const detail = this.detail;
detail.startX = detail.currentX = pointerCoordX(ev);
detail.startY = detail.currentY = pointerCoordY(ev);
updateDetail(ev, detail);
detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp = timeStamp;
detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
detail.directionX = detail.directionY = detail.velocityDirectionX = detail.velocityDirectionY = null;
detail.event = ev;
this.positions.length = 0;
assert(this.hasFiredStart, 'fired start must be false');
assert(!this.hasStartedPan, 'pan can be started at this point');
assert(!this.hasCapturedPan, 'pan can be started at this point')
assert(!this.isMoveQueued, 'some move is still queued');
assert(this.positions.length === 0, 'positions must be emprty');
// Check if gesture can start
if (this.canStart && this.canStart(detail) === false) {
return false;
}
@ -145,11 +169,8 @@ export class Gesture {
if (this.pan) {
this.hasStartedPan = true;
this.hasCapturedPan = false;
this.pan.start(detail.startX, detail.startY);
}
return true;
}
@ -159,7 +180,6 @@ export class Gesture {
@Listen('touchmove', { passive: true, enabled: false })
onTouchMove(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev);
this.pointerMove(ev);
}
@ -167,7 +187,6 @@ export class Gesture {
@Listen('document:mousemove', { passive: true, enabled: false })
onMoveMove(ev: TouchEvent) {
const timeStamp = now(ev);
if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) {
this.detail.timeStamp = timeStamp;
this.pointerMove(ev);
@ -175,28 +194,20 @@ export class Gesture {
}
private pointerMove(ev: UIEvent) {
assert(!!this.pan, 'pan must be non null');
if (this.hasCapturedPan) {
if (!this.isMoveQueued && this.hasFiredStart) {
this.isMoveQueued = true;
this.calcGestureData(ev);
Context.dom.write(this.fireOnMoveFunc);
}
return;
}
const detail = this.detail;
this.calcGestureData(ev);
if (this.pan) {
if (this.hasCapturedPan) {
if (!this.isMoveQueued) {
this.isMoveQueued = true;
Context.dom.write(() => {
this.isMoveQueued = false;
detail.type = 'pan';
if (this.onMove) {
this.onMove(detail);
} else {
this.ionGestureMove.emit(this.detail);
}
});
}
} else if (this.pan.detect(detail.currentX, detail.currentY)) {
if (this.pan.detect(detail.currentX, detail.currentY)) {
if (this.pan.isGesture() !== 0) {
if (!this.tryToCapturePan(ev)) {
this.abortGesture();
@ -204,78 +215,103 @@ export class Gesture {
}
}
}
private fireOnMove() {
const detail = this.detail;
this.isMoveQueued = false;
if (this.onMove) {
this.onMove(detail);
} else {
this.ionGestureMove.emit(detail);
}
}
private calcGestureData(ev: UIEvent) {
const detail = this.detail;
detail.currentX = pointerCoordX(ev);
detail.currentY = pointerCoordY(ev);
detail.deltaX = (detail.currentX - detail.startX);
detail.deltaY = (detail.currentY - detail.startY);
updateDetail(ev, detail);
const currentX = detail.currentX;
const currentY = detail.currentY;
const timestamp = detail.timeStamp;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev;
// figure out which direction we're movin'
detail.directionX = detail.velocityDirectionX = (detail.deltaX > 0 ? 'left' : (detail.deltaX < 0 ? 'right' : null));
detail.directionY = detail.velocityDirectionY = (detail.deltaY > 0 ? 'up' : (detail.deltaY < 0 ? 'down' : null));
const timeRange = timestamp - 100;
const positions = this.positions;
positions.push(detail.currentX, detail.currentY, detail.timeStamp);
var endPos = (positions.length - 1);
var startPos = endPos;
var timeRange = (detail.timeStamp - 100);
let startPos = positions.length - 1;
// move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) {
startPos = i;
}
for (;
startPos > 0 && positions[startPos] > timeRange;
startPos -= 3) { }
if (startPos !== endPos) {
if (startPos > 1) {
// compute relative movement between these two points
var movedX = (positions[startPos - 2] - positions[endPos - 2]);
var movedY = (positions[startPos - 1] - positions[endPos - 1]);
var factor = 16.67 / (positions[endPos] - positions[startPos]);
var frequency = 1 / (positions[startPos] - timestamp);
var movedY = positions[startPos - 1] - currentY;
var movedX = positions[startPos - 2] - currentX;
// based on XXms compute the movement to apply for each render step
detail.velocityX = movedX * factor;
detail.velocityY = movedY * factor;
detail.velocityDirectionX = (movedX > 0 ? 'left' : (movedX < 0 ? 'right' : null));
detail.velocityDirectionY = (movedY > 0 ? 'up' : (movedY < 0 ? 'down' : null));
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
}
positions.push(currentX, currentY, timestamp);
}
private tryToCapturePan(ev: UIEvent): boolean {
if (this.gesture && !this.gesture.capture()) {
return false;
}
this.hasCapturedPan = true;
this.hasFiredStart = false;
this.calcGestureData(ev);
if (this.onWillStart) {
this.onWillStart(this.detail).then(this.fireOnStart.bind(this));
} else {
this.fireOnStart();
}
return true;
}
this.detail.event = ev;
private fireOnStart() {
assert(!this.hasFiredStart, 'has fired must be false');
if (this.onStart) {
this.onStart(this.detail);
} else {
this.ionGestureStart.emit(this.detail);
}
this.hasCapturedPan = true;
return true;
this.hasFiredStart = true;
}
private abortGesture() {
this.hasStartedPan = false;
this.hasCapturedPan = false;
this.gesture && this.gesture.release();
this.reset();
this.enable(false);
this.notCaptured && this.notCaptured(this.detail);
}
private reset() {
this.hasCapturedPan = false;
this.hasStartedPan = false;
this.hasFiredStart = true;
this.gesture && this.gesture.release();
}
// END *************************
@Listen('touchcancel', { passive: true, enabled: false })
onTouchCancel(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev);
this.pointerUp(ev);
this.enableTouch(false);
}
@Listen('touchend', { passive: true, enabled: false })
onTouchEnd(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev);
@ -298,27 +334,33 @@ export class Gesture {
private pointerUp(ev: UIEvent) {
const hasCaptured = this.hasCapturedPan;
const hasFiredStart = this.hasFiredStart;
this.reset();
if (!hasFiredStart) {
return;
}
const detail = this.detail;
this.gesture && this.gesture.release();
detail.event = ev;
this.calcGestureData(ev);
if (this.pan) {
if (this.hasCapturedPan) {
// Try to capture press
if (hasCaptured) {
detail.type = 'pan';
if (this.onEnd) {
this.onEnd(detail);
} else {
this.ionGestureEnd.emit(detail);
}
return;
}
} else if (this.hasPress) {
this.detectPress();
// Try to capture press
if (this.hasPress && this.detectPress()) {
return;
}
} else {
// Not captured any event
if (this.notCaptured) {
this.notCaptured(detail);
} else {
@ -326,19 +368,12 @@ export class Gesture {
}
}
} else if (this.hasPress) {
this.detectPress();
}
this.hasCapturedPan = false;
this.hasStartedPan = false;
}
private detectPress() {
private detectPress(): boolean {
const detail = this.detail;
if (Math.abs(detail.startX - detail.currentX) < 10 && Math.abs(detail.startY - detail.currentY) < 10) {
const vecX = detail.deltaX;
const vecY = detail.deltaY;
const dis = vecX * vecX + vecY * vecY;
if (dis < 100) {
detail.type = 'press';
if (this.onPress) {
@ -346,25 +381,27 @@ export class Gesture {
} else {
this.ionPress.emit(detail);
}
return true;
}
return false;
}
// ENABLE LISTENERS *************************
private enableMouse(shouldEnable: boolean) {
if (this.requiresMove) {
Context.enableListener(this, 'document:mousemove', shouldEnable);
if (this.pan) {
Context.enableListener(this, 'document:mousemove', shouldEnable, this.attachTo);
}
Context.enableListener(this, 'document:mouseup', shouldEnable);
Context.enableListener(this, 'document:mouseup', shouldEnable, this.attachTo);
}
private enableTouch(shouldEnable: boolean) {
if (this.requiresMove) {
Context.enableListener(this, 'touchmove', shouldEnable);
if (this.pan) {
Context.enableListener(this, 'touchmove', shouldEnable, this.attachTo);
}
Context.enableListener(this, 'touchend', shouldEnable);
Context.enableListener(this, 'touchcancel', shouldEnable, this.attachTo);
Context.enableListener(this, 'touchend', shouldEnable, this.attachTo);
}
@ -413,10 +450,6 @@ export interface GestureDetail {
velocityY?: number;
deltaX?: number;
deltaY?: number;
directionX?: 'left'|'right';
directionY?: 'up'|'down';
velocityDirectionX?: 'left'|'right';
velocityDirectionY?: 'up'|'down';
timeStamp?: number;
}

View File

@ -0,0 +1,15 @@
import { Animation } from '../../../index';
/**
* @hidden
* Menu Type
* Base class which is extended by the various types. Each
* type will provide their own animations for open and close
* and registers itself with Menu.
*/
export default function baseAnimation(Animation: Animation): Animation {
return new Animation()
.easing('cubic-bezier(0.0, 0.0, 0.2, 1)')
.easingReverse('cubic-bezier(0.4, 0.0, 0.6, 1)')
.duration(280);
}

View File

@ -0,0 +1,35 @@
import { Animation, Menu } from '../../../index';
import baseAnimation from './base';
/**
* @hidden
* Menu Overlay Type
* The menu slides over the content. The content
* itself, which is under the menu, does not move.
*/
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation {
let closedX: string, openedX: string;
const width = menu.getWidth();
if (menu.isRightSide) {
// right side
closedX = 8 + width + 'px';
openedX = '0px';
} else {
// left side
closedX = -(8 + width) + 'px';
openedX = '0px';
}
const menuAni = new Animation()
.addElement(menu.getMenuElement())
.fromTo('translateX', closedX, openedX);
const backdropApi = new Animation()
.addElement(menu.getBackdropElement())
.fromTo('opacity', 0.01, 0.35)
return baseAnimation(Animation)
.add(menuAni)
.add(backdropApi);
}

View File

@ -0,0 +1,36 @@
import { Animation, Menu } from '../../../index';
import baseAnimation from './base';
/**
* @hidden
* Menu Push Type
* The content slides over to reveal the menu underneath.
* The menu itself also slides over to reveal its bad self.
*/
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation {
let contentOpenedX: string, menuClosedX: string, menuOpenedX: string;
const width = menu.getWidth();
if (menu.isRightSide) {
contentOpenedX = -width + 'px';
menuClosedX = width + 'px';
menuOpenedX = '0px';
} else {
contentOpenedX = width + 'px';
menuOpenedX = '0px';
menuClosedX = -width + 'px';
}
const menuAni = new Animation()
.addElement(menu.getMenuElement())
.fromTo('translateX', menuClosedX, menuOpenedX);
const contentAni = new Animation()
.addElement(menu.getContentElement())
.fromTo('translateX', '0px', contentOpenedX);
return baseAnimation(Animation)
.add(menuAni)
.add(contentAni);
}

View File

@ -0,0 +1,19 @@
import { Animation, Menu } from '../../../index';
import baseAnimation from './base';
/**
* @hidden
* Menu Reveal Type
* The content slides over to reveal the menu underneath.
* The menu itself, which is under the content, does not move.
*/
export default function(Animation: Animation, _: HTMLElement, menu: Menu): Animation {
const openedX = (menu.getWidth() * (menu.isRightSide ? -1 : 1)) + 'px';
const contentOpen = new Animation()
.addElement(menu.getContentElement())
.fromTo('translateX', '0px', openedX);
return baseAnimation(Animation)
.add(contentOpen);
}

View File

@ -1,15 +1,29 @@
import { Menu, MenuType } from '../../index';
import { MenuRevealType, MenuPushType, MenuOverlayType } from './menu-types';
import { Menu, AnimationController, AnimationBuilder, Animation } from '../../index';
import { Component, Method, Prop } from '@stencil/core';
import MenuOverlayAnimation from './animations/overlay';
import MenuRevealAnimation from './animations/reveal';
import MenuPushAnimation from './animations/push';
@Component({
tag: 'ion-menu-controller'
})
export class MenuController {
private _menus: Array<Menu> = [];
private _menuTypes: { [name: string]: new(...args: any[]) => MenuType } = {};
private menus: Menu[] = [];
private menuAnimations: { [name: string]: AnimationBuilder } = {};
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
constructor() {
this.registerType('reveal', MenuRevealType);
this.registerType('push', MenuPushType);
this.registerType('overlay', MenuOverlayType);
this.registerAnimation('reveal', MenuRevealAnimation);
this.registerAnimation('push', MenuPushAnimation);
this.registerAnimation('overlay', MenuOverlayAnimation);
}
@Method()
getInstance(): MenuController {
return this;
}
/**
@ -17,6 +31,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu is fully opened
*/
@Method()
open(menuId?: string): Promise<boolean> {
const menu = this.get(menuId);
if (menu && !this.isAnimating()) {
@ -36,6 +51,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu is fully closed
*/
@Method()
close(menuId?: string): Promise<boolean> {
let menu: Menu;
@ -63,6 +79,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu has been toggled
*/
@Method()
toggle(menuId?: string): Promise<boolean> {
const menu = this.get(menuId);
if (menu && !this.isAnimating()) {
@ -83,6 +100,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/
@Method()
enable(shouldEnable: boolean, menuId?: string): Menu {
const menu = this.get(menuId);
return (menu && menu.enable(shouldEnable)) || null;
@ -94,6 +112,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/
@Method()
swipeEnable(shouldEnable: boolean, menuId?: string): Menu {
const menu = this.get(menuId);
return (menu && menu.swipeEnable(shouldEnable)) || null;
@ -104,10 +123,11 @@ export class MenuController {
* @return {boolean} Returns true if the specified menu is currently open, otherwise false.
* If the menuId is not specified, it returns true if ANY menu is currenly open.
*/
@Method()
isOpen(menuId?: string): boolean {
if (menuId) {
var menu = this.get(menuId);
return menu && menu.isOpen || false;
return menu && menu.isOpen() || false;
} else {
return !!this.getOpen();
}
@ -117,6 +137,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {boolean} Returns true if the menu is currently enabled, otherwise false.
*/
@Method()
isEnabled(menuId?: string): boolean {
const menu = this.get(menuId);
return menu && menu.enabled || false;
@ -131,87 +152,94 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Menu} Returns the instance of the menu if found, otherwise `null`.
*/
@Method()
get(menuId?: string): Menu {
var menu: Menu;
if (menuId === 'left' || menuId === 'right') {
// there could be more than one menu on the same side
// so first try to get the enabled one
menu = this._menus.find(m => m.side === menuId && m.enabled);
menu = this.menus.find(m => m.side === menuId && m.enabled);
if (menu) {
return menu;
}
// didn't find a menu side that is enabled
// so try to get the first menu side found
return this._menus.find(m => m.side === menuId) || null;
return this.menus.find(m => m.side === menuId) || null;
} else if (menuId) {
// the menuId was not left or right
// so try to get the menu by its "id"
return this._menus.find(m => m.id === menuId) || null;
return this.menus.find(m => m.id === menuId) || null;
}
// return the first enabled menu
menu = this._menus.find(m => m.enabled);
menu = this.menus.find(m => m.enabled);
if (menu) {
return menu;
}
// get the first menu in the array, if one exists
return (this._menus.length ? this._menus[0] : null);
return (this.menus.length > 0 ? this.menus[0] : null);
}
/**
* @return {Menu} Returns the instance of the menu already opened, otherwise `null`.
*/
@Method()
getOpen(): Menu {
return this._menus.find(m => m.isOpen);
return this.menus.find(m => m.isOpen());
}
/**
* @return {Array<Menu>} Returns an array of all menu instances.
*/
getMenus(): Array<Menu> {
return this._menus;
@Method()
getMenus(): Menu[] {
return this.menus;
}
/**
* @hidden
* @return {boolean} if any menu is currently animating
*/
@Method()
isAnimating(): boolean {
return this._menus.some(menu => menu.isAnimating);
return this.menus.some(menu => menu.isAnimating());
}
/**
* @hidden
*/
@Method()
_register(menu: Menu) {
if (this._menus.indexOf(menu) < 0) {
this._menus.push(menu);
if (this.menus.indexOf(menu) < 0) {
this.menus.push(menu);
}
}
/**
* @hidden
*/
@Method()
_unregister(menu: Menu) {
const index = this._menus.indexOf(menu);
const index = this.menus.indexOf(menu);
if (index > -1) {
this._menus.splice(index, 1);
this.menus.splice(index, 1);
}
}
/**
* @hidden
*/
@Method()
_setActiveMenu(menu: Menu) {
// if this menu should be enabled
// then find all the other menus on this same side
// and automatically disable other same side menus
const side = menu.side;
this._menus
this.menus
.filter(m => m.side === side && m !== menu)
.map(m => m.enable(false));
}
@ -220,15 +248,17 @@ export class MenuController {
/**
* @hidden
*/
registerType(name: string, cls: new(...args: any[]) => MenuType) {
this._menuTypes[name] = cls;
registerAnimation(name: string, cls: AnimationBuilder) {
this.menuAnimations[name] = cls;
}
/**
* @hidden
*/
create(type: string, menuCmp: Menu) {
return new this._menuTypes[type](menuCmp);
@Method()
create(type: string, menuCmp: Menu): Promise<Animation> {
const animationBuilder = this.menuAnimations[type];
return this.animationCtrl.create(animationBuilder, null, menuCmp);
}
}

View File

@ -1,113 +0,0 @@
// import { Menu } from './menu';
// import { DomController } from '../../platform/dom-controller';
// import { GestureController, GESTURE_PRIORITY_MENU_SWIPE, GESTURE_MENU_SWIPE } from '../../gestures/gesture-controller';
// import { Platform } from '../../platform/platform';
// import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture';
// import { SlideData } from '../../gestures/slide-gesture';
// /**
// * Gesture attached to the content which the menu is assigned to
// */
// export class MenuContentGesture extends SlideEdgeGesture {
// constructor(
// plt: Platform,
// public menu: Menu,
// gestureCtrl: GestureController,
// domCtrl: DomController,
// ) {
// super(plt, plt.doc().body, {
// direction: 'x',
// edge: menu.side,
// threshold: 5,
// maxEdgeStart: menu.maxEdgeStart || 50,
// zone: false,
// passive: true,
// domController: domCtrl,
// gesture: gestureCtrl.createGesture({
// name: GESTURE_MENU_SWIPE,
// priority: GESTURE_PRIORITY_MENU_SWIPE,
// disableScroll: true
// })
// });
// }
// canStart(ev: any): boolean {
// const menu = this.menu;
// if (!menu.canSwipe()) {
// return false;
// }
// if (menu.isOpen) {
// return true;
// } else if (menu.getMenuController().getOpen()) {
// return false;
// }
// return super.canStart(ev);
// }
// // Set CSS, then wait one frame for it to apply before sliding starts
// onSlideBeforeStart(ev: any) {
// console.debug('menu gesture, onSlideBeforeStart', this.menu.side);
// this.menu._swipeBeforeStart();
// }
// onSlideStart() {
// console.debug('menu gesture, onSlideStart', this.menu.side);
// this.menu._swipeStart();
// }
// onSlide(slide: SlideData, ev: any) {
// const z = (this.menu.isRightSide ? slide.min : slide.max);
// const stepValue = (slide.distance / z);
// this.menu._swipeProgress(stepValue);
// }
// onSlideEnd(slide: SlideData, ev: any) {
// let z = (this.menu.isRightSide ? slide.min : slide.max);
// const currentStepValue = (slide.distance / z);
// const velocity = slide.velocity;
// z = Math.abs(z * 0.5);
// const shouldCompleteRight = (velocity >= 0)
// && (velocity > 0.2 || slide.delta > z);
// const shouldCompleteLeft = (velocity <= 0)
// && (velocity < -0.2 || slide.delta < -z);
// console.debug('menu gesture, onSlideEnd', this.menu.side,
// 'distance', slide.distance,
// 'delta', slide.delta,
// 'velocity', velocity,
// 'min', slide.min,
// 'max', slide.max,
// 'shouldCompleteLeft', shouldCompleteLeft,
// 'shouldCompleteRight', shouldCompleteRight,
// 'currentStepValue', currentStepValue);
// this.menu._swipeEnd(shouldCompleteLeft, shouldCompleteRight, currentStepValue, velocity);
// }
// getElementStartPos(slide: SlideData, ev: any) {
// const menu = this.menu;
// if (menu.isRightSide) {
// return menu.isOpen ? slide.min : slide.max;
// }
// // left menu
// return menu.isOpen ? slide.max : slide.min;
// }
// getSlideBoundaries(): { min: number, max: number } {
// const menu = this.menu;
// if (menu.isRightSide) {
// return {
// min: -menu.width(),
// max: 0
// };
// }
// // left menu
// return {
// min: 0,
// max: menu.width()
// };
// }
// }

View File

@ -1,169 +0,0 @@
import { Animation } from '../../index';
/**
* @hidden
* Menu Type
* Base class which is extended by the various types. Each
* type will provide their own animations for open and close
* and registers itself with Menu.
*/
export class MenuType {
ani: Animation;
isOpening: boolean;
constructor() {
// Ionic.createAnimation().then(Animation => {
// this.ani = new Animation();
// });;
// this.ani
// .easing('cubic-bezier(0.0, 0.0, 0.2, 1)')
// .easingReverse('cubic-bezier(0.4, 0.0, 0.6, 1)')
// .duration(280);
}
setOpen(shouldOpen: boolean, animated: boolean, done: (animation: Animation) => void) {
const ani = this.ani
.onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true })
.reverse(!shouldOpen);
if (animated) {
ani.play();
} else {
ani.syncPlay();
}
}
setProgressStart(isOpen: boolean) {
this.isOpening = !isOpen;
// the cloned animation should not use an easing curve during seek
this.ani
.reverse(isOpen)
.progressStart();
}
setProgessStep(stepValue: number) {
// adjust progress value depending if it opening or closing
this.ani.progressStep(stepValue);
}
setProgressEnd(shouldComplete: boolean, currentStepValue: number, velocity: number, done: Function) {
let isOpen = (this.isOpening && shouldComplete);
if (!this.isOpening && !shouldComplete) {
isOpen = true;
}
const ani = this.ani;
ani.onFinish(() => {
this.isOpening = false;
done(isOpen);
}, { clearExistingCallacks: true });
const factor = 1 - Math.min(Math.abs(velocity) / 4, 0.7);
const dur = ani.getDuration() * factor;
ani.progressEnd(shouldComplete, currentStepValue, dur);
}
destroy() {
this.ani.destroy();
this.ani = null;
}
}
/**
* @hidden
* Menu Reveal Type
* The content slides over to reveal the menu underneath.
* The menu itself, which is under the content, does not move.
*/
export class MenuRevealType extends MenuType {
// constructor(menu: Menu) {
// super();
// const openedX = (menu.width() * (menu.isRightSide ? -1 : 1)) + 'px';
// const contentOpen = Ionic.createAnimation(menu.getContentElement());
// contentOpen.fromTo('translateX', '0px', openedX);
// this.ani.add(contentOpen);
// }
}
/**
* @hidden
* Menu Push Type
* The content slides over to reveal the menu underneath.
* The menu itself also slides over to reveal its bad self.
*/
export class MenuPushType extends MenuType {
// constructor(menu: Menu) {
// super();
// let contentOpenedX: string, menuClosedX: string, menuOpenedX: string;
// const width = menu.width();
// if (menu.isRightSide) {
// // right side
// contentOpenedX = -width + 'px';
// menuClosedX = width + 'px';
// menuOpenedX = '0px';
// } else {
// contentOpenedX = width + 'px';
// menuOpenedX = '0px';
// menuClosedX = -width + 'px';
// }
// const menuAni = Ionic.createAnimation(menu.getMenuElement());
// menuAni.fromTo('translateX', menuClosedX, menuOpenedX);
// this.ani.add(menuAni);
// const contentApi = Ionic.createAnimation(menu.getContentElement());
// contentApi.fromTo('translateX', '0px', contentOpenedX);
// this.ani.add(contentApi);
// }
}
/**
* @hidden
* Menu Overlay Type
* The menu slides over the content. The content
* itself, which is under the menu, does not move.
*/
export class MenuOverlayType extends MenuType {
// constructor(menu: Menu) {
// super();
// let closedX: string, openedX: string;
// const width = menu.width();
// if (menu.isRightSide) {
// // right side
// closedX = 8 + width + 'px';
// openedX = '0px';
// } else {
// // left side
// closedX = -(8 + width) + 'px';
// openedX = '0px';
// }
// const menuAni = Ionic.createAnimation(menu.getMenuElement());
// menuAni.fromTo('translateX', closedX, openedX);
// this.ani.add(menuAni);
// const backdropApi = Ionic.createAnimation(menu.getBackdropElement());
// backdropApi.fromTo('opacity', 0.01, 0.35);
// this.ani.add(backdropApi);
// }
}

View File

@ -19,14 +19,14 @@ $menu-ios-box-shadow: 0 0 10px $menu-ios-box-shadow-color !default;
background: $menu-ios-background;
}
.ios .menu-content-reveal {
.menu-ios .menu-content-reveal {
box-shadow: $menu-ios-box-shadow;
}
.ios .menu-content-push {
.menu-ios .menu-content-push {
box-shadow: $menu-ios-box-shadow;
}
.ios ion-menu[type=overlay] .menu-inner {
ion-menu[type=overlay] .menu-ios {
box-shadow: $menu-ios-box-shadow;
}

View File

@ -15,7 +15,7 @@ $menu-md-box-shadow-color: rgba(0, 0, 0, .25) !default;
$menu-md-box-shadow: 0 0 10px $menu-md-box-shadow-color !default;
.menu-md {
.menu-md .menu-inner {
background: $menu-md-background;
}

View File

@ -1,8 +1,10 @@
import { Component, Element, Event, EventEmitter, Prop, PropDidChange } from '@stencil/core';
import { Config } from '../../index';
import { Config, Animation } from '../../index';
import { MenuController } from './menu-controller';
import { MenuType } from './menu-types';
import { isRightSide, Side, assert, checkEdgeSide } from '../../utils/helpers';
export type Lazy<T> =
{[P in keyof T]: (...args: any[]) => Promise<any>; };
@Component({
tag: 'ion-menu',
@ -16,46 +18,44 @@ import { MenuType } from './menu-types';
}
})
export class Menu {
@Element() private el: HTMLElement;
private _backdropElm: HTMLElement;
private _ctrl: MenuController;
private _backdropEle: HTMLElement;
private _menuInnerEle: HTMLElement;
private _unregCntClick: Function;
private _unregBdClick: Function;
private _activeBlock: string;
private _cntElm: HTMLElement;
private _type: MenuType;
private _animation: Animation;
private _init = false;
private _isPane = false;
private _isAnimating: boolean = false;
private _isOpen: boolean = false;
private _width: number = null;
mode: string;
color: string;
/**
* @hidden
*/
isRightSide: boolean = false;
@Element() private el: HTMLElement;
@Event() ionDrag: EventEmitter;
@Event() ionOpen: EventEmitter;
@Event() ionClose: EventEmitter;
@Prop({ context: 'config' }) config: Config;
/**
* @hidden
*/
@Prop() isOpen: boolean = false;
@Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy<MenuController>;
menuCtrl: MenuController;
/**
* @hidden
* @input {string} The content's id the menu should use.
*/
@Prop() isAnimating: boolean = false;
/**
* @hidden
*/
isRightSide: boolean = false;
/**
* @input {any} A reference to the content element the menu should use.
*/
@Prop() content: any;
@Prop() content: string;
/**
* @input {string} An id for the menu.
@ -67,27 +67,22 @@ export class Menu {
* see the `menuType` in the [config](../../config/Config). Available options:
* `"overlay"`, `"reveal"`, `"push"`.
*/
@Prop() type: string;
@Prop() type: string = 'overlay';
/**
* @input {boolean} If true, the menu is enabled. Default `true`.
*/
@Prop() enabled: boolean;
@Prop({ mutable: true }) enabled: boolean;
/**
* @input {string} Which side of the view the menu should be placed. Default `"start"`.
*/
@Prop() side: string = 'start';
@Prop() side: Side = 'start';
/**
* @input {boolean} If true, swiping the menu is enabled. Default `true`.
*/
@Prop() swipeEnabled: boolean;
@PropDidChange('swipeEnabled')
swipeEnabledChange(isEnabled: boolean) {
this.swipeEnable(isEnabled);
}
@Prop() swipeEnabled: boolean = true;
/**
* @input {boolean} If true, the menu will persist on child pages.
@ -97,43 +92,64 @@ export class Menu {
/**
* @hidden
*/
@Prop() maxEdgeStart: number;
@Prop() maxEdgeStart: number = 50;
// @PropDidChange('side')
// sideChanged(side: Side) {
// // TODO: const isRTL = this._plt.isRTL;
// const isRTL = false;
// // this.isRightSide = isRightSide(side, isRTL);
// }
@PropDidChange('enabled')
enabledChanged() {
this._updateState();
}
@PropDidChange('swipeEnabled')
swipeEnabledChange() {
this._updateState();
}
ionViewWillLoad() {
return this.lazyMenuCtrl.getInstance().then(menu => this.menuCtrl = menu);
}
/**
* @hidden
*/
ionViewDidLoad() {
this._backdropElm = this.el.querySelector('.menu-backdrop') as HTMLElement;
assert(!!this.menuCtrl, "menucontroller was not initialized");
this._init = true;
this._menuInnerEle = this.el.querySelector('.menu-inner') as HTMLElement;
this._backdropEle = this.el.querySelector('.menu-backdrop') as HTMLElement;
if (this.content) {
if ((this.content).tagName as HTMLElement) {
this._cntElm = this.content;
} else if (typeof this.content === 'string') {
this._cntElm = document.querySelector(this.content) as any;
}
}
if (!this._cntElm || !this._cntElm.tagName) {
const contentQuery = (this.content)
? '> #' + this.content
: '[main]';
const parent = this.el.parentElement;
const content = this._cntElm = parent.querySelector(contentQuery) as HTMLElement;
if (!content || !content.tagName) {
// requires content element
return console.error('Menu: must have a "content" element to listen for drag events on.');
}
// TODO: make PropDidChange work
this.isRightSide = isRightSide(this.side, false);
// add menu's content classes
this._cntElm.classList.add('menu-content');
this._cntElm.classList.add('menu-content-' + this.type);
content.classList.add('menu-content');
content.classList.add('menu-content-' + this.type);
let isEnabled = this.enabled;
if (isEnabled === true || typeof isEnabled === 'undefined') {
// check if more than one menu is on the same side
isEnabled = !this._ctrl.getMenus().some(m => {
const menus = this.menuCtrl.getMenus();
isEnabled = !menus.some(m => {
return m.side === this.side && m.enabled;
});
}
// register this menu with the app's menu controller
this._ctrl._register(this);
this.menuCtrl._register(this);
// mask it as enabled / disabled
this.enable(isEnabled);
@ -143,30 +159,34 @@ export class Menu {
return {
attrs: {
'role': 'navigation',
'side': this.side,
'side': this.getSide(),
'type': this.type
},
class: {
'menu-enabled': this.enabled
'menu-enabled': this._canOpen()
}
};
}
render() {
// normalize the "type"
if (!this.type) {
this.type = this.config.get('menuType', 'overlay');
getSide(): string {
return this.isRightSide ? 'right' : 'left';
}
return [
render() {
return ([
<div class='menu-inner'>
<slot></slot>
</div>,
<ion-gesture class='menu-backdrop' props={{
// 'canStart': this.canStart.bind(this),
// 'onStart': this.onDragStart.bind(this),
// 'onMove': this.onDragMove.bind(this),
// 'onEnd': this.onDragEnd.bind(this),
<ion-backdrop class="menu-backdrop"></ion-backdrop> ,
<ion-gesture props={{
'canStart': this.canStart.bind(this),
'onWillStart': this._swipeWillStart.bind(this),
'onStart': this._swipeStart.bind(this),
'onMove': this._swipeProgress.bind(this),
'onEnd': this._swipeEnd.bind(this),
'maxEdgeStart': this.maxEdgeStart,
'edge': this.side,
'enabled': this._canOpen() && this.swipeEnabled,
'gestureName': 'menu-swipe',
'gesturePriority': 10,
'type': 'pan',
@ -176,7 +196,7 @@ export class Menu {
'disableScroll': true,
'block': this._activeBlock
}}></ion-gesture>
];
]);
}
/**
@ -185,21 +205,25 @@ export class Menu {
onBackdropClick(ev: UIEvent) {
ev.preventDefault();
ev.stopPropagation();
this._ctrl.close();
this.menuCtrl.close();
}
/**
* @hidden
*/
private _getType(): MenuType {
if (!this._type) {
this._type = this._ctrl.create(this.type, this);
if (this.config.getBoolean('animate') === false) {
this._type.ani.duration(0);
private prepareAnimation(): Promise<void> {
const width = this._menuInnerEle.offsetWidth;
if (width === this._width) {
return Promise.resolve();
}
if (this._animation) {
this._animation.destroy();
this._animation = null;
}
return this._type;
this._width = width;
return this.menuCtrl.create(this.type, this).then(ani => {
this._animation = ani;
});
}
/**
@ -207,23 +231,44 @@ export class Menu {
*/
setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// If the menu is disabled or it is currenly being animated, let's do nothing
if ((shouldOpen === this.isOpen) || !this._canOpen() || this.isAnimating) {
return Promise.resolve(this.isOpen);
if ((shouldOpen === this._isOpen) || !this._canOpen() || this._isAnimating) {
return Promise.resolve(this._isOpen);
}
return new Promise(resolve => {
this._before();
this._getType().setOpen(shouldOpen, animated, () => {
this.prepareAnimation()
.then(() => this._startAnimation(shouldOpen, animated))
.then(() => {
this._after(shouldOpen);
resolve(this.isOpen);
});
return this._isOpen;
});
}
_startAnimation(shouldOpen: boolean, animated: boolean): Promise<Animation> {
let done;
const promise = new Promise<Animation>(resolve => done = resolve);
const ani = this._animation
.onFinish(done, {oneTimeCallback: true, clearExistingCallacks: true })
.reverse(!shouldOpen);
if (animated) {
ani.play();
} else {
ani.syncPlay();
}
return promise;
}
_forceClosing() {
this.isAnimating = true;
this._getType().setOpen(false, false, () => {
this._after(false);
});
assert(this._isOpen, 'menu cannot be closed');
this._isAnimating = true;
this._startAnimation(false, false)
.then(() => this._after(false));
}
getWidth(): number {
return this._width;
}
/**
@ -231,77 +276,120 @@ export class Menu {
*/
canSwipe(): boolean {
return this.swipeEnabled &&
!this.isAnimating &&
!this._isAnimating &&
this._canOpen();
// TODO: && this._app.isEnabled();
}
_swipeBeforeStart() {
if (!this.canSwipe()) {
return;
/**
* @hidden
*/
isAnimating(): boolean {
return this._isAnimating;
}
/**
* @hidden
*/
isOpen(): boolean {
return this._isOpen;
}
_swipeWillStart(): Promise<void> {
this._before();
return this.prepareAnimation();
}
_swipeStart() {
if (!this.isAnimating) {
assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return;
}
this._getType().setProgressStart(this.isOpen);
// the cloned animation should not use an easing curve during seek
this._animation
.reverse(this._isOpen)
.progressStart();
}
_swipeProgress(stepValue: number) {
if (!this.isAnimating) {
_swipeProgress(slide: any) {
assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return;
}
this._getType().setProgessStep(stepValue);
// const isRTL = false;
const z = this._width;
// const z = (this.isRightSide !== isRTL ? slide.min : slide.max);
const stepValue = (Math.abs(slide.deltaX) / z);
this.ionDrag.emit({ menu: this });
this._animation.progressStep(stepValue);
// TODO: this.ionDrag.emit({ menu: this });
}
_swipeEnd(shouldCompleteLeft: boolean, shouldCompleteRight: boolean, stepValue: number, velocity: number) {
if (!this.isAnimating) {
_swipeEnd(slide: any) {
assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return;
}
const width = this._width;
const delta = Math.abs(slide.deltaX)
const stepValue = delta / width;
const velocity = slide.velocityX;
const z = width / 2;
const shouldCompleteRight = (velocity >= 0)
&& (velocity > 0.2 || slide.deltaX > z);
const shouldCompleteLeft = (velocity <= 0)
&& (velocity < -0.2 || slide.deltaX < -z);
// user has finished dragging the menu
const isRightSide = this.isRightSide;
const opening = !this.isOpen;
const opening = !this._isOpen;
const shouldComplete = (opening)
? isRightSide ? shouldCompleteLeft : shouldCompleteRight
: isRightSide ? shouldCompleteRight : shouldCompleteLeft;
this._getType().setProgressEnd(shouldComplete, stepValue, velocity, (isOpen: boolean) => {
console.debug('menu, swipeEnd', this.side);
this._after(isOpen);
});
let isOpen = (opening && shouldComplete);
if (!opening && !shouldComplete) {
isOpen = true;
}
const missing = shouldComplete ? 1 - stepValue : stepValue;
const missingDistance = missing * width;
const dur = missingDistance / Math.abs(velocity);
const realDur = Math.min(dur, 380);
this._animation
.onFinish(() => this._after(isOpen), { clearExistingCallacks: true })
.progressEnd(shouldComplete, stepValue, realDur);
}
private _before() {
assert(!this._isAnimating, '_before() should not be called while animating');
// this places the menu into the correct location before it animates in
// this css class doesn't actually kick off any animations
this.el.classList.add('show-menu');
this._backdropElm.classList.add('show-backdrop');
this._backdropEle.classList.add('show-backdrop');
this.resize();
// TODO: this._keyboard.close();
this.isAnimating = true;
this._isAnimating = true;
}
private _after(isOpen: boolean) {
assert(this._isAnimating, '_before() should be called while animating');
// TODO: this._app.setEnabled(false, 100);
// 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
// emit opened/closed events
this.isOpen = isOpen;
this.isAnimating = false;
this._isOpen = isOpen;
this._isAnimating = false;
// add/remove backdrop click listeners
this._backdropClick(isOpen);
@ -311,9 +399,7 @@ export class Menu {
this._activeBlock = GESTURE_BLOCKER;
// add css class
Context.dom.write(() => {
this._cntElm.classList.add('menu-content-open');
});
// emit open event
this.ionOpen.emit({ menu: this });
@ -323,11 +409,9 @@ export class Menu {
this._activeBlock = null;
// remove css classes
Context.dom.write(() => {
this.el.classList.remove('show-menu');
this._cntElm.classList.remove('menu-content-open');
this._cntElm.classList.remove('show-menu');
this._backdropElm.classList.remove('show-menu');
});
this._backdropEle.classList.remove('show-menu');
// emit close event
this.ionClose.emit({ menu: this });
@ -359,11 +443,23 @@ export class Menu {
// content && content.resize();
}
canStart(detail: any): boolean {
if (!this.canSwipe()) {
return false;
}
if (this._isOpen) {
return true;
} else if (this.getMenuController().getOpen()) {
return false;
}
return checkEdgeSide(detail.currentX, this.isRightSide, this.maxEdgeStart);
}
/**
* @hidden
*/
toggle(): Promise<boolean> {
return this.setOpen(!this.isOpen);
return this.setOpen(!this._isOpen);
}
_canOpen(): boolean {
@ -373,40 +469,30 @@ export class Menu {
/**
* @hidden
*/
// @PropDidChange('swipeEnabled')
// @PropDidChange('enabled')
_updateState() {
const canOpen = this._canOpen();
// Close menu inmediately
if (!canOpen && this.isOpen) {
if (!canOpen && this._isOpen) {
assert(this._init, 'menu must be initialized');
// close if this menu is open, and should not be enabled
this._forceClosing();
}
if (this.enabled && this._ctrl) {
this._ctrl._setActiveMenu(this);
if (this.enabled && this.menuCtrl) {
this.menuCtrl._setActiveMenu(this);
}
if (!this._init) {
return;
}
// TODO
// const gesture = this._gesture;
// // only listen/unlisten if the menu has initialized
// if (canOpen && this.swipeEnabled && !gesture.isListening) {
// // should listen, but is not currently listening
// console.debug('menu, gesture listen', this.side);
// gesture.listen();
// } else if (gesture.isListening && (!canOpen || !this.swipeEnabled)) {
// // should not listen, but is currently listening
// console.debug('menu, gesture unlisten', this.side);
// gesture.unlisten();
// }
if (this.isOpen || (this._isPane && this.enabled)) {
if (this._isOpen || (this._isPane && this.enabled)) {
this.resize();
}
assert(!this._isAnimating, 'can not be animating');
}
/**
@ -414,7 +500,6 @@ export class Menu {
*/
enable(shouldEnable: boolean): Menu {
this.enabled = shouldEnable;
this._updateState();
return this;
}
@ -438,7 +523,6 @@ export class Menu {
*/
swipeEnable(shouldEnable: boolean): Menu {
this.swipeEnabled = shouldEnable;
this._updateState();
return this;
}
@ -460,21 +544,14 @@ export class Menu {
* @hidden
*/
getBackdropElement(): HTMLElement {
return this._backdropElm;
}
/**
* @hidden
*/
width(): number {
return this.getMenuElement().offsetWidth;
return this._backdropEle;
}
/**
* @hidden
*/
getMenuController(): MenuController {
return this._ctrl;
return this.menuCtrl;
}
private _backdropClick(shouldAdd: boolean) {
@ -497,10 +574,10 @@ export class Menu {
ionViewDidUnload() {
this._backdropClick(false);
this._ctrl._unregister(this);
this._type && this._type.destroy();
this.menuCtrl._unregister(this);
this._animation && this._animation.destroy();
this._ctrl = this._type = this._cntElm = this._backdropElm = null;
this.menuCtrl = this._animation = this._cntElm = this._backdropEle = null;
}
}

View File

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Ionic Item Sliding</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-menu side="left">
<ion-header>
<ion-toolbar color="secondary">
<ion-title>Left Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
Open Right Menu
</ion-item>
<ion-item menuClose="left" class="e2eCloseLeftMenu" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
<ion-item menuClose="left" detail-none>
Close Menu
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar color="secondary">
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
</ion-menu>
<ion-menu side="right" >
<ion-header id="id">
<ion-toolbar>
<ion-title>Hola</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
hola macho
</ion-content>
</ion-menu>
<ion-page main class="show-page">
<ion-header>
<ion-toolbar>
<ion-title>Menu Basic Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-button onclick="openLeft()">
Open left menu
</ion-button>
<ion-button onclick="openRight()">
Open right menu
</ion-button>
</ion-content>
</ion-page>
</ion-app>
<script>
function getMenu() {
return document.querySelector('ion-menu-controller');
}
function openLeft() {
console.log('Open left menu');
getMenu().open('left');
}
function openRight() {
console.log('Open right menu');
getMenu().open('right');
}
</script>
</body>
</html>

View File

@ -82,9 +82,6 @@ export class Scroll {
}
}
detail.directionX = detail.velocityDirectionX = (detail.deltaX > 0 ? 'left' : (detail.deltaX < 0 ? 'right' : null));
detail.directionY = detail.velocityDirectionY = (detail.deltaY > 0 ? 'up' : (detail.deltaY < 0 ? 'down' : null));
// actively scrolling
positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp);
@ -106,15 +103,11 @@ export class Scroll {
// compute relative movement between these two points
var movedTop = (positions[startPos - 2] - positions[endPos - 2]);
var movedLeft = (positions[startPos - 1] - positions[endPos - 1]);
var factor = 16.67 / (positions[endPos] - positions[startPos]);
var factor = 16.67 / (positions[startPos] - positions[endPos]);
// based on XXms compute the movement to apply for each render step
detail.velocityY = movedTop * factor;
detail.velocityX = movedLeft * factor;
// figure out which direction we're scrolling
detail.velocityDirectionX = (movedLeft > 0 ? 'left' : (movedLeft < 0 ? 'right' : null));
detail.velocityDirectionY = (movedTop > 0 ? 'up' : (movedTop < 0 ? 'down' : null));
}
}

View File

@ -10,7 +10,6 @@ import { Loading, LoadingEvent, LoadingOptions } from './components/loading/load
import { LoadingController } from './components/loading-controller/loading-controller';
import { GestureDetail, GestureCallback } from './components/gesture/gesture';
import { Menu } from './components/menu/menu';
import { MenuType } from './components/menu/menu-types';
import { MenuController } from './components/menu/menu-controller';
import { Modal, ModalOptions, ModalEvent } from './components/modal/modal';
import { ModalController } from './components/modal-controller/modal-controller';
@ -77,7 +76,6 @@ export {
LoadingEvent,
Menu,
MenuController,
MenuType,
Modal,
ModalController,
ModalOptions,

View File

@ -41,7 +41,7 @@ export function assert(bool: boolean, msg: string) {
if (!bool) {
console.error(msg);
}
};
}
export function toDashCase(str: string) {
return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase());
@ -64,6 +64,26 @@ export function pointerCoordX(ev: any): number {
return 0;
}
export function updateDetail(ev: any, detail: any) {
// get X coordinates for either a mouse click
// or a touch depending on the given event
let x = 0;
let y = 0;
if (ev) {
var changedTouches = ev.changedTouches;
if (changedTouches && changedTouches.length > 0) {
var touch = changedTouches[0];
x = touch.clientX;
y = touch.clientY;
}else if (ev.pageX !== undefined) {
x = ev.pageX;
y = ev.pageY;
}
}
detail.currentX = x;
detail.currentY = y;
}
export function pointerCoordY(ev: any): number {
// get Y coordinates for either a mouse click
// or a touch depending on the given event
@ -79,7 +99,9 @@ export function pointerCoordY(ev: any): number {
return 0;
}
export function getElementReference(elm: any, ref: string) {
export type ElementRef = 'child' | 'parent' | 'body' | 'document' | 'window';
export function getElementReference(elm: any, ref: ElementRef) {
if (ref === 'child') {
return elm.firstElementChild;
}
@ -139,9 +161,16 @@ export function getToolbarHeight(toolbarTagName: string, pageChildren: HTMLEleme
return '';
}
/** @hidden */
export type Side = 'left' | 'right' | 'start' | 'end';
export function checkEdgeSide(posX: number, isRightSide: boolean, maxEdgeStart: number): boolean {
if (isRightSide) {
return posX >= window.innerWidth - maxEdgeStart;
} else {
return posX <= maxEdgeStart;
}
}
/**
* @hidden
* Given a side, return if it should be on the right

View File

@ -19,7 +19,7 @@ exports.config = {
{ components: ['ion-item', 'ion-item-divider', 'ion-item-sliding', 'ion-item-options', 'ion-item-option', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] },
{ components: ['ion-input', 'ion-textarea'] },
{ components: ['ion-loading', 'ion-loading-controller'] },
{ components: ['ion-menu'], priority: 'low' },
{ components: ['ion-menu', 'ion-menu-controller'], priority: 'low' },
{ components: ['ion-modal', 'ion-modal-controller'] },
{ components: ['ion-popover', 'ion-popover-controller'] },
{ components: ['ion-radio', 'ion-radio-group'] },