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": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
@ -1519,6 +1511,14 @@
"strip-ansi": "3.0.1" "strip-ansi": "3.0.1"
} }
}, },
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": { "stringstream": {
"version": "0.0.5", "version": "0.0.5",
"bundled": true, "bundled": true,
@ -3136,15 +3136,6 @@
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
"dev": true "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": { "string-template": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@ -3162,6 +3153,15 @@
"strip-ansi": "3.0.1" "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": { "stringstream": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", "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 { export class Animator {
private _afterAddClasses: string[]; private _afterAddClasses: string[];
private _afterRemoveClasses: string[]; private _afterRemoveClasses: string[];
private _afterStyles: { [property: string]: any; }; private _afterStyles: { [property: string]: any; };
@ -639,7 +640,7 @@ export class Animator {
// flip the number if we're going in reverse // flip the number if we're going in reverse
if (this._isReverse) { if (this._isReverse) {
stepValue = ((stepValue * -1) + 1); stepValue = 1 - stepValue;
} }
var i = 0; var i = 0;
var j = 0; var j = 0;
@ -1023,12 +1024,6 @@ export class Animator {
children[i].progressStep(stepValue); 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 **************** // ******** DOM WRITE ****************
this._progress(stepValue); 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 { BlockerDelegate, GestureController, GestureDelegate, BLOCK_ALL } from '../gesture-controller/gesture-controller';
import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core';
import { PanRecognizer } from './recognizers'; import { PanRecognizer } from './recognizers';
@Component({ @Component({
tag: 'ion-gesture' tag: 'ion-gesture'
}) })
export class Gesture { export class Gesture {
@Element() private el: HTMLElement; @Element() private el: HTMLElement;
private detail: GestureDetail = {}; private detail: GestureDetail = {};
private positions: number[] = []; private positions: number[] = [];
@ -18,9 +18,10 @@ export class Gesture {
private hasCapturedPan = false; private hasCapturedPan = false;
private hasPress = false; private hasPress = false;
private hasStartedPan = false; private hasStartedPan = false;
private requiresMove = false; private hasFiredStart = true;
private isMoveQueued = false; private isMoveQueued = false;
private blocker: BlockerDelegate; private blocker: BlockerDelegate;
private fireOnMoveFunc: any;
@Event() private ionGestureMove: EventEmitter; @Event() private ionGestureMove: EventEmitter;
@Event() private ionGestureStart: EventEmitter; @Event() private ionGestureStart: EventEmitter;
@ -28,7 +29,8 @@ export class Gesture {
@Event() private ionGestureNotCaptured: EventEmitter; @Event() private ionGestureNotCaptured: EventEmitter;
@Event() private ionPress: EventEmitter; @Event() private ionPress: EventEmitter;
@Prop() attachTo: string = 'child'; @Prop() enabled: boolean = true;
@Prop() attachTo: ElementRef = 'child';
@Prop() autoBlockAll: boolean = false; @Prop() autoBlockAll: boolean = false;
@Prop() block: string = null; @Prop() block: string = null;
@Prop() disableScroll: boolean = false; @Prop() disableScroll: boolean = false;
@ -40,12 +42,16 @@ export class Gesture {
@Prop() type: string = 'pan'; @Prop() type: string = 'pan';
@Prop() canStart: GestureCallback; @Prop() canStart: GestureCallback;
@Prop() onWillStart: (_: GestureDetail) => Promise<void>;
@Prop() onStart: GestureCallback; @Prop() onStart: GestureCallback;
@Prop() onMove: GestureCallback; @Prop() onMove: GestureCallback;
@Prop() onEnd: GestureCallback; @Prop() onEnd: GestureCallback;
@Prop() onPress: GestureCallback; @Prop() onPress: GestureCallback;
@Prop() notCaptured: GestureCallback; @Prop() notCaptured: GestureCallback;
constructor() {
this.fireOnMoveFunc = this.fireOnMove.bind(this);
}
ionViewDidLoad() { ionViewDidLoad() {
// in this case, we already know the GestureController and Gesture are already // 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); this.gesture = this.ctrl.createGesture(this.gestureName, this.gesturePriority, this.disableScroll);
const types = this.type.replace(/\s/g, '').toLowerCase().split(','); const types = this.type.replace(/\s/g, '').toLowerCase().split(',');
if (types.indexOf('pan') > -1) { if (types.indexOf('pan') > -1) {
this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle); this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle);
this.requiresMove = true;
} }
this.hasPress = (types.indexOf('press') > -1); this.hasPress = (types.indexOf('press') > -1);
this.enabledChange(true);
if (this.pan || this.hasPress) { if (this.pan || this.hasPress) {
Context.enableListener(this, 'touchstart', true, this.attachTo);
Context.enableListener(this, 'mousedown', true, this.attachTo);
Context.dom.write(() => { Context.dom.write(() => {
applyStyles(getElementReference(this.el, this.attachTo), GESTURE_INLINE_STYLES); 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') @PropDidChange('block')
blockChange(block: string) { blockChange(block: string) {
@ -94,10 +109,12 @@ export class Gesture {
onTouchStart(ev: TouchEvent) { onTouchStart(ev: TouchEvent) {
this.lastTouch = now(ev); this.lastTouch = now(ev);
this.enableMouse(false); if (this.pointerDown(ev, this.lastTouch)) {
this.enableTouch(true); 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); const timeStamp = now(ev);
if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) {
this.enableMouse(true); if (this.pointerDown(ev, timeStamp)) {
this.enableTouch(false); this.enableMouse(true);
this.enableTouch(false);
this.pointerDown(ev, timeStamp); } else {
this.abortGesture();
}
} }
} }
private pointerDown(ev: UIEvent, timeStamp: number): boolean { private pointerDown(ev: UIEvent, timeStamp: number): boolean {
if (!this.gesture || this.hasStartedPan) { if (!this.gesture || this.hasStartedPan || !this.hasFiredStart) {
return false; return false;
} }
const detail = this.detail; const detail = this.detail;
detail.startX = detail.currentX = pointerCoordX(ev); updateDetail(ev, detail);
detail.startY = detail.currentY = pointerCoordY(ev); detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp = timeStamp; detail.startTimeStamp = detail.timeStamp = timeStamp;
detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0; detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
detail.directionX = detail.directionY = detail.velocityDirectionX = detail.velocityDirectionY = null;
detail.event = ev; detail.event = ev;
this.positions.length = 0; 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) { if (this.canStart && this.canStart(detail) === false) {
return false; return false;
} }
@ -145,11 +169,8 @@ export class Gesture {
if (this.pan) { if (this.pan) {
this.hasStartedPan = true; this.hasStartedPan = true;
this.hasCapturedPan = false;
this.pan.start(detail.startX, detail.startY); this.pan.start(detail.startX, detail.startY);
} }
return true; return true;
} }
@ -159,7 +180,6 @@ export class Gesture {
@Listen('touchmove', { passive: true, enabled: false }) @Listen('touchmove', { passive: true, enabled: false })
onTouchMove(ev: TouchEvent) { onTouchMove(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev); this.lastTouch = this.detail.timeStamp = now(ev);
this.pointerMove(ev); this.pointerMove(ev);
} }
@ -167,7 +187,6 @@ export class Gesture {
@Listen('document:mousemove', { passive: true, enabled: false }) @Listen('document:mousemove', { passive: true, enabled: false })
onMoveMove(ev: TouchEvent) { onMoveMove(ev: TouchEvent) {
const timeStamp = now(ev); const timeStamp = now(ev);
if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) {
this.detail.timeStamp = timeStamp; this.detail.timeStamp = timeStamp;
this.pointerMove(ev); this.pointerMove(ev);
@ -175,107 +194,124 @@ export class Gesture {
} }
private pointerMove(ev: UIEvent) { 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; const detail = this.detail;
this.calcGestureData(ev); this.calcGestureData(ev);
if (this.pan.detect(detail.currentX, detail.currentY)) {
if (this.pan) { if (this.pan.isGesture() !== 0) {
if (this.hasCapturedPan) { if (!this.tryToCapturePan(ev)) {
this.abortGesture();
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.isGesture() !== 0) {
if (!this.tryToCapturePan(ev)) {
this.abortGesture();
}
} }
} }
} }
} }
private fireOnMove() {
const detail = this.detail;
this.isMoveQueued = false;
if (this.onMove) {
this.onMove(detail);
} else {
this.ionGestureMove.emit(detail);
}
}
private calcGestureData(ev: UIEvent) { private calcGestureData(ev: UIEvent) {
const detail = this.detail; const detail = this.detail;
detail.currentX = pointerCoordX(ev); updateDetail(ev, detail);
detail.currentY = pointerCoordY(ev);
detail.deltaX = (detail.currentX - detail.startX); const currentX = detail.currentX;
detail.deltaY = (detail.currentY - detail.startY); const currentY = detail.currentY;
const timestamp = detail.timeStamp;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev; detail.event = ev;
// figure out which direction we're movin' const timeRange = timestamp - 100;
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 positions = this.positions; const positions = this.positions;
positions.push(detail.currentX, detail.currentY, detail.timeStamp); let startPos = positions.length - 1;
var endPos = (positions.length - 1);
var startPos = endPos;
var timeRange = (detail.timeStamp - 100);
// move pointer to position measured 100ms ago // move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) { for (;
startPos = i; startPos > 0 && positions[startPos] > timeRange;
} startPos -= 3) { }
if (startPos !== endPos) { if (startPos > 1) {
// compute relative movement between these two points // compute relative movement between these two points
var movedX = (positions[startPos - 2] - positions[endPos - 2]); var frequency = 1 / (positions[startPos] - timestamp);
var movedY = (positions[startPos - 1] - positions[endPos - 1]); var movedY = positions[startPos - 1] - currentY;
var factor = 16.67 / (positions[endPos] - positions[startPos]); var movedX = positions[startPos - 2] - currentX;
// based on XXms compute the movement to apply for each render step // based on XXms compute the movement to apply for each render step
detail.velocityX = movedX * factor; // velocity = space/time = s*(1/t) = s*frequency
detail.velocityY = movedY * factor; detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
detail.velocityDirectionX = (movedX > 0 ? 'left' : (movedX < 0 ? 'right' : null)); } else {
detail.velocityDirectionY = (movedY > 0 ? 'up' : (movedY < 0 ? 'down' : null)); detail.velocityX = 0;
detail.velocityY = 0;
} }
positions.push(currentX, currentY, timestamp);
} }
private tryToCapturePan(ev: UIEvent): boolean { private tryToCapturePan(ev: UIEvent): boolean {
if (this.gesture && !this.gesture.capture()) { if (this.gesture && !this.gesture.capture()) {
return false; 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) { if (this.onStart) {
this.onStart(this.detail); this.onStart(this.detail);
} else { } else {
this.ionGestureStart.emit(this.detail); this.ionGestureStart.emit(this.detail);
} }
this.hasFiredStart = true;
this.hasCapturedPan = true;
return true;
} }
private abortGesture() { private abortGesture() {
this.hasStartedPan = false; this.reset();
this.hasCapturedPan = false;
this.gesture && this.gesture.release();
this.enable(false); this.enable(false);
this.notCaptured && this.notCaptured(this.detail); this.notCaptured && this.notCaptured(this.detail);
} }
private reset() {
this.hasCapturedPan = false;
this.hasStartedPan = false;
this.hasFiredStart = true;
this.gesture && this.gesture.release();
}
// END ************************* // 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 }) @Listen('touchend', { passive: true, enabled: false })
onTouchEnd(ev: TouchEvent) { onTouchEnd(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev); this.lastTouch = this.detail.timeStamp = now(ev);
@ -298,47 +334,46 @@ export class Gesture {
private pointerUp(ev: UIEvent) { private pointerUp(ev: UIEvent) {
const hasCaptured = this.hasCapturedPan;
const hasFiredStart = this.hasFiredStart;
this.reset();
if (!hasFiredStart) {
return;
}
const detail = this.detail; const detail = this.detail;
this.gesture && this.gesture.release();
detail.event = ev;
this.calcGestureData(ev); this.calcGestureData(ev);
if (this.pan) { // Try to capture press
if (this.hasCapturedPan) { if (hasCaptured) {
detail.type = 'pan'; detail.type = 'pan';
if (this.onEnd) { if (this.onEnd) {
this.onEnd(detail); this.onEnd(detail);
} else {
this.ionGestureEnd.emit(detail);
}
} else if (this.hasPress) {
this.detectPress();
} else { } else {
if (this.notCaptured) { this.ionGestureEnd.emit(detail);
this.notCaptured(detail);
} else {
this.ionGestureNotCaptured.emit(detail);
}
} }
return;
} else if (this.hasPress) {
this.detectPress();
} }
this.hasCapturedPan = false; // Try to capture press
this.hasStartedPan = false; if (this.hasPress && this.detectPress()) {
return;
}
// Not captured any event
if (this.notCaptured) {
this.notCaptured(detail);
} else {
this.ionGestureNotCaptured.emit(detail);
}
} }
private detectPress(): boolean {
private detectPress() {
const detail = this.detail; const detail = this.detail;
const vecX = detail.deltaX;
if (Math.abs(detail.startX - detail.currentX) < 10 && Math.abs(detail.startY - detail.currentY) < 10) { const vecY = detail.deltaY;
const dis = vecX * vecX + vecY * vecY;
if (dis < 100) {
detail.type = 'press'; detail.type = 'press';
if (this.onPress) { if (this.onPress) {
@ -346,25 +381,27 @@ export class Gesture {
} else { } else {
this.ionPress.emit(detail); this.ionPress.emit(detail);
} }
return true;
} }
return false;
} }
// ENABLE LISTENERS ************************* // ENABLE LISTENERS *************************
private enableMouse(shouldEnable: boolean) { private enableMouse(shouldEnable: boolean) {
if (this.requiresMove) { if (this.pan) {
Context.enableListener(this, 'document:mousemove', shouldEnable); 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) { private enableTouch(shouldEnable: boolean) {
if (this.requiresMove) { if (this.pan) {
Context.enableListener(this, 'touchmove', shouldEnable); 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; velocityY?: number;
deltaX?: number; deltaX?: number;
deltaY?: number; deltaY?: number;
directionX?: 'left'|'right';
directionY?: 'up'|'down';
velocityDirectionX?: 'left'|'right';
velocityDirectionY?: 'up'|'down';
timeStamp?: number; 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 { Menu, AnimationController, AnimationBuilder, Animation } from '../../index';
import { MenuRevealType, MenuPushType, MenuOverlayType } from './menu-types'; 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 { 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() { constructor() {
this.registerType('reveal', MenuRevealType); this.registerAnimation('reveal', MenuRevealAnimation);
this.registerType('push', MenuPushType); this.registerAnimation('push', MenuPushAnimation);
this.registerType('overlay', MenuOverlayType); 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. * @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu is fully opened * @return {Promise} returns a promise when the menu is fully opened
*/ */
@Method()
open(menuId?: string): Promise<boolean> { open(menuId?: string): Promise<boolean> {
const menu = this.get(menuId); const menu = this.get(menuId);
if (menu && !this.isAnimating()) { if (menu && !this.isAnimating()) {
@ -36,6 +51,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side. * @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu is fully closed * @return {Promise} returns a promise when the menu is fully closed
*/ */
@Method()
close(menuId?: string): Promise<boolean> { close(menuId?: string): Promise<boolean> {
let menu: Menu; let menu: Menu;
@ -63,6 +79,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side. * @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Promise} returns a promise when the menu has been toggled * @return {Promise} returns a promise when the menu has been toggled
*/ */
@Method()
toggle(menuId?: string): Promise<boolean> { toggle(menuId?: string): Promise<boolean> {
const menu = this.get(menuId); const menu = this.get(menuId);
if (menu && !this.isAnimating()) { if (menu && !this.isAnimating()) {
@ -83,6 +100,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side. * @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. * @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/ */
@Method()
enable(shouldEnable: boolean, menuId?: string): Menu { enable(shouldEnable: boolean, menuId?: string): Menu {
const menu = this.get(menuId); const menu = this.get(menuId);
return (menu && menu.enable(shouldEnable)) || null; 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. * @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. * @return {Menu} Returns the instance of the menu, which is useful for chaining.
*/ */
@Method()
swipeEnable(shouldEnable: boolean, menuId?: string): Menu { swipeEnable(shouldEnable: boolean, menuId?: string): Menu {
const menu = this.get(menuId); const menu = this.get(menuId);
return (menu && menu.swipeEnable(shouldEnable)) || null; 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. * @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. * If the menuId is not specified, it returns true if ANY menu is currenly open.
*/ */
@Method()
isOpen(menuId?: string): boolean { isOpen(menuId?: string): boolean {
if (menuId) { if (menuId) {
var menu = this.get(menuId); var menu = this.get(menuId);
return menu && menu.isOpen || false; return menu && menu.isOpen() || false;
} else { } else {
return !!this.getOpen(); return !!this.getOpen();
} }
@ -117,6 +137,7 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side. * @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {boolean} Returns true if the menu is currently enabled, otherwise false. * @return {boolean} Returns true if the menu is currently enabled, otherwise false.
*/ */
@Method()
isEnabled(menuId?: string): boolean { isEnabled(menuId?: string): boolean {
const menu = this.get(menuId); const menu = this.get(menuId);
return menu && menu.enabled || false; return menu && menu.enabled || false;
@ -131,87 +152,94 @@ export class MenuController {
* @param {string} [menuId] Optionally get the menu by its id, or side. * @param {string} [menuId] Optionally get the menu by its id, or side.
* @return {Menu} Returns the instance of the menu if found, otherwise `null`. * @return {Menu} Returns the instance of the menu if found, otherwise `null`.
*/ */
@Method()
get(menuId?: string): Menu { get(menuId?: string): Menu {
var menu: Menu; var menu: Menu;
if (menuId === 'left' || menuId === 'right') { if (menuId === 'left' || menuId === 'right') {
// there could be more than one menu on the same side // there could be more than one menu on the same side
// so first try to get the enabled one // 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) { if (menu) {
return menu; return menu;
} }
// didn't find a menu side that is enabled // didn't find a menu side that is enabled
// so try to get the first menu side found // 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) { } else if (menuId) {
// the menuId was not left or right // the menuId was not left or right
// so try to get the menu by its "id" // 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 // return the first enabled menu
menu = this._menus.find(m => m.enabled); menu = this.menus.find(m => m.enabled);
if (menu) { if (menu) {
return menu; return menu;
} }
// get the first menu in the array, if one exists // 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`. * @return {Menu} Returns the instance of the menu already opened, otherwise `null`.
*/ */
@Method()
getOpen(): Menu { 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. * @return {Array<Menu>} Returns an array of all menu instances.
*/ */
getMenus(): Array<Menu> { @Method()
return this._menus; getMenus(): Menu[] {
return this.menus;
} }
/** /**
* @hidden * @hidden
* @return {boolean} if any menu is currently animating * @return {boolean} if any menu is currently animating
*/ */
@Method()
isAnimating(): boolean { isAnimating(): boolean {
return this._menus.some(menu => menu.isAnimating); return this.menus.some(menu => menu.isAnimating());
} }
/** /**
* @hidden * @hidden
*/ */
@Method()
_register(menu: Menu) { _register(menu: Menu) {
if (this._menus.indexOf(menu) < 0) { if (this.menus.indexOf(menu) < 0) {
this._menus.push(menu); this.menus.push(menu);
} }
} }
/** /**
* @hidden * @hidden
*/ */
@Method()
_unregister(menu: Menu) { _unregister(menu: Menu) {
const index = this._menus.indexOf(menu); const index = this.menus.indexOf(menu);
if (index > -1) { if (index > -1) {
this._menus.splice(index, 1); this.menus.splice(index, 1);
} }
} }
/** /**
* @hidden * @hidden
*/ */
@Method()
_setActiveMenu(menu: Menu) { _setActiveMenu(menu: Menu) {
// if this menu should be enabled // if this menu should be enabled
// then find all the other menus on this same side // then find all the other menus on this same side
// and automatically disable other same side menus // and automatically disable other same side menus
const side = menu.side; const side = menu.side;
this._menus this.menus
.filter(m => m.side === side && m !== menu) .filter(m => m.side === side && m !== menu)
.map(m => m.enable(false)); .map(m => m.enable(false));
} }
@ -220,15 +248,17 @@ export class MenuController {
/** /**
* @hidden * @hidden
*/ */
registerType(name: string, cls: new(...args: any[]) => MenuType) { registerAnimation(name: string, cls: AnimationBuilder) {
this._menuTypes[name] = cls; this.menuAnimations[name] = cls;
} }
/** /**
* @hidden * @hidden
*/ */
create(type: string, menuCmp: Menu) { @Method()
return new this._menuTypes[type](menuCmp); 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; background: $menu-ios-background;
} }
.ios .menu-content-reveal { .menu-ios .menu-content-reveal {
box-shadow: $menu-ios-box-shadow; box-shadow: $menu-ios-box-shadow;
} }
.ios .menu-content-push { .menu-ios .menu-content-push {
box-shadow: $menu-ios-box-shadow; 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; 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-box-shadow: 0 0 10px $menu-md-box-shadow-color !default;
.menu-md { .menu-md .menu-inner {
background: $menu-md-background; background: $menu-md-background;
} }

View File

@ -1,8 +1,10 @@
import { Component, Element, Event, EventEmitter, Prop, PropDidChange } from '@stencil/core'; 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 { 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({ @Component({
tag: 'ion-menu', tag: 'ion-menu',
@ -16,46 +18,44 @@ import { MenuType } from './menu-types';
} }
}) })
export class Menu { export class Menu {
@Element() private el: HTMLElement;
private _backdropElm: HTMLElement; private _backdropEle: HTMLElement;
private _ctrl: MenuController; private _menuInnerEle: HTMLElement;
private _unregCntClick: Function; private _unregCntClick: Function;
private _unregBdClick: Function; private _unregBdClick: Function;
private _activeBlock: string; private _activeBlock: string;
private _cntElm: HTMLElement; private _cntElm: HTMLElement;
private _type: MenuType; private _animation: Animation;
private _init = false; private _init = false;
private _isPane = false; private _isPane = false;
private _isAnimating: boolean = false;
private _isOpen: boolean = false;
private _width: number = null;
mode: string; mode: string;
color: string; color: string;
/**
* @hidden
*/
isRightSide: boolean = false;
@Element() private el: HTMLElement;
@Event() ionDrag: EventEmitter; @Event() ionDrag: EventEmitter;
@Event() ionOpen: EventEmitter; @Event() ionOpen: EventEmitter;
@Event() ionClose: EventEmitter; @Event() ionClose: EventEmitter;
@Prop({ context: 'config' }) config: Config; @Prop({ context: 'config' }) config: Config;
/** @Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy<MenuController>;
* @hidden menuCtrl: MenuController;
*/
@Prop() isOpen: boolean = false;
/** /**
* @hidden * @input {string} The content's id the menu should use.
*/ */
@Prop() isAnimating: boolean = false; @Prop() content: string;
/**
* @hidden
*/
isRightSide: boolean = false;
/**
* @input {any} A reference to the content element the menu should use.
*/
@Prop() content: any;
/** /**
* @input {string} An id for the menu. * @input {string} An id for the menu.
@ -67,27 +67,22 @@ export class Menu {
* see the `menuType` in the [config](../../config/Config). Available options: * see the `menuType` in the [config](../../config/Config). Available options:
* `"overlay"`, `"reveal"`, `"push"`. * `"overlay"`, `"reveal"`, `"push"`.
*/ */
@Prop() type: string; @Prop() type: string = 'overlay';
/** /**
* @input {boolean} If true, the menu is enabled. Default `true`. * @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"`. * @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`. * @input {boolean} If true, swiping the menu is enabled. Default `true`.
*/ */
@Prop() swipeEnabled: boolean; @Prop() swipeEnabled: boolean = true;
@PropDidChange('swipeEnabled')
swipeEnabledChange(isEnabled: boolean) {
this.swipeEnable(isEnabled);
}
/** /**
* @input {boolean} If true, the menu will persist on child pages. * @input {boolean} If true, the menu will persist on child pages.
@ -97,43 +92,64 @@ export class Menu {
/** /**
* @hidden * @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 * @hidden
*/ */
ionViewDidLoad() { 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) { const contentQuery = (this.content)
if ((this.content).tagName as HTMLElement) { ? '> #' + this.content
this._cntElm = this.content; : '[main]';
} else if (typeof this.content === 'string') { const parent = this.el.parentElement;
this._cntElm = document.querySelector(this.content) as any; const content = this._cntElm = parent.querySelector(contentQuery) as HTMLElement;
} if (!content || !content.tagName) {
}
if (!this._cntElm || !this._cntElm.tagName) {
// requires content element // requires content element
return console.error('Menu: must have a "content" element to listen for drag events on.'); 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 // add menu's content classes
this._cntElm.classList.add('menu-content'); content.classList.add('menu-content');
this._cntElm.classList.add('menu-content-' + this.type); content.classList.add('menu-content-' + this.type);
let isEnabled = this.enabled; let isEnabled = this.enabled;
if (isEnabled === true || typeof isEnabled === 'undefined') { if (isEnabled === true || typeof isEnabled === 'undefined') {
// check if more than one menu is on the same side const menus = this.menuCtrl.getMenus();
isEnabled = !this._ctrl.getMenus().some(m => { isEnabled = !menus.some(m => {
return m.side === this.side && m.enabled; return m.side === this.side && m.enabled;
}); });
} }
// register this menu with the app's menu controller // register this menu with the app's menu controller
this._ctrl._register(this); this.menuCtrl._register(this);
// mask it as enabled / disabled // mask it as enabled / disabled
this.enable(isEnabled); this.enable(isEnabled);
@ -143,30 +159,34 @@ export class Menu {
return { return {
attrs: { attrs: {
'role': 'navigation', 'role': 'navigation',
'side': this.side, 'side': this.getSide(),
'type': this.type 'type': this.type
}, },
class: { class: {
'menu-enabled': this.enabled 'menu-enabled': this._canOpen()
} }
}; };
} }
render() { getSide(): string {
// normalize the "type" return this.isRightSide ? 'right' : 'left';
if (!this.type) { }
this.type = this.config.get('menuType', 'overlay');
}
return [ render() {
return ([
<div class='menu-inner'> <div class='menu-inner'>
<slot></slot> <slot></slot>
</div>, </div>,
<ion-gesture class='menu-backdrop' props={{ <ion-backdrop class="menu-backdrop"></ion-backdrop> ,
// 'canStart': this.canStart.bind(this), <ion-gesture props={{
// 'onStart': this.onDragStart.bind(this), 'canStart': this.canStart.bind(this),
// 'onMove': this.onDragMove.bind(this), 'onWillStart': this._swipeWillStart.bind(this),
// 'onEnd': this.onDragEnd.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', 'gestureName': 'menu-swipe',
'gesturePriority': 10, 'gesturePriority': 10,
'type': 'pan', 'type': 'pan',
@ -176,7 +196,7 @@ export class Menu {
'disableScroll': true, 'disableScroll': true,
'block': this._activeBlock 'block': this._activeBlock
}}></ion-gesture> }}></ion-gesture>
]; ]);
} }
/** /**
@ -185,21 +205,25 @@ export class Menu {
onBackdropClick(ev: UIEvent) { onBackdropClick(ev: UIEvent) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this._ctrl.close(); this.menuCtrl.close();
} }
/** /**
* @hidden * @hidden
*/ */
private _getType(): MenuType { private prepareAnimation(): Promise<void> {
if (!this._type) { const width = this._menuInnerEle.offsetWidth;
this._type = this._ctrl.create(this.type, this); if (width === this._width) {
return Promise.resolve();
if (this.config.getBoolean('animate') === false) {
this._type.ani.duration(0);
}
} }
return this._type; if (this._animation) {
this._animation.destroy();
this._animation = null;
}
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> { setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// If the menu is disabled or it is currenly being animated, let's do nothing // If the menu is disabled or it is currenly being animated, let's do nothing
if ((shouldOpen === this.isOpen) || !this._canOpen() || this.isAnimating) { if ((shouldOpen === this._isOpen) || !this._canOpen() || this._isAnimating) {
return Promise.resolve(this.isOpen); return Promise.resolve(this._isOpen);
} }
return new Promise(resolve => { this._before();
this._before(); this.prepareAnimation()
this._getType().setOpen(shouldOpen, animated, () => { .then(() => this._startAnimation(shouldOpen, animated))
.then(() => {
this._after(shouldOpen); 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() { _forceClosing() {
this.isAnimating = true; assert(this._isOpen, 'menu cannot be closed');
this._getType().setOpen(false, false, () => {
this._after(false); 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 { canSwipe(): boolean {
return this.swipeEnabled && return this.swipeEnabled &&
!this.isAnimating && !this._isAnimating &&
this._canOpen(); this._canOpen();
// TODO: && this._app.isEnabled(); // TODO: && this._app.isEnabled();
} }
/**
* @hidden
*/
isAnimating(): boolean {
return this._isAnimating;
}
_swipeBeforeStart() { /**
if (!this.canSwipe()) { * @hidden
return; */
} isOpen(): boolean {
return this._isOpen;
}
_swipeWillStart(): Promise<void> {
this._before(); this._before();
return this.prepareAnimation();
} }
_swipeStart() { _swipeStart() {
if (!this.isAnimating) { assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return; 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) { _swipeProgress(slide: any) {
if (!this.isAnimating) { assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return; 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) { _swipeEnd(slide: any) {
if (!this.isAnimating) { assert(!!this._animation, '_type is undefined');
if (!this._isAnimating) {
assert(false, '_isAnimating has to be true');
return; 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 isRightSide = this.isRightSide;
const opening = !this.isOpen; const opening = !this._isOpen;
const shouldComplete = (opening) const shouldComplete = (opening)
? isRightSide ? shouldCompleteLeft : shouldCompleteRight ? isRightSide ? shouldCompleteLeft : shouldCompleteRight
: isRightSide ? shouldCompleteRight : shouldCompleteLeft; : isRightSide ? shouldCompleteRight : shouldCompleteLeft;
this._getType().setProgressEnd(shouldComplete, stepValue, velocity, (isOpen: boolean) => { let isOpen = (opening && shouldComplete);
console.debug('menu, swipeEnd', this.side); if (!opening && !shouldComplete) {
this._after(isOpen); 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() { 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 places the menu into the correct location before it animates in
// this css class doesn't actually kick off any animations // this css class doesn't actually kick off any animations
this.el.classList.add('show-menu'); this.el.classList.add('show-menu');
this._backdropElm.classList.add('show-backdrop'); this._backdropEle.classList.add('show-backdrop');
this.resize(); this.resize();
this._isAnimating = true;
// TODO: this._keyboard.close();
this.isAnimating = true;
} }
private _after(isOpen: boolean) { private _after(isOpen: boolean) {
assert(this._isAnimating, '_before() should be called while animating');
// TODO: this._app.setEnabled(false, 100); // TODO: this._app.setEnabled(false, 100);
// keep opening/closing the menu disabled for a touch more yet // keep opening/closing the menu disabled for a touch more yet
// only add listeners/css if it's enabled and isOpen // only add listeners/css if it's enabled and isOpen
// and only remove listeners/css if it's not open // and only remove listeners/css if it's not open
// emit opened/closed events // emit opened/closed events
this.isOpen = isOpen; this._isOpen = isOpen;
this.isAnimating = false; this._isAnimating = false;
// add/remove backdrop click listeners // add/remove backdrop click listeners
this._backdropClick(isOpen); this._backdropClick(isOpen);
@ -311,9 +399,7 @@ export class Menu {
this._activeBlock = GESTURE_BLOCKER; this._activeBlock = GESTURE_BLOCKER;
// add css class // add css class
Context.dom.write(() => { this._cntElm.classList.add('menu-content-open');
this._cntElm.classList.add('menu-content-open');
});
// emit open event // emit open event
this.ionOpen.emit({ menu: this }); this.ionOpen.emit({ menu: this });
@ -323,11 +409,9 @@ export class Menu {
this._activeBlock = null; this._activeBlock = null;
// remove css classes // remove css classes
Context.dom.write(() => { this.el.classList.remove('show-menu');
this._cntElm.classList.remove('menu-content-open'); this._cntElm.classList.remove('menu-content-open');
this._cntElm.classList.remove('show-menu'); this._backdropEle.classList.remove('show-menu');
this._backdropElm.classList.remove('show-menu');
});
// emit close event // emit close event
this.ionClose.emit({ menu: this }); this.ionClose.emit({ menu: this });
@ -359,11 +443,23 @@ export class Menu {
// content && content.resize(); // 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 * @hidden
*/ */
toggle(): Promise<boolean> { toggle(): Promise<boolean> {
return this.setOpen(!this.isOpen); return this.setOpen(!this._isOpen);
} }
_canOpen(): boolean { _canOpen(): boolean {
@ -373,40 +469,30 @@ export class Menu {
/** /**
* @hidden * @hidden
*/ */
// @PropDidChange('swipeEnabled')
// @PropDidChange('enabled')
_updateState() { _updateState() {
const canOpen = this._canOpen(); const canOpen = this._canOpen();
// Close menu inmediately // 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 // close if this menu is open, and should not be enabled
this._forceClosing(); this._forceClosing();
} }
if (this.enabled && this._ctrl) { if (this.enabled && this.menuCtrl) {
this._ctrl._setActiveMenu(this); this.menuCtrl._setActiveMenu(this);
} }
if (!this._init) { if (!this._init) {
return; return;
} }
// TODO if (this._isOpen || (this._isPane && this.enabled)) {
// 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)) {
this.resize(); this.resize();
} }
assert(!this._isAnimating, 'can not be animating');
} }
/** /**
@ -414,7 +500,6 @@ export class Menu {
*/ */
enable(shouldEnable: boolean): Menu { enable(shouldEnable: boolean): Menu {
this.enabled = shouldEnable; this.enabled = shouldEnable;
this._updateState();
return this; return this;
} }
@ -438,7 +523,6 @@ export class Menu {
*/ */
swipeEnable(shouldEnable: boolean): Menu { swipeEnable(shouldEnable: boolean): Menu {
this.swipeEnabled = shouldEnable; this.swipeEnabled = shouldEnable;
this._updateState();
return this; return this;
} }
@ -460,21 +544,14 @@ export class Menu {
* @hidden * @hidden
*/ */
getBackdropElement(): HTMLElement { getBackdropElement(): HTMLElement {
return this._backdropElm; return this._backdropEle;
}
/**
* @hidden
*/
width(): number {
return this.getMenuElement().offsetWidth;
} }
/** /**
* @hidden * @hidden
*/ */
getMenuController(): MenuController { getMenuController(): MenuController {
return this._ctrl; return this.menuCtrl;
} }
private _backdropClick(shouldAdd: boolean) { private _backdropClick(shouldAdd: boolean) {
@ -497,10 +574,10 @@ export class Menu {
ionViewDidUnload() { ionViewDidUnload() {
this._backdropClick(false); this._backdropClick(false);
this._ctrl._unregister(this); this.menuCtrl._unregister(this);
this._type && this._type.destroy(); 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 // actively scrolling
positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp); positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp);
@ -106,15 +103,11 @@ export class Scroll {
// compute relative movement between these two points // compute relative movement between these two points
var movedTop = (positions[startPos - 2] - positions[endPos - 2]); var movedTop = (positions[startPos - 2] - positions[endPos - 2]);
var movedLeft = (positions[startPos - 1] - positions[endPos - 1]); 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 // based on XXms compute the movement to apply for each render step
detail.velocityY = movedTop * factor; detail.velocityY = movedTop * factor;
detail.velocityX = movedLeft * 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 { LoadingController } from './components/loading-controller/loading-controller';
import { GestureDetail, GestureCallback } from './components/gesture/gesture'; import { GestureDetail, GestureCallback } from './components/gesture/gesture';
import { Menu } from './components/menu/menu'; import { Menu } from './components/menu/menu';
import { MenuType } from './components/menu/menu-types';
import { MenuController } from './components/menu/menu-controller'; import { MenuController } from './components/menu/menu-controller';
import { Modal, ModalOptions, ModalEvent } from './components/modal/modal'; import { Modal, ModalOptions, ModalEvent } from './components/modal/modal';
import { ModalController } from './components/modal-controller/modal-controller'; import { ModalController } from './components/modal-controller/modal-controller';
@ -77,7 +76,6 @@ export {
LoadingEvent, LoadingEvent,
Menu, Menu,
MenuController, MenuController,
MenuType,
Modal, Modal,
ModalController, ModalController,
ModalOptions, ModalOptions,

View File

@ -41,7 +41,7 @@ export function assert(bool: boolean, msg: string) {
if (!bool) { if (!bool) {
console.error(msg); console.error(msg);
} }
}; }
export function toDashCase(str: string) { export function toDashCase(str: string) {
return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase()); return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase());
@ -64,6 +64,26 @@ export function pointerCoordX(ev: any): number {
return 0; 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 { export function pointerCoordY(ev: any): number {
// get Y coordinates for either a mouse click // get Y coordinates for either a mouse click
// or a touch depending on the given event // or a touch depending on the given event
@ -79,7 +99,9 @@ export function pointerCoordY(ev: any): number {
return 0; 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') { if (ref === 'child') {
return elm.firstElementChild; return elm.firstElementChild;
} }
@ -139,9 +161,16 @@ export function getToolbarHeight(toolbarTagName: string, pageChildren: HTMLEleme
return ''; return '';
} }
/** @hidden */
export type Side = 'left' | 'right' | 'start' | 'end'; 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 * @hidden
* Given a side, return if it should be on the right * 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-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-input', 'ion-textarea'] },
{ components: ['ion-loading', 'ion-loading-controller'] }, { 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-modal', 'ion-modal-controller'] },
{ components: ['ion-popover', 'ion-popover-controller'] }, { components: ['ion-popover', 'ion-popover-controller'] },
{ components: ['ion-radio', 'ion-radio-group'] }, { components: ['ion-radio', 'ion-radio-group'] },