import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core'; import { raf } from '@utils/helpers'; import { createLockController } from '@utils/lock-controller'; import { printIonWarning } from '@utils/logging'; import { createDelegateController, createTriggerController, BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall, setOverlayId, } from '@utils/overlays'; import { getClassMap } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; import type { AnimationBuilder, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface'; import type { OverlayEventDetail } from '../../utils/overlays-interface'; import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; import type { PickerButton, PickerColumn } from './picker-interface'; // TODO(FW-2832): types /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. */ @Component({ tag: 'ion-picker-legacy', styleUrls: { ios: 'picker.ios.scss', md: 'picker.md.scss', }, scoped: true, }) export class Picker implements ComponentInterface, OverlayInterface { private readonly delegateController = createDelegateController(this); private readonly lockController = createLockController(); private readonly triggerController = createTriggerController(); private durationTimeout?: ReturnType; lastFocus?: HTMLElement; @Element() el!: HTMLIonPickerLegacyElement; @State() presented = false; /** @internal */ @Prop() overlayIndex!: number; /** @internal */ @Prop() delegate?: FrameworkDelegate; /** @internal */ @Prop() hasController = false; /** * If `true`, the keyboard will be automatically dismissed when the overlay is presented. */ @Prop() keyboardClose = true; /** * Animation to use when the picker is presented. */ @Prop() enterAnimation?: AnimationBuilder; /** * Animation to use when the picker is dismissed. */ @Prop() leaveAnimation?: AnimationBuilder; /** * Array of buttons to be displayed at the top of the picker. */ @Prop() buttons: PickerButton[] = []; /** * Array of columns to be displayed in the picker. */ @Prop() columns: PickerColumn[] = []; /** * Additional classes to apply for custom CSS. If multiple classes are * provided they should be separated by spaces. */ @Prop() cssClass?: string | string[]; /** * Number of milliseconds to wait before dismissing the picker. */ @Prop() duration = 0; /** * If `true`, a backdrop will be displayed behind the picker. */ @Prop() showBackdrop = true; /** * If `true`, the picker will be dismissed when the backdrop is clicked. */ @Prop() backdropDismiss = true; /** * If `true`, the picker will animate. */ @Prop() animated = true; /** * Additional attributes to pass to the picker. */ @Prop() htmlAttributes?: { [key: string]: any }; /** * If `true`, the picker will open. If `false`, the picker will close. * Use this if you need finer grained control over presentation, otherwise * just use the pickerController or the `trigger` property. * Note: `isOpen` will not automatically be set back to `false` when * the picker dismisses. You will need to do that in your code. */ @Prop() isOpen = false; @Watch('isOpen') onIsOpenChange(newValue: boolean, oldValue: boolean) { if (newValue === true && oldValue === false) { this.present(); } else if (newValue === false && oldValue === true) { this.dismiss(); } } /** * An ID corresponding to the trigger element that * causes the picker to open when clicked. */ @Prop() trigger: string | undefined; @Watch('trigger') triggerChanged() { const { trigger, el, triggerController } = this; if (trigger) { triggerController.addClickListener(el, trigger); } } /** * Emitted after the picker has presented. */ @Event({ eventName: 'ionPickerDidPresent' }) didPresent!: EventEmitter; /** * Emitted before the picker has presented. */ @Event({ eventName: 'ionPickerWillPresent' }) willPresent!: EventEmitter; /** * Emitted before the picker has dismissed. */ @Event({ eventName: 'ionPickerWillDismiss' }) willDismiss!: EventEmitter; /** * Emitted after the picker has dismissed. */ @Event({ eventName: 'ionPickerDidDismiss' }) didDismiss!: EventEmitter; /** * Emitted after the picker has presented. * Shorthand for ionPickerWillDismiss. */ @Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter; /** * Emitted before the picker has presented. * Shorthand for ionPickerWillPresent. */ @Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter; /** * Emitted before the picker has dismissed. * Shorthand for ionPickerWillDismiss. */ @Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter; /** * Emitted after the picker has dismissed. * Shorthand for ionPickerDidDismiss. */ @Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter; connectedCallback() { prepareOverlay(this.el); this.triggerChanged(); } disconnectedCallback() { this.triggerController.removeClickListener(); } componentWillLoad() { if (!this.htmlAttributes?.id) { setOverlayId(this.el); } } componentDidLoad() { printIonWarning( '[ion-picker-legacy] - ion-picker-legacy and ion-picker-legacy-column have been deprecated in favor of new versions of the ion-picker and ion-picker-column components. These new components display inline with your page content allowing for more presentation flexibility than before.', this.el ); /** * If picker was rendered with isOpen="true" * then we should open picker immediately. */ if (this.isOpen === true) { raf(() => this.present()); } /** * When binding values in frameworks such as Angular * it is possible for the value to be set after the Web Component * initializes but before the value watcher is set up in Stencil. * As a result, the watcher callback may not be fired. * We work around this by manually calling the watcher * callback when the component has loaded and the watcher * is configured. */ this.triggerChanged(); } /** * Present the picker overlay after it has been created. */ @Method() async present(): Promise { const unlock = await this.lockController.lock(); await this.delegateController.attachViewToDom(); await present(this, 'pickerEnter', iosEnterAnimation, iosEnterAnimation, undefined); if (this.duration > 0) { this.durationTimeout = setTimeout(() => this.dismiss(), this.duration); } unlock(); } /** * Dismiss the picker overlay after it has been presented. * * @param data Any data to emit in the dismiss events. * @param role The role of the element that is dismissing the picker. * This can be useful in a button handler for determining which button was * clicked to dismiss the picker. * Some examples include: ``"cancel"`, `"destructive"`, "selected"`, and `"backdrop"`. */ @Method() async dismiss(data?: any, role?: string): Promise { const unlock = await this.lockController.lock(); if (this.durationTimeout) { clearTimeout(this.durationTimeout); } const dismissed = await dismiss(this, data, role, 'pickerLeave', iosLeaveAnimation, iosLeaveAnimation); if (dismissed) { this.delegateController.removeViewFromDom(); } unlock(); return dismissed; } /** * Returns a promise that resolves when the picker did dismiss. */ @Method() onDidDismiss(): Promise> { return eventMethod(this.el, 'ionPickerDidDismiss'); } /** * Returns a promise that resolves when the picker will dismiss. */ @Method() onWillDismiss(): Promise> { return eventMethod(this.el, 'ionPickerWillDismiss'); } /** * Get the column that matches the specified name. * * @param name The name of the column. */ @Method() getColumn(name: string): Promise { return Promise.resolve(this.columns.find((column) => column.name === name)); } private async buttonClick(button: PickerButton) { const role = button.role; if (isCancel(role)) { return this.dismiss(undefined, role); } const shouldDismiss = await this.callButtonHandler(button); if (shouldDismiss) { return this.dismiss(this.getSelected(), button.role); } return Promise.resolve(); } private async callButtonHandler(button: PickerButton | undefined) { if (button) { // a handler has been provided, execute it // pass the handler the values from the inputs const rtn = await safeCall(button.handler, this.getSelected()); if (rtn === false) { // if the return value of the handler is false then do not dismiss return false; } } return true; } private getSelected() { const selected: { [k: string]: any } = {}; this.columns.forEach((col, index) => { const selectedColumn = col.selectedIndex !== undefined ? col.options[col.selectedIndex] : undefined; selected[col.name] = { text: selectedColumn ? selectedColumn.text : undefined, value: selectedColumn ? selectedColumn.value : undefined, columnIndex: index, }; }); return selected; } private onBackdropTap = () => { this.dismiss(undefined, BACKDROP); }; private dispatchCancelHandler = (ev: CustomEvent) => { const role = ev.detail.role; if (isCancel(role)) { const cancelButton = this.buttons.find((b) => b.role === 'cancel'); this.callButtonHandler(cancelButton); } }; render() { const { htmlAttributes } = this; const mode = getIonMode(this); return ( ); } } const buttonWrapperClass = (button: PickerButton): CssClassMap => { return { [`picker-toolbar-${button.role}`]: button.role !== undefined, 'picker-toolbar-button': true, }; }; const buttonClass = (button: PickerButton): CssClassMap => { return { 'picker-button': true, 'ion-activatable': true, ...getClassMap(button.cssClass), }; };