From 98e4336c16dbbd8fa395c68189503b1105c4545c Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Tue, 22 Dec 2015 21:45:28 -0600 Subject: [PATCH] wip --- ionic/components.ios.scss | 1 + ionic/components.ts | 1 + ionic/components/alert/alert.ios.scss | 129 +++++++++++ ionic/components/alert/alert.scss | 80 +++++++ ionic/components/alert/alert.ts | 214 +++++++++++++++++- ionic/components/alert/test/basic/index.ts | 52 ++++- ionic/components/nav/nav-controller.ts | 46 ++-- ionic/components/nav/view-controller.ts | 25 +- .../components/overlay/overlay-controller.ts | 64 ++---- ionic/components/overlay/overlay.ts | 2 +- ionic/config/config.ts | 3 + ionic/config/modes.ts | 12 +- ionic/ionic.ts | 1 - 13 files changed, 534 insertions(+), 96 deletions(-) create mode 100644 ionic/components/alert/alert.ios.scss create mode 100644 ionic/components/alert/alert.scss diff --git a/ionic/components.ios.scss b/ionic/components.ios.scss index df83042b56..c3aa00b223 100644 --- a/ionic/components.ios.scss +++ b/ionic/components.ios.scss @@ -7,6 +7,7 @@ @import "components/app/app.ios", "components/action-sheet/action-sheet.ios", + "components/alert/alert.ios", "components/badge/badge.ios", "components/button/button.ios", "components/card/card.ios", diff --git a/ionic/components.ts b/ionic/components.ts index 54c7cc048d..e6a5656255 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -25,6 +25,7 @@ export * from './components/nav/nav-push' export * from './components/nav/nav-router' export * from './components/navbar/navbar' export * from './components/overlay/overlay' +export * from './components/overlay/overlay-controller' export * from './components/popup/popup' export * from './components/slides/slides' export * from './components/radio/radio' diff --git a/ionic/components/alert/alert.ios.scss b/ionic/components/alert/alert.ios.scss new file mode 100644 index 0000000000..e7b2b47074 --- /dev/null +++ b/ionic/components/alert/alert.ios.scss @@ -0,0 +1,129 @@ +@import "../../globals.ios"; +@import "./alert"; + +// iOS Alerts +// -------------------------------------------------- + +$alert-ios-max-width: 270px !default; +$alert-ios-background: rgba(0,0,0,0) !default; +$alert-ios-border-radius: 13px !default; +$alert-ios-background-color: #f8f8f8 !default; + +$alert-ios-head-padding: 12px 16px 20px !default; +$alert-ios-head-text-align: center !default; + +$alert-ios-title-margin-top: 12px !default; +$alert-ios-title-font-weight: bold !default; +$alert-ios-title-font-size: 17px !default; +$alert-ios-sub-title-font-size: 14px !default; +$alert-ios-sub-title-text-color: #666 !default; + +$alert-ios-body-padding: 0px 16px 24px !default; +$alert-ios-body-text-color: inherit !default; +$alert-ios-body-text-align: center !default; +$alert-ios-body-font-size: 13px !default; + +$alert-ios-input-padding: 6px !default; +$alert-ios-input-margin-top: 24px !default; +$alert-ios-input-background-color: #fff !default; +$alert-ios-input-border: 1px solid #ccc !default; +$alert-ios-input-border-radius: 4px !default; + +$alert-ios-button-min-height: 44px !default; +$alert-ios-button-font-size: 17px !default; +$alert-ios-button-border-color: #c8c7cc !default; +$alert-ios-button-activated-background-color: #e9e9e9 !default; + + +ion-alert { + background: $alert-ios-background; +} + +.alert-wrapper { + border-radius: $alert-ios-border-radius; + background-color: $alert-ios-background-color; + max-width: $alert-ios-max-width; + overflow: hidden; +} + +.alert-head { + padding: $alert-ios-head-padding; + text-align: $alert-ios-head-text-align; +} + +.alert-title { + margin-top: $alert-ios-title-margin-top; + font-weight: $alert-ios-title-font-weight; + font-size: $alert-ios-title-font-size; +} + +.alert-sub-title { + font-size: $alert-ios-sub-title-font-size; + color: $alert-ios-sub-title-text-color; +} + +.alert-body { + padding: $alert-ios-body-padding; + color: $alert-ios-body-text-color; + text-align: $alert-ios-body-text-align; + font-size: $alert-ios-body-font-size; +} + +.alert-input { + padding: $alert-ios-input-padding; + margin-top: $alert-ios-input-margin-top; + + background-color: $alert-ios-input-background-color; + border: $alert-ios-input-border; + border-radius: $alert-ios-input-border-radius; + -webkit-appearance: none; +} + +.alert-buttons { + :last-child { + font-weight: bold; + border-right: 0; + } +} + +.alert-button { + margin: 0; + flex: 1; + border-radius: 0; + font-size: $alert-ios-button-font-size; + min-height: $alert-ios-button-min-height; + border-right: 1px solid $alert-ios-button-border-color; + + &.activated { + opacity: 1; + background-color: $alert-ios-button-activated-background-color; + } + + &:hover:not(.disable-hover) { + opacity: 1; + } + + &:before { + position: absolute; + top: 0; + right: 0; + left: 0; + border-top: 1px solid $alert-ios-button-border-color; + content: ''; + pointer-events: none; + } +} + +&.hairlines { + .alert-input { + border-width: 0.55px; + } + + .alert-button { + border-right-width: 0.55px; + + &:before { + border-top-width: 0.55px; + } + } +} diff --git a/ionic/components/alert/alert.scss b/ionic/components/alert/alert.scss new file mode 100644 index 0000000000..24feed628f --- /dev/null +++ b/ionic/components/alert/alert.scss @@ -0,0 +1,80 @@ +@import "../../globals.core"; + +// Alerts +// -------------------------------------------------- + +$alert-min-width: 250px !default; +$alert-max-height: 90% !default; + +$alert-button-line-height: 20px !default; +$alert-button-font-size: 14px !default; +$alert-button-margin-right: 8px !default; + + +ion-alert { + position: absolute; + z-index: $z-index-overlay; + top: 0; + left: 0; + bottom: 0; + right: 0; + + display: flex; + justify-content: center; + align-items: center; + + input { + width: 100%; + } +} + +.alert-wrapper { + z-index: $z-index-overlay-wrapper; + min-width: $alert-min-width; + max-height: $alert-max-height; + + display: flex; + flex-direction: column; + + opacity: 0; +} + +.alert-title { + margin: 0; + padding: 0; +} + +.alert-sub-title { + margin: 5px 0 0 0; + padding: 0; + font-weight: normal; +} + +.alert-body { + overflow: auto; + + &:empty { + padding: 0; + } +} + +.alert-input { + @include placeholder(); + + border: 0; + background: inherit; + padding: 10px 0; +} + +.alert-buttons { + display: flex; + flex-direction: row; +} + +.alert-button { + display: block; + margin: 0; + line-height: $alert-button-line-height; + font-size: $alert-button-font-size; + margin-right: $alert-button-margin-right; +} diff --git a/ionic/components/alert/alert.ts b/ionic/components/alert/alert.ts index b5fe5488a9..850fc28477 100644 --- a/ionic/components/alert/alert.ts +++ b/ionic/components/alert/alert.ts @@ -1,25 +1,217 @@ -import {Component, ElementRef, Injectable, Renderer} from 'angular2/core'; +import {Component, ElementRef, Renderer} from 'angular2/core'; import {NgClass, NgIf, NgFor, FORM_DIRECTIVES} from 'angular2/common'; -import {OverlayController} from '../overlay/overlay-controller'; -import {Config} from '../../config/config'; -import {Animation} from '../../animations/animation'; import {NavParams} from '../nav/nav-controller'; +import {ViewController} from '../nav/view-controller'; +import {Animation} from '../../animations/animation'; import {Button} from '../button/button'; -import {extend} from '../../util/util'; +import {extend, isDefined} from '../../util/util'; -@Injectable() -export class Alert { +export class Alert extends ViewController { - constructor(private _ctrl: OverlayController, private _config: Config) { + constructor(opts={}) { + super(null, AlertCmp, opts); + this.data.inputs = this.data.inputs || []; + let buttons = this.data.buttons || []; + this.data.buttons = []; + for (let button of buttons) { + this.addButton(button); + } + + this.enterAnimationKey = 'alertEnter'; + this.leaveAnimationKey = 'alertLeave'; } - static create() { - let alert = new Alert(); + setTitle(title) { + this.data.title = title; + } - return alert; + setSubTitle(subTitle) { + this.data.subTitle = subTitle; + } + + setBodyText(text) { + this.data.text = text; + } + + addInput(input) { + input.value = isDefined(input.value) ? input.value : ''; + this.data.inputs.push(input); + } + + addButton(button) { + if (typeof button === 'string') { + button = { + text: button + }; + } + this.data.buttons.push(button); + } + + close() { + let index = this._nav.indexOf(this); + this._nav.remove(index, { animateFirst: true }); + } + + onClose(handler) { + this.data.onClose = handler; + } + + static create(opts={}) { + return new Alert(opts); } } + + +@Component({ + selector: 'ion-alert', + template: + '' + + '
' + + '
' + + '

