mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +08:00
475 lines
13 KiB
TypeScript
475 lines
13 KiB
TypeScript
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, Watch } from '@stencil/core';
|
|
|
|
import { AlertButton, AlertInput, Animation, AnimationBuilder, Config, CssClassMap, Mode, OverlayEventDetail, OverlayInterface } from '../../interface';
|
|
import { BACKDROP, dismiss, eventMethod, isCancel, present } from '../../utils/overlays';
|
|
import { getClassMap } from '../../utils/theme';
|
|
|
|
import { iosEnterAnimation } from './animations/ios.enter';
|
|
import { iosLeaveAnimation } from './animations/ios.leave';
|
|
import { mdEnterAnimation } from './animations/md.enter';
|
|
import { mdLeaveAnimation } from './animations/md.leave';
|
|
|
|
@Component({
|
|
tag: 'ion-alert',
|
|
styleUrls: {
|
|
ios: 'alert.ios.scss',
|
|
md: 'alert.md.scss'
|
|
},
|
|
scoped: true
|
|
})
|
|
export class Alert implements OverlayInterface {
|
|
|
|
private activeId?: string;
|
|
private inputType?: string;
|
|
private processedInputs: AlertInput[] = [];
|
|
private processedButtons: AlertButton[] = [];
|
|
|
|
presented = false;
|
|
animation?: Animation;
|
|
|
|
@Element() el!: HTMLStencilElement;
|
|
|
|
@Prop({ connect: 'ion-animation-controller' }) animationCtrl!: HTMLIonAnimationControllerElement;
|
|
@Prop({ context: 'config' }) config!: Config;
|
|
@Prop() overlayIndex!: number;
|
|
|
|
/**
|
|
* The mode determines which platform styles to use.
|
|
* Possible values are: `"ios"` or `"md"`.
|
|
*/
|
|
@Prop() mode!: Mode;
|
|
|
|
/**
|
|
* If true, the keyboard will be automatically dismissed when the overlay is presented.
|
|
*/
|
|
@Prop() keyboardClose = true;
|
|
|
|
/**
|
|
* Animation to use when the alert is presented.
|
|
*/
|
|
@Prop() enterAnimation?: AnimationBuilder;
|
|
|
|
/**
|
|
* Animation to use when the alert is dismissed.
|
|
*/
|
|
@Prop() leaveAnimation?: AnimationBuilder;
|
|
|
|
/**
|
|
* Additional classes to apply for custom CSS. If multiple classes are
|
|
* provided they should be separated by spaces.
|
|
*/
|
|
@Prop() cssClass?: string | string[];
|
|
|
|
/**
|
|
* The main title in the heading of the alert.
|
|
*/
|
|
@Prop() header?: string;
|
|
|
|
/**
|
|
* The subtitle in the heading of the alert. Displayed under the title.
|
|
*/
|
|
@Prop() subHeader?: string;
|
|
|
|
/**
|
|
* The main message to be displayed in the alert.
|
|
*/
|
|
@Prop() message?: string;
|
|
|
|
/**
|
|
* Array of buttons to be added to the alert.
|
|
*/
|
|
@Prop() buttons: (AlertButton | string)[] = [];
|
|
|
|
/**
|
|
* Array of input to show in the alert.
|
|
*/
|
|
@Prop({ mutable: true }) inputs: AlertInput[] = [];
|
|
|
|
/**
|
|
* If true, the alert will be dismissed when the backdrop is clicked. Defaults to `true`.
|
|
*/
|
|
@Prop() backdropDismiss = true;
|
|
|
|
/**
|
|
* If true, the alert will be translucent. Defaults to `false`.
|
|
*/
|
|
@Prop() translucent = false;
|
|
|
|
/**
|
|
* If true, the alert will animate. Defaults to `true`.
|
|
*/
|
|
@Prop() animated = true;
|
|
|
|
/**
|
|
* Emitted after the alert has presented.
|
|
*/
|
|
@Event() ionAlertDidLoad!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the alert has presented.
|
|
*/
|
|
@Event() ionAlertDidUnload!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted after the alert has presented.
|
|
*/
|
|
@Event({ eventName: 'ionAlertDidPresent' }) didPresent!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the alert has presented.
|
|
*/
|
|
@Event({ eventName: 'ionAlertWillPresent' }) willPresent!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the alert has dismissed.
|
|
*/
|
|
@Event({ eventName: 'ionAlertWillDismiss' }) willDismiss!: EventEmitter<OverlayEventDetail>;
|
|
|
|
/**
|
|
* Emitted after the alert has dismissed.
|
|
*/
|
|
@Event({ eventName: 'ionAlertDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
|
|
|
@Watch('buttons')
|
|
buttonsChanged() {
|
|
const buttons = this.buttons;
|
|
this.processedButtons = buttons.map(btn => {
|
|
return (typeof btn === 'string')
|
|
? { text: btn, role: btn.toLowerCase() === 'cancel' ? 'cancel' : undefined }
|
|
: btn;
|
|
});
|
|
}
|
|
|
|
@Watch('inputs')
|
|
inputsChanged() {
|
|
const inputs = this.inputs;
|
|
|
|
// An alert can be created with several different inputs. Radios,
|
|
// checkboxes and inputs are all accepted, but they cannot be mixed.
|
|
const inputTypes = new Set(inputs.map(i => i.type));
|
|
if (inputTypes.has('checkbox') && inputTypes.has('radio')) {
|
|
console.warn(`Alert cannot mix input types: ${(Array.from(inputTypes.values()).join('/'))}. Please see alert docs for more info.`);
|
|
}
|
|
this.inputType = inputTypes.values().next().value;
|
|
this.processedInputs = inputs.map((i, index) => ({
|
|
type: i.type || 'text',
|
|
name: i.name || `${index}`,
|
|
placeholder: i.placeholder || '',
|
|
value: i.value || '',
|
|
label: i.label,
|
|
checked: !!i.checked,
|
|
disabled: !!i.disabled,
|
|
id: i.id || `alert-input-${this.overlayIndex}-${index}`,
|
|
handler: i.handler,
|
|
min: i.min,
|
|
max: i.max
|
|
}) as AlertInput);
|
|
}
|
|
|
|
componentWillLoad() {
|
|
this.inputsChanged();
|
|
this.buttonsChanged();
|
|
}
|
|
|
|
componentDidLoad() {
|
|
this.ionAlertDidLoad.emit();
|
|
}
|
|
|
|
componentDidUnload() {
|
|
this.ionAlertDidUnload.emit();
|
|
}
|
|
|
|
@Listen('ionBackdropTap')
|
|
protected onBackdropTap() {
|
|
return this.dismiss(undefined, BACKDROP);
|
|
}
|
|
|
|
@Listen('ionAlertWillDismiss')
|
|
protected dispatchCancelHandler(ev: CustomEvent) {
|
|
const role = ev.detail.role;
|
|
if (isCancel(role)) {
|
|
const cancelButton = this.processedButtons.find(b => b.role === 'cancel');
|
|
this.callButtonHandler(cancelButton);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Present the alert overlay after it has been created.
|
|
*/
|
|
@Method()
|
|
present(): Promise<void> {
|
|
return present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation);
|
|
}
|
|
|
|
/**
|
|
* Dismiss the alert overlay after it has been presented.
|
|
*/
|
|
@Method()
|
|
dismiss(data?: any, role?: string): Promise<boolean> {
|
|
return dismiss(this, data, role, 'alertLeave', iosLeaveAnimation, mdLeaveAnimation);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the alert did dismiss.
|
|
*
|
|
*/
|
|
@Method()
|
|
onDidDismiss(): Promise<OverlayEventDetail> {
|
|
return eventMethod(this.el, 'ionAlertDidDismiss');
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that resolves when the alert will dismiss.
|
|
*
|
|
*/
|
|
@Method()
|
|
onWillDismiss(): Promise<OverlayEventDetail> {
|
|
return eventMethod(this.el, 'ionAlertWillDismiss');
|
|
}
|
|
|
|
private rbClick(selectedInput: AlertInput) {
|
|
for (const input of this.processedInputs) {
|
|
input.checked = input === selectedInput;
|
|
}
|
|
this.activeId = selectedInput.id;
|
|
if (selectedInput.handler) {
|
|
selectedInput.handler(selectedInput);
|
|
}
|
|
this.el.forceUpdate();
|
|
}
|
|
|
|
private cbClick(selectedInput: AlertInput) {
|
|
selectedInput.checked = !selectedInput.checked;
|
|
if (selectedInput.handler) {
|
|
selectedInput.handler(selectedInput);
|
|
}
|
|
this.el.forceUpdate();
|
|
}
|
|
|
|
private buttonClick(button: AlertButton) {
|
|
const role = button.role;
|
|
const values = this.getValues();
|
|
if (isCancel(role)) {
|
|
return this.dismiss({ values }, role);
|
|
}
|
|
const returnData = this.callButtonHandler(button, values);
|
|
if (returnData !== false) {
|
|
return this.dismiss({ values, ...returnData }, button.role);
|
|
}
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
private callButtonHandler(button: AlertButton | undefined, data?: any) {
|
|
if (button && button.handler) {
|
|
// a handler has been provided, execute it
|
|
// pass the handler the values from the inputs
|
|
const returnData = button.handler(data);
|
|
if (returnData === false) {
|
|
// if the return value of the handler is false then do not dismiss
|
|
return false;
|
|
}
|
|
if (typeof returnData === 'object') {
|
|
return returnData;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
private getValues(): any {
|
|
if (this.processedInputs.length === 0) {
|
|
// this is an alert without any options/inputs at all
|
|
return undefined;
|
|
}
|
|
|
|
if (this.inputType === 'radio') {
|
|
// this is an alert with radio buttons (single value select)
|
|
// return the one value which is checked, otherwise undefined
|
|
const checkedInput = this.processedInputs.find(i => !!i.checked);
|
|
return checkedInput ? checkedInput.value : undefined;
|
|
}
|
|
|
|
if (this.inputType === 'checkbox') {
|
|
// this is an alert with checkboxes (multiple value select)
|
|
// return an array of all the checked values
|
|
return this.processedInputs.filter(i => i.checked).map(i => i.value);
|
|
}
|
|
|
|
// this is an alert with text inputs
|
|
// return an object of all the values with the input name as the key
|
|
const values: {[k: string]: string} = {};
|
|
this.processedInputs.forEach(i => {
|
|
values[i.name!] = i.value || '';
|
|
});
|
|
return values;
|
|
}
|
|
|
|
private renderAlertInputs(labelledBy: string | undefined) {
|
|
switch (this.inputType) {
|
|
case 'checkbox': return this.renderCheckbox(labelledBy);
|
|
case 'radio': return this.renderRadio(labelledBy);
|
|
default: return this.renderInput(labelledBy);
|
|
}
|
|
}
|
|
|
|
private renderCheckbox(labelledby: string | undefined) {
|
|
const inputs = this.processedInputs;
|
|
if (inputs.length === 0) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div class="alert-checkbox-group" aria-labelledby={labelledby}>
|
|
{ inputs.map(i => (
|
|
<button
|
|
type="button"
|
|
onClick={() => this.cbClick(i)}
|
|
aria-checked={i.checked ? 'true' : null}
|
|
id={i.id}
|
|
disabled={i.disabled}
|
|
tabIndex={0}
|
|
role="checkbox"
|
|
class="alert-tappable alert-checkbox alert-checkbox-button"
|
|
>
|
|
<div class="alert-button-inner">
|
|
<div class="alert-checkbox-icon">
|
|
<div class="alert-checkbox-inner"></div>
|
|
</div>
|
|
<div class="alert-checkbox-label">
|
|
{i.label}
|
|
</div>
|
|
</div>
|
|
{this.mode === 'md' && <ion-ripple-effect />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
private renderRadio(labelledby: string | undefined) {
|
|
const inputs = this.processedInputs;
|
|
if (inputs.length === 0) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div class="alert-radio-group" role="radiogroup" aria-labelledby={labelledby} aria-activedescendant={this.activeId}>
|
|
{ inputs.map(i => (
|
|
<button
|
|
type="button"
|
|
onClick={() => this.rbClick(i)}
|
|
aria-checked={i.checked ? 'true' : null}
|
|
disabled={i.disabled}
|
|
id={i.id}
|
|
tabIndex={0}
|
|
class="alert-radio-button alert-tappable alert-radio"
|
|
role="radio"
|
|
>
|
|
<div class="alert-button-inner">
|
|
<div class="alert-radio-icon"><div class="alert-radio-inner"></div></div>
|
|
<div class="alert-radio-label">
|
|
{i.label}
|
|
</div>
|
|
</div>
|
|
{this.mode === 'md' && <ion-ripple-effect />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
private renderInput(labelledby: string | undefined) {
|
|
const inputs = this.processedInputs;
|
|
if (inputs.length === 0) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div class="alert-input-group" aria-labelledby={labelledby}>
|
|
{ inputs.map(i => (
|
|
<div class="alert-input-wrapper">
|
|
<input
|
|
placeholder={i.placeholder}
|
|
value={i.value}
|
|
type={i.type}
|
|
min={i.min}
|
|
max={i.max}
|
|
onInput={e => i.value = (e.target as any).value}
|
|
id={i.id}
|
|
disabled={i.disabled}
|
|
tabIndex={0}
|
|
class="alert-input"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
hostData() {
|
|
return {
|
|
role: 'alertdialog',
|
|
style: {
|
|
zIndex: 20000 + this.overlayIndex,
|
|
},
|
|
class: {
|
|
...getClassMap(this.cssClass),
|
|
'alert-translucent': this.translucent
|
|
}
|
|
};
|
|
}
|
|
|
|
private renderAlertButtons() {
|
|
const buttons = this.processedButtons;
|
|
const alertButtonGroupClass = {
|
|
'alert-button-group': true,
|
|
'alert-button-group-vertical': buttons.length > 2
|
|
};
|
|
return (
|
|
<div class={alertButtonGroupClass}>
|
|
{buttons.map(button =>
|
|
<button type="button" ion-activable class={buttonClass(button)} tabIndex={0} onClick={() => this.buttonClick(button)}>
|
|
<span class="alert-button-inner">
|
|
{button.text}
|
|
</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const hdrId = `alert-${this.overlayIndex}-hdr`;
|
|
const subHdrId = `alert-${this.overlayIndex}-sub-hdr`;
|
|
const msgId = `alert-${this.overlayIndex}-msg`;
|
|
|
|
let labelledById: string | undefined;
|
|
if (this.header !== undefined) {
|
|
labelledById = hdrId;
|
|
} else if (this.subHeader !== undefined) {
|
|
labelledById = subHdrId;
|
|
}
|
|
|
|
return [
|
|
<ion-backdrop tappable={this.backdropDismiss}/>,
|
|
|
|
<div class="alert-wrapper">
|
|
|
|
<div class="alert-head">
|
|
{this.header && <h2 id={hdrId} class="alert-title">{this.header}</h2>}
|
|
{this.subHeader && <h2 id={subHdrId} class="alert-sub-title">{this.subHeader}</h2>}
|
|
</div>
|
|
|
|
<div id={msgId} class="alert-message" innerHTML={this.message}></div>
|
|
|
|
{this.renderAlertInputs(labelledById)}
|
|
{this.renderAlertButtons()}
|
|
|
|
</div>
|
|
];
|
|
}
|
|
}
|
|
|
|
function buttonClass(button: AlertButton): CssClassMap {
|
|
return {
|
|
'alert-button': true,
|
|
...getClassMap(button.cssClass)
|
|
};
|
|
}
|