mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 05:58:26 +08:00
refactor(overlays): inject overlay providers
BREAKING CHANGES: - Overlay components, such as Alert or Modals, should now be created using its injected provider. - Overlays now have the `present()` method on the overlay’s instance, rather than using `nav.present(overlayInstance)`. - All overlays now present on top of all app content, to include menus. - Below is an example of the change to `Alert`, but the pattern is the same for all overlays: ActionSheet, Loading, Modal, Picker, Popover, Toast WAS: ``` import { NavController, Alert } from ‘ionic-angular’; constructor(private nav: NavController) { } doAlert() { let alert = Alert.create({ title: 'Alert', }); this.nav.present(alert); } ``` NOW: ``` import { AlertController } from ‘ionic-angular’; constructor(private alertCtrl: AlertController) { } doAlert() { let alert = this.alertCtrl.create({ title: 'Alert' }); alert.present(); } ```
This commit is contained in:
@ -1,21 +1,67 @@
|
||||
import { Component, ComponentResolver, ElementRef, HostListener, Renderer, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { addSelector } from '../../config/bootstrap';
|
||||
import { Animation } from '../../animations/animation';
|
||||
import { Config } from '../../config/config';
|
||||
import { CSS, nativeRaf } from '../../util/dom';
|
||||
import { isPresent, pascalCaseToDashCase } from '../../util/util';
|
||||
import { Key } from '../../util/key';
|
||||
import { NavParams } from '../nav/nav-params';
|
||||
import { PageTransition } from '../../transitions/page-transition';
|
||||
import { TransitionOptions } from '../../transitions/transition';
|
||||
import { App } from '../app/app';
|
||||
import { isPresent } from '../../util/util';
|
||||
import { NavOptions } from '../nav/nav-options';
|
||||
import { PopoverCmp } from './popover-component';
|
||||
import { PopoverOptions } from './popover-options';
|
||||
import { ViewController } from '../nav/view-controller';
|
||||
|
||||
const POPOVER_IOS_BODY_PADDING = 2;
|
||||
const POPOVER_MD_BODY_PADDING = 12;
|
||||
|
||||
/**
|
||||
* @name Popover
|
||||
* @private
|
||||
*/
|
||||
export class Popover extends ViewController {
|
||||
private _app: App;
|
||||
|
||||
constructor(app: App, componentType: any, data: any = {}, opts: PopoverOptions = {}) {
|
||||
opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true;
|
||||
opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true;
|
||||
|
||||
data.componentType = componentType;
|
||||
data.opts = opts;
|
||||
super(PopoverCmp, data);
|
||||
this._app = app;
|
||||
this.isOverlay = true;
|
||||
|
||||
// by default, popovers should not fire lifecycle events of other views
|
||||
// for example, when a popover enters, the current active view should
|
||||
// not fire its lifecycle events because it's not conceptually leaving
|
||||
this.fireOtherLifecycles = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getTransitionName(direction: string) {
|
||||
let key = (direction === 'back' ? 'popoverLeave' : 'popoverEnter');
|
||||
return this._nav && this._nav.config.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the popover instance.
|
||||
*
|
||||
* @param {NavOptions} [opts={}] Nav options to go with this transition.
|
||||
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
|
||||
*/
|
||||
present(navOptions: NavOptions = {}) {
|
||||
return this._app.present(this, navOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DEPRECATED: Please inject PopoverController instead
|
||||
*/
|
||||
static create(componentType: any, data = {}, opts: PopoverOptions = {}) {
|
||||
// deprecated warning: added beta.11 2016-06-27
|
||||
console.warn('Popover.create(..) has been deprecated. Please inject PopoverController instead');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @name PopoverController
|
||||
* @description
|
||||
* A Popover is a dialog that appears on top of the current page.
|
||||
* It can be used for anything, but generally it is used for overflow
|
||||
@ -65,11 +111,11 @@ const POPOVER_MD_BODY_PADDING = 12;
|
||||
* ```ts
|
||||
* @Component({})
|
||||
* class MyPage {
|
||||
* constructor(private nav: NavController) {}
|
||||
* constructor(private popoverCtrl: PopoverController) {}
|
||||
*
|
||||
* presentPopover(myEvent) {
|
||||
* let popover = Popover.create(PopoverPage);
|
||||
* this.nav.present(popover, {
|
||||
* let popover = this.popoverCtrl.create(PopoverPage);
|
||||
* popover.present({
|
||||
* ev: myEvent
|
||||
* });
|
||||
* }
|
||||
@ -104,388 +150,27 @@ const POPOVER_MD_BODY_PADDING = 12;
|
||||
*
|
||||
* @demo /docs/v2/demos/popover/
|
||||
*/
|
||||
export class Popover extends ViewController {
|
||||
@Injectable()
|
||||
export class PopoverController {
|
||||
|
||||
constructor(componentType: any, data: any = {}, opts: PopoverOptions = {}) {
|
||||
opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true;
|
||||
opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true;
|
||||
|
||||
data.componentType = componentType;
|
||||
data.opts = opts;
|
||||
super(PopoverCmp, data);
|
||||
this.isOverlay = true;
|
||||
|
||||
// by default, popovers should not fire lifecycle events of other views
|
||||
// for example, when a popover enters, the current active view should
|
||||
// not fire its lifecycle events because it's not conceptually leaving
|
||||
this.fireOtherLifecycles = false;
|
||||
}
|
||||
constructor(private _app: App) {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Create a popover with the following options
|
||||
*
|
||||
* | Option | Type | Description |
|
||||
* |-----------------------|------------|------------------------------------------------------------------------------------------------------------------|
|
||||
* | cssClass |`string` | An additional class for custom styles. |
|
||||
* | showBackdrop |`boolean` | Whether to show the backdrop. Default true. |
|
||||
* | enableBackdropDismiss |`boolean` | Whether the popover should be dismissed by tapping the backdrop. Default true. |
|
||||
*
|
||||
*
|
||||
* @param {object} componentType The Popover
|
||||
* @param {object} data Any data to pass to the Popover view
|
||||
* @param {PopoverOptions} opts Popover options
|
||||
*/
|
||||
getTransitionName(direction: string) {
|
||||
let key = (direction === 'back' ? 'popoverLeave' : 'popoverEnter');
|
||||
return this._nav && this._nav.config.get(key);
|
||||
create(componentType: any, data = {}, opts: PopoverOptions = {}): Popover {
|
||||
return new Popover(this._app, componentType, data, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a popover with the following options
|
||||
*
|
||||
* | Option | Type | Description |
|
||||
* |-----------------------|------------|------------------------------------------------------------------------------------------------------------------|
|
||||
* | cssClass |`string` | An additional class for custom styles. |
|
||||
* | showBackdrop |`boolean` | Whether to show the backdrop. Default true. |
|
||||
* | enableBackdropDismiss |`boolean` | Whether the popover should be dismissed by tapping the backdrop. Default true. |
|
||||
*
|
||||
*
|
||||
* @param {object} componentType The Popover
|
||||
* @param {object} data Any data to pass to the Popover view
|
||||
* @param {object} opts Popover options
|
||||
*/
|
||||
static create(componentType: any, data = {}, opts: PopoverOptions = {}) {
|
||||
return new Popover(componentType, data, opts);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ion-popover',
|
||||
template:
|
||||
'<ion-backdrop (click)="bdClick($event)" [class.hide-backdrop]="!d.showBackdrop"></ion-backdrop>' +
|
||||
'<div class="popover-wrapper">' +
|
||||
'<div class="popover-arrow"></div>' +
|
||||
'<div class="popover-content">' +
|
||||
'<div class="popover-viewport">' +
|
||||
'<div #viewport nav-viewport></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
})
|
||||
class PopoverCmp {
|
||||
@ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef;
|
||||
|
||||
private d: any;
|
||||
private enabled: boolean;
|
||||
private id: number;
|
||||
private showSpinner: boolean;
|
||||
|
||||
constructor(
|
||||
private _compiler: ComponentResolver,
|
||||
private _elementRef: ElementRef,
|
||||
private _renderer: Renderer,
|
||||
private _config: Config,
|
||||
private _navParams: NavParams,
|
||||
private _viewCtrl: ViewController
|
||||
) {
|
||||
this.d = _navParams.data.opts;
|
||||
|
||||
if (this.d.cssClass) {
|
||||
_renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true);
|
||||
}
|
||||
|
||||
this.id = (++popoverIds);
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
addSelector(this._navParams.data.componentType, 'ion-popover-inner');
|
||||
|
||||
this._compiler.resolveComponent(this._navParams.data.componentType).then((componentFactory) => {
|
||||
let componentRef = this.viewport.createComponent(componentFactory, this.viewport.length, this.viewport.parentInjector);
|
||||
|
||||
this._viewCtrl.setInstance(componentRef.instance);
|
||||
|
||||
// manually fire ionViewWillEnter() since PopoverCmp's ionViewWillEnter already happened
|
||||
this._viewCtrl.fireWillEnter();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
let activeElement: any = document.activeElement;
|
||||
if (document.activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
dismiss(role: any): Promise<any> {
|
||||
return this._viewCtrl.dismiss(null, role);
|
||||
}
|
||||
|
||||
bdTouch(ev: UIEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
bdClick() {
|
||||
if (this.enabled && this.d.enableBackdropDismiss) {
|
||||
this.dismiss('backdrop');
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('body:keyup', ['$event'])
|
||||
private _keyUp(ev: KeyboardEvent) {
|
||||
if (this.enabled && ev.keyCode === Key.ESCAPE && this._viewCtrl.isLast()) {
|
||||
this.bdClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PopoverOptions {
|
||||
cssClass?: string;
|
||||
showBackdrop?: boolean;
|
||||
enableBackdropDismiss?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animations for popover
|
||||
*/
|
||||
class PopoverTransition extends PageTransition {
|
||||
constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) {
|
||||
super(enteringView, leavingView, opts);
|
||||
}
|
||||
|
||||
mdPositionView(nativeEle: HTMLElement, ev: any) {
|
||||
let originY = 'top';
|
||||
let originX = 'left';
|
||||
|
||||
let popoverWrapperEle = <HTMLElement>nativeEle.querySelector('.popover-wrapper');
|
||||
|
||||
// Popover content width and height
|
||||
let popoverEle = <HTMLElement>nativeEle.querySelector('.popover-content');
|
||||
let popoverDim = popoverEle.getBoundingClientRect();
|
||||
let popoverWidth = popoverDim.width;
|
||||
let popoverHeight = popoverDim.height;
|
||||
|
||||
// Window body width and height
|
||||
let bodyWidth = window.innerWidth;
|
||||
let bodyHeight = window.innerHeight;
|
||||
|
||||
// If ev was passed, use that for target element
|
||||
let targetDim = ev && ev.target && ev.target.getBoundingClientRect();
|
||||
|
||||
let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2);
|
||||
let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2) - (popoverWidth / 2);
|
||||
|
||||
let targetWidth = targetDim && targetDim.width || 0;
|
||||
let targetHeight = targetDim && targetDim.height || 0;
|
||||
|
||||
let popoverCSS = {
|
||||
top: targetTop,
|
||||
left: targetLeft
|
||||
};
|
||||
|
||||
// If the popover left is less than the padding it is off screen
|
||||
// to the left so adjust it, else if the width of the popover
|
||||
// exceeds the body width it is off screen to the right so adjust
|
||||
if (popoverCSS.left < POPOVER_MD_BODY_PADDING) {
|
||||
popoverCSS.left = POPOVER_MD_BODY_PADDING;
|
||||
} else if (popoverWidth + POPOVER_MD_BODY_PADDING + popoverCSS.left > bodyWidth) {
|
||||
popoverCSS.left = bodyWidth - popoverWidth - POPOVER_MD_BODY_PADDING;
|
||||
originX = 'right';
|
||||
}
|
||||
|
||||
// If the popover when popped down stretches past bottom of screen,
|
||||
// make it pop up if there's room above
|
||||
if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) {
|
||||
popoverCSS.top = targetTop - popoverHeight;
|
||||
nativeEle.className = nativeEle.className + ' popover-bottom';
|
||||
originY = 'bottom';
|
||||
// If there isn't room for it to pop up above the target cut it off
|
||||
} else if (targetTop + targetHeight + popoverHeight > bodyHeight) {
|
||||
popoverEle.style.bottom = POPOVER_MD_BODY_PADDING + 'px';
|
||||
}
|
||||
|
||||
popoverEle.style.top = popoverCSS.top + 'px';
|
||||
popoverEle.style.left = popoverCSS.left + 'px';
|
||||
|
||||
popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX;
|
||||
|
||||
// Since the transition starts before styling is done we
|
||||
// want to wait for the styles to apply before showing the wrapper
|
||||
popoverWrapperEle.style.opacity = '1';
|
||||
}
|
||||
|
||||
iosPositionView(nativeEle: HTMLElement, ev: any) {
|
||||
let originY = 'top';
|
||||
let originX = 'left';
|
||||
|
||||
let popoverWrapperEle = <HTMLElement>nativeEle.querySelector('.popover-wrapper');
|
||||
|
||||
// Popover content width and height
|
||||
let popoverEle = <HTMLElement>nativeEle.querySelector('.popover-content');
|
||||
let popoverDim = popoverEle.getBoundingClientRect();
|
||||
let popoverWidth = popoverDim.width;
|
||||
let popoverHeight = popoverDim.height;
|
||||
|
||||
// Window body width and height
|
||||
let bodyWidth = window.innerWidth;
|
||||
let bodyHeight = window.innerHeight;
|
||||
|
||||
// If ev was passed, use that for target element
|
||||
let targetDim = ev && ev.target && ev.target.getBoundingClientRect();
|
||||
|
||||
let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2);
|
||||
let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2);
|
||||
let targetWidth = targetDim && targetDim.width || 0;
|
||||
let targetHeight = targetDim && targetDim.height || 0;
|
||||
|
||||
// The arrow that shows above the popover on iOS
|
||||
var arrowEle = <HTMLElement>nativeEle.querySelector('.popover-arrow');
|
||||
let arrowDim = arrowEle.getBoundingClientRect();
|
||||
var arrowWidth = arrowDim.width;
|
||||
var arrowHeight = arrowDim.height;
|
||||
|
||||
// If no ev was passed, hide the arrow
|
||||
if (!targetDim) {
|
||||
arrowEle.style.display = 'none';
|
||||
}
|
||||
|
||||
let arrowCSS = {
|
||||
top: targetTop + targetHeight,
|
||||
left: targetLeft + (targetWidth / 2) - (arrowWidth / 2)
|
||||
};
|
||||
|
||||
let popoverCSS = {
|
||||
top: targetTop + targetHeight + (arrowHeight - 1),
|
||||
left: targetLeft + (targetWidth / 2) - (popoverWidth / 2)
|
||||
};
|
||||
|
||||
// If the popover left is less than the padding it is off screen
|
||||
// to the left so adjust it, else if the width of the popover
|
||||
// exceeds the body width it is off screen to the right so adjust
|
||||
if (popoverCSS.left < POPOVER_IOS_BODY_PADDING) {
|
||||
popoverCSS.left = POPOVER_IOS_BODY_PADDING;
|
||||
} else if (popoverWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left > bodyWidth) {
|
||||
popoverCSS.left = bodyWidth - popoverWidth - POPOVER_IOS_BODY_PADDING;
|
||||
originX = 'right';
|
||||
}
|
||||
|
||||
// If the popover when popped down stretches past bottom of screen,
|
||||
// make it pop up if there's room above
|
||||
if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) {
|
||||
arrowCSS.top = targetTop - (arrowHeight + 1);
|
||||
popoverCSS.top = targetTop - popoverHeight - (arrowHeight - 1);
|
||||
nativeEle.className = nativeEle.className + ' popover-bottom';
|
||||
originY = 'bottom';
|
||||
// If there isn't room for it to pop up above the target cut it off
|
||||
} else if (targetTop + targetHeight + popoverHeight > bodyHeight) {
|
||||
popoverEle.style.bottom = POPOVER_IOS_BODY_PADDING + '%';
|
||||
}
|
||||
|
||||
arrowEle.style.top = arrowCSS.top + 'px';
|
||||
arrowEle.style.left = arrowCSS.left + 'px';
|
||||
|
||||
popoverEle.style.top = popoverCSS.top + 'px';
|
||||
popoverEle.style.left = popoverCSS.left + 'px';
|
||||
|
||||
popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX;
|
||||
|
||||
// Since the transition starts before styling is done we
|
||||
// want to wait for the styles to apply before showing the wrapper
|
||||
popoverWrapperEle.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverPopIn extends PopoverTransition {
|
||||
constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) {
|
||||
super(enteringView, leavingView, opts);
|
||||
|
||||
let ele = enteringView.pageRef().nativeElement;
|
||||
|
||||
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
|
||||
let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
|
||||
|
||||
wrapper.fromTo('opacity', 0.01, 1);
|
||||
backdrop.fromTo('opacity', 0.01, 0.08);
|
||||
|
||||
this
|
||||
.easing('ease')
|
||||
.duration(100)
|
||||
.add(backdrop)
|
||||
.add(wrapper);
|
||||
}
|
||||
|
||||
play() {
|
||||
nativeRaf(() => {
|
||||
this.iosPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev);
|
||||
super.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
PageTransition.register('popover-pop-in', PopoverPopIn);
|
||||
|
||||
|
||||
class PopoverPopOut extends PopoverTransition {
|
||||
constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) {
|
||||
super(enteringView, leavingView, opts);
|
||||
|
||||
let ele = leavingView.pageRef().nativeElement;
|
||||
let backdrop = new Animation(ele.querySelector('ion-backdrop'));
|
||||
let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
|
||||
|
||||
wrapper.fromTo('opacity', 0.99, 0);
|
||||
backdrop.fromTo('opacity', 0.08, 0);
|
||||
|
||||
this
|
||||
.easing('ease')
|
||||
.duration(500)
|
||||
.add(backdrop)
|
||||
.add(wrapper);
|
||||
}
|
||||
}
|
||||
PageTransition.register('popover-pop-out', PopoverPopOut);
|
||||
|
||||
|
||||
class PopoverMdPopIn extends PopoverTransition {
|
||||
constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) {
|
||||
super(enteringView, leavingView, opts);
|
||||
|
||||
let ele = enteringView.pageRef().nativeElement;
|
||||
|
||||
let content = new Animation(ele.querySelector('.popover-content'));
|
||||
let viewport = new Animation(ele.querySelector('.popover-viewport'));
|
||||
|
||||
content.fromTo('scale', 0.001, 1);
|
||||
viewport.fromTo('opacity', 0.01, 1);
|
||||
|
||||
this
|
||||
.easing('cubic-bezier(0.36,0.66,0.04,1)')
|
||||
.duration(300)
|
||||
.add(content)
|
||||
.add(viewport);
|
||||
}
|
||||
|
||||
play() {
|
||||
nativeRaf(() => {
|
||||
this.mdPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev);
|
||||
super.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
PageTransition.register('popover-md-pop-in', PopoverMdPopIn);
|
||||
|
||||
|
||||
class PopoverMdPopOut extends PopoverTransition {
|
||||
constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) {
|
||||
super(enteringView, leavingView, opts);
|
||||
|
||||
let ele = leavingView.pageRef().nativeElement;
|
||||
let wrapper = new Animation(ele.querySelector('.popover-wrapper'));
|
||||
|
||||
wrapper.fromTo('opacity', 0.99, 0);
|
||||
|
||||
this
|
||||
.easing('ease')
|
||||
.duration(500)
|
||||
.fromTo('opacity', 0.01, 1)
|
||||
.add(wrapper);
|
||||
}
|
||||
}
|
||||
PageTransition.register('popover-md-pop-out', PopoverMdPopOut);
|
||||
|
||||
|
||||
let popoverIds = -1;
|
||||
|
Reference in New Issue
Block a user