feat(toast): add toast

This commit is contained in:
mhartington
2017-08-18 12:58:12 -04:00
parent aec053fa28
commit 8cebb411aa
13 changed files with 679 additions and 5 deletions

View File

@ -9,10 +9,10 @@ import { Animator } from './animator';
export class AnimationControllerImpl implements AnimationController {
@Method()
create(animationBuilder?: AnimationBuilder, baseElm?: any): Promise<Animation> {
create(animationBuilder?: AnimationBuilder, baseElm?: any, opts?: any): Promise<Animation> {
return new Promise(resolve => {
if (animationBuilder) {
resolve(animationBuilder(Animator as any, baseElm));
resolve(animationBuilder(Animator as any, baseElm, opts));
} else {
resolve(new Animator() as any);
}

View File

@ -1,5 +1,5 @@
export interface AnimationController {
create(animationBuilder?: AnimationBuilder, baseElm?: any): Promise<Animation>;
create(animationBuilder?: AnimationBuilder, baseElm?: any, opts?: any): Promise<Animation>;
}
export interface Animation {
@ -38,7 +38,7 @@ export interface Animation {
export interface AnimationBuilder {
(Animation: Animation, baseElm?: HTMLElement): Animation;
(Animation: Animation, baseElm?: HTMLElement, opts?: any): Animation;
}

View File

@ -0,0 +1,66 @@
import { Component, Listen, Method } from '@stencil/core';
import { ToastEvent, ToastOptions, Toast } from '../../index';
@Component({
tag: 'ion-toast-controller'
})
export class ToastController {
private ids = 0;
private toastResolves: { [toastId: string]: Function } = {};
private toasts: Toast[] = [];
@Method()
create(opts?: ToastOptions) {
// create ionic's wrapping ion-toast component
const toast = document.createElement('ion-toast');
const id = this.ids++;
// give this toast a unique id
toast.id = `toast-${id}`;
toast.style.zIndex = (10000 + id).toString();
// convert the passed in toast options into props
// that get passed down into the new toast
Object.assign(toast, opts);
// append the toast element to the document body
const appRoot = document.querySelector('ion-app') || document.body;
appRoot.appendChild(toast as any);
// store the resolve function to be called later up when the toast loads
return new Promise<Toast>(resolve => {
this.toastResolves[toast.id] = resolve;
});
}
@Listen('body:ionToastDidLoad')
protected viewDidLoad(ev: ToastEvent) {
const toast = ev.detail.toast;
const toastResolve = this.toastResolves[toast.id];
if (toastResolve) {
toastResolve(toast);
delete this.toastResolves[toast.id];
}
}
@Listen('body:ionToastWillPresent')
protected willPresent(ev: ToastEvent) {
this.toasts.push(ev.detail.toast);
}
@Listen('body:ionToastWillDismiss, body:ionToastDidUnload')
protected willDismiss(ev: ToastEvent) {
const index = this.toasts.indexOf(ev.detail.toast);
if (index > -1) {
this.toasts.splice(index, 1);
}
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
const lastToast = this.toasts[this.toasts.length - 1];
if (lastToast) {
lastToast.dismiss();
}
}
}

View File

@ -0,0 +1,37 @@
import { Animation } from '../../../index';
/**
* iOS Toast Enter Animation
*/
export default function(
Animation: Animation,
baseElm: HTMLElement,
position: string
) {
const baseAnimation = new Animation();
const wrapperAnimation = new Animation();
const wrapperEle = <HTMLElement> baseElm.querySelector('.toast-wrapper');
wrapperAnimation.addElement(wrapperEle);
switch (position) {
case 'top':
wrapperAnimation.fromTo('translateY', '-100%', `${10}px`);
break;
case 'middle':
let topPosition = Math.floor(
baseElm.clientHeight / 2 - wrapperEle.clientHeight / 2
);
wrapperEle.style.top = `${topPosition}px`;
wrapperAnimation.fromTo('opacity', 0.01, 1);
break;
default:
wrapperAnimation.fromTo('translateY', '100%', `${0 - 10}px`);
break;
}
return baseAnimation
.addElement(baseElm)
.easing('cubic-bezier(.36,.66,.04,1)')
.duration(400)
.add(wrapperAnimation);
}

View File

@ -0,0 +1,32 @@
import { Animation } from '../../../index';
/**
* iOS Toast Leave Animation
*/
export default function(
Animation: Animation,
baseElm: HTMLElement,
position: string
) {
const baseAnimation = new Animation();
const wrapperAnimation = new Animation();
const wrapperEle = <HTMLElement> baseElm.querySelector('.toast-wrapper');
wrapperAnimation.addElement(wrapperEle);
switch (position) {
case 'top':
wrapperAnimation.fromTo('translateY', `${10}px`, '-100%');
break;
case 'middle':
wrapperAnimation.fromTo('opacity', 0.99, 0);
break;
default:
wrapperAnimation.fromTo('translateY', `${0 - 10}px`, '100%');
break;
}
return baseAnimation
.addElement(baseElm)
.easing('cubic-bezier(.36,.66,.04,1)')
.duration(300)
.add(wrapperAnimation);
}

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Ionic Toast</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-header>
<ion-toolbar color="primary">
<ion-title>Popover</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-button block onclick="presentToast('bottom')">Show Toast Bottom</ion-button>
<ion-button block onclick="presentToast('top')">Show Toast Top</ion-button>
<ion-button block onclick="presentToast('middle')">Show Toast Middle</ion-button>
<ion-button block onclick="presentToastWithOptions({message: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea voluptatibus quibusdam eum nihil optio, ullam accusamus magni, nobis suscipit reprehenderit, sequi quam amet impedit. Accusamus dolorem voluptates laborum dolor obcaecati.', duration: 2000})">Show Toast with long message</ion-button>
<ion-button block onclick="presentToastWithOptions({message: 'click to close', showCloseButton: true})">Show Toast with close button</ion-button>
<ion-button block onclick="presentToastWithOptions({message: 'click to close', showCloseButton: true, closeButtonText: 'closing time'})">Show Toast with close button and custom text</ion-button>
<!-- <ion&#45;button block onclick="presentToastWithOptions()">Show Toast Middle</ion&#45;button> -->
<ion-toast-controller></ion-toast-controller>
</ion-content>
</ion-app>
<script>
function presentToast(position) {
var toastController = document.querySelector('ion-toast-controller');
toastController.create({
message: 'Hellooo',
position,
duration: 2000
})
.then(toast => {
toast.present();
});
}
function presentToastWithOptions(opts) {
var toastController = document.querySelector('ion-toast-controller');
toastController.create(opts)
.then(toast => {
toast.present();
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,74 @@
@import "../../themes/ionic.globals.ios";
@import "./toast";
// iOS Toast
// --------------------------------------------------
/// @prop - Background of the toast wrapper
$toast-ios-background: rgba(0, 0, 0, .9) !default;
/// @prop - Border radius of the toast wrapper
$toast-ios-border-radius: .65rem !default;
/// @prop - Color of the toast title
$toast-ios-title-color: #fff !default;
/// @prop - Font size of the toast title
$toast-ios-title-font-size: 1.4rem !default;
// deprecated
$toast-ios-title-padding: null !default;
/// @prop - Padding top of the toast title
$toast-ios-title-padding-top: 1.5rem !default;
/// @prop - Padding end of the toast title
$toast-ios-title-padding-end: $toast-ios-title-padding-top !default;
/// @prop - Padding bottom of the toast title
$toast-ios-title-padding-bottom: $toast-ios-title-padding-top !default;
/// @prop - Padding start of the toast title
$toast-ios-title-padding-start: $toast-ios-title-padding-end !default;
.toast-ios .toast-wrapper {
@include position-horizontal(10px, 10px);
@include margin(auto);
@include border-radius($toast-ios-border-radius);
position: absolute;
z-index: $z-index-overlay-wrapper;
display: block;
max-width: $toast-max-width;
background: $toast-ios-background;
}
.toast-ios .toast-wrapper.toast-top {
@include transform(translate3d(0, -100%, 0));
top: 0;
}
.toast-ios .toast-wrapper.toast-bottom {
@include transform(translate3d(0, 100%, 0));
bottom: 0;
}
.toast-ios .toast-wrapper.toast-middle {
opacity: .01;
}
.toast-ios .toast-message {
font-size: $toast-ios-title-font-size;
color: $toast-ios-title-color;
@include deprecated-variable(padding, $toast-ios-title-padding) {
@include padding($toast-ios-title-padding-top, $toast-ios-title-padding-end, $toast-ios-title-padding-bottom, $toast-ios-title-padding-start);
}
}

View File

@ -0,0 +1,71 @@
@import "../../themes/ionic.globals.md";
@import "./toast";
// Material Design Toast
// --------------------------------------------------
/// @prop - Background of the toast wrapper
$toast-md-background: #333 !default;
/// @prop - Color of the toast title
$toast-md-title-color: #fff !default;
/// @prop - Font size of the toast title
$toast-md-title-font-size: 1.5rem !default;
// deprecated
$toast-md-title-padding: null !default;
/// @prop - Padding top of the toast title
$toast-md-title-padding-top: 19px !default;
/// @prop - Padding end of the toast title
$toast-md-title-padding-end: 16px !default;
/// @prop - Padding bottom of the toast title
$toast-md-title-padding-bottom: 17px !default;
/// @prop - Padding start of the toast title
$toast-md-title-padding-start: $toast-md-title-padding-end !default;
.toast-md .toast-wrapper {
@include position-horizontal(0, 0);
@include margin(auto);
position: absolute;
z-index: $z-index-overlay-wrapper;
display: block;
width: $toast-width;
max-width: $toast-max-width;
background: $toast-md-background;
}
.toast-md .toast-wrapper.toast-top {
@include transform(translate3d(0, -100%, 0));
top: 0;
}
.toast-md .toast-wrapper.toast-bottom {
@include transform(translate3d(0, 100%, 0));
bottom: 0;
}
.toast-md .toast-wrapper.toast-middle {
opacity: .01;
}
.toast-md .toast-message {
font-size: $toast-md-title-font-size;
color: $toast-md-title-color;
@include deprecated-variable(padding, $toast-md-title-padding) {
@include padding($toast-md-title-padding-top, $toast-md-title-padding-end, $toast-md-title-padding-bottom, $toast-md-title-padding-start);
}
}

View File

@ -0,0 +1,49 @@
@import "../../themes/ionic.globals";
// Toast
// --------------------------------------------------
/// @prop - Width of the toast
$toast-width: 100% !default;
/// @prop - Max width of the toast
$toast-max-width: 700px !default;
ion-toast {
@include position(0, null, null, 0);
position: absolute;
z-index: $z-index-overlay;
display: block;
width: $toast-width;
height: $toast-width;
pointer-events: none;
contain: strict;
}
.toast-container {
display: flex;
align-items: center;
pointer-events: auto;
contain: content;
}
.toast-button {
@include padding(19px, 16px, 17px);
font-size: 1.5rem;
}
.toast-message {
flex: 1;
}

View File

@ -0,0 +1,202 @@
import { Component, Element, Event, EventEmitter, Listen, Prop, State } from '@stencil/core';
import { AnimationBuilder, Animation, AnimationController, Config, CssClassMap } from '../../index';
import { createThemedClasses } from '../../utils/theme';
import iOSEnterAnimation from './animations/ios.enter';
import iOSLeaveAnimation from './animations/ios.leave';
@Component({
tag: 'ion-toast',
styleUrls: {
ios: 'toast.ios.scss',
md: 'toast.md.scss',
wp: 'toast.wp.scss'
},
host: {
theme: 'toast'
}
})
export class Toast {
private animation: Animation;
@Element() private el: HTMLElement;
@Event() private ionToastDidLoad: EventEmitter;
@Event() private ionToastDidPresent: EventEmitter;
@Event() private ionToastWillPresent: EventEmitter;
@Event() private ionToastWillDismiss: EventEmitter;
@Event() private ionToastDidDismiss: EventEmitter;
@Event() private ionToastDidUnload: EventEmitter;
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController;
@Prop({ context: 'config' }) config: Config;
@Prop() message: string;
@Prop() cssClass: string;
@Prop() duration: number;
@Prop() showCloseButton: boolean;
@Prop() closeButtonText: string;
@Prop() dismissOnPageChange: boolean;
@Prop() position: string;
@Prop() enterAnimation: AnimationBuilder;
@Prop() exitAnimation: AnimationBuilder;
@Prop() id: string;
present() {
return new Promise<void>(resolve => {
this._present(resolve);
});
}
private _present(resolve: Function) {
if (this.animation) {
this.animation.destroy();
this.animation = null;
}
this.ionToastWillPresent.emit({ actionSheet: this });
// get the user's animation fn if one was provided
let animationBuilder = this.enterAnimation;
if (!animationBuilder) {
// user did not provide a custom animation fn
// decide from the config which animation to use
animationBuilder = iOSEnterAnimation;
}
// build the animation and kick it off
this.animationCtrl.create(animationBuilder, this.el, this.position).then(animation => {
this.animation = animation;
animation.onFinish((a: any) => {
a.destroy();
this.ionViewDidEnter();
resolve();
}).play();
});
}
dismiss() {
if (this.animation) {
this.animation.destroy();
this.animation = null;
}
return new Promise(resolve => {
this.ionToastWillDismiss.emit({ toast: this });
// get the user's animation fn if one was provided
let animationBuilder = this.exitAnimation;
if (!animationBuilder) {
// user did not provide a custom animation fn
// decide from the config which animation to use
animationBuilder = iOSLeaveAnimation;
}
// build the animation and kick it off
this.animationCtrl.create(animationBuilder, this.el, this.position).then(animation => {
this.animation = animation;
animation.onFinish((a: any) => {
a.destroy();
this.ionToastDidDismiss.emit({ toast: this });
Context.dom.write(() => {
this.el.parentNode.removeChild(this.el);
});
resolve();
}).play();
});
});
}
protected ionViewDidUnload() {
this.ionToastDidUnload.emit({ toast: this });
}
@Listen('ionDismiss')
protected onDismiss(ev: UIEvent) {
ev.stopPropagation();
ev.preventDefault();
this.dismiss();
}
protected ionViewDidLoad() {
this.ionToastDidLoad.emit({ toast: this });
}
protected ionViewDidEnter() {
this.ionToastDidPresent.emit({ toast: this });
if(this.duration){
setTimeout(()=>{
this.dismiss();
}, this.duration)
}
}
protected click(button: HTMLElement) {
console.log(button)
// let shouldDismiss = true;
// if (button.handler) {
// if (button.handler() === false) {
// shouldDismiss = false;
// }
// }
// if (shouldDismiss) {
// this.dismiss();
// }
}
protected render() {
let userCssClass = 'toast-content';
if (this.cssClass) {
userCssClass += ' ' + this.cssClass;
}
return (
<div class={this.wrapperClass()}>
<div class="toast-container">
{this.message
? <div class="toast-message">{this.message}</div>
: null}
{this.showCloseButton
? <ion-button clear color="light" class="toast-button" onClick={()=>this.dismiss()}>
{this.closeButtonText || 'Close'}
</ion-button>
: null}
</div>
</div>
);
}
wrapperClass(): CssClassMap {
let wrapperClass: string[] = !this.position
? ['toast-wrapper','toast-bottom']
: [`toast-wrapper`, `toast-${this.position}`];
return wrapperClass.reduce((prevValue: any, cssClass: any) => {
prevValue[cssClass] = true;
return prevValue;
}, {});
}
}
export interface ToastOptions {
message?: string;
cssClass?: string;
duration?: number;
showCloseButton?: boolean;
closeButtonText?: string;
dismissOnPageChange?: boolean;
position?: string;
enterAnimation?: AnimationBuilder;
exitAnimation?: AnimationBuilder;
}
export interface ToastEvent {
detail: {
toast: Toast;
};
}

View File

@ -0,0 +1,81 @@
@import "../../themes/ionic.globals.wp";
@import "./toast";
// Windows Phone Toast
// --------------------------------------------------
/// @prop - Background of the toast wrapper
$toast-wp-background: rgba(0, 0, 0, 1) !default;
/// @prop - Border radius of the toast wrapper
$toast-wp-border-radius: 0 !default;
/// @prop - Color of the toast button
$toast-wp-button-color: #fff !default;
/// @prop - Color of the toast title
$toast-wp-title-color: #fff !default;
/// @prop - Font size of the toast title
$toast-wp-title-font-size: 1.4rem !default;
// deprecated
$toast-wp-title-padding: null !default;
/// @prop - Padding top of the toast title
$toast-wp-title-padding-top: 1.5rem !default;
/// @prop - Padding end of the toast title
$toast-wp-title-padding-end: $toast-wp-title-padding-top !default;
/// @prop - Padding bottom of the toast title
$toast-wp-title-padding-bottom: $toast-wp-title-padding-top !default;
/// @prop - Padding start of the toast title
$toast-wp-title-padding-start: $toast-wp-title-padding-end !default;
.toast-wp .toast-wrapper {
@include position-horizontal(0, 0);
@include margin(auto);
@include border-radius($toast-wp-border-radius);
position: absolute;
z-index: $z-index-overlay-wrapper;
display: block;
max-width: $toast-max-width;
background: $toast-wp-background;
}
.toast-wp .toast-wrapper.toast-top {
top: 0;
opacity: .01;
}
.toast-wp .toast-wrapper.toast-bottom {
bottom: 0;
opacity: .01;
}
.toast-wp .toast-wrapper.toast-middle {
opacity: .01;
}
.toast-message {
font-size: $toast-wp-title-font-size;
color: $toast-wp-title-color;
@include deprecated-variable(padding, $toast-wp-title-padding) {
@include padding($toast-wp-title-padding-top, $toast-wp-title-padding-end, $toast-wp-title-padding-bottom, $toast-wp-title-padding-start);
}
}
.toast-button {
color: $toast-wp-button-color;
}

View File

@ -17,6 +17,10 @@ import { PopoverController } from './components/popover-controller/popover-contr
import { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
import { Segment } from './components/segment/segment';
import { SegmentButton, SegmentButtonEvent } from './components/segment-button/segment-button';
import { Toast, ToastEvent, ToastOptions } from './components/toast/toast'
import { ToastController } from './components/toast-controller/toast-controller'
import * as Stencil from '@stencil/core';
@ -78,5 +82,9 @@ export {
ScrollDetail,
Segment,
SegmentButton,
SegmentButtonEvent
SegmentButtonEvent,
Toast,
ToastEvent,
ToastOptions,
ToastController
}