mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
442 lines
12 KiB
Vue
442 lines
12 KiB
Vue
<template>
|
|
<transition name="fade-in-linear" @after-leave="$emit('vanish')">
|
|
<el-overlay
|
|
v-show="visible"
|
|
:z-index="zIndex"
|
|
:overlay-class="['is-message-box', modalClass]"
|
|
:mask="modal"
|
|
@click.self="handleWrapperClick"
|
|
>
|
|
<div
|
|
ref="root"
|
|
v-trap-focus
|
|
role="dialog"
|
|
:aria-label="title || 'dialog'"
|
|
aria-modal="true"
|
|
:class="[
|
|
'el-message-box',
|
|
customClass,
|
|
{ 'el-message-box--center': center },
|
|
]"
|
|
:style="customStyle"
|
|
>
|
|
<div
|
|
v-if="title !== null && title !== undefined"
|
|
class="el-message-box__header"
|
|
>
|
|
<div class="el-message-box__title">
|
|
<el-icon
|
|
v-if="iconComponent && center"
|
|
class="el-message-box__status"
|
|
:class="typeClass"
|
|
>
|
|
<component :is="iconComponent" />
|
|
</el-icon>
|
|
<span>{{ title }}</span>
|
|
</div>
|
|
<button
|
|
v-if="showClose"
|
|
type="button"
|
|
class="el-message-box__headerbtn"
|
|
aria-label="Close"
|
|
@click="
|
|
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
|
|
"
|
|
@keydown.prevent.enter="
|
|
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
|
|
"
|
|
>
|
|
<el-icon class="el-message-box__close"><close /></el-icon>
|
|
</button>
|
|
</div>
|
|
<div class="el-message-box__content">
|
|
<div class="el-message-box__container">
|
|
<el-icon
|
|
v-if="iconComponent && !center && hasMessage"
|
|
class="el-message-box__status"
|
|
:class="typeClass"
|
|
>
|
|
<component :is="iconComponent" />
|
|
</el-icon>
|
|
<div v-if="hasMessage" class="el-message-box__message">
|
|
<slot>
|
|
<p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
|
|
<p v-else v-html="message"></p>
|
|
</slot>
|
|
</div>
|
|
</div>
|
|
<div v-show="showInput" class="el-message-box__input">
|
|
<el-input
|
|
ref="inputRef"
|
|
v-model="inputValue"
|
|
:type="inputType"
|
|
:placeholder="inputPlaceholder"
|
|
:class="{ invalid: validateError }"
|
|
@keydown.prevent.enter="handleInputEnter"
|
|
/>
|
|
<div
|
|
class="el-message-box__errormsg"
|
|
:style="{
|
|
visibility: !!editorErrorMessage ? 'visible' : 'hidden',
|
|
}"
|
|
>
|
|
{{ editorErrorMessage }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="el-message-box__btns">
|
|
<el-button
|
|
v-if="showCancelButton"
|
|
:loading="cancelButtonLoading"
|
|
:class="[cancelButtonClass]"
|
|
:round="roundButton"
|
|
:size="buttonSize || ''"
|
|
@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="buttonSize || ''"
|
|
@click="handleAction('confirm')"
|
|
@keydown.prevent.enter="handleAction('confirm')"
|
|
>
|
|
{{ confirmButtonText || t('el.messagebox.confirm') }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</el-overlay>
|
|
</transition>
|
|
</template>
|
|
<script lang="ts">
|
|
import {
|
|
defineComponent,
|
|
nextTick,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
computed,
|
|
watch,
|
|
reactive,
|
|
ref,
|
|
toRefs,
|
|
} from 'vue'
|
|
import ElButton from '@element-plus/components/button'
|
|
import { TrapFocus } from '@element-plus/directives'
|
|
import {
|
|
useModal,
|
|
useLockscreen,
|
|
useLocale,
|
|
useRestoreActive,
|
|
usePreventGlobal,
|
|
} from '@element-plus/hooks'
|
|
import ElInput from '@element-plus/components/input'
|
|
import { ElOverlay } from '@element-plus/components/overlay'
|
|
import { PopupManager } from '@element-plus/utils/popup-manager'
|
|
import { on, off } from '@element-plus/utils/dom'
|
|
import { EVENT_CODE } from '@element-plus/utils/aria'
|
|
import { isValidComponentSize } from '@element-plus/utils/validators'
|
|
import { ElIcon } from '@element-plus/components/icon'
|
|
import { TypeComponents, TypeComponentsMap } from '@element-plus/utils/icon'
|
|
|
|
import type { ComponentPublicInstance, PropType } from 'vue'
|
|
import type { ComponentSize } from '@element-plus/utils/types'
|
|
import type {
|
|
Action,
|
|
MessageBoxState,
|
|
MessageBoxType,
|
|
} from './message-box.type'
|
|
|
|
export default defineComponent({
|
|
name: 'ElMessageBox',
|
|
directives: {
|
|
TrapFocus,
|
|
},
|
|
components: {
|
|
ElButton,
|
|
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,
|
|
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 visible = ref(false)
|
|
// s represents state
|
|
const state = reactive<MessageBoxState>({
|
|
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: PopupManager.nextZIndex(),
|
|
})
|
|
|
|
const typeClass = computed(() => {
|
|
const type = state.type
|
|
return type && TypeComponentsMap[type]
|
|
? `el-message-box-icon--${type}`
|
|
: ''
|
|
})
|
|
|
|
const iconComponent = computed(
|
|
() => state.icon || TypeComponentsMap[state.type] || ''
|
|
)
|
|
const hasMessage = computed(() => !!state.message)
|
|
const inputRef = ref<ComponentPublicInstance>(null)
|
|
const confirmRef = ref<ComponentPublicInstance>(null)
|
|
|
|
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 === 'alert' || props.boxType === 'confirm') {
|
|
nextTick().then(() => {
|
|
confirmRef.value?.$el?.focus?.()
|
|
})
|
|
}
|
|
state.zIndex = PopupManager.nextZIndex()
|
|
}
|
|
if (props.boxType !== 'prompt') return
|
|
if (val) {
|
|
nextTick().then(() => {
|
|
if (inputRef.value && inputRef.value.$el) {
|
|
getInputElement().focus()
|
|
}
|
|
})
|
|
} else {
|
|
state.editorErrorMessage = ''
|
|
state.validateError = false
|
|
}
|
|
}
|
|
)
|
|
|
|
onMounted(async () => {
|
|
await nextTick()
|
|
if (props.closeOnHashChange) {
|
|
on(window, 'hashchange', doClose)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (props.closeOnHashChange) {
|
|
off(window, '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 handleInputEnter = () => {
|
|
if (state.inputType !== 'textarea') {
|
|
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.
|
|
if (props.closeOnPressEscape) {
|
|
useModal(
|
|
{
|
|
handleClose,
|
|
},
|
|
visible
|
|
)
|
|
} else {
|
|
usePreventGlobal(
|
|
visible,
|
|
'keydown',
|
|
(e: KeyboardEvent) => e.code === EVENT_CODE.esc
|
|
)
|
|
}
|
|
|
|
// locks the screen to prevent scroll
|
|
if (props.lockScroll) {
|
|
useLockscreen(visible)
|
|
}
|
|
|
|
// restore to prev active element.
|
|
useRestoreActive(visible)
|
|
|
|
return {
|
|
...toRefs(state),
|
|
visible,
|
|
hasMessage,
|
|
typeClass,
|
|
iconComponent,
|
|
confirmButtonClasses,
|
|
inputRef,
|
|
confirmRef,
|
|
doClose, // for outside usage
|
|
handleClose, // for out side usage
|
|
handleWrapperClick,
|
|
handleInputEnter,
|
|
handleAction,
|
|
t,
|
|
}
|
|
},
|
|
})
|
|
</script>
|