mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
feat(message-box): migrate MessageBox (#300)
* feat(message-box): migrate MessageBox * refactor(message-box): import sleep from test-utils * refactor(message-box): optimize props * refactor(message-box): optimize message-box * refactor(message-box): optimize message-box * refactor(message-box): optimize message-box * refactor(message-box): remove `removeClass` & `addClass` * refactor(message-box): replace 'HTMLElement' with 'HTMLButtonElement' * refactor(message-box): review optimize * refactor(message-box): __test__ optimize * refactor(message-box): replace addEvent with on and off * refactor(message-box): remove doc * feat(message-box): jest add @babel/plugin-proposal-class-properties Co-authored-by: YuLinXi <yumengyuan@klicen.com>
This commit is contained in:
parent
73fb527f49
commit
c2951875b3
@ -23,7 +23,10 @@ module.exports = {
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['@vue/babel-plugin-jsx'],
|
||||
plugins: [
|
||||
'@vue/babel-plugin-jsx',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -21,6 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@commitlint/cli": "^9.1.2",
|
||||
|
@ -48,6 +48,7 @@ import ElColorPicker from '@element-plus/color-picker'
|
||||
import ElSelect from '@element-plus/select'
|
||||
import ElTimeSelect from '@element-plus/time-select'
|
||||
import ElPagination from '@element-plus/pagination'
|
||||
import ElMessageBox from '@element-plus/message-box'
|
||||
import ElInputNumber from '@element-plus/input-number'
|
||||
import ElPopover from '@element-plus/popover'
|
||||
|
||||
@ -100,6 +101,7 @@ export {
|
||||
ElSelect,
|
||||
ElTimeSelect,
|
||||
ElPagination,
|
||||
ElMessageBox,
|
||||
ElInputNumber,
|
||||
ElPopover,
|
||||
}
|
||||
@ -145,6 +147,7 @@ const install = (app: App): void => {
|
||||
ElCalendar(app)
|
||||
ElInfiniteScroll(app)
|
||||
ElMessage(app)
|
||||
ElMessageBox(app)
|
||||
ElDrawer(app)
|
||||
ElPopconfirm(app)
|
||||
ElForm(app)
|
||||
|
189
packages/message-box/__tests__/message-box.spec.ts
Normal file
189
packages/message-box/__tests__/message-box.spec.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import MessageBox from '../src/messageBox'
|
||||
import { sleep } from '@element-plus/test-utils'
|
||||
|
||||
const selector = '.el-message-box__wrapper'
|
||||
|
||||
describe('MessageBox', () => {
|
||||
|
||||
afterEach(() => {
|
||||
const el = document.querySelector('.el-message-box__wrapper')
|
||||
if (!el) return
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
MessageBox.close()
|
||||
})
|
||||
|
||||
test('create and close', async () => {
|
||||
MessageBox({
|
||||
type: 'success',
|
||||
title: '消息',
|
||||
message: '这是一段内容',
|
||||
})
|
||||
const msgbox: HTMLElement = document.querySelector(selector)
|
||||
expect(msgbox).toBeDefined()
|
||||
await sleep()
|
||||
expect(msgbox.querySelector('.el-message-box__title span').textContent).toEqual('消息')
|
||||
expect(msgbox.querySelector('.el-message-box__message').querySelector('p').textContent).toEqual('这是一段内容')
|
||||
MessageBox.close()
|
||||
await sleep(250)
|
||||
expect(msgbox.style.display).toEqual('none')
|
||||
})
|
||||
|
||||
test('invoke with strings', () => {
|
||||
MessageBox('消息', '这是一段内容')
|
||||
const msgbox = document.querySelector(selector)
|
||||
expect(msgbox).toBeDefined()
|
||||
})
|
||||
|
||||
test('custom icon', async () => {
|
||||
MessageBox({
|
||||
type: 'warning',
|
||||
iconClass: 'el-icon-question',
|
||||
message: '这是一段内容',
|
||||
})
|
||||
await sleep()
|
||||
const icon = document.querySelector('.el-message-box__status')
|
||||
expect(icon.classList.contains('el-icon-question')).toBe(true)
|
||||
})
|
||||
|
||||
test('html string', async () => {
|
||||
MessageBox({
|
||||
title: 'html string',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: '<strong>html string</strong>',
|
||||
})
|
||||
await sleep()
|
||||
const message = document.querySelector('.el-message-box__message strong')
|
||||
expect(message.textContent).toEqual('html string')
|
||||
})
|
||||
|
||||
test('distinguish cancel and close', async () => {
|
||||
let msgAction = ''
|
||||
MessageBox({
|
||||
title: '消息',
|
||||
message: '这是一段内容',
|
||||
distinguishCancelAndClose: true,
|
||||
callback: action => {
|
||||
msgAction = action
|
||||
},
|
||||
})
|
||||
await sleep()
|
||||
const btn = document.querySelector('.el-message-box__close') as HTMLButtonElement
|
||||
btn.click()
|
||||
await sleep()
|
||||
expect(msgAction).toEqual('close')
|
||||
})
|
||||
|
||||
test('alert', async () => {
|
||||
MessageBox.alert('这是一段内容', {
|
||||
title: '标题名称',
|
||||
type: 'warning',
|
||||
})
|
||||
await sleep()
|
||||
const vModal: HTMLElement = document.querySelector('.v-modal')
|
||||
vModal.click()
|
||||
await sleep(250)
|
||||
const msgbox: HTMLElement = document.querySelector(selector)
|
||||
expect(msgbox.style.display).toEqual('')
|
||||
expect(msgbox.querySelector('.el-icon-warning')).toBeDefined()
|
||||
})
|
||||
|
||||
test('confirm', async () => {
|
||||
MessageBox.confirm('这是一段内容', {
|
||||
title: '标题名称',
|
||||
type: 'warning',
|
||||
})
|
||||
await sleep()
|
||||
const btn = document.querySelector(selector).querySelector('.el-button--primary') as HTMLButtonElement
|
||||
btn.click()
|
||||
await sleep(250)
|
||||
const msgbox: HTMLElement = document.querySelector(selector)
|
||||
expect(msgbox.style.display).toEqual('none')
|
||||
})
|
||||
|
||||
test('prompt', async () => {
|
||||
MessageBox.prompt('这是一段内容', {
|
||||
title: '标题名称',
|
||||
inputPattern: /test/,
|
||||
inputErrorMessage: 'validation failed',
|
||||
})
|
||||
await sleep(0)
|
||||
const inputElm = document.querySelector(selector).querySelector('.el-message-box__input')
|
||||
const haveFocus = inputElm.querySelector('input').isSameNode(document.activeElement)
|
||||
expect(inputElm).toBeDefined()
|
||||
expect(haveFocus).toBe(true)
|
||||
})
|
||||
|
||||
test('prompt: focus on textarea', async () => {
|
||||
MessageBox.prompt('这是一段内容', {
|
||||
inputType: 'textarea',
|
||||
title: '标题名称',
|
||||
})
|
||||
await sleep()
|
||||
const textareaElm = document.querySelector(selector).querySelector('textarea')
|
||||
const haveFocus = textareaElm.isSameNode(document.activeElement)
|
||||
expect(haveFocus).toBe(true)
|
||||
})
|
||||
|
||||
test('callback', async () => {
|
||||
let msgAction = ''
|
||||
MessageBox({
|
||||
title: '消息',
|
||||
message: '这是一段内容',
|
||||
callback: action => {
|
||||
msgAction = action
|
||||
},
|
||||
})
|
||||
await sleep()
|
||||
const closeBtn = document.querySelector('.el-message-box__close') as HTMLButtonElement
|
||||
closeBtn.click()
|
||||
await sleep()
|
||||
expect(msgAction).toEqual('cancel')
|
||||
})
|
||||
|
||||
test('beforeClose', async() => {
|
||||
let msgAction = ''
|
||||
MessageBox({
|
||||
title: '消息',
|
||||
message: '这是一段内容',
|
||||
beforeClose: (action, instance) => {
|
||||
instance.close()
|
||||
},
|
||||
}, action => {
|
||||
msgAction = action
|
||||
})
|
||||
await sleep();
|
||||
(document.querySelector('.el-message-box__wrapper .el-button--primary') as HTMLButtonElement).click()
|
||||
await sleep()
|
||||
expect(msgAction).toEqual('confirm')
|
||||
})
|
||||
|
||||
describe('promise', () => {
|
||||
test('resolve',async () => {
|
||||
let msgAction = ''
|
||||
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
|
||||
.then(action => {
|
||||
msgAction = action
|
||||
})
|
||||
await sleep()
|
||||
const btn = document.querySelector('.el-message-box__btns .el-button--primary') as HTMLButtonElement
|
||||
btn.click()
|
||||
await sleep()
|
||||
expect(msgAction).toEqual('confirm')
|
||||
})
|
||||
|
||||
test('reject', async () => {
|
||||
let msgAction = ''
|
||||
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
|
||||
.catch(action => {
|
||||
msgAction = action
|
||||
})
|
||||
await sleep()
|
||||
const btn = document.querySelectorAll('.el-message-box__btns .el-button') as NodeListOf<HTMLButtonElement>
|
||||
btn[0].click()
|
||||
await sleep()
|
||||
expect(msgAction).toEqual('cancel')
|
||||
})
|
||||
})
|
||||
})
|
9
packages/message-box/index.ts
Normal file
9
packages/message-box/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { App } from 'vue'
|
||||
import MessageBox from './src/messageBox'
|
||||
export default (app: App): void => {
|
||||
app.config.globalProperties.$msgbox = MessageBox
|
||||
app.config.globalProperties.$messageBox = MessageBox
|
||||
app.config.globalProperties.$alert = MessageBox.alert
|
||||
app.config.globalProperties.$confirm = MessageBox.confirm
|
||||
app.config.globalProperties.$prompt = MessageBox.prompt
|
||||
}
|
12
packages/message-box/package.json
Normal file
12
packages/message-box/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/message-box",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
385
packages/message-box/src/index.vue
Normal file
385
packages/message-box/src/index.vue
Normal file
@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<transition name="msgbox-fade">
|
||||
<div
|
||||
v-show="visible"
|
||||
:aria-label="title || 'dialog'"
|
||||
class="el-message-box__wrapper"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@click.self="handleWrapperClick"
|
||||
>
|
||||
<div class="el-message-box" :class="[customClass, center && 'el-message-box--center']">
|
||||
<div v-if="title !== null && title !== undefined" class="el-message-box__header">
|
||||
<div class="el-message-box__title">
|
||||
<div
|
||||
v-if="icon && center"
|
||||
:class="['el-message-box__status', icon]"
|
||||
>
|
||||
</div>
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="showClose"
|
||||
type="button"
|
||||
class="el-message-box__headerbtn"
|
||||
aria-label="Close"
|
||||
@click="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
|
||||
@keydown.enter="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
|
||||
>
|
||||
<i class="el-message-box__close el-icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="el-message-box__content">
|
||||
<div class="el-message-box__container">
|
||||
<div
|
||||
v-if="icon && !center && hasMessage"
|
||||
:class="['el-message-box__status', icon]"
|
||||
>
|
||||
</div>
|
||||
<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="input"
|
||||
v-model="inputValue"
|
||||
:type="inputType"
|
||||
:placeholder="inputPlaceholder"
|
||||
:class="{ invalid: validateError }"
|
||||
@keydown.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="small"
|
||||
@click="handleAction('cancel')"
|
||||
@keydown.enter="handleAction('cancel')"
|
||||
>
|
||||
{{ cancelButtonText || t('el.messagebox.cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-show="showConfirmButton"
|
||||
ref="confirm"
|
||||
:loading="confirmButtonLoading"
|
||||
:class="[ confirmButtonClasses ]"
|
||||
:round="roundButton"
|
||||
size="small"
|
||||
@click="handleAction('confirm')"
|
||||
@keydown.enter="handleAction('confirm')"
|
||||
>
|
||||
{{ confirmButtonText || t('el.messagebox.confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import {
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
computed,
|
||||
watch,
|
||||
onBeforeMount,
|
||||
getCurrentInstance,
|
||||
reactive,
|
||||
toRefs,
|
||||
} from 'vue'
|
||||
import ElButton from '@element-plus/button/src/button.vue'
|
||||
import ElInput from '@element-plus/input/src/index.vue'
|
||||
import { t } from '@element-plus/locale'
|
||||
import Dialog from '@element-plus/utils/aria-dialog'
|
||||
import usePopup from '@element-plus/utils/popup/usePopup'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
|
||||
let dialog
|
||||
|
||||
const TypeMap: Indexable<string> = {
|
||||
success: 'success',
|
||||
info: 'info',
|
||||
warning: 'warning',
|
||||
error: 'error',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMessageBox',
|
||||
components: {
|
||||
ElButton,
|
||||
ElInput,
|
||||
},
|
||||
props: {
|
||||
openDelay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeDelay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
},
|
||||
modalFade: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
modalClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modalAppendToBody: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
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: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
roundButton: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
} ,
|
||||
setup(props) {
|
||||
let vm
|
||||
const popup = usePopup(props, doClose)
|
||||
const state = reactive<State>({
|
||||
uid: 1,
|
||||
title: undefined,
|
||||
message: '',
|
||||
type: '',
|
||||
iconClass: '',
|
||||
customClass: '',
|
||||
showInput: false,
|
||||
inputValue: null,
|
||||
inputPlaceholder: '',
|
||||
inputType: 'text',
|
||||
inputPattern: null,
|
||||
inputValidator: null,
|
||||
inputErrorMessage: '',
|
||||
showConfirmButton: true,
|
||||
showCancelButton: false,
|
||||
action: '',
|
||||
confirmButtonText: '',
|
||||
cancelButtonText: '',
|
||||
confirmButtonLoading: false,
|
||||
cancelButtonLoading: false,
|
||||
confirmButtonClass: '',
|
||||
confirmButtonDisabled: false,
|
||||
cancelButtonClass: '',
|
||||
editorErrorMessage: null,
|
||||
callback: null,
|
||||
dangerouslyUseHTMLString: false,
|
||||
focusAfterClosed: null,
|
||||
isOnComposition: false,
|
||||
distinguishCancelAndClose: false,
|
||||
type$: '',
|
||||
visible: false,
|
||||
validateError: false,
|
||||
})
|
||||
const icon = computed(() => state.iconClass || (state.type && TypeMap[state.type] ? `el-icon-${ TypeMap[state.type] }` : ''))
|
||||
const hasMessage = computed(() => !!state.message)
|
||||
|
||||
const confirmButtonClasses = computed(() => `el-button--primary ${ state.confirmButtonClass }`)
|
||||
|
||||
watch(() => state.inputValue, val => {
|
||||
nextTick().then(() => {
|
||||
if (state.type$ === 'prompt' && val !== null) {
|
||||
validate()
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => state.visible, val => {
|
||||
popup.state.visible = val
|
||||
if (val) {
|
||||
state.uid++
|
||||
if (state.type$ === 'alert' || state.type$ === 'confirm') {
|
||||
nextTick().then(() => { vm.refs.confirm.$el.focus() })
|
||||
}
|
||||
state.focusAfterClosed = document.activeElement
|
||||
dialog = new Dialog(vm.vnode.el, state.focusAfterClosed, getFirstFocus())
|
||||
}
|
||||
if (state.type$ !== 'prompt') return
|
||||
if (val) {
|
||||
nextTick().then(() => {
|
||||
if (vm.refs.input && vm.refs.input.$el) {
|
||||
getInputElement().focus()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
state.editorErrorMessage = ''
|
||||
state.validateError = false
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
vm = getCurrentInstance()
|
||||
vm.setupInstall = {
|
||||
state,
|
||||
doClose,
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (props.closeOnHashChange) {
|
||||
on(window, 'hashchange', popup.close)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (props.closeOnHashChange) {
|
||||
off(window, 'hashchange', popup.close)
|
||||
}
|
||||
setTimeout(() => {
|
||||
dialog.closeDialog()
|
||||
})
|
||||
})
|
||||
|
||||
function getSafeClose () {
|
||||
const currentId = state.uid
|
||||
return () => {
|
||||
nextTick().then(() => {
|
||||
if (currentId === state.uid) doClose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function doClose() {
|
||||
if (!state.visible) return
|
||||
state.visible = false
|
||||
popup.updateClosingFlag(true)
|
||||
dialog.closeDialog()
|
||||
if (props.lockScroll) {
|
||||
setTimeout(popup.restoreBodyStyle, 200)
|
||||
}
|
||||
popup.state.opened = false
|
||||
popup.doAfterClose()
|
||||
setTimeout(() => {
|
||||
if (state.action) state.callback(state.action, state)
|
||||
})
|
||||
}
|
||||
|
||||
const getFirstFocus = () => {
|
||||
const btn = vm.vnode.el.querySelector('.el-message-box__btns .el-button')
|
||||
const title = vm.vnode.el.querySelector('.el-message-box__btns .el-message-box__title')
|
||||
return btn || title
|
||||
}
|
||||
|
||||
const handleWrapperClick = () => {
|
||||
if (props.closeOnClickModal) {
|
||||
handleAction(state.distinguishCancelAndClose ? 'close' : 'cancel')
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputEnter = () => {
|
||||
if (state.inputType !== 'textarea') {
|
||||
return handleAction('confirm')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAction = action => {
|
||||
if (state.type$ === 'prompt' && action === 'confirm' && !validate()) {
|
||||
return
|
||||
}
|
||||
state.action = action
|
||||
if (typeof vm.setupInstall.state.beforeClose === 'function') {
|
||||
vm.setupInstall.state.close = getSafeClose()
|
||||
vm.setupInstall.state.beforeClose(action, state, popup.close)
|
||||
} else {
|
||||
doClose()
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
if (state.type$ === '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 = vm.refs.input.$refs
|
||||
return inputRefs.input || inputRefs.textarea
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
handleAction('close')
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
hasMessage,
|
||||
icon,
|
||||
confirmButtonClasses,
|
||||
handleWrapperClick,
|
||||
handleInputEnter,
|
||||
handleAction,
|
||||
handleClose,
|
||||
t,
|
||||
doClose,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
161
packages/message-box/src/message-box.d.ts
vendored
Normal file
161
packages/message-box/src/message-box.d.ts
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
import { VNode } from 'vue'
|
||||
export type MessageType = 'success' | 'warning' | 'info' | 'error'
|
||||
export type MessageBoxCloseAction = 'confirm' | 'cancel' | 'close'
|
||||
export type MessageBoxData = MessageBoxInputData
|
||||
|
||||
export interface MessageBoxInputData {
|
||||
value: string
|
||||
action: MessageBoxCloseAction
|
||||
}
|
||||
|
||||
export interface MessageBoxInputValidator {
|
||||
(value: string): boolean | string
|
||||
}
|
||||
|
||||
export declare class ElMessageBoxComponent {
|
||||
title: string
|
||||
message: string
|
||||
type: MessageType
|
||||
iconClass: string
|
||||
customClass: string
|
||||
showInput: boolean
|
||||
showClose: boolean
|
||||
inputValue: string
|
||||
inputPlaceholder: string
|
||||
inputType: string
|
||||
inputPattern: RegExp
|
||||
inputValidator: MessageBoxInputValidator
|
||||
inputErrorMessage: string
|
||||
showConfirmButton: boolean
|
||||
showCancelButton: boolean
|
||||
action: MessageBoxCloseAction
|
||||
dangerouslyUseHTMLString: boolean
|
||||
confirmButtonText: string
|
||||
cancelButtonText: string
|
||||
confirmButtonLoading: boolean
|
||||
cancelButtonLoading: boolean
|
||||
confirmButtonClass: string
|
||||
confirmButtonDisabled: boolean
|
||||
cancelButtonClass: string
|
||||
editorErrorMessage: string
|
||||
|
||||
close(): any
|
||||
}
|
||||
|
||||
/** Options used in MessageBox */
|
||||
export interface ElMessageBoxOptions {
|
||||
/** Title of the MessageBox */
|
||||
title?: string
|
||||
|
||||
/** Content of the MessageBox */
|
||||
message?: string | VNode
|
||||
|
||||
/** Message type, used for icon display */
|
||||
type?: MessageType
|
||||
|
||||
/** Custom icon's class */
|
||||
iconClass?: string
|
||||
|
||||
/** Custom class name for MessageBox */
|
||||
customClass?: string
|
||||
|
||||
/** MessageBox closing callback if you don't prefer Promise */
|
||||
callback?: (action: MessageBoxCloseAction, instance: ElMessageBoxComponent) => void
|
||||
|
||||
/** Callback before MessageBox closes, and it will prevent MessageBox from closing */
|
||||
beforeClose?: (action: MessageBoxCloseAction, instance: ElMessageBoxComponent, done: (() => void)) => void
|
||||
|
||||
/** Whether to lock body scroll when MessageBox prompts */
|
||||
lockScroll?: boolean
|
||||
|
||||
/** Whether to show a cancel button */
|
||||
showCancelButton?: boolean
|
||||
|
||||
/** Whether to show a confirm button */
|
||||
showConfirmButton?: boolean
|
||||
|
||||
/** Whether to show a close button */
|
||||
showClose?: boolean
|
||||
|
||||
/** Text content of cancel button */
|
||||
cancelButtonText?: string
|
||||
|
||||
/** Text content of confirm button */
|
||||
confirmButtonText?: string
|
||||
|
||||
/** Custom class name of cancel button */
|
||||
cancelButtonClass?: string
|
||||
|
||||
/** Custom class name of confirm button */
|
||||
confirmButtonClass?: string
|
||||
|
||||
/** Whether to align the content in center */
|
||||
center?: boolean
|
||||
|
||||
/** Whether message is treated as HTML string */
|
||||
dangerouslyUseHTMLString?: boolean
|
||||
|
||||
/** Whether to use round button */
|
||||
roundButton?: boolean
|
||||
|
||||
/** Whether MessageBox can be closed by clicking the mask */
|
||||
closeOnClickModal?: boolean
|
||||
|
||||
/** Whether MessageBox can be closed by pressing the ESC */
|
||||
closeOnPressEscape?: boolean
|
||||
|
||||
/** Whether to close MessageBox when hash changes */
|
||||
closeOnHashChange?: boolean
|
||||
|
||||
/** Whether to show an input */
|
||||
showInput?: boolean
|
||||
|
||||
/** Placeholder of input */
|
||||
inputPlaceholder?: string
|
||||
|
||||
/** Initial value of input */
|
||||
inputValue?: string
|
||||
|
||||
/** Regexp for the input */
|
||||
inputPattern?: RegExp
|
||||
|
||||
/** Input Type: text, textArea, password or number */
|
||||
inputType?: string
|
||||
|
||||
/** Validation function for the input. Should returns a boolean or string. If a string is returned, it will be assigned to inputErrorMessage */
|
||||
inputValidator?: MessageBoxInputValidator
|
||||
|
||||
/** Error message when validation fails */
|
||||
inputErrorMessage?: string
|
||||
|
||||
/** Whether to distinguish canceling and closing */
|
||||
distinguishCancelAndClose?: boolean
|
||||
}
|
||||
|
||||
export interface ElMessageBoxShortcutMethod {
|
||||
(message: string, title: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
|
||||
(message: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
|
||||
}
|
||||
|
||||
export interface ElMessageBox {
|
||||
/** Show a message box */
|
||||
(message: string, title?: string, type?: string): Promise<MessageBoxData>
|
||||
|
||||
/** Show a message box */
|
||||
(options: ElMessageBoxOptions): Promise<MessageBoxData>
|
||||
|
||||
/** Show an alert message box */
|
||||
alert: ElMessageBoxShortcutMethod
|
||||
|
||||
/** Show a confirm message box */
|
||||
confirm: ElMessageBoxShortcutMethod
|
||||
|
||||
/** Show a prompt message box */
|
||||
prompt: ElMessageBoxShortcutMethod
|
||||
|
||||
/** Set default options of message boxes */
|
||||
setDefaults (defaults: ElMessageBoxOptions): void
|
||||
|
||||
/** Close current message box */
|
||||
close (): void
|
||||
}
|
230
packages/message-box/src/messageBox.ts
Normal file
230
packages/message-box/src/messageBox.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import { createVNode, render } from 'vue'
|
||||
import MessageBoxConstructor from './index.vue'
|
||||
import isServer from '@element-plus/utils/isServer'
|
||||
import { isVNode } from '../../utils/util'
|
||||
import { ElMessageBoxOptions } from './message-box'
|
||||
|
||||
let currentMsg, instance
|
||||
|
||||
// component default props
|
||||
const PROP_KEYS = [
|
||||
'lockScroll',
|
||||
'showClose',
|
||||
'closeOnClickModal',
|
||||
'closeOnPressEscape',
|
||||
'closeOnHashChange',
|
||||
'center',
|
||||
'roundButton',
|
||||
'closeDelay',
|
||||
'zIndex',
|
||||
'modal',
|
||||
'modalFade',
|
||||
'modalClass',
|
||||
'modalAppendToBody',
|
||||
'lockScroll',
|
||||
]
|
||||
|
||||
// component default merge props & data
|
||||
const defaults = {
|
||||
title: null,
|
||||
message: '',
|
||||
type: '',
|
||||
iconClass: '',
|
||||
showInput: false,
|
||||
showClose: true,
|
||||
modalFade: true,
|
||||
lockScroll: true,
|
||||
closeOnClickModal: true,
|
||||
closeOnPressEscape: true,
|
||||
closeOnHashChange: true,
|
||||
inputValue: null,
|
||||
inputPlaceholder: '',
|
||||
inputType: 'text',
|
||||
inputPattern: null,
|
||||
inputValidator: null,
|
||||
inputErrorMessage: '',
|
||||
showConfirmButton: true,
|
||||
showCancelButton: false,
|
||||
confirmButtonPosition: 'right',
|
||||
confirmButtonHighlight: false,
|
||||
cancelButtonHighlight: false,
|
||||
confirmButtonText: '',
|
||||
cancelButtonText: '',
|
||||
confirmButtonClass: '',
|
||||
cancelButtonClass: '',
|
||||
customClass: '',
|
||||
beforeClose: null,
|
||||
dangerouslyUseHTMLString: false,
|
||||
center: false,
|
||||
roundButton: false,
|
||||
distinguishCancelAndClose: false,
|
||||
}
|
||||
|
||||
let msgQueue = []
|
||||
|
||||
const defaultCallback = (action, ctx) => {
|
||||
if (currentMsg) {
|
||||
const callback = currentMsg.callback
|
||||
if (typeof callback === 'function') {
|
||||
if (ctx.showInput) {
|
||||
callback(ctx.inputValue, action)
|
||||
} else {
|
||||
callback(action)
|
||||
}
|
||||
}
|
||||
if (currentMsg.resolve) {
|
||||
if (action === 'confirm') {
|
||||
if (ctx.showInput) {
|
||||
currentMsg.resolve({ value: ctx.inputValue, action })
|
||||
} else {
|
||||
currentMsg.resolve(action)
|
||||
}
|
||||
} else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {
|
||||
currentMsg.reject(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initInstance = () => {
|
||||
const container = document.createElement('div')
|
||||
const vnode = createVNode(MessageBoxConstructor)
|
||||
render(vnode, container)
|
||||
instance = vnode.component
|
||||
}
|
||||
|
||||
const showNextMsg = async () => {
|
||||
if (!instance) {
|
||||
initInstance()
|
||||
}
|
||||
if (instance && instance.setupInstall.state.visible) { return }
|
||||
if (msgQueue.length > 0) {
|
||||
const props = {}
|
||||
const state = {}
|
||||
currentMsg = msgQueue.shift()
|
||||
const options = currentMsg.options
|
||||
Object.keys(options).forEach(key => {
|
||||
if (PROP_KEYS.includes(key)) {
|
||||
props[key] = options[key]
|
||||
} else {
|
||||
state[key] = options[key]
|
||||
}
|
||||
})
|
||||
// update props to instance/**/
|
||||
const vmProps = instance.props
|
||||
for (const prop in props) {
|
||||
if (props.hasOwnProperty(prop)) {
|
||||
vmProps[prop] = props[prop]
|
||||
}
|
||||
}
|
||||
const vmState = instance.setupInstall.state
|
||||
vmState.action = ''
|
||||
if (options.callback === undefined) {
|
||||
options.callback = defaultCallback
|
||||
}
|
||||
for (const prop in state) {
|
||||
if (state.hasOwnProperty(prop)) {
|
||||
vmState[prop] = state[prop]
|
||||
}
|
||||
}
|
||||
if (isVNode(options.message)) {
|
||||
instance.slots.default = () => [options.message]
|
||||
}
|
||||
const oldCb = options.callback
|
||||
vmState.callback = (action, inst) => {
|
||||
oldCb(action, inst)
|
||||
showNextMsg()
|
||||
}
|
||||
document.body.appendChild(instance.vnode.el)
|
||||
vmState.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
const MessageBox = function(options: ElMessageBoxOptions | string, callback?): Promise<any> {
|
||||
if (isServer) return
|
||||
if (typeof options === 'string' || isVNode(options)) {
|
||||
options = {
|
||||
message: options,
|
||||
}
|
||||
if (typeof callback === 'string') {
|
||||
options.title = callback
|
||||
}
|
||||
} else if (options.callback && !callback) {
|
||||
callback = options.callback
|
||||
}
|
||||
if (typeof Promise !== 'undefined') {
|
||||
return new Promise((resolve, reject) => {
|
||||
msgQueue.push({
|
||||
options: Object.assign({}, defaults, options),
|
||||
callback: callback,
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
})
|
||||
|
||||
showNextMsg()
|
||||
})
|
||||
} else {
|
||||
msgQueue.push({
|
||||
options: Object.assign({}, defaults, options),
|
||||
callback: callback,
|
||||
})
|
||||
|
||||
showNextMsg()
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.alert = (message, title, options?: ElMessageBoxOptions) => {
|
||||
if (typeof title === 'object') {
|
||||
options = title
|
||||
title = ''
|
||||
} else if (title === undefined) {
|
||||
title = ''
|
||||
}
|
||||
return MessageBox(Object.assign({
|
||||
title: title,
|
||||
message: message,
|
||||
type$: 'alert',
|
||||
closeOnPressEscape: false,
|
||||
closeOnClickModal: false,
|
||||
}, options))
|
||||
}
|
||||
|
||||
MessageBox.confirm = (message, title, options?: ElMessageBoxOptions) => {
|
||||
if (typeof title === 'object') {
|
||||
options = title
|
||||
title = ''
|
||||
} else if (title === undefined) {
|
||||
title = ''
|
||||
}
|
||||
return MessageBox(Object.assign({
|
||||
title: title,
|
||||
message: message,
|
||||
type$: 'confirm',
|
||||
showCancelButton: true,
|
||||
}, options))
|
||||
}
|
||||
|
||||
MessageBox.prompt = (message, title, options?: ElMessageBoxOptions) => {
|
||||
if (typeof title === 'object') {
|
||||
options = title
|
||||
title = ''
|
||||
} else if (title === undefined) {
|
||||
title = ''
|
||||
}
|
||||
return MessageBox(Object.assign({
|
||||
title: title,
|
||||
message: message,
|
||||
showCancelButton: true,
|
||||
showInput: true,
|
||||
type$: 'prompt',
|
||||
}, options))
|
||||
}
|
||||
|
||||
MessageBox.close = () => {
|
||||
instance.setupInstall.doClose()
|
||||
instance.setupInstall.state.visible = false
|
||||
msgQueue = []
|
||||
currentMsg = null
|
||||
}
|
||||
|
||||
export default MessageBox
|
206
packages/utils/popup/usePopup.ts
Normal file
206
packages/utils/popup/usePopup.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import {
|
||||
nextTick,
|
||||
reactive,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import PopupManager from '../popup-manager'
|
||||
import getScrollBarWidth from '../scrollbar-width'
|
||||
import { getStyle, addClass, removeClass, hasClass } from '../dom'
|
||||
import isServer from '../isServer'
|
||||
|
||||
let idSeed = 1
|
||||
|
||||
let scrollBarWidth
|
||||
|
||||
export const usePopup1 = {
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
if (this._opening) return
|
||||
if (!this.rendered) {
|
||||
this.rendered = true
|
||||
nextTick(() => {
|
||||
this.open()
|
||||
}).then(() => {
|
||||
//
|
||||
})
|
||||
} else {
|
||||
this.open()
|
||||
}
|
||||
} else {
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const usePopup = (props, doClose) => {
|
||||
let _popupId
|
||||
let _opening = false
|
||||
let _closing = false
|
||||
let vm
|
||||
let _closeTimer = null
|
||||
let _openTimer = null
|
||||
const state = reactive<State>({
|
||||
opened: false,
|
||||
bodyPaddingRight: null,
|
||||
computedBodyPaddingRight: 0,
|
||||
withoutHiddenClass: true,
|
||||
rendered: false,
|
||||
visible: false,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
vm = getCurrentInstance()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
_popupId = 'popup-' + idSeed++
|
||||
PopupManager.register(_popupId, vm)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
PopupManager.deregister(_popupId)
|
||||
PopupManager.closeModal(_popupId)
|
||||
restoreBodyStyle()
|
||||
})
|
||||
|
||||
const doOpen = merProps => {
|
||||
if (isServer) return
|
||||
if (state.opened) return
|
||||
|
||||
_opening = true
|
||||
|
||||
const dom = vm.vnode.el
|
||||
|
||||
const modal = merProps.modal
|
||||
|
||||
const zIndex = merProps.zIndex
|
||||
if (zIndex) {
|
||||
PopupManager.zIndex = zIndex
|
||||
}
|
||||
|
||||
if (modal) {
|
||||
if (_closing) {
|
||||
PopupManager.closeModal(_popupId)
|
||||
_closing = false
|
||||
}
|
||||
PopupManager.openModal(_popupId, PopupManager.nextZIndex(), props.modalAppendToBody ? undefined : dom, merProps.modalClass, merProps.modalFade)
|
||||
if (merProps.lockScroll) {
|
||||
state.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden')
|
||||
if (state.withoutHiddenClass) {
|
||||
state.bodyPaddingRight = document.body.style.paddingRight
|
||||
state.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10)
|
||||
}
|
||||
scrollBarWidth = getScrollBarWidth()
|
||||
const bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight
|
||||
const bodyOverflowY = getStyle(document.body, 'overflowY')
|
||||
if (scrollBarWidth > 0 && (bodyHasOverflow || bodyOverflowY === 'scroll') && state.withoutHiddenClass) {
|
||||
document.body.style.paddingRight = state.computedBodyPaddingRight + scrollBarWidth + 'px'
|
||||
}
|
||||
addClass(document.body, 'el-popup-parent--hidden')
|
||||
}
|
||||
}
|
||||
|
||||
if (getComputedStyle(dom).position === 'static') {
|
||||
dom.style.position = 'absolute'
|
||||
}
|
||||
|
||||
dom.style.zIndex = PopupManager.nextZIndex()
|
||||
state.opened = true
|
||||
|
||||
doAfterOpen()
|
||||
}
|
||||
|
||||
const open = function (options?) {
|
||||
if (!state.rendered) {
|
||||
state.rendered = true
|
||||
}
|
||||
const _props = Object.assign({}, props || vm.proxy, options)
|
||||
|
||||
if (_closeTimer) {
|
||||
clearTimeout(_closeTimer)
|
||||
_closeTimer = null
|
||||
}
|
||||
clearTimeout(_openTimer)
|
||||
|
||||
const openDelay = Number(_props.openDelay)
|
||||
if (openDelay > 0) {
|
||||
_openTimer = setTimeout(() => {
|
||||
_openTimer = null
|
||||
doOpen(_props)
|
||||
}, openDelay)
|
||||
} else {
|
||||
doOpen(_props)
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (_openTimer !== null) {
|
||||
clearTimeout(_openTimer)
|
||||
_openTimer = null
|
||||
}
|
||||
clearTimeout(_closeTimer)
|
||||
|
||||
const closeDelay = Number(props.closeDelay)
|
||||
|
||||
if (closeDelay > 0) {
|
||||
_closeTimer = setTimeout(() => {
|
||||
_closeTimer = null
|
||||
doClose()
|
||||
}, closeDelay)
|
||||
} else {
|
||||
doClose()
|
||||
}
|
||||
}
|
||||
|
||||
const doAfterOpen = () => {
|
||||
_opening = false
|
||||
}
|
||||
|
||||
const restoreBodyStyle = () => {
|
||||
if (props.modal && state.withoutHiddenClass) {
|
||||
document.body.style.paddingRight = state.bodyPaddingRight
|
||||
removeClass(document.body, 'el-popup-parent--hidden')
|
||||
}
|
||||
state.withoutHiddenClass = true
|
||||
}
|
||||
|
||||
const doAfterClose = () => {
|
||||
PopupManager.closeModal(_popupId)
|
||||
_closing = false
|
||||
}
|
||||
|
||||
const updateClosingFlag = value => {
|
||||
_closing = value
|
||||
}
|
||||
|
||||
watch(() => state.visible, async val => {
|
||||
if (val) {
|
||||
if (_opening) return
|
||||
if (!state.rendered) {
|
||||
state.rendered = true
|
||||
await nextTick()
|
||||
open()
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
state,
|
||||
close,
|
||||
doAfterClose,
|
||||
updateClosingFlag,
|
||||
restoreBodyStyle,
|
||||
}
|
||||
}
|
||||
|
||||
export default usePopup
|
Loading…
Reference in New Issue
Block a user