diff --git a/packages/element3/packages/message-box/__tests__/MessageBox.spec.js b/packages/element3/packages/message-box/__tests__/MessageBox.spec.js deleted file mode 100644 index da4d87b75..000000000 --- a/packages/element3/packages/message-box/__tests__/MessageBox.spec.js +++ /dev/null @@ -1,3 +0,0 @@ -describe('MessageBox.vue', () => { - test('todo', () => {}) -}) diff --git a/packages/element3/packages/theme-chalk/src/index.scss b/packages/element3/packages/theme-chalk/src/index.scss index 22e27c655..6f71cb034 100644 --- a/packages/element3/packages/theme-chalk/src/index.scss +++ b/packages/element3/packages/theme-chalk/src/index.scss @@ -72,6 +72,7 @@ @import "./input.scss"; @import "./link.scss"; @import "./message.scss"; +@import "./messageBox.scss"; @import "./notification.scss"; @import "./progress.scss"; @import "./radio.scss"; diff --git a/packages/element3/packages/theme-chalk/src/messageBox.scss b/packages/element3/packages/theme-chalk/src/messageBox.scss new file mode 100644 index 000000000..03db363b0 --- /dev/null +++ b/packages/element3/packages/theme-chalk/src/messageBox.scss @@ -0,0 +1,226 @@ +@import 'mixins/mixins'; +@import 'common/var'; +@import 'common/popup'; +@import 'button'; +@import 'input'; + +@include b(message-box) { + display: inline-block; + width: $--msgbox-width; + padding-bottom: 10px; + vertical-align: middle; + background-color: $--color-white; + border-radius: $--msgbox-border-radius; + border: 1px solid $--border-color-lighter; + font-size: $--messagebox-font-size; + box-shadow: $--box-shadow-light; + text-align: left; + overflow: hidden; + backface-visibility: hidden; + + @include e(wrapper) { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + text-align: center; + + &::after { + content: ''; + display: inline-block; + height: 100%; + width: 0; + vertical-align: middle; + } + } + + @include e(header) { + position: relative; + padding: $--msgbox-padding-primary; + padding-bottom: 10px; + } + + @include e(title) { + padding-left: 0; + margin-bottom: 0; + font-size: $--messagebox-font-size; + line-height: 1; + color: $--messagebox-title-color; + } + + @include e(headerbtn) { + position: absolute; + top: $--msgbox-padding-primary; + right: $--msgbox-padding-primary; + padding: 0; + border: none; + outline: none; + background: transparent; + font-size: $--message-close-size; + cursor: pointer; + + .el-message-box__close { + color: $--color-info; + } + + &:focus, + &:hover { + .el-message-box__close { + color: $--color-primary; + } + } + } + + @include e(content) { + padding: 10px $--msgbox-padding-primary; + color: $--messagebox-content-color; + font-size: $--messagebox-content-font-size; + } + + @include e(container) { + position: relative; + } + + @include e(input) { + padding-top: 15px; + + & input.invalid { + border-color: $--color-danger; + &:focus { + border-color: $--color-danger; + } + } + } + + @include e(status) { + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 24px !important; + + &::before { + // 防止图标切割 + padding-left: 1px; + } + + + .el-message-box__message { + padding-left: 36px; + padding-right: 12px; + } + + &.el-icon-success { + color: $--messagebox-success-color; + } + + &.el-icon-info { + color: $--messagebox-info-color; + } + + &.el-icon-warning { + color: $--messagebox-warning-color; + } + + &.el-icon-error { + color: $--messagebox-danger-color; + } + } + + @include e(message) { + margin: 0; + + & p { + margin: 0; + line-height: 24px; + } + } + + @include e(errormsg) { + color: $--color-danger; + font-size: $--messagebox-error-font-size; + min-height: 18px; + margin-top: 2px; + } + + @include e(btns) { + padding: 5px 15px 0; + text-align: right; + + & button:nth-child(2) { + margin-left: 10px; + } + } + + @include e(btns-reverse) { + flex-direction: row-reverse; + } + + // centerAlign 布局 + @include m(center) { + padding-bottom: 30px; + + @include e(header) { + padding-top: 30px; + } + + @include e(title) { + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + @include e(status) { + position: relative; + top: auto; + padding-right: 5px; + text-align: center; + transform: translateY(-1px); + } + + @include e(message) { + margin-left: 0; + } + + @include e((btns, content)) { + text-align: center; + } + + @include e(content) { + $padding-horizontal: $--msgbox-padding-primary + 12px; + + padding-left: $padding-horizontal; + padding-right: $padding-horizontal; + } + } +} + +.msgbox-fade-enter-active { + animation: msgbox-fade-in 0.3s; +} + +.msgbox-fade-leave-active { + animation: msgbox-fade-out 0.3s; +} + +@keyframes msgbox-fade-in { + 0% { + transform: translate3d(0, -20px, 0); + opacity: 0; + } + 100% { + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes msgbox-fade-out { + 0% { + transform: translate3d(0, 0, 0); + opacity: 1; + } + 100% { + transform: translate3d(0, -20px, 0); + opacity: 0; + } +} diff --git a/packages/element3/src/components/MessageBox/index.js b/packages/element3/src/components/MessageBox/index.js new file mode 100644 index 000000000..ecd39579e --- /dev/null +++ b/packages/element3/src/components/MessageBox/index.js @@ -0,0 +1 @@ +export { MessageBox as Msgbox } from './src/MessageBox.js' diff --git a/packages/element3/src/components/MessageBox/src/MessageBox.js b/packages/element3/src/components/MessageBox/src/MessageBox.js new file mode 100644 index 000000000..34d0c3ee1 --- /dev/null +++ b/packages/element3/src/components/MessageBox/src/MessageBox.js @@ -0,0 +1,129 @@ +import { isVNode } from 'vue' +import { isObject, isUndefined } from '../../../utils/types' +import { createComponent } from '../../../composables/component' +import msgboxVue from './MessageBox.vue' + +let currentMsg, instance +let msgQueue = [] + +const defaultCallback = (action) => { + if (currentMsg && currentMsg.resolve) { + const isConfirm = action === 'confirm' + const isCancelOrClose = action === 'cancel' || action === 'close' + const isReject = currentMsg.reject && isCancelOrClose + if (isReject) { + currentMsg.reject({ action }) + return + } + const isShow = instance.proxy.showInput + const value = instance.proxy.inputValue + const result = isShow ? { value, action } : { action } + if (isConfirm) { + currentMsg.resolve(result) + } + } +} + +const initInstance = (currentMsg, VNode = null) => { + instance = createComponent(msgboxVue, currentMsg.options, VNode) + MessageBox.instance = instance +} + +const showNextMsg = () => { + if (msgQueue.length <= 0) return + currentMsg = msgQueue.shift() + const options = currentMsg.options + + if (isUndefined(options.callback)) options.callback = defaultCallback + + const oldCb = options.callback + options.callback = (action, instance) => { + oldCb(action, instance) + } + + initInstance( + currentMsg, + isVNode(options.message) ? () => options.message : null + ) + document.body.appendChild(instance.vnode.el) +} + +const MessageBox = function (options) { + let callback = null + if (options.callback) { + callback = options.callback + } + let promiseInstance = new Promise((resolve, reject) => { + // eslint-disable-line + msgQueue.push({ + options: options, + callback: callback, + resolve: resolve, + reject: reject + }) + showNextMsg() + }) + promiseInstance.instance = instance + return promiseInstance +} + +const mergeCondition = (message, title, options) => { + if (isObject(title)) { + options = title + title = '' + } else if (isUndefined(title)) { + title = '' + } + if (isObject(message)) { + options = message + message = '' + } + return Object.assign( + { + title: title, + message: message, + confirmButtonText: '确认', + cancelButtonText: '取消' + }, + options + ) +} + +const kindOfMessageBox = { + alert: { + category: 'alert', + closeOnPressEscape: false + }, + confirm: { + type: 'info', + category: 'confirm', + showCancelButton: true + }, + prompt: { + showInput: true, + category: 'prompt', + showCancelButton: true, + inputErrorMessage: '输入的数据不合法!' + }, + msgbox: MessageBox +} + +for (let key in kindOfMessageBox) { + MessageBox[key] = (message, title, options) => { + return MessageBox( + Object.assign( + kindOfMessageBox[key], + mergeCondition(message, title, options) + ) + ) + } +} + +MessageBox.close = () => { + instance.proxy.closeHandle() + msgQueue = [] + currentMsg = null +} + +export default MessageBox +export { MessageBox } diff --git a/packages/element3/src/components/MessageBox/src/MessageBox.vue b/packages/element3/src/components/MessageBox/src/MessageBox.vue new file mode 100644 index 000000000..904061e95 --- /dev/null +++ b/packages/element3/src/components/MessageBox/src/MessageBox.vue @@ -0,0 +1,144 @@ + + + diff --git a/packages/element3/src/components/MessageBox/src/props.js b/packages/element3/src/components/MessageBox/src/props.js new file mode 100644 index 000000000..561dc0173 --- /dev/null +++ b/packages/element3/src/components/MessageBox/src/props.js @@ -0,0 +1,150 @@ +import { t } from '../../../../src/locale' +const props = { + inputPattern: { + type: RegExp, + default: null + }, + inputValidator: { + type: [Function], + default: null + }, + inputErrorMessage: { + type: String, + default: () => t('el.messagebox.error') + }, + modalFade: { + type: Boolean, + default: true + }, + callback: { + type: Function, + default: () => {} + }, + closeOnHashChange: { + type: Boolean, + default: true + }, + closeOnPressEscape: { + type: Boolean, + default: true + }, + distinguishCancelAndClose: { + type: Boolean, + default: false + }, + closeOnClickModal: { + type: Boolean, + default: true + }, + cancelButtonLoading: { + type: Boolean, + default: false + }, + roundButton: { + type: Boolean, + default: false + }, + cancelButtonClass: { + type: String, + default: null + }, + confirmButtonClass: { + type: String, + default: null + }, + showCancelButton: { + type: Boolean, + default: false + }, + showConfirmButton: { + type: Boolean, + default: true + }, + confirmButtonText: { + type: String, + default: () => t('el.messagebox.cancel') + }, + cancelButtonText: { + type: String, + default: () => t('el.messagebox.confirm') + }, + category: { + type: String, + default: 'alert', + validator(val) { + return ['confirm', 'prompt', 'alert'].includes(val) + } + }, + inputValue: { + type: String, + default: '' + }, + inputPlaceholder: { + type: String, + default: '' + }, + inputType: { + type: String, + default: 'text' + }, + showInput: { + type: Boolean, + default: false + }, + dangerouslyUseHTMLString: { + type: Boolean, + default: false + }, + message: { + type: [Object, String], + default() { + return {} + } + }, + lockScroll: { + type: Boolean, + default: true + }, + modalAppendToBody: { + type: Boolean, + default: false + }, + modal: { + type: Boolean, + default: true + }, + center: { + type: Boolean, + default: false + }, + title: { + type: String, + default: null + }, + customClass: { + type: String, + default: null + }, + type: { + type: [String, null], + default: null, + validator(val) { + return ( + ['success', 'warning', 'info', 'error'].includes(val) || val === null + ) + } + }, + iconClass: { + type: String, + default: null + }, + showClose: { + type: Boolean, + default: true + }, + beforeClose: { + type: Function, + default: null + } +} +export default props diff --git a/packages/element3/src/components/MessageBox/src/use.js b/packages/element3/src/components/MessageBox/src/use.js new file mode 100644 index 000000000..19db8791d --- /dev/null +++ b/packages/element3/src/components/MessageBox/src/use.js @@ -0,0 +1,89 @@ +import { toRefs, nextTick, unref, onMounted, onUnmounted, computed } from 'vue' +import { usePopup } from '../../../composables/popup' +import { isFunction } from '@vue/shared' +export function useHandleList(state, instance, validate) { + const { close } = usePopup(state) + const { + visible, + callback, + category, + inputType, + distinguishCancelAndClose, + closeOnClickModal, + action, + beforeClose + } = toRefs(state) + const closeHandle = () => { + visible.value = false + close() + nextTick(() => { + unref(callback)(state.action, instance.proxy) + }) + } + const handleAction = (_action) => { + if (unref(category) === 'prompt' && _action === 'confirm' && !validate()) { + return + } + action.value = _action + if (isFunction(beforeClose.value)) { + beforeClose.value(_action, instance.proxy, closeHandle) + } else { + closeHandle() + } + } + + const handleInputEnter = () => { + if (unref(inputType) !== 'textarea') { + return handleAction('confirm') + } + } + + const handleWrapperClick = () => { + if (unref(closeOnClickModal)) { + handleAction(unref(distinguishCancelAndClose) ? 'close' : 'cancel') + } + } + + return { + closeHandle, + handleAction, + handleWrapperClick, + handleInputEnter + } +} + +export function watchElement(state, handleAction, closeHandle) { + const { + closeOnHashChange, + closeOnPressEscape, + distinguishCancelAndClose + } = toRefs(state) + const { open } = usePopup(state) + const handleKeyup = (element = {}) => { + if (element.code !== 'Escape') return + if (unref(closeOnPressEscape)) { + handleAction(unref(distinguishCancelAndClose) ? 'close' : 'cancel') + } + } + onMounted(() => { + if (unref(closeOnHashChange)) { + window.addEventListener('hashchange', closeHandle) + } + window.addEventListener('keyup', handleKeyup) + nextTick(() => { + open() + }) + }) + onUnmounted(() => { + if (unref(closeOnHashChange)) { + window.removeEventListener('hashchange', closeHandle) + } + window.removeEventListener('keyup', handleKeyup) + }) +} + +export function classIcon(iconClass, type) { + return computed(() => { + return unref(iconClass) || (unref(type) ? `el-icon-${unref(type)}` : '') + }) +} diff --git a/packages/element3/src/components/MessageBox/src/validate.js b/packages/element3/src/components/MessageBox/src/validate.js new file mode 100644 index 000000000..7f47b9fbf --- /dev/null +++ b/packages/element3/src/components/MessageBox/src/validate.js @@ -0,0 +1,57 @@ +import { toRefs, unref, ref, watch, nextTick, computed } from 'vue' +import { addClass, removeClass } from '../../../utils/dom' +export default (state, instance) => { + const editorErrorMessage = ref(null) + const { + category, + inputPattern, + inputValue, + inputValidator, + inputErrorMessage + } = toRefs(state) + const getInputElement = () => { + const inputRefs = instance.refs.input.$refs + return inputRefs.input || inputRefs.textarea + } + function getValidateResult(errorMsg, value) { + editorErrorMessage.value = errorMsg + value + ? removeClass(getInputElement(), 'invalid') + : addClass(getInputElement(), 'invalid') + return value + } + const isCategoryPrompt = computed(() => { + return unref(category) === 'prompt' + }) + const isEegularResult = computed(() => { + const v = + unref(inputPattern) && !unref(inputPattern).test(unref(inputValue)) + return unref(isCategoryPrompt) && v + }) + + const validate = () => { + if (unref(isEegularResult)) { + return getValidateResult(unref(inputErrorMessage), false) + } + const _inputValidator = unref(inputValidator) + if (typeof _inputValidator === 'function' && unref(isCategoryPrompt)) { + const validateResult = _inputValidator(unref(inputValue)) + const isString = typeof validateResult === 'string' + if (isString) return getValidateResult(validateResult, false) + if (!validateResult) + return getValidateResult(unref(inputErrorMessage), false) + } + return getValidateResult('', true) + } + watch(inputValue, (val) => { + nextTick(() => { + if (unref(category) === 'prompt' && val !== null) { + validate() + } + }) + }) + return { + validate, + editorErrorMessage + } +} diff --git a/packages/element3/src/components/MessageBox/tests/MessageBox.spec.js b/packages/element3/src/components/MessageBox/tests/MessageBox.spec.js new file mode 100644 index 000000000..0382b1412 --- /dev/null +++ b/packages/element3/src/components/MessageBox/tests/MessageBox.spec.js @@ -0,0 +1,134 @@ +import { flushPromises } from '@vue/test-utils' +import { nextTick, h } from 'vue' +import { merge } from 'lodash-es' +import messageBox, { MessageBox } from '../src/MessageBox.js' +const selector = '.el-message-box__wrapper' +function testCallback(name, options = {}) { + const message = '请输入邮箱' + const o = { + prompt: { + message, + confirmButtonText: '确定', + cancelButtonText: '取消', + cancelButtonClass: 'mmm' + }, + confirm: { + message, + confirmButtonText: '确定', + cancelButtonText: '取消', + confirmButtonClass: 'mmm' + }, + msgbox: { + message, + confirmButtonText: '确定', + cancelButtonText: '取消', + confirmButtonClass: 'mmm' + } + } + return messageBox[name](merge(o[name], options)) +} +describe('MessageBox.js', () => { + afterEach(() => { + const el = document.querySelector('.el-message-box__wrapper') + if (!el) return + if (el.parentNode) { + el.parentNode.removeChild(el) + } + }) + test('alert', async () => { + const { instance } = messageBox.alert({ + title: '消息', + message: '这是一段内容' + }) + + expect(instance.props.title).toBe('消息') + expect(instance.props.message).toEqual('这是一段内容') + }) + test('messageBox of message is html', async () => { + let instanceProprety = '' + const callback = jest.fn((action, instance) => { + instanceProprety = instance + }) + const { instance } = messageBox.alert( + `这是 HTML 片段`, + `html片段`, + { + dangerouslyUseHTMLString: true, + callback + } + ) + expect(instance.proxy.message).toBe( + '这是 HTML 片段' + ) + expect(instance.proxy.title).toBe('html片段') + expect(instance.proxy.dangerouslyUseHTMLString).toBeTruthy() + instance.proxy.closeHandle() + await flushPromises() + expect(instanceProprety.message).toBeTruthy() + expect(callback).toHaveBeenCalled() + }) + test('confirm', async () => { + const { instance } = messageBox.confirm({ + type: 'warning', + title: '消息', + message: '这是一段内容' + }) + expect(instance.props.type).toBe('warning') + }) + test('kind of prompt', async () => { + let v = '' + const callback = jest.fn(({ value }) => { + v = value + }) + const instance = testCallback('prompt', { + confirmButtonClass: 'mmmm', + inputPattern: /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/, + inputErrorMessage: '邮箱格式不正确' + }) + const btn = document.querySelector('.mmmm') + instance.then(callback) + btn.click() + await flushPromises() + expect(callback).not.toHaveBeenCalled() + instance.instance.proxy.inputValue = '409187100@qq.com' + btn.click() + await flushPromises() + expect(callback).toHaveBeenCalled() + expect(v).toBeTruthy() + }) + test('was invoked', async () => { + const callback = jest.fn(() => {}) + const instance = testCallback('prompt', { + message: '请输入邮箱' + }) + + instance.catch(callback) + await nextTick() + const btn = document.querySelector('.mmm') + await btn.click() + await flushPromises() + expect(callback).toHaveBeenCalled() + }) + test('paramter', async () => { + const { instance } = messageBox.alert('请输入邮箱', { title: 'aaa' }) + expect(instance.proxy.title).toBe('aaa') + expect(instance.proxy.message).toBe('请输入邮箱') + }) + + test('showInput is false', async () => { + const callback = jest.fn(() => {}) + const instance = testCallback('confirm') + instance.then(callback) + const btn = document.querySelector('.mmm') + await btn.click() + await flushPromises() + expect(callback).toHaveBeenCalled() + }) + test('message is vnode', async () => { + const message = h('div', '4') + const { instance } = MessageBox.alert({ + message + }) + expect(instance.proxy.$slots.default()[0]).toEqual(message) + }) +}) diff --git a/packages/element3/src/components/MessageBox/tests/MessageBox.vue.spec.js b/packages/element3/src/components/MessageBox/tests/MessageBox.vue.spec.js new file mode 100644 index 000000000..3eac0d2e6 --- /dev/null +++ b/packages/element3/src/components/MessageBox/tests/MessageBox.vue.spec.js @@ -0,0 +1,343 @@ +import { mount } from '@vue/test-utils' +import { nextTick, h, ref } from 'vue' +import MessageBox from '../src/MessageBox.vue' +describe('MessageBox.vue', () => { + describe('snapshot', () => { + const wrapper = mount(MessageBox) + expect(wrapper.element).toMatchSnapshot() + }) + describe('proprety', () => { + it('proprety title', () => { + const wrapper = mount(MessageBox, { + props: { + title: 'chushihua' + } + }) + expect(wrapper.get('.el-message-box__title')).toHaveTextContent( + 'chushihua' + ) + }) + it('proprety center', () => { + const wrapper = mount(MessageBox, { + props: { + center: true + } + }) + expect(wrapper.get('.el-message-box--center').exists()).toBeTruthy() + }) + it('proprety customClass', () => { + const wrapper = mount(MessageBox, { + props: { + customClass: 'customClass' + } + }) + expect(wrapper.get('.customClass').exists()).toBeTruthy() + }) + it('proprety iconClass', () => { + const wrapper = mount(MessageBox, { + props: { + iconClass: 'iconClass' + } + }) + expect(wrapper.vm.icon).toBe('iconClass') + }) + it('iconclass with center', () => { + const wrapper = mount(MessageBox, { + props: { + iconClass: 'iconClass', + center: true, + title: 'title' + } + }) + expect(wrapper.get('.iconClass').exists()).toBeTruthy() + }) + it('proprety type', () => { + const wrapper = mount(MessageBox, { + props: { + title: 'title', + center: true, + type: 'info' + } + }) + expect(wrapper.find('.el-icon-info').exists()).toBeTruthy() + }) + it('showClose', () => { + const wrapper = mount(MessageBox, { + props: { + title: '12', + showClose: false + } + }) + expect(wrapper.find('.el-message-box__headerbtn').exists()).toBeFalsy() + }) + it('showClose toBeTruthy', () => { + const wrapper = mount(MessageBox, { + props: { + title: '12' + } + }) + expect(wrapper.get('.el-message-box__headerbtn').exists()).toBeTruthy() + }) + it('proprety beforeClose', async () => { + let object = {} + const wrapper = mount(MessageBox, { + props: { + title: '12', + beforeClose: (action, instance, done) => { + object.action = action + object.instance = instance + object.done = done + } + } + }) + await wrapper.get('.el-message-box__headerbtn').trigger('click') + expect(wrapper.componentVM).toEqual(object.instance) + expect(object.action).toBe('cancel') + }) + it('review messageBox when value was done', async () => { + const wrapper = mount(MessageBox, { + props: { + title: '12', + beforeClose: (action, instance, done) => { + done() + } + } + }) + await wrapper.get('.el-message-box__headerbtn').trigger('click') + expect(wrapper.get('.el-message-box__headerbtn').isVisible()).toBeFalsy() + expect(wrapper.find('.v-modal').exists()).toBeFalsy() + }) + it('showClose lockScroll', () => { + document.body.classList.remove('el-popup-parent--hidden') + mount(MessageBox, { + props: { + title: '12', + lockScroll: false + }, + attachTo: document.getElementById('body') + }) + expect(document.body.className).not.toBe('el-popup-parent--hidden') + }) + it('meesage of normal', () => { + const wrapper = mount(MessageBox, { + props: { + message: '333' + } + }) + expect(wrapper.get('.el-message-box__message > p')).toHaveTextContent( + '333' + ) + }) + it('meesage is VNode', () => { + const v = h('p', null, [ + h('span', null, '内容可以是 '), + h('i', { style: 'color: teal' }, 'VNode') + ]) + const wrapper = mount(MessageBox, { + slots: { + default: v + } + }) + expect(wrapper.componentVM.$slots.default()[0]).toEqual(v) + }) + it('dangerouslyUseHTMLString', () => { + const wrapper = mount(MessageBox, { + props: { + dangerouslyUseHTMLString: true, + message: '
444
' + } + }) + expect(wrapper.get('.mmm').exists()).toBeTruthy() + }) + it('handleInputEnter', async () => { + const wrapper = mount(MessageBox, { + props: { + inputType: 'texg' + } + }) + wrapper.componentVM.handleInputEnter() + await nextTick() + expect(wrapper.componentVM.visible).toBeFalsy() + }) + it('closeOnPressEscape', async () => { + const wrapper = mount(MessageBox, { + props: { + closeOnPressEscape: true + } + }) + const event = new KeyboardEvent('keyup', { + code: 'Escape' + }) + window.dispatchEvent(event) + expect(wrapper.get('.el-message-box__wrapper').isVisible()).toBeTruthy() + expect(wrapper.componentVM.visible).toBeFalsy() + }) + it('handleWrapperClick', async () => { + const wrapper = mount(MessageBox, { + props: { + closeOnClickModal: true + } + }) + await wrapper.get('.el-message-box__wrapper').trigger('click') + await nextTick() + expect(wrapper.componentVM.action).toBe('cancel') + }) + it('closeOnHashChange', async () => { + const wrapper = mount(MessageBox, { + props: { + closeOnHashChange: true + } + }) + window.dispatchEvent(new Event('hashchange')) + expect(wrapper.get('.el-message-box__wrapper').isVisible()).toBeTruthy() + expect(wrapper.componentVM.visible).toBeFalsy() + }) + it('showInput', () => { + const wrapper = mount(MessageBox, { + props: { + showInput: true + } + }) + expect(wrapper.get('.el-message-box__input').isVisible()).toBeTruthy() + }) + it('inputValue inputPlaceholder', () => { + const wrapper = mount(MessageBox, { + props: { + showInput: true, + inputValue: '444', + inputPlaceholder: '555' + } + }) + expect(wrapper.get('.el-input__inner').element.value).toEqual('444') + expect(wrapper.get('.el-input__inner').attributes('placeholder')).toEqual( + '555' + ) + }) + it('cancelButtonText, showCancelButton', () => { + const wrapper = mount(MessageBox, { + props: { + cancelButtonText: 'cancel', + showCancelButton: true, + cancelButtonClass: 'vvv' + } + }) + expect(wrapper.get('.vvv')).toHaveTextContent('cancel') + expect(wrapper.get('.vvv').isVisible()).toBeTruthy() + }) + it('confirmButtonClass showConfirmButton, confirmButtonText', () => { + const wrapper = mount(MessageBox, { + props: { + confirmButtonText: 'cancel', + showConfirmButton: true, + confirmButtonClass: 'mmm' + } + }) + expect(wrapper.get('.el-button--primary').classes()).toContain('mmm') + expect(wrapper.get('.mmm')).toHaveTextContent('cancel') + expect(wrapper.get('.el-message-box__btns').isVisible()).toBeTruthy() + }) + it('proprety callback', async () => { + let object = ref(null) + const wrapper = mount(MessageBox, { + props: { + title: '12', + callback(action) { + object.value = action + } + } + }) + await wrapper.get('.el-message-box__headerbtn').trigger('click') + await nextTick() + expect(object.value).toBe('cancel') + }) + it('validate correct', async () => { + const wrapper = mount(MessageBox, { + props: { + title: '12', + category: 'prompt', + inputPattern: /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/, + confirmButtonText: '确定', + cancelButtonText: '取消' + } + }) + await wrapper.findComponent({ ref: 'input' }).setValue('2323') + await nextTick() + expect(wrapper.componentVM.editorErrorMessage).toBe('输入的数据不合法!') + expect(wrapper.get('.el-input__inner').classes()).toHaveLength(2) + }) + it('validate fail', async () => { + const wrapper = mount(MessageBox, { + props: { + title: '12', + category: 'prompt', + inputValidator() { + return '失败' + }, + confirmButtonText: '确定', + cancelButtonText: '取消' + } + }) + await wrapper.findComponent({ ref: 'input' }).setValue('2323') + await nextTick() + expect(wrapper.get('.el-input__inner').classes()).toHaveLength(2) + expect(wrapper.componentVM.editorErrorMessage).toBe('失败') + }) + it('inputValidator', async () => { + const wrapper = mount(MessageBox, { + props: { + title: '12', + category: 'prompt', + inputValidator() { + return false + }, + confirmButtonText: '确定', + cancelButtonText: '取消' + } + }) + await wrapper.findComponent({ ref: 'input' }).setValue('2323') + await nextTick() + expect(wrapper.get('.el-input__inner').classes()).toHaveLength(2) + }) + }) + describe('test modal closeOnClickModal', () => { + it('open modal', async () => { + const wrapper = mount(MessageBox, { + props: { + modal: true, + closeOnClickModal: true + } + }) + await nextTick() + expect(wrapper.get('.v-modal').exists()).toBeTruthy() + await wrapper.get('.v-modal').trigger('click') + await nextTick() + expect(wrapper.find('.v-moda').exists()).toBeFalsy() + }) + it('open modal', async () => { + const wrapper = mount(MessageBox, { + props: { + modal: true, + closeOnClickModal: true + } + }) + await nextTick() + expect(wrapper.get('.v-modal').exists()).toBeTruthy() + await wrapper.get('.v-modal').trigger('keyup', { keyCode: 'Escape' }) + await nextTick() + expect(wrapper.find('.v-moda').exists()).toBeFalsy() + }) + it('open modal', async () => { + const wrapper = mount(MessageBox, { + props: { + modal: true, + closeOnClickModal: true + } + }) + await nextTick() + expect(wrapper.get('.v-modal').exists()).toBeTruthy() + await wrapper.get('.v-modal').trigger('hashchange') + await nextTick() + expect(wrapper.find('.v-moda').exists()).toBeFalsy() + }) + }) +}) diff --git a/packages/element3/src/components/MessageBox/tests/Prop.spec.js b/packages/element3/src/components/MessageBox/tests/Prop.spec.js new file mode 100644 index 000000000..4e1264919 --- /dev/null +++ b/packages/element3/src/components/MessageBox/tests/Prop.spec.js @@ -0,0 +1,10 @@ +import props from '../src/props.js' +describe('porps', () => { + test('type', () => { + expect(props.type.validator('success')).toBeTruthy() + expect(props.type.validator()).toBeFalsy() + }) + test('category', () => { + expect(props.category.validator('alert')).toBeTruthy() + }) +}) diff --git a/packages/element3/src/components/MessageBox/tests/Validate.spec.js b/packages/element3/src/components/MessageBox/tests/Validate.spec.js new file mode 100644 index 000000000..5b6749636 --- /dev/null +++ b/packages/element3/src/components/MessageBox/tests/Validate.spec.js @@ -0,0 +1,57 @@ +import validateFunction from '../src/validate' +import { mount } from '@vue/test-utils' +import { ElInput } from '../../Input' +import { reactive, getCurrentInstance } from 'vue' +describe('it was the validate functional ', () => { + function createComponent(data) { + return { + setup() { + const instance = getCurrentInstance() + const state = data + const { validate } = validateFunction(state, instance) + return { + validate, + inputValue: state.inputValue, + inputErrorMessage: state.inputErrorMessage + } + }, + components: { + ElInput + }, + template: `
` + } + } + it('the validate returned false when property was not have inputValidator', async () => { + let data = reactive({ + inputValue: null, + category: 'prompt', + inputPattern: /^1.+/, + inputErrorMessage: 'aaa' + }) + const wrapper = mount(createComponent(data)) + expect(wrapper.vm.validate()).toBeFalsy() + expect(wrapper.find('.invalid')).toBeTruthy() + expect(wrapper.vm.inputErrorMessage).toBe('aaa') + }) + it('when the inputValidator was the function and was the true or the false', async () => { + let data = reactive({ + inputValue: 232, + category: 'prompt', + inputErrorMessage: 'aaa', + inputValidator(value) { + return value === null ? false : value === true ? true : 'true' + } + }) + const wrapper = mount(createComponent(data)) + expect(wrapper.vm.validate()).toBeFalsy() + expect(wrapper.find('.invalid')).toBeTruthy() + expect(wrapper.vm.inputErrorMessage).toBe('aaa') + wrapper.vm.inputValue = '2' + expect(wrapper.vm.validate()).toBeFalsy() + expect(wrapper.find('.invalid')).toBeTruthy() + expect(wrapper.vm.inputErrorMessage).toBe('aaa') + data.inputValue = true + expect(wrapper.vm.validate()).toBeTruthy() + expect(wrapper.find('.invalid').exists()).toBeFalsy() + }) +}) diff --git a/packages/element3/src/components/MessageBox/tests/__snapshots__/MessageBox.vue.spec.js.snap b/packages/element3/src/components/MessageBox/tests/__snapshots__/MessageBox.vue.spec.js.snap new file mode 100644 index 000000000..dd382c022 --- /dev/null +++ b/packages/element3/src/components/MessageBox/tests/__snapshots__/MessageBox.vue.spec.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = ` + +
+
+ +
+
+ +
+ +

+ {} +

+ +
+
+ +
+ + +
+
+
+ +`; diff --git a/packages/element3/src/composables/popup.js b/packages/element3/src/composables/popup.js index 1b51d04ff..aedf061af 100644 --- a/packages/element3/src/composables/popup.js +++ b/packages/element3/src/composables/popup.js @@ -7,9 +7,9 @@ import { watch, toRefs } from 'vue' +import { merge } from 'lodash-es' import PopupManager from '../../src/utils/popup/popup-manager' import getScrollBarWidth from '../../src/utils/scrollbar-width' -import merge from '../../src/utils/merge' import { getStyle, addClass, removeClass, hasClass } from '../../src/utils/dom' let idSeed = 1 @@ -53,6 +53,7 @@ function usePopup(props) { const { visible, modal, modalAppendToBody, lockScroll, closeDelay } = toRefs( props ) + const opened = ref(false) const bodyPaddingRight = ref(null) const computedBodyPaddingRight = ref(0) @@ -68,9 +69,8 @@ function usePopup(props) { if (!rendered.value) { rendered.value = true } - - const props = merge({}, instance.proxy, options) - + // instance.proxy, options) + const props = merge(instance.proxy, options) if (_closeTimer) { clearTimeout(_closeTimer) _closeTimer = 0 @@ -87,7 +87,7 @@ function usePopup(props) { } } const doOpen = (props) => { - if (instance.proxy.$isServer) return + // if (instance.proxy.$isServer) return if (_opening) return if (opened.value) return _opening = true @@ -106,7 +106,7 @@ function usePopup(props) { PopupManager.openModal( _popupId, PopupManager.nextZIndex(), - modalAppendToBody.value ? undefined : dom, + modalAppendToBody?.value ? undefined : dom, props.modalClass, props.modalFade ) diff --git a/packages/element3/src/index.js b/packages/element3/src/index.js index cb7532c22..c70ab1a08 100644 --- a/packages/element3/src/index.js +++ b/packages/element3/src/index.js @@ -57,7 +57,7 @@ import ElLoading from '../packages/loading' import { Message } from './components/Message' -import { Msgbox } from '../packages/message-box' +import { Msgbox } from './components/MessageBox' import { Notification } from './components/Notification' // Navigation diff --git a/packages/element3/types/element3.d.ts b/packages/element3/types/element3.d.ts index 67515324f..275a214ba 100644 --- a/packages/element3/types/element3.d.ts +++ b/packages/element3/types/element3.d.ts @@ -43,7 +43,7 @@ export { ElRate } from './rate' export { Message } from './message' export { useNotify } from './notification' export { ElLoading } from './loading' -export { useMsgbox } from './message-box' +export { Msgbox } from './message-box' export { ElSteps } from './steps' export { ElUpload } from './upload' export { ElTabs } from './tabs' diff --git a/packages/element3/types/message-box.d.ts b/packages/element3/types/message-box.d.ts index 67e3394e1..e007e251f 100644 --- a/packages/element3/types/message-box.d.ts +++ b/packages/element3/types/message-box.d.ts @@ -173,7 +173,6 @@ declare module '@vue/runtime-core' { export interface ComponentCustomProperties { /** Show a message box */ $msgbox: ElMessageBox - /** Show an alert message box */ $alert: ElMessageBoxShortcutMethod diff --git a/packages/element3/types/messageBox.d.ts b/packages/element3/types/messageBox.d.ts new file mode 100644 index 000000000..27731e546 --- /dev/null +++ b/packages/element3/types/messageBox.d.ts @@ -0,0 +1,183 @@ +import Vue, { VNode } from 'vue' +import { MessageType } from './message' + +export type MessageBoxCloseAction = 'confirm' | 'cancel' | 'close' +export type MessageBoxData = MessageBoxInputData | MessageBoxCloseAction + +export interface MessageBoxInputData { + value: string + action: MessageBoxCloseAction +} + +export interface MessageBoxInputValidator { + (value: string): boolean | string +} + +export const Msgbox: ElMessageBox + +interface IMessageBox { + 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 +} + +/** 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: ElMessageBox) => void + + /** Callback before MessageBox closes, and it will prevent MessageBox from closing */ + beforeClose?: ( + action: MessageBoxCloseAction, + instance: ElMessageBox, + 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 + (message: string, options?: ElMessageBoxOptions): Promise +} + +export interface ElMessageBox { + /** Show a message box */ + (message: string, title?: string, type?: string): Promise + + /** Show a message box */ + (options: ElMessageBoxOptions): Promise + + /** 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 +} + +declare module '@vue/runtime-core' { + export interface ComponentCustomProperties { + /** Show an alert message box */ + $alert: ElMessageBoxShortcutMethod + + /** Show a confirm message box */ + $confirm: ElMessageBoxShortcutMethod + + /** Show a prompt message box */ + $prompt: ElMessageBoxShortcutMethod + } +} diff --git a/packages/website/src/docs/message-box.md b/packages/website/src/docs/message-box.md index 6ea74e25f..414763289 100644 --- a/packages/website/src/docs/message-box.md +++ b/packages/website/src/docs/message-box.md @@ -3,7 +3,7 @@ 模拟系统的消息提示框而实现的一套模态对话框组件,用于消息提示、确认消息和提交内容。 :::tip -从场景上说,MessageBox 的作用是美化系统自带的 `alert`、`confirm` 和 `prompt`,因此适合展示较为简单的内容。如果需要弹出较为复杂的内容,请使用 Dialog。 +`msgbox`、`alert`、`confirm` 和 `prompt`,因此适合展示较为简单的内容。如果需要弹出较为复杂的内容,请使用 Dialog。 ::: ### 消息提示 @@ -175,11 +175,16 @@ done() } } - }).then((action) => { + }).then(({action}) => { Message({ type: 'info', message: 'action: ' + action }) + }).catch(() => { + Message({ + type: 'info', + message: '已取消' + }) }) } } @@ -217,7 +222,17 @@ { dangerouslyUseHTMLString: true } - ) + ).then(({action}) => { + Message({ + type: 'info', + message: 'action: ' + action + }) + }).catch(() => { + Message({ + type: 'info', + message: '已取消' + }) + }) } } }