mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 04:53:58 +08:00
feat(menu): adds ion-menu and ion-menu-controller
This commit is contained in:
34
packages/core/package-lock.json
generated
34
packages/core/package-lock.json
generated
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
15
packages/core/src/components/menu/animations/base.ts
Normal file
15
packages/core/src/components/menu/animations/base.ts
Normal 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);
|
||||
}
|
35
packages/core/src/components/menu/animations/overlay.ts
Normal file
35
packages/core/src/components/menu/animations/overlay.ts
Normal 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);
|
||||
}
|
36
packages/core/src/components/menu/animations/push.ts
Normal file
36
packages/core/src/components/menu/animations/push.ts
Normal 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);
|
||||
}
|
19
packages/core/src/components/menu/animations/reveal.ts
Normal file
19
packages/core/src/components/menu/animations/reveal.ts
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
// };
|
||||
// }
|
||||
// }
|
@ -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);
|
||||
// }
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
135
packages/core/src/components/menu/tests/basic.html
Normal file
135
packages/core/src/components/menu/tests/basic.html
Normal 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>
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
2
packages/core/src/index.d.ts
vendored
2
packages/core/src/index.d.ts
vendored
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'] },
|
||||
|
Reference in New Issue
Block a user