element-plus/packages/components/message-box/src/index.vue
LIUCHAO 7bb6b6d99f
refactor(utils): drop off/on/once, use EventListener instead (#8960)
* refactor(utils): drop off/on/once, use EventListener instead

* refactor(utils): use useEventListener

* refactor(utils): use useEventListener

* style(components): [roving-focus-group] useEventListener  pass ref
2022-07-26 23:01:07 +08:00

506 lines
14 KiB
Vue

<template>
<transition name="fade-in-linear" @after-leave="$emit('vanish')">
<el-overlay
v-show="visible"
:z-index="zIndex"
:overlay-class="[ns.is('message-box'), modalClass]"
:mask="modal"
>
<div
role="dialog"
:aria-label="title"
aria-modal="true"
:aria-describedby="!showInput ? contentId : undefined"
:class="`${ns.namespace.value}-overlay-message-box`"
@click="overlayEvent.onClick"
@mousedown="overlayEvent.onMousedown"
@mouseup="overlayEvent.onMouseup"
>
<el-focus-trap
loop
:trapped="visible"
:focus-trap-el="rootRef"
:focus-start-el="focusStartRef"
@release-requested="onCloseRequested"
>
<div
ref="rootRef"
:class="[
ns.b(),
customClass,
ns.is('draggable', draggable),
{ [ns.m('center')]: center },
]"
:style="customStyle"
tabindex="-1"
@click.stop=""
>
<div
v-if="title !== null && title !== undefined"
ref="headerRef"
:class="ns.e('header')"
>
<div :class="ns.e('title')">
<el-icon
v-if="iconComponent && center"
:class="[ns.e('status'), typeClass]"
>
<component :is="iconComponent" />
</el-icon>
<span>{{ title }}</span>
</div>
<button
v-if="showClose"
type="button"
:class="ns.e('headerbtn')"
:aria-label="t('el.messagebox.close')"
@click="
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
"
@keydown.prevent.enter="
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
"
>
<el-icon :class="ns.e('close')">
<close />
</el-icon>
</button>
</div>
<div :id="contentId" :class="ns.e('content')">
<div :class="ns.e('container')">
<el-icon
v-if="iconComponent && !center && hasMessage"
:class="[ns.e('status'), typeClass]"
>
<component :is="iconComponent" />
</el-icon>
<div v-if="hasMessage" :class="ns.e('message')">
<slot>
<component
:is="showInput ? 'label' : 'p'"
v-if="!dangerouslyUseHTMLString"
:for="showInput ? inputId : undefined"
>
{{ !dangerouslyUseHTMLString ? message : '' }}
</component>
<component
:is="showInput ? 'label' : 'p'"
v-else
:for="showInput ? inputId : undefined"
v-html="message"
/>
</slot>
</div>
</div>
<div v-show="showInput" :class="ns.e('input')">
<el-input
:id="inputId"
ref="inputRef"
v-model="inputValue"
:type="inputType"
:placeholder="inputPlaceholder"
:aria-invalid="validateError"
:class="{ invalid: validateError }"
@keydown.enter="handleInputEnter"
/>
<div
:class="ns.e('errormsg')"
:style="{
visibility: !!editorErrorMessage ? 'visible' : 'hidden',
}"
>
{{ editorErrorMessage }}
</div>
</div>
</div>
<div :class="ns.e('btns')">
<el-button
v-if="showCancelButton"
:loading="cancelButtonLoading"
:class="[cancelButtonClass]"
:round="roundButton"
:size="btnSize"
@click="handleAction('cancel')"
@keydown.prevent.enter="handleAction('cancel')"
>
{{ cancelButtonText || t('el.messagebox.cancel') }}
</el-button>
<el-button
v-show="showConfirmButton"
ref="confirmRef"
type="primary"
:loading="confirmButtonLoading"
:class="[confirmButtonClasses]"
:round="roundButton"
:disabled="confirmButtonDisabled"
:size="btnSize"
@click="handleAction('confirm')"
@keydown.prevent.enter="handleAction('confirm')"
>
{{ confirmButtonText || t('el.messagebox.confirm') }}
</el-button>
</div>
</div>
</el-focus-trap>
</div>
</el-overlay>
</transition>
</template>
<script lang="ts">
// @ts-nocheck
import {
computed,
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
reactive,
ref,
toRefs,
watch,
} from 'vue'
import ElButton from '@element-plus/components/button'
import { TrapFocus } from '@element-plus/directives'
import {
useDraggable,
useId,
useLocale,
useLockscreen,
useNamespace,
useRestoreActive,
useSameTarget,
useSize,
useZIndex,
} from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import { ElOverlay } from '@element-plus/components/overlay'
import {
TypeComponents,
TypeComponentsMap,
isValidComponentSize,
} from '@element-plus/utils'
import { ElIcon } from '@element-plus/components/icon'
import ElFocusTrap from '@element-plus/components/focus-trap'
import type { ComponentPublicInstance, PropType } from 'vue'
import type { ComponentSize } from '@element-plus/constants'
import type {
Action,
MessageBoxState,
MessageBoxType,
} from './message-box.type'
export default defineComponent({
name: 'ElMessageBox',
directives: {
TrapFocus,
},
components: {
ElButton,
ElFocusTrap,
ElInput,
ElOverlay,
ElIcon,
...TypeComponents,
},
inheritAttrs: false,
props: {
buttonSize: {
type: String as PropType<ComponentSize>,
validator: isValidComponentSize,
},
modal: {
type: Boolean,
default: true,
},
lockScroll: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
closeOnClickModal: {
type: Boolean,
default: true,
},
closeOnPressEscape: {
type: Boolean,
default: true,
},
closeOnHashChange: {
type: Boolean,
default: true,
},
center: Boolean,
draggable: Boolean,
roundButton: {
default: false,
type: Boolean,
},
container: {
type: String, // default append to body
default: 'body',
},
boxType: {
type: String as PropType<MessageBoxType>,
default: '',
},
},
emits: ['vanish', 'action'],
setup(props, { emit }) {
// const popup = usePopup(props, doClose)
const { t } = useLocale()
const ns = useNamespace('message-box')
const visible = ref(false)
const { nextZIndex } = useZIndex()
// s represents state
const state = reactive<MessageBoxState>({
// autofocus element when open message-box
autofocus: true,
beforeClose: null,
callback: null,
cancelButtonText: '',
cancelButtonClass: '',
confirmButtonText: '',
confirmButtonClass: '',
customClass: '',
customStyle: {},
dangerouslyUseHTMLString: false,
distinguishCancelAndClose: false,
icon: '',
inputPattern: null,
inputPlaceholder: '',
inputType: 'text',
inputValue: null,
inputValidator: null,
inputErrorMessage: '',
message: null,
modalFade: true,
modalClass: '',
showCancelButton: false,
showConfirmButton: true,
type: '',
title: undefined,
showInput: false,
action: '' as Action,
confirmButtonLoading: false,
cancelButtonLoading: false,
confirmButtonDisabled: false,
editorErrorMessage: '',
// refer to: https://github.com/ElemeFE/element/commit/2999279ae34ef10c373ca795c87b020ed6753eed
// seemed ok for now without this state.
// isOnComposition: false, // temporary remove
validateError: false,
zIndex: nextZIndex(),
})
const typeClass = computed(() => {
const type = state.type
return { [ns.bm('icon', type)]: type && TypeComponentsMap[type] }
})
const contentId = useId()
const inputId = useId()
const btnSize = useSize(
computed(() => props.buttonSize),
{ prop: true, form: true, formItem: true }
)
const iconComponent = computed(
() => state.icon || TypeComponentsMap[state.type] || ''
)
const hasMessage = computed(() => !!state.message)
const rootRef = ref<HTMLElement>()
const headerRef = ref<HTMLElement>()
const focusStartRef = ref<HTMLElement>()
const inputRef = ref<ComponentPublicInstance>()
const confirmRef = ref<ComponentPublicInstance>()
const confirmButtonClasses = computed(() => state.confirmButtonClass)
watch(
() => state.inputValue,
async (val) => {
await nextTick()
if (props.boxType === 'prompt' && val !== null) {
validate()
}
},
{ immediate: true }
)
watch(
() => visible.value,
(val) => {
if (val) {
if (props.boxType !== 'prompt') {
if (state.autofocus) {
focusStartRef.value = confirmRef.value?.$el ?? rootRef.value
} else {
focusStartRef.value = rootRef.value
}
}
state.zIndex = nextZIndex()
}
if (props.boxType !== 'prompt') return
if (val) {
nextTick().then(() => {
if (inputRef.value && inputRef.value.$el) {
if (state.autofocus) {
focusStartRef.value = getInputElement() ?? rootRef.value
} else {
focusStartRef.value = rootRef.value
}
}
})
} else {
state.editorErrorMessage = ''
state.validateError = false
}
}
)
const draggable = computed(() => props.draggable)
useDraggable(rootRef, headerRef, draggable)
onMounted(async () => {
await nextTick()
if (props.closeOnHashChange) {
window.addEventListener('hashchange', doClose)
}
})
onBeforeUnmount(() => {
if (props.closeOnHashChange) {
window.removeEventListener('hashchange', doClose)
}
})
function doClose() {
if (!visible.value) return
visible.value = false
nextTick(() => {
if (state.action) emit('action', state.action)
})
}
const handleWrapperClick = () => {
if (props.closeOnClickModal) {
handleAction(state.distinguishCancelAndClose ? 'close' : 'cancel')
}
}
const overlayEvent = useSameTarget(handleWrapperClick)
const handleInputEnter = (e: KeyboardEvent) => {
if (state.inputType !== 'textarea') {
e.preventDefault()
return handleAction('confirm')
}
}
const handleAction = (action: Action) => {
if (props.boxType === 'prompt' && action === 'confirm' && !validate()) {
return
}
state.action = action
if (state.beforeClose) {
state.beforeClose?.(action, state, doClose)
} else {
doClose()
}
}
const validate = () => {
if (props.boxType === 'prompt') {
const inputPattern = state.inputPattern
if (inputPattern && !inputPattern.test(state.inputValue || '')) {
state.editorErrorMessage =
state.inputErrorMessage || t('el.messagebox.error')
state.validateError = true
return false
}
const inputValidator = state.inputValidator
if (typeof inputValidator === 'function') {
const validateResult = inputValidator(state.inputValue)
if (validateResult === false) {
state.editorErrorMessage =
state.inputErrorMessage || t('el.messagebox.error')
state.validateError = true
return false
}
if (typeof validateResult === 'string') {
state.editorErrorMessage = validateResult
state.validateError = true
return false
}
}
}
state.editorErrorMessage = ''
state.validateError = false
return true
}
const getInputElement = () => {
const inputRefs = inputRef.value.$refs
return (inputRefs.input || inputRefs.textarea) as HTMLElement
}
const handleClose = () => {
handleAction('close')
}
// when close on press escape is disabled, pressing esc should not callout
// any other message box and close any other dialog-ish elements
// e.g. Dialog has a close on press esc feature, and when it closes, it calls
// props.beforeClose method to make a intermediate state by callout a message box
// for some verification or alerting. then if we allow global event liek this
// to dispatch, it could callout another message box.
const onCloseRequested = () => {
if (props.closeOnPressEscape) {
handleClose()
}
}
// locks the screen to prevent scroll
if (props.lockScroll) {
useLockscreen(visible)
}
// restore to prev active element.
useRestoreActive(visible)
return {
...toRefs(state),
ns,
overlayEvent,
visible,
hasMessage,
typeClass,
contentId,
inputId,
btnSize,
iconComponent,
confirmButtonClasses,
rootRef,
focusStartRef,
headerRef,
inputRef,
confirmRef,
doClose, // for outside usage
handleClose, // for out side usage
onCloseRequested,
handleWrapperClick,
handleInputEnter,
handleAction,
t,
}
},
})
</script>