{{d.title}}

' + + '

{{d.subTitle}}

' + + '
' + + '
{{d.text}}
' + + '
' + + '
' + + '
{{i.title}}
' + + '' + + '
' + + '
' + + '
' + + '' + + '
' + + '
', + host: { + 'role': 'dialog' + }, + directives: [NgClass, NgIf, NgFor] +}) +class AlertCmp { + + constructor( + private _viewCtrl: ViewController, + elementRef: ElementRef, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + if (this.d.cssClass) { + renderer.setElementClass(elementRef, this.d.cssClass, true); + } + } + + click(button, ev) { + let shouldClose = true; + + if (button.handler) { + // a handler has been provided, run it + if (button.handler() === false) { + // if the return value is a false then do not close + shouldClose = false; + } + } + + if (shouldClose) { + this.close(); + } + } + + close() { + this._viewCtrl.close(); + } + + onPageDidLeave() { + let values = this.d.inputs.map(i => i.value); + this.d.onClose && this.d.onClose(values); + } +} + + +/** + * Animations for alerts + */ +class AlertPopIn extends Animation { + constructor(enteringView, leavingView, opts) { + super(null, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1'); + backdrop.fromTo('opacity', '0.01', '0.3'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop, wrapper); + } +} +Animation.register('alert-pop-in', AlertPopIn); + + +class AlertPopOut extends Animation { + constructor(enteringView, leavingView, opts) { + super(null, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9'); + backdrop.fromTo('opacity', '0.3', '0'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop, wrapper); + } +} +Animation.register('alert-pop-out', AlertPopOut); + + +class AlertMdPopIn extends Animation { + constructor(enteringView, leavingView, opts) { + super(null, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', '0.01', '1').fromTo('scale', '1.1', '1'); + backdrop.fromTo('opacity', '0.01', '0.5'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop, wrapper); + } +} +Animation.register('alert-md-pop-in', AlertMdPopIn); + + +class AlertMdPopOut extends Animation { + constructor(enteringView, leavingView, opts) { + super(null, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('.backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', '1', '0').fromTo('scale', '1', '0.9'); + backdrop.fromTo('opacity', '0.5', '0'); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop, wrapper); + } +} +Animation.register('alert-md-pop-out', AlertMdPopOut); diff --git a/ionic/components/alert/test/basic/index.ts b/ionic/components/alert/test/basic/index.ts index 75d0632a4e..b61b892b88 100644 --- a/ionic/components/alert/test/basic/index.ts +++ b/ionic/components/alert/test/basic/index.ts @@ -1,4 +1,4 @@ -import {App, Alert} from 'ionic/ionic'; +import {App, Alert, OverlayController} from 'ionic/ionic'; @App({ @@ -6,7 +6,7 @@ import {App, Alert} from 'ionic/ionic'; }) class E2EApp { - constructor() { + constructor(private overlay: OverlayController) { this.alertOpen = false; this.confirmOpen = false; this.confirmResult = ''; @@ -15,12 +15,56 @@ class E2EApp { } doAlert() { - debugger; - let alert = Alert.create(); + let alert = Alert.create({ + title: 'Alert!', + subTitle: 'My alert subtitle', + bodyText: 'My alert body text', + buttons: ['Ok'] + }); + + alert.onClose(() => { + this.alertOpen = false; + }); + + this.overlay.push(alert).then(() => { + this.alertOpen = true; + }); } doPrompt() { + let alert = Alert.create(); + alert.setTitle('Prompt!'); + alert.addInput({ + label: 'Input Label', + placeholder: 'Placeholder' + }); + alert.addButton({ + text: 'Cancel', + handler: () => { + console.log('500ms delayed prompt close'); + setTimeout(() => { + console.log('Prompt close'); + alert.close(); + }, 500); + + return false; + } + }); + alert.addButton({ + text: 'Ok', + handler: () => { + console.log('Prompt Ok'); + } + }); + + alert.onClose(data => { + this.promptOpen = false; + }); + + this.overlay.push(alert).then(() => { + this.promptOpen = true; + }); } doConfirm() { diff --git a/ionic/components/nav/nav-controller.ts b/ionic/components/nav/nav-controller.ts index bf984d76e9..0f230f040c 100644 --- a/ionic/components/nav/nav-controller.ts +++ b/ionic/components/nav/nav-controller.ts @@ -230,10 +230,10 @@ export class NavController extends Ion { * @param {Object} [opts={}] Any options you want to use pass to transtion * @returns {Promise} Returns a promise when the transition is completed */ - push(componentType, params = {}, opts = {}, callback) { + push(componentType, params={}, opts={}, callback) { if (!componentType) { let errMsg = 'invalid componentType to push'; - console.error(errMsg) + console.error(errMsg); return Promise.reject(errMsg); } @@ -245,13 +245,27 @@ export class NavController extends Ion { return Promise.reject('nav controller actively transitioning'); } - this.setTransitioning(true, 500); - let promise = null; if (!callback) { promise = new Promise(res => { callback = res; }); } + // create a new ViewController + let enteringView = new ViewController(this, componentType, params); + enteringView.pageType = opts.pageType; + enteringView.handle = opts.handle || null; + + this.pushView(enteringView, opts, callback); + + return promise; + } + + /** + * @private + */ + pushView(enteringView, opts, callback) { + this.setTransitioning(true, 500); + // do not animate if this is the first in the stack if (!this._views.length && !opts.animateFirst) { opts.animate = false; @@ -260,6 +274,10 @@ export class NavController extends Ion { // default the direction to "forward" opts.direction = opts.direction || 'forward'; + if (!opts.animation) { + opts.animation = this.config.get(enteringView.enterAnimationKey); + } + // the active view is going to be the leaving one (if one exists) let leavingView = this.getActive() || new ViewController(); leavingView.shouldCache = (isBoolean(opts.cacheLeavingView) ? opts.cacheLeavingView : true); @@ -268,25 +286,16 @@ export class NavController extends Ion { leavingView.willUnload(); } - // create a new ViewController - let enteringView = new ViewController(this, componentType, params); - enteringView.shouldDestroy = false; - enteringView.shouldCache = false; - enteringView.pageType = opts.pageType; - enteringView.handle = opts.handle || null; - // add the view to the stack this._add(enteringView); if (this.router) { // notify router of the state change - this.router.stateChange('push', enteringView, params); + this.router.stateChange('push', enteringView, enteringView.params); } // start the transition this._transition(enteringView, leavingView, opts, callback); - - return promise; } /** @@ -333,6 +342,10 @@ export class NavController extends Ion { leavingView.willUnload(); } + if (!opts.animation) { + opts.animation = this.config.get(leavingView.leaveAnimationKey); + } + // the entering view is now the new last view // Note: we might not have an entering view if this is the // only view on the history stack. @@ -655,9 +668,6 @@ export class NavController extends Ion { return done(enteringView); } - if (!opts.animation) { - opts.animation = this.config.get('pageTransition'); - } if (this.config.get('animate') === false) { opts.animate = false; } @@ -909,7 +919,7 @@ export class NavController extends Ion { let providers = this.providers.concat(Injector.resolve([ provide(ViewController, {useValue: viewCtrl}), - provide(NavParams, {useValue: viewCtrl.params}) + provide(NavParams, {useValue: viewCtrl.getNavParams()}) ])); let location = this.elementRef; diff --git a/ionic/components/nav/view-controller.ts b/ionic/components/nav/view-controller.ts index 0523adc001..73dc8fd7eb 100644 --- a/ionic/components/nav/view-controller.ts +++ b/ionic/components/nav/view-controller.ts @@ -1,6 +1,5 @@ import {NavParams} from './nav-controller'; - /** * @name ViewController * @description @@ -18,14 +17,26 @@ import {NavParams} from './nav-controller'; */ export class ViewController { - constructor(navCtrl, componentType, params = {}) { - this.navCtrl = navCtrl; + constructor(navCtrl, componentType, data={}) { + this.setNav(navCtrl); this.componentType = componentType; - this.params = new NavParams(params); + this.data = data; this.instance = {}; this.state = 0; this._destroys = []; this._loaded = false; + this.shouldDestroy = false; + this.shouldCache = false; + this.enterAnimationKey = 'pageTransition'; + this.leaveAnimationKey = 'pageTransition'; + } + + setNav(navCtrl) { + this._nav = navCtrl; + } + + getNavParams() { + return new NavParams(this.data); } /** @@ -35,8 +46,8 @@ export class ViewController { */ enableBack() { // update if it's possible to go back from this nav item - if (this.navCtrl) { - let previousItem = this.navCtrl.getPrevious(this); + if (this._nav) { + let previousItem = this._nav.getPrevious(this); // the previous view may exist, but if it's about to be destroyed // it shouldn't be able to go back to return !!(previousItem && !previousItem.shouldDestroy); @@ -74,7 +85,7 @@ export class ViewController { * @returns {Number} Returns the index of this page within its NavController. */ get index() { - return (this.navCtrl ? this.navCtrl.indexOf(this) : -1); + return (this._nav ? this._nav.indexOf(this) : -1); } /** diff --git a/ionic/components/overlay/overlay-controller.ts b/ionic/components/overlay/overlay-controller.ts index 084e9a78d6..c3a1aa8053 100644 --- a/ionic/components/overlay/overlay-controller.ts +++ b/ionic/components/overlay/overlay-controller.ts @@ -1,63 +1,31 @@ -import {Animation} from '../../animations/animation'; -import {extend} from '../../util'; +import {Injectable} from 'angular2/core'; + +import {ViewController} from '../nav/view-controller'; +import {Config} from '../../config/config'; +import {IonicApp} from '../app/app'; -/** - * @private - */ +@Injectable() export class OverlayController { - open(componentType, params = {}, opts = {}) { - if (!this.nav) { - console.error(' required in root template (app.html) to use: ' + opts.pageType); - return Promise.reject(); - } + constructor(private _config: Config) {} - let resolve, reject; - let promise = new Promise((res, rej) => { resolve = res; reject = rej; }); - - opts.animation = opts.enterAnimation; + push(overlayView, opts={}) { + overlayView.setNav(this._nav); opts.animateFirst = true; - this.nav.push(componentType, params, opts).then(viewCtrl => { - if (viewCtrl && viewCtrl.instance) { - - let self = this; - function escape(ev) { - if (ev.keyCode == 27 && self.nav.last() === viewCtrl) { - viewCtrl.instance.close(); - } - } - - viewCtrl.instance.close = (data, closeOpts={}) => { - extend(opts, closeOpts); - opts.animation = opts.leaveAnimation; - viewCtrl.instance.onClose && viewCtrl.instance.onClose(data); - this.nav.pop(opts); - document.removeEventListener('keyup', escape, true); - }; - - document.addEventListener('keyup', escape, true); - resolve(viewCtrl.instance); - - } else { - reject(); - } - }, rejectReason => { - console.error(rejectReason); + return new Promise(resolve => { + this._nav.pushView(overlayView, opts, resolve); }); - - return promise; } - getByType(overlayType) { - let overlay = this.nav.getByType(overlayType); - return overlay && overlay.instance; + pop(opts={}) { + opts.animateFirst = true; + return this._nav.pop(opts); } - getByHandle(handle, overlayType) { - let overlay = this.nav.getByHandle(handle); - return overlay && overlay.instance; + setNav(nav) { + this._nav = nav; } } diff --git a/ionic/components/overlay/overlay.ts b/ionic/components/overlay/overlay.ts index f21f461fd4..8213e4c8ec 100644 --- a/ionic/components/overlay/overlay.ts +++ b/ionic/components/overlay/overlay.ts @@ -35,7 +35,7 @@ export class OverlayNav extends NavController { } this.initZIndex = 1000; - overlayCtrl.nav = this; + overlayCtrl.setNav(this); } } diff --git a/ionic/config/config.ts b/ionic/config/config.ts index ff732ad01e..43eef4c5c8 100644 --- a/ionic/config/config.ts +++ b/ionic/config/config.ts @@ -186,6 +186,9 @@ export class Config { get(key) { if (!isDefined(this._c[key])) { + if (!isDefined(key)) { + throw 'config key is not defined'; + } // if the value was already set this will all be skipped // if there was no user config then it'll check each of diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 78b01a75a9..6524039bf8 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -11,6 +11,9 @@ Config.setModeConfig('ios', { actionSheetCancelIcon: '', actionSheetDestructiveIcon: '', + alertEnter: 'alert-pop-in', + alertLeave: 'alert-pop-out', + backButtonText: 'Back', backButtonIcon: 'ion-ios-arrow-back', @@ -24,9 +27,6 @@ Config.setModeConfig('ios', { pageTransition: 'ios-transition', pageTransitionDelay: 16, - popupEnter: 'popup-pop-in', - popupLeave: 'popup-pop-out', - tabbarPlacement: 'bottom', }); @@ -40,6 +40,9 @@ Config.setModeConfig('md', { actionSheetCancelIcon: 'ion-md-close', actionSheetDestructiveIcon: 'ion-md-trash', + alertEnter: 'alert-md-pop-in', + alertLeave: 'alert-md-pop-out', + backButtonText: '', backButtonIcon: 'ion-md-arrow-back', @@ -53,9 +56,6 @@ Config.setModeConfig('md', { pageTransition: 'md-transition', pageTransitionDelay: 120, - popupEnter: 'popup-md-pop-in', - popupLeave: 'popup-md-pop-out', - tabbarHighlight: true, tabbarPlacement: 'top', diff --git a/ionic/ionic.ts b/ionic/ionic.ts index 478c0f55ec..3164e39979 100644 --- a/ionic/ionic.ts +++ b/ionic/ionic.ts @@ -18,7 +18,6 @@ export * from './util/keyboard' export * from './animations/animation' - export * from './translation/translate' export * from './translation/translate_pipe'