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

@ -46,7 +46,7 @@ var tscReporter = {
};
var flagConfig = {
string: ['port', 'version', 'ngVersion'],
string: ['port', 'version', 'ngVersion', 'animations'],
alias: {'p': 'port', 'v': 'version', 'a': 'ngVersion'},
default: { port: 8000 }
};
@ -172,11 +172,21 @@ gulp.task('bundle.ionic', ['transpile'], function() {
var insert = require('gulp-insert');
var concat = require('gulp-concat');
var prepend = [];
// force the web animations api polyfill to kick in
if (flags.animations == 'polyfill') {
prepend.push('window.Element.prototype.animate=undefined;');
}
// prepend correct system paths
prepend.push('System.config({ "paths": { "ionic/*": "ionic/*", "rx": "rx" } });');
return gulp.src([
'dist/src/es5/system/ionic/**/*.js'
])
.pipe(concat('ionic.js'))
.pipe(insert.prepend('System.config({ "paths": { "ionic/*": "ionic/*", "rx": "rx" } });\n'))
.pipe(insert.prepend(prepend.join('\n')))
.pipe(gulp.dest('dist/js/'));
//TODO minify + sourcemaps
});

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;
reverse() {
return this.playbackRate(-1);
}
return this._fill || (this._parent && this._parent.fill());
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.ani.playbackRate = self.rate;
}
self.player.onfinish = () => {
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.player = null;
self.ani = null;
self.resolve();
callback && callback();
};
}
return self.promise;
}
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

@ -5,152 +5,53 @@
$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-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;
}
}
ion-aside[side=right] {
right: 0;
left: auto;
}
&[type=overlay] {
z-index: $z-index-aside-overlay;
&:not(.open):not(.changing) {
ion-aside backdrop {
z-index: -1;
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;
right: 0;
left: 0;
bottom: 0;
position: fixed;
background-color: rgb(0,0,0);
}
.aside-content {
transition: $aside-transition;
transform: translate3d(0,0,0);
box-shadow: $aside-shadow;
&.aside-open-left {
transform: translate3d($aside-width,0,0);
pointer-events: none;
transform: translate3d(0px, 0px, 0px);
}
&.aside-open-right {
transform: translate3d(-$aside-width,0,0);
.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.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);
let self = this;
this.onContentClick = function(ev) {
ev.preventDefault();
ev.stopPropagation();
self.close();
};
}
_initGesture() {
switch(this.side) {
case 'right':
this._gesture = new gestures.RightAsideGesture(this);
break;
this.gestureDelegate = this.getDelegate('gesture');
this.typeDelegate = this.getDelegate('type');
}
onDestroy() {
this.contentElement.removeEventListener('click', this.contentClickFn);
}
/**
* 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.
*/
@Component({
selector: 'ion-aside-backdrop',
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
*/
@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();
}
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);
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();
}
});
}
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);
}
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();
}
export class AsideTypeReveal extends AsideType {
setSliding(sliding) {
contentManipulator.setSliding.call(this, sliding);
// the cloned animation should not use an easing curve during seek
this.seek.easing('linear').progressStart();
}
setOpen(sliding) {
asideManipulator.setOpen.call(this, sliding);
contentManipulator.setOpen.call(this, sliding);
setProgess(value) {
// adjust progress value depending if it opening or closing
if (!this.isOpening) {
value = 1 - value;
}
setTransform(t) {
contentManipulator.setTransform.call(this, t);
this.seek.progress(value);
}
setDoneTransforming(willOpen) {
contentManipulator.setOpen.call(this, willOpen);
contentManipulator.setTransform.call(this, null);
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();
}
}
/**
* 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();
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

@ -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) {

View File

@ -127,9 +127,13 @@
if ((property == 'direction') && (directions.indexOf(timingInput[property]) == -1)) {
return;
}
if (property == 'playbackRate' && timingInput[property] !== 1 && shared.isDeprecated('AnimationEffectTiming.playbackRate', '2014-11-28', 'Use Animation.playbackRate instead.')) {
return;
}
// IONIC HACK
// NATIVE CHROME STILL USES THIS, SO DON'T HAVE THE POLYFILL THROW ERRORS
// if (property == 'playbackRate' && timingInput[property] !== 1 && shared.isDeprecated('AnimationEffectTiming.playbackRate', '2014-11-28', 'Use Animation.playbackRate instead.')) {
// return;
// }
timing[property] = timingInput[property];
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long