feat(aside): reveal/overlay aside using Animation

This commit is contained in:
Adam Bradley
2015-09-10 20:54:40 -05:00
parent 33665668f8
commit b31ab1b0be
27 changed files with 791 additions and 583 deletions

View File

@@ -1,4 +1,5 @@
import {CSS} from '../util/dom';
import {extend} from '../util/util';
const RENDER_DELAY = 36;
let AnimationRegistry = {};
@@ -123,15 +124,15 @@ export class Animation {
}
return this;
}
return this._rate || (this._parent && this._parent.playbackRate());
return (typeof this._rate !== 'undefined' ? this._rate : this._parent && this._parent.playbackRate());
}
fill(value) {
if (arguments.length) {
this._fill = value;
return this;
}
return this._fill || (this._parent && this._parent.fill());
reverse() {
return this.playbackRate(-1);
}
forward() {
return this.playbackRate(1);
}
from(property, value) {
@@ -193,23 +194,22 @@ export class Animation {
play() {
const self = this;
const animations = self._ani;
const children = self._chld;
let promises = [];
let i, l;
// the actual play() method which may or may not start async
function beginPlay() {
let i, l;
let promises = [];
for (i = 0, l = children.length; i < l; i++) {
promises.push( children[i].play() );
for (let i = 0, l = self._chld.length; i < l; i++) {
promises.push( self._chld[i].play() );
}
for (i = 0, l = animations.length; i < l; i++) {
promises.push( animations[i].play() );
}
self._ani.forEach(animation => {
promises.push(
new Promise(resolve => {
animation.play(resolve);
})
);
});
return Promise.all(promises);
}
@@ -290,8 +290,7 @@ export class Animation {
this._to,
this.duration(),
this.easing(),
this.playbackRate(),
this.fill() );
this.playbackRate() );
if (animation.shouldAnimate) {
this._ani.push(animation);
@@ -310,6 +309,7 @@ export class Animation {
// after the RENDER_DELAY
// before the animations have started
let i;
this._isFinished = false;
for (i = 0; i < this._chld.length; i++) {
this._chld[i]._onPlay();
@@ -322,7 +322,7 @@ export class Animation {
_onFinish() {
// after the animations have finished
if (!this._isFinished) {
if (!this._isFinished && !this.isProgress) {
this._isFinished = true;
let i, j, ele;
@@ -367,8 +367,6 @@ export class Animation {
}
pause() {
this._hasFinished = false;
let i;
for (i = 0; i < this._chld.length; i++) {
this._chld[i].pause();
@@ -379,34 +377,96 @@ export class Animation {
}
}
progressStart() {
this.isProgress = true;
for (let i = 0; i < this._chld.length; i++) {
this._chld[i].progressStart();
}
this.play();
this.pause();
}
progress(value) {
this.isProgress = true;
let i;
for (i = 0; i < this._chld.length; i++) {
this._chld[i].progress(value);
}
if (!this._initProgress) {
this._initProgress = true;
this.play();
this.pause();
}
for (i = 0; i < this._ani.length; i++) {
this._ani[i].progress(value);
}
}
onReady(fn) {
progressFinish(shouldComplete, rate=1) {
let promises = [];
this.isProgress = false;
for (let i = 0; i < this._chld.length; i++) {
promises.push( this._chld[i].progressFinish(shouldComplete) );
}
this._ani.forEach(animation => {
if (shouldComplete) {
animation.playbackRate(rate);
} else {
animation.playbackRate(rate * -1);
}
promises.push(
new Promise(resolve => {
animation.play(resolve);
})
);
});
return Promise.all(promises);
}
onReady(fn, clear) {
if (clear) {
this._readys = [];
}
this._readys.push(fn);
return this;
}
onPlay(fn) {
onPlay(fn, clear) {
if (clear) {
this._plays = [];
}
this._plays.push(fn);
return this;
}
onFinish(fn) {
onFinish(fn, clear) {
if (clear) {
this._finishes = [];
}
this._finishes.push(fn);
return this;
}
clone() {
function copy(dest, src) {
// undo what stage() may have already done
extend(dest, src);
dest._isFinished = dest._isStaged = dest.isProgress = false;
dest._chld = [];
dest._ani = [];
for (let i = 0; i < src._chld.length; i++) {
dest.add( copy(new Animation(), src._chld[i]) );
}
return dest;
}
return copy(new Animation(), this);
}
dispose() {
@@ -444,7 +504,7 @@ export class Animation {
class Animate {
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate, fill) {
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate) {
// https://w3c.github.io/web-animations/
// not using the direct API methods because they're still in flux
// however, element.animate() seems locked in and uses the latest
@@ -462,24 +522,21 @@ class Animate {
return inlineStyle(ele, this.toEffect);
}
this.fill = fill;
this.ele = ele;
this.promise = new Promise(res => { this.resolve = res; });
// stage where the element will start from
fromEffect = parseEffect(fromEffect);
inlineStyle(ele, fromEffect);
this.fromEffect = parseEffect(fromEffect);
inlineStyle(ele, this.fromEffect);
this.duration = duration;
this.rate = playbackRate;
this.rate = (typeof playbackRate !== 'undefined' ? playbackRate : 1);
this.easing = easingConfig && easingConfig.name || 'linear';
this.effects = [ convertProperties(fromEffect) ];
this.effects = [ convertProperties(this.fromEffect) ];
if (this.easing in EASING_FN) {
insertEffects(this.effects, fromEffect, this.toEffect, easingConfig);
insertEffects(this.effects, this.fromEffect, this.toEffect, easingConfig);
} else if (this.easing in CUBIC_BEZIERS) {
this.easing = 'cubic-bezier(' + CUBIC_BEZIERS[this.easing] + ')';
@@ -488,68 +545,72 @@ class Animate {
this.effects.push( convertProperties(this.toEffect) );
}
play() {
play(callback) {
const self = this;
if (self.player) {
self.player.play();
if (self.ani) {
self.ani.play();
} else {
self.player = self.ele.animate(self.effects, {
// https://developers.google.com/web/updates/2014/05/Web-Animations---element-animate-is-now-in-Chrome-36
// https://w3c.github.io/web-animations/
// Future versions will use "new window.Animation" rather than "element.animate()"
self.ani = self.ele.animate(self.effects, {
duration: self.duration || 0,
easing: self.easing,
playbackRate: self.rate || 1,
fill: self.fill
playbackRate: self.rate // old way of setting playbackRate, but still necessary
});
self.player.onfinish = () => {
// lock in where the element will stop at
// if the playbackRate is negative then it needs to return
// to its "from" effects
inlineStyle(self.ele, self.rate < 0 ? self.fromEffect : self.toEffect);
self.player = null;
self.resolve();
};
self.ani.playbackRate = self.rate;
}
return self.promise;
self.ani.onfinish = () => {
// lock in where the element will stop at
// if the playbackRate is negative then it needs to return
// to its "from" effects
inlineStyle(self.ele, self.rate < 0 ? self.fromEffect : self.toEffect);
self.ani = null;
callback && callback();
};
}
pause() {
this.player && this.player.pause();
this.ani && this.ani.pause();
}
progress(value) {
let player = this.player;
let animation = this.ani;
if (player) {
if (animation) {
// passed a number between 0 and 1
value = Math.max(0, Math.min(1, value));
if (value >= 1) {
player.currentTime = (this.duration * 0.999);
return player.play();
if (animation.playState !== 'paused') {
animation.pause();
}
if (player.playState !== 'paused') {
player.pause();
if (value < 0.999) {
animation.currentTime = (this.duration * value);
} else {
// don't let the progress finish the animation
animation.currentTime = (this.duration * 0.999);
}
player.currentTime = (this.duration * value);
}
}
playbackRate(value) {
this.rate = value;
if (this.player) {
this.player.playbackRate = value;
if (this.ani) {
this.ani.playbackRate = value;
}
}
dispose() {
this.ele = this.player = this.effects = this.toEffect = null;
this.ele = this.ani = this.effects = this.toEffect = null;
}
}
@@ -595,7 +656,7 @@ function parseEffect(inputEffect) {
for (property in inputEffect) {
val = inputEffect[property];
r = val.toString().match(/(\d*\.?\d*)(.*)/);
r = val.toString().match(/(^-?\d*\.?\d*)(.*)/);
num = parseFloat(r[1]);
outputEffect[property] = {

View File

@@ -3,6 +3,7 @@ export * from 'ionic/components/app/app'
export * from 'ionic/components/app/id'
export * from 'ionic/components/action-menu/action-menu'
export * from 'ionic/components/aside/aside'
export * from 'ionic/components/aside/extensions/types'
export * from 'ionic/components/aside/aside-toggle'
export * from 'ionic/components/button/button'
export * from 'ionic/components/card/card'

View File

@@ -39,7 +39,7 @@ export class IonicApp {
*/
constructor() {
this.overlays = [];
this._isTransitioning = false;
this._transTime = 0;
// Our component registry map
this.components = {};
@@ -82,7 +82,7 @@ export class IonicApp {
* @param {bool} isTransitioning
*/
setTransitioning(isTransitioning) {
this._isTransitioning = !!isTransitioning;
this._transTime = (isTransitioning ? Date.now() : 0);
}
/**
@@ -90,7 +90,7 @@ export class IonicApp {
* @return {bool}
*/
isTransitioning() {
return this._isTransitioning;
return (this._transTime + 800 > Date.now());
}
/**

View File

@@ -33,6 +33,5 @@ export class AsideToggle {
*/
toggle(event) {
this.aside && this.aside.toggle();
console.log('Aside toggle');
}
}

View File

@@ -2,155 +2,56 @@
// Aside
// --------------------------------------------------
$aside-width: 304px !default;
$aside-small-width: $aside-width - 40px !default;
$aside-width: 304px !default;
$aside-small-width: $aside-width - 40px !default;
$aside-transition: 0.2s ease transform !default;
$aside-backdrop-transition: 0.2s ease background-color !default;
$aside-background: $background-color !default;
$aside-shadow: -1px 0px 8px rgba(0, 0, 0, 0.2) !default;
$aside-background: $background-color !default;
$aside-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25) !default;
.aside {
ion-aside {
position: absolute;
top: 0;
right: auto;
bottom: 0;
left: 0;
left: -$aside-width;
width: $aside-width;
display: flex;
flex-direction: column;
background: $aside-background;
transform: translate3d(0, 0, 0);
transition: $aside-transition;
&.no-transition {
ion-aside-backdrop {
transition: none;
}
}
&[type=overlay] {
z-index: $z-index-aside-overlay;
&:not(.open):not(.changing) {
display: none;
}
}
&[type=reveal] {
left: 0;
&:not(.open):not(.changing) {
display: none;
}
}
&.open, {
&[type=reveal],
&[type=push] {
left: 0;
}
&[type=overlay] {
transform: translate3d($aside-width,0,0);
box-shadow: 1px 2px 16px rgba(0, 0, 0, 0.3);
ion-aside-backdrop {
opacity: 0.5;
}
}
}
&[side=right] {
width: $aside-width;
left: 100%;
top: 0;
bottom: 0;
transform: translate3d(0,0,0);
&.open,
&[type=reveal] {
transform: translate3d(-$aside-width,0,0);
}
}
&[side=top] {
height: $aside-width;
top: -$aside-width;
left: 0;
right: 0;
transform: translate3d(0,0,0);
&.open,
&[type=reveal] {
transform: translate3d(0,$aside-width,0);
}
}
&[side=bottom] {
height: $aside-width;
top: 100%;
left: 0;
right: 0;
transform: translate3d(0,0,0);
&.open,
&.type-reveal {
transform: translate3d(0,-$aside-width,0);
}
}
}
ion-aside-backdrop {
z-index: $z-index-aside-backdrop;
transition: $aside-backdrop-transition;
transform: translateX($aside-width);
top: 0;
ion-aside[side=right] {
right: 0;
left: 0;
bottom: 0;
position: fixed;
background-color: rgb(0,0,0);
left: auto;
}
ion-aside backdrop {
z-index: -1;
display: none;
}
.aside-content {
transition: $aside-transition;
transform: translate3d(0,0,0);
transform: translate3d(0px, 0px, 0px);
}
box-shadow: $aside-shadow;
&.aside-open-left {
transform: translate3d($aside-width,0,0);
pointer-events: none;
}
&.aside-open-right {
transform: translate3d(-$aside-width,0,0);
pointer-events: none;
}
.aside-content-open ion-pane,
.aside-content-open ion-content,
.aside-content-open .toolbar {
// the containing element itself should be clickable but
// everything inside of it should not clickable when aside is open
pointer-events: none;
}
@media (max-width: 340px) {
.aside {
left: -$aside-small-width;
ion-aside {
width: $aside-small-width;
}
.aside-content {
&.aside-open-left {
transform: translate3d($aside-small-width, 0, 0);
}
&.aside-open-right {
transform: translate3d(-$aside-small-width, 0, 0);
}
}
}

View File

@@ -1,13 +1,11 @@
import {forwardRef, Component, Host, View, EventEmitter, ElementRef} from 'angular2/angular2';
import {forwardRef, Directive, Host, View, EventEmitter, ElementRef} from 'angular2/angular2';
import {Ion} from '../ion';
import {IonicApp} from '../app/app';
import {IonicConfig} from '../../config/config';
import {IonicComponent} from '../../config/annotations';
import * as types from './extensions/types'
import * as gestures from './extensions/gestures'
import * as util from 'ionic/util/util'
import {dom} from 'ionic/util'
import * as gestures from './extensions/gestures';
/**
* Aside is a side-menu navigation that can be dragged out or toggled to show. Aside supports two
@@ -25,152 +23,74 @@ import {dom} from 'ionic/util'
'side': 'left',
'type': 'reveal'
},
delegates: {
gesture: [
//[instance => instance.side == 'top', gestures.TopAsideGesture],
//[instance => instance.side == 'bottom', gestures.BottomAsideGesture],
[instance => instance.side == 'right', gestures.RightAsideGesture],
[instance => instance.side == 'left', gestures.LeftAsideGesture],
],
type: [
[instance => instance.type == 'overlay', types.AsideTypeOverlay],
[instance => instance.type == 'reveal', types.AsideTypeReveal],
//[instance => instance.type == 'push', types.AsideTypePush],
]
},
events: ['opening']
})
@View({
template: '<ng-content></ng-content><ion-aside-backdrop></ion-aside-backdrop>',
template: '<ng-content></ng-content><backdrop tappable></backdrop>',
directives: [forwardRef(() => AsideBackdrop)]
})
export class Aside extends Ion {
/**
* TODO
* @param {IonicApp} app TODO
* @param {ElementRef} elementRef Reference to the element.
*/
constructor(app: IonicApp, elementRef: ElementRef, config: IonicConfig) {
super(elementRef, config);
this.app = app;
this.opening = new EventEmitter('opening');
//this.animation = new Animation(element.querySelector('backdrop'));
this.contentClickFn = (e) => {
if(!this.isOpen || this.isChanging) { return; }
this.close();
};
this.finishChanging = util.debounce(() => {
this.setChanging(false);
});
// TODO: Use Animation Class
this.getNativeElement().addEventListener('transitionend', ev => {
//this.setChanging(false)
clearTimeout(this.setChangeTimeout);
this.setChangeTimeout = setInterval(this.finishChanging, 400);
})
this.isOpen = false;
this._disableTime = 0;
}
/**
* TODO
*/
onDestroy() {
app.unregister(this);
}
/**
* TODO
*/
onInit() {
super.onInit();
this.contentElement = (this.content instanceof Node) ? this.content : this.content.getNativeElement();
if(!this.id) {
if (!this.contentElement) {
return console.error('Aside: must have a [content] element to listen for drag events on. Example:\n\n<ion-aside [content]="content"></ion-aside>\n\n<ion-content #content></ion-content>');
}
if (!this.id) {
// Auto register
this.app.register('menu', this);
}
if(this.contentElement) {
this.contentElement.addEventListener('transitionend', ev => {
//this.setChanging(false)
clearTimeout(this.setChangeTimeout);
this.setChangeTimeout = setInterval(this.finishChanging, 400);
})
this.contentElement.addEventListener('click', this.contentClickFn);
} else {
console.error('Aside: must have a [content] element to listen for drag events on. Supply one like this:\n\n<ion-aside [content]="content"></ion-aside>\n\n<ion-content #content>');
}
this._initGesture();
this._initType(this.type);
this.contentElement.classList.add('aside-content');
this.contentElement.classList.add('aside-content-' + this.type);
this.gestureDelegate = this.getDelegate('gesture');
this.typeDelegate = this.getDelegate('type');
let self = this;
this.onContentClick = function(ev) {
ev.preventDefault();
ev.stopPropagation();
self.close();
};
}
onDestroy() {
this.contentElement.removeEventListener('click', this.contentClickFn);
}
_initGesture() {
switch(this.side) {
case 'right':
this._gesture = new gestures.RightAsideGesture(this);
break;
/**
* TODO
* @return {Element} The Aside's content element.
*/
getContentElement() {
return this.contentElement;
}
/**
* TODO
* @param {TODO} v TODO
*/
setOpenAmt(v) {
this.opening.next(v);
}
/**
* TODO
* @param {boolean} willOpen TODO
*/
setDoneTransforming(willOpen) {
this.typeDelegate.setDoneTransforming(willOpen);
}
/**
* TODO
* @param {TODO} transform TODO
*/
setTransform(transform) {
this.typeDelegate.setTransform(transform)
}
/**
* TODO
* @param {boolean} isSliding TODO
*/
setSliding(isSliding) {
if (isSliding !== this.isSliding) {
this.typeDelegate.setSliding(isSliding)
case 'left':
this._gesture = new gestures.LeftAsideGesture(this);
break;
}
}
/**
* TODO
* @param {boolean} isChanging TODO
*/
setChanging(isChanging) {
_initType(type) {
type = type && type.trim().toLowerCase() || FALLBACK_ASIDE_TYPE;
// Stop any last changing end operations
clearTimeout(this.setChangeTimeout);
if (isChanging !== this.isChanging) {
this.isChanging = isChanging
this.getNativeElement().classList[isChanging ? 'add' : 'remove']('changing');
let asideTypeCls = asideTypes[type];
if (!asideTypeCls) {
type = FALLBACK_ASIDE_TYPE;
asideTypeCls = asideTypes[type];
}
this._type = new asideTypeCls(this);
this.type = type;
}
/**
@@ -178,18 +98,79 @@ export class Aside extends Ion {
* @param {boolean} isOpen If the Aside is open or not.
* @return {Promise} TODO
*/
setOpen(isOpen) {
if (isOpen !== this.isOpen) {
this.isOpen = isOpen;
this.setChanging(true);
// Set full or closed amount
this.setOpenAmt(isOpen ? 1 : 0);
return dom.rafPromise().then(() => {
this.typeDelegate.setOpen(isOpen)
})
setOpen(shouldOpen) {
// _isDisabled is used to prevent unwanted opening/closing after swiping open/close
// or swiping open the menu while pressing down on the aside-toggle button
if (shouldOpen === this.isOpen || this._isDisabled()) {
return Promise.resolve();
}
this._before();
return this._type.setOpen(shouldOpen).then(() => {
this._after(shouldOpen);
});
}
setProgressStart() {
// user started swiping the aside open/close
if (this._isDisabled()) return;
this._before();
this._type.setProgressStart(this.isOpen);
}
setProgess(value) {
// user actively dragging the menu
this._disable();
this._type.setProgess(value);
}
setProgressFinish(shouldComplete) {
// user has finished dragging the menu
this._disable();
this._type.setProgressFinish(shouldComplete).then(isOpen => {
this._after(isOpen);
});
}
_before() {
// this places the aside into the correct location before it animates in
// this css class doesn't actually kick off any animations
this.getNativeElement().classList.add('show-aside');
this.getBackdropElement().classList.add('show-backdrop');
this._disable();
this.app.setTransitioning(true);
}
_after(isOpen) {
this._disable();
this.isOpen = isOpen;
this.contentElement.classList[isOpen ? 'add' : 'remove']('aside-content-open');
this.contentElement.removeEventListener('click', this.onContentClick);
if (isOpen) {
this.contentElement.addEventListener('click', this.onContentClick);
} else {
this.getNativeElement().classList.remove('show-aside');
this.getBackdropElement().classList.remove('show-backdrop');
}
this.app.setTransitioning(false);
}
_disable() {
// used to prevent unwanted opening/closing after swiping open/close
// or swiping open the menu while pressing down on the aside-toggle
this._disableTime = Date.now();
}
_isDisabled() {
return this._disableTime + 300 > Date.now();
}
/**
@@ -216,55 +197,75 @@ export class Aside extends Ion {
return this.setOpen(!this.isOpen);
}
/**
* TODO
* @return {Element} The Aside element.
*/
getAsideElement() {
return this.getNativeElement();
}
/**
* TODO
* @return {Element} The Aside's associated content element.
*/
getContentElement() {
return this.contentElement;
}
/**
* TODO
* @return {Element} The Aside's associated content element.
*/
getBackdropElement() {
return this.backdrop.elementRef.nativeElement;
}
static register(name, cls) {
asideTypes[name] = cls;
}
onDestroy() {
this.app.unregister(this);
this._type && this._type.onDestroy();
this.contentElement = null;
}
}
let asideTypes = {};
const FALLBACK_ASIDE_TYPE = 'reveal';
/**
* TODO
*/
@Component({
selector: 'ion-aside-backdrop',
@Directive({
selector: 'backdrop',
host: {
'[style.width]': 'width',
'[style.height]': 'height',
'[style.opacity]': 'opacity',
'(click)': 'clicked($event)'
}
})
@View({
template: ''
})
export class AsideBackdrop extends Ion {
class AsideBackdrop {
/**
* TODO
* @param {ElementReg} elementRef TODO
* @param {IonicConfig} config TODO
* @param {Aside} aside TODO
*/
constructor(elementRef: ElementRef, config: IonicConfig, @Host() aside: Aside) {
super(elementRef, config);
aside.backdrop = this;
constructor(@Host() aside: Aside, elementRef: ElementRef) {
this.aside = aside;
this.opacity = 0;
}
/**
* TODO
*/
onInit() {
let ww = window.innerWidth;
let wh = window.innerHeight;
this.width = ww + 'px';
this.height = wh + 'px';
this.elementRef = elementRef;
aside.backdrop = this;
}
/**
* TODO
* @param {TODO} event TODO
*/
clicked(event) {
clicked(ev) {
ev.preventDefault();
ev.stopPropagation();
this.aside.close();
}
}

View File

@@ -1,47 +1,37 @@
import {Aside} from 'ionic/components/aside/aside';
//TODO: figure out way to get rid of all the ../../../../
import {Aside} from '../aside';
import {SlideEdgeGesture} from 'ionic/gestures/slide-edge-gesture';
class AsideTargetGesture extends SlideEdgeGesture {
constructor(aside: Aside) {
let asideElement = aside.getNativeElement();
super(asideElement, {
class AsideGenericGestureHandler extends SlideEdgeGesture {
constructor(aside: Aside, targetElement, threshold) {
super(targetElement, {
direction: (aside.side === 'left' || aside.side === 'right') ? 'x' : 'y',
edge: aside.side,
threshold: 0
threshold: threshold
});
this.aside = aside;
this.listen();
}
canStart(ev) {
return this.aside.isOpen;
}
// Set CSS, then wait one frame for it to apply before sliding starts
onSlideBeforeStart(slide, ev) {
this.aside.setSliding(true);
this.aside.setChanging(true);
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
this.aside.setProgressStart();
}
onSlide(slide, ev) {
this.aside.setOpenAmt(slide.distance / slide.max);
this.aside.setTransform(slide.distance);
this.aside.setProgess(slide.distance / slide.max);
}
onSlideEnd(slide, ev) {
this.aside.setSliding(false);
if (Math.abs(ev.velocityX) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5) {
this.aside.setOpen(!this.aside.isOpen);
this.aside.setDoneTransforming(!this.aside.isOpen);
} else {
this.aside.setDoneTransforming(this.aside.isOpen);
}
let shouldComplete = (Math.abs(ev.velocityX) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5);
this.aside.setProgressFinish(shouldComplete);
}
getElementStartPos(slide, ev) {
return this.aside.isOpen ? slide.max : slide.min;
}
getSlideBoundaries() {
return {
min: 0,
@@ -50,64 +40,26 @@ class AsideTargetGesture extends SlideEdgeGesture {
}
}
class AsideGesture extends SlideEdgeGesture {
export class AsideContentGesture extends AsideGenericGestureHandler {
constructor(aside: Aside) {
// TODO figure out the sliding element, dont just use the parent
let contentElement = aside.getContentElement();
super(contentElement, {
direction: (aside.side === 'left' || aside.side === 'right') ? 'x' : 'y',
edge: aside.side,
threshold: 75
});
this.aside = aside;
this.slideElement = contentElement;
this.listen();
let contentGesture = new AsideTargetGesture(aside);
contentGesture.listen();
super(aside, aside.getContentElement(), 75);
}
canStart(ev) {
// Only restrict edges if the aside is closed
return this.aside.isOpen ? true : super.canStart(ev);
}
}
// Set CSS, then wait one frame for it to apply before sliding starts
onSlideBeforeStart(slide, ev) {
this.aside.setSliding(true);
this.aside.setChanging(true);
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}
onSlide(slide, ev) {
this.aside.setOpenAmt(slide.distance / slide.max);
this.aside.setTransform(slide.distance);
}
onSlideEnd(slide, ev) {
this.aside.setSliding(false);
if (Math.abs(ev.velocityX) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5) {
this.aside.setOpen(!this.aside.isOpen);
this.aside.setDoneTransforming(!this.aside.isOpen);
} else {
this.aside.setDoneTransforming(false);
}
}
getElementStartPos(slide, ev) {
return this.aside.isOpen ? slide.max : slide.min;
}
getSlideBoundaries() {
return {
min: 0,
max: this.aside.width()
};
export class LeftAsideGesture extends AsideContentGesture {
constructor(aside: Aside) {
super(aside);
}
}
export class LeftAsideGesture extends AsideGesture {}
export class RightAsideGesture extends LeftAsideGesture {
constructor(aside: Aside) {
super(aside);
}
getElementStartPos(slide, ev) {
return this.aside.isOpen ? slide.min : slide.max;
}

View File

@@ -0,0 +1,41 @@
// Aside Reveal
// --------------------------------------------------
// The content slides over to reveal the aside underneath.
// The aside menu itself, which is under the content, does not move.
ion-aside[type=reveal] {
transform: translate3d(-9999px, 0px, 0px);
&.show-aside {
transform: translate3d(0px, 0px, 0px);
}
}
.aside-content-reveal {
box-shadow: $aside-shadow;
}
// Aside Overlay
// --------------------------------------------------
// The aside menu slides over the content. The content
// itself, which is under the aside, does not move.
ion-aside[type=overlay] {
z-index: $z-index-aside-overlay;
box-shadow: $aside-shadow;
transform: translate3d(-9999px, 0px, 0px);
backdrop {
display: block;
transform: translate3d(-9999px, 0px, 0px);
opacity: 0.01;
width: 3000px;
&.show-backdrop {
transform: translate3d(0px, 0px, 0px);
}
}
}

View File

@@ -1,124 +1,147 @@
import {Aside} from 'ionic/components/aside/aside';
import {Animtion} from 'ionic/aside/aside';
import {CSS} from 'ionic/util/dom'
import {Aside} from '../aside';
import {Animation} from 'ionic/animations/animation';
// TODO use setters instead of direct dom manipulation
const asideManipulator = {
setSliding(sliding) {
this.aside.getNativeElement().classList[sliding ? 'add' : 'remove']('no-transition');
},
setOpen(open) {
this.aside.getNativeElement().classList[open ? 'add' : 'remove']('open');
},
setTransform(t) {
if(t === null) {
this.aside.getNativeElement().style[CSS.transform] = '';
} else {
this.aside.getNativeElement().style[CSS.transform] = 'translate3d(' + t + 'px,0,0)';
}
}
}
const contentManipulator = {
setSliding(sliding) {
this.aside.contentElement.classList[sliding ? 'add' : 'remove']('no-transition');
},
setOpen(open) {
this.aside.contentElement.classList[open ? 'add' : 'remove'](
`aside-open-${this.aside.side}`
)
},
setTransform(t) {
if(t === null) {
this.aside.contentElement.style[CSS.transform] = '';
} else {
this.aside.contentElement.style[CSS.transform] = 'translate3d(' + t + 'px,0,0)';
}
}
}
const backdropManipulator = {
setSliding(sliding) {
this.aside.backdrop.isTransitioning = sliding;
//.classList[sliding ? 'add' : 'remove']('no-transition');
},
setOpen(open) {
let amt = open ? 0.5 : 0;
this.aside.backdrop.opacity = amt;
},
setTransform(t) {
if(t === null) {
t = this.aside.width();
}
let fade = 0.5 * t / this.aside.width();
this.aside.backdrop.opacity = fade;
}
}
/**
* Aside 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 Aside.
*/
export class AsideType {
constructor(aside: Aside) {
this.aside = aside;
setTimeout(() => {
aside.contentElement.classList.add('aside-content')
})
this.open = new Animation();
this.close = new Animation();
}
setOpen(shouldOpen) {
return new Promise(resolve => {
if (shouldOpen) {
this.open.playbackRate(1).onFinish(resolve, true).play();
} else {
this.close.playbackRate(1).onFinish(resolve, true).play();
}
});
}
setProgressStart(isOpen) {
this.isOpening = !isOpen;
this.seek && this.seek.dispose();
// clone the correct animation depending on open/close
if (this.isOpening) {
this.seek = this.open.clone();
} else {
this.seek = this.close.clone();
}
// the cloned animation should not use an easing curve during seek
this.seek.easing('linear').progressStart();
}
setProgess(value) {
// adjust progress value depending if it opening or closing
if (!this.isOpening) {
value = 1 - value;
}
this.seek.progress(value);
}
setProgressFinish(shouldComplete) {
let resolve;
let promise = new Promise(res => { resolve = res });
let isOpen = (this.isOpening && shouldComplete);
if (!this.isOpening && !shouldComplete) {
isOpen = true;
}
this.seek.progressFinish(shouldComplete).then(() => {
this.isOpening = false;
resolve(isOpen);
});
return promise;
}
onDestory() {
this.open && this.open.dispose();
this.close && this.close.dispose();
this.seek && this.seek.dispose();
}
}
export class AsideTypeOverlay extends AsideType {
setSliding(sliding) {
asideManipulator.setSliding.call(this, sliding);
backdropManipulator.setSliding.call(this, sliding);
}
setOpen(open) {
asideManipulator.setOpen.call(this, open);
backdropManipulator.setOpen.call(this, open);
}
setTransform(t) {
asideManipulator.setTransform.call(this, t);
backdropManipulator.setTransform.call(this, t);
}
setDoneTransforming(willOpen) {
asideManipulator.setTransform.call(this, null);
backdropManipulator.setTransform.call(this, null);
asideManipulator.setOpen.call(this, willOpen);
backdropManipulator.setOpen.call(this, willOpen);
}
}
export class AsideTypePush extends AsideType {
setSliding(sliding) {
asideManipulator.setSliding.call(this, sliding);
contentManipulator.setSliding.call(this, sliding);
}
setOpen(open) {
asideManipulator.setOpen.call(this, open);
contentManipulator.setOpen.call(this, open);
}
setTransform(t) {
asideManipulator.setTransform.call(this, t);
contentManipulator.setTransform.call(this, t);
}
setDoneTransforming(willOpen) {
asideManipulator.setOpen.call(this, willOpen);
asideManipulator.setTransform.call(this, null);
contentManipulator.setOpen.call(this, willOpen);
contentManipulator.setTransform.call(this, null);
}
}
/**
* Aside Reveal Type
* The content slides over to reveal the aside underneath.
* The aside menu itself, which is under the content, does not move.
*/
class AsideRevealType extends AsideType {
constructor(aside) {
super();
export class AsideTypeReveal extends AsideType {
setSliding(sliding) {
contentManipulator.setSliding.call(this, sliding);
}
setOpen(sliding) {
asideManipulator.setOpen.call(this, sliding);
contentManipulator.setOpen.call(this, sliding);
}
setTransform(t) {
contentManipulator.setTransform.call(this, t);
}
setDoneTransforming(willOpen) {
contentManipulator.setOpen.call(this, willOpen);
contentManipulator.setTransform.call(this, null);
let easing = 'ease';
let duration = 250;
let openedX = (aside.width() * (aside.side == 'right' ? -1 : 1)) + 'px';
this.open.easing(easing).duration(duration);
this.close.easing(easing).duration(duration);
let contentOpen = new Animation(aside.getContentElement());
contentOpen.fromTo(TRANSLATE_X, CENTER, openedX);
this.open.add(contentOpen);
let contentClose = new Animation(aside.getContentElement());
contentClose.fromTo(TRANSLATE_X, openedX, CENTER);
this.close.add(contentClose);
}
}
Aside.register('reveal', AsideRevealType);
/**
* Aside Overlay Type
* The aside menu slides over the content. The content
* itself, which is under the aside, does not move.
*/
class AsideOverlayType extends AsideType {
constructor(aside) {
super();
let easing = 'ease';
let duration = 250;
let backdropOpacity = 0.5;
let closedX = (aside.width() * (aside.side == 'right' ? 1 : -1)) + 'px';
this.open.easing(easing).duration(duration);
this.close.easing(easing).duration(duration);
let asideOpen = new Animation(aside.getAsideElement());
asideOpen.fromTo(TRANSLATE_X, closedX, CENTER);
this.open.add(asideOpen);
let backdropOpen = new Animation(aside.getBackdropElement());
backdropOpen.fromTo(OPACITY, 0.01, backdropOpacity);
this.open.add(backdropOpen);
let asideClose = new Animation(aside.getAsideElement());
asideClose.fromTo(TRANSLATE_X, CENTER, closedX);
this.close.add(asideClose);
let backdropClose = new Animation(aside.getBackdropElement());
backdropClose.fromTo(OPACITY, backdropOpacity, 0.01);
this.close.add(backdropClose);
}
}
Aside.register('overlay', AsideOverlayType);
const OPACITY = 'opacity';
const TRANSLATE_X = 'translateX';
const CENTER = '0px';

View File

@@ -20,6 +20,6 @@
<button id="e2eContentToggleAside" aside-toggle>Toggle Aside</button>
</p>
<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>
<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>
</ion-content>

View File

@@ -0,0 +1,27 @@
import {App, IonicApp, IonicView} from 'ionic/ionic';
@IonicView({templateUrl: 'page1.html'})
class Page1 {}
@App({
templateUrl: 'main.html'
})
class E2EApp {
constructor(app: IonicApp) {
this.app = app;
this.rootView = Page1;
}
openPage(aside, page) {
// close the menu when clicking a link from the aside
aside.close();
// Reset the content nav to have just this page
// we wouldn't want the back button to show in this scenario
let nav = this.app.getComponent('nav');
nav.setRoot(page.component);
}
}

View File

@@ -0,0 +1,41 @@
<ion-aside [content]="content" id="leftMenu" type="overlay" side="left">
<ion-toolbar secondary>
<ion-title>Left Menu</ion-title>
</ion-toolbar>
<ion-content>
<ion-list>
<button ion-item aside-toggle="leftMenu">
Close Left Menu
</button>
</ion-list>
</ion-content>
</ion-aside>
<!-- <ion-aside [content]="content" id="rightMenu" type="reveal" side="right">
<ion-toolbar secondary>
<ion-title>Right Menu</ion-title>
</ion-toolbar>
<ion-content>
<ion-list>
<button ion-item aside-toggle="rightMenu">
Close Right Menu
</button>
</ion-list>
</ion-content>
</ion-aside> -->
<ion-nav id="nav" [root]="rootView" #content swipe-back-enabled="false"></ion-nav>

View File

@@ -0,0 +1,29 @@
<ion-navbar *navbar>
<button aside-toggle="leftMenu">
<icon menu></icon>
</button>
<ion-title>
Overlay Aside
</ion-title>
</ion-navbar>
<ion-content #content padding>
<h3>Content</h3>
<p>
<button aside-toggle="leftMenu">Toggle Left Aside</button>
</p>
<p>
<button aside-toggle="rightMenu">Toggle Right Aside</button>
</p>
<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>
</ion-content>

View File

@@ -0,0 +1,27 @@
import {App, IonicApp, IonicView} from 'ionic/ionic';
@IonicView({templateUrl: 'page1.html'})
class Page1 {}
@App({
templateUrl: 'main.html'
})
class E2EApp {
constructor(app: IonicApp) {
this.app = app;
this.rootView = Page1;
}
openPage(aside, page) {
// close the menu when clicking a link from the aside
aside.close();
// Reset the content nav to have just this page
// we wouldn't want the back button to show in this scenario
let nav = this.app.getComponent('nav');
nav.setRoot(page.component);
}
}

View File

@@ -0,0 +1,41 @@
<ion-aside [content]="content" id="leftMenu" type="reveal" side="left">
<ion-toolbar secondary>
<ion-title>Left Menu</ion-title>
</ion-toolbar>
<ion-content>
<ion-list>
<button ion-item aside-toggle="leftMenu">
Close Left Menu
</button>
</ion-list>
</ion-content>
</ion-aside>
<!-- <ion-aside [content]="content" id="rightMenu" type="reveal" side="right">
<ion-toolbar secondary>
<ion-title>Right Menu</ion-title>
</ion-toolbar>
<ion-content>
<ion-list>
<button ion-item aside-toggle="rightMenu">
Close Right Menu
</button>
</ion-list>
</ion-content>
</ion-aside> -->
<ion-nav id="nav" [root]="rootView" #content swipe-back-enabled="false"></ion-nav>

View File

@@ -0,0 +1,29 @@
<ion-navbar *navbar>
<button aside-toggle="leftMenu">
<icon menu></icon>
</button>
<ion-title>
Reveal Aside
</ion-title>
</ion-navbar>
<ion-content #content padding>
<h3>Content</h3>
<p>
<button aside-toggle="leftMenu">Toggle Left Aside</button>
</p>
<p>
<button aside-toggle="rightMenu">Toggle Right Aside</button>
</p>
<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>
</ion-content>

View File

@@ -1,4 +1,5 @@
import {IonicConfig} from '../config/config';
import {Platform} from '../platform/platform';
import * as util from 'ionic/util';
@@ -76,11 +77,11 @@ export class Ion {
}
width() {
return this.getNativeElement().offsetWidth;
return Platform.getDimensions(this).w;
}
height() {
return this.getNativeElement().offsetHeight;
return Platform.getDimensions(this).h;
}
}

View File

@@ -45,7 +45,7 @@ export class SwipeHandle {
self.onDragHorizontal(ev);
}
gesture.on('panend', gestureEv => { self.onDragEnd(gestureEv.gesture); });
gesture.on('panend', gesture => { self.onDragEnd(gesture); });
gesture.on('panleft', dragHorizontal);
gesture.on('panright', dragHorizontal);
});
@@ -85,16 +85,14 @@ export class SwipeHandle {
}
this.zone.run(() => {
this.viewCtrl.swipeBackEnd(completeSwipeBack, progress, playbackRate);
this.viewCtrl.swipeBackFinish(completeSwipeBack, playbackRate);
});
this.startX = null;
}
onDragHorizontal(gestureEv) {
onDragHorizontal(gesture) {
this.zone.run(() => {
let gesture = gestureEv.gesture;
if (this.startX === null) {
// starting drag
gesture.srcEvent.preventDefault();

View File

@@ -338,6 +338,8 @@ export class ViewController extends Ion {
enteringItem.shouldCache = false;
enteringItem.willEnter();
this.app.setTransitioning(true);
// wait for the new item to complete setup
enteringItem.stage(() => {
@@ -348,8 +350,7 @@ export class ViewController extends Ion {
// init the transition animation
this.sbTransition = Transition.create(this, opts);
this.sbTransition.easing('linear');
this.sbTransition.stage();
this.sbTransition.easing('linear').progressStart();
let swipeBackPromise = new Promise(res => { this.sbResolve = res; });
@@ -408,27 +409,16 @@ export class ViewController extends Ion {
* @param {TODO} progress TODO
* @param {TODO} playbackRate TODO
*/
swipeBackEnd(completeSwipeBack, progress, playbackRate) {
swipeBackFinish(completeSwipeBack, playbackRate) {
// to reverse the animation use a negative playbackRate
if (this.sbTransition && this.sbActive) {
this.sbActive = false;
if (progress <= 0) {
this.swipeBackProgress(0.0001);
} else if (progress >= 1) {
this.swipeBackProgress(0.9999);
}
if (!completeSwipeBack) {
playbackRate = playbackRate * -1;
}
this.sbTransition.playbackRate(playbackRate);
this.sbTransition.play().then(() => {
this.sbTransition.progressFinish(completeSwipeBack, playbackRate).then(() => {
this.sbResolve && this.sbResolve(completeSwipeBack);
this.sbTransition && this.sbTransition.dispose();
this.sbResolve = this.sbTransition = null;
this.app.setTransitioning(false);
});
}
}

View File

@@ -17,6 +17,7 @@
"components/toolbar/toolbar",
"components/action-menu/action-menu",
"components/aside/aside",
"components/aside/extensions/types",
"components/badge/badge",
"components/button/button",
"components/button/button-clear",

View File

@@ -14,6 +14,8 @@ export class PlatformCtrl {
this._registry = {};
this._default = null;
this._onResizes = [];
this._dimensions = {};
this._dimIds = 0;
this._readyPromise = new Promise(res => { this._readyResolve = res; } );
}
@@ -173,11 +175,11 @@ export class PlatformCtrl {
}
winResize() {
Platform._w = Platform._h = 0;
clearTimeout(Platform._resizeTimer);
Platform._resizeTimer = setTimeout(() => {
Platform.flushDimensions();
for (let i = 0; i < Platform._onResizes.length; i++) {
try {
Platform._onResizes[i]();
@@ -193,6 +195,35 @@ export class PlatformCtrl {
this._onResizes.push(cb);
}
/**
* Get the element offsetWidth and offsetHeight. Values are cached to
* reduce DOM reads, and reset on a window resize.
* @param {TODO} platformConfig TODO
*/
getDimensions(component) {
// cache
if (!component._dimId) {
component._dimId = ++this._dimIds;
}
let dimensions = this._dimensions[component._dimId];
if (!dimensions) {
let ele = component.getNativeElement();
dimensions = this._dimensions[component._dimId] = {
w: ele.offsetWidth,
h: ele.offsetHeight
};
}
return dimensions;
}
flushDimensions() {
this._dimensions = {};
this._w = this._h = 0;
}
// Registry
// **********************************************
@@ -237,8 +268,8 @@ export class PlatformCtrl {
* @returns {boolean} TODO
*/
testUserAgent(userAgentExpression) {
let rx = new RegExp(userAgentExpression, 'i');
return rx.test(this._ua);
let rgx = new RegExp(userAgentExpression, 'i');
return rgx.test(this._ua);
}
/**

View File

@@ -43,7 +43,7 @@ export class Activator {
bindDom('touchcancel', function(ev) {
self.isTouch = true;
self.touchCancel(ev);
self.pointerCancel(ev);
});
bindDom('mousedown', function(ev) {