Skip to content

Commit

Permalink
refactor(components): [message] fix typings and switch to script setup (
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Jun 21, 2022
1 parent 3db3d62 commit 786360b
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 255 deletions.
1 change: 0 additions & 1 deletion internal/build/src/type-unsafe-stricter.json
Expand Up @@ -15,7 +15,6 @@
"packages/components/loading/",
"packages/components/menu/",
"packages/components/message-box/",
"packages/components/message/",
"packages/components/notification/",
"packages/components/option-group/",
"packages/components/option/",
Expand Down
Expand Up @@ -59,7 +59,7 @@ describe('Message on command', () => {
const getTopValue = (elm: Element): number =>
Number.parseInt(getStyle(elm as HTMLElement, 'top'), 10)

const topValues = []
const topValues: number[] = []
elements.forEach((e) => {
topValues.push(getTopValue(e))
})
Expand Down
6 changes: 2 additions & 4 deletions packages/components/message/__tests__/message.test.ts
Expand Up @@ -91,10 +91,8 @@ describe('Message.vue', () => {
const type = 'some-type'
const wrapper = _mount({ props: { type } })

for (const key in TypeComponentsMap) {
expect(wrapper.findComponent(TypeComponentsMap[key]).exists()).toBe(
false
)
for (const component of Object.values(TypeComponentsMap)) {
expect(wrapper.findComponent(component).exists()).toBe(false)
}
console.warn = consoleWarn
})
Expand Down
269 changes: 141 additions & 128 deletions packages/components/message/src/message-method.ts
Expand Up @@ -5,86 +5,46 @@ import {
isElement,
isFunction,
isNumber,
isObject,
isString,
isVNode,
} from '@element-plus/utils'
import { useZIndex } from '@element-plus/hooks'
import { messageConfig } from '@element-plus/components/config-provider/src/config-provider'
import MessageConstructor from './message.vue'
import { messageTypes } from './message'

import type { AppContext, ComponentPublicInstance, VNode } from 'vue'
import type { Message, MessageFn, MessageProps, MessageQueue } from './message'
import { messageDefaults, messageTypes } from './message'

import type { AppContext } from 'vue'
import type {
Message,
MessageFn,
MessageHandler,
MessageInstance,
MessageOptions,
MessageParams,
MessageParamsNormalized,
MessageQueue,
MessageQueueItem,
} from './message'

const instances: MessageQueue = []
let seed = 1

// TODO: Since Notify.ts is basically the same like this file. So we could do some encapsulation against them to reduce code duplication.

const message: MessageFn & Partial<Message> & { _context: AppContext | null } =
function (options = {}, context?: AppContext | null) {
if (!isClient) return { close: () => undefined }
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
return { close: () => undefined }
}
const normalizeOptions = (params?: MessageParams) => {
const options: MessageOptions =
!params || isString(params) || isVNode(params) || isFunction(params)
? { message: params }
: params

if (
!isVNode(options) &&
isObject(options) &&
options.grouping &&
!isVNode(options.message) &&
instances.length
) {
const tempVm: any = instances.find(
(item) =>
`${item.vm.props?.message ?? ''}` ===
`${(options as any).message ?? ''}`
)
if (tempVm) {
tempVm.vm.component!.props.repeatNum += 1
tempVm.vm.component!.props.type = options?.type || 'info'
return {
close: () =>
((
vm.component!.proxy as ComponentPublicInstance<{
visible: boolean
}>
).visible = false),
}
}
}
const normalized = {
...messageDefaults,
...options,
}

if (isString(options) || isVNode(options)) {
options = { message: options }
}
if (isString(normalized.appendTo)) {
let appendTo = document.querySelector<HTMLElement>(normalized.appendTo)

let verticalOffset = options.offset || 20
instances.forEach(({ vm }) => {
verticalOffset += (vm.el?.offsetHeight || 0) + 16
})
verticalOffset += 16

const { nextZIndex } = useZIndex()

const id = `message_${seed++}`
const userOnClose = options.onClose
const props: Partial<MessageProps> = {
zIndex: nextZIndex(),
...options,
offset: verticalOffset,
id,
onClose: () => {
close(id, userOnClose)
},
}

let appendTo: HTMLElement | null = document.body
if (isElement(options.appendTo)) {
appendTo = options.appendTo
} else if (isString(options.appendTo)) {
appendTo = document.querySelector(options.appendTo)
}
// should fallback to default value with a warning
if (!isElement(appendTo)) {
debugWarn(
Expand All @@ -94,89 +54,142 @@ const message: MessageFn & Partial<Message> & { _context: AppContext | null } =
appendTo = document.body
}

const container = document.createElement('div')
normalized.appendTo = appendTo
}

container.className = `container_${id}`
return normalized as MessageParamsNormalized
}

const messageContent = props.message
const vm = createVNode(
MessageConstructor,
props,
isFunction(messageContent)
? { default: messageContent }
: isVNode(messageContent)
? { default: () => messageContent }
: null
)
const closeMessage = (instance: MessageQueueItem) => {
const idx = instances.indexOf(instance)
if (idx === -1) return

vm.appContext = context || message._context
instances.splice(idx, 1)
const { vnode, handler } = instance
handler.close()

const removedHeight = vnode.el!.offsetHeight
// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
Number.parseInt(instances[i].vnode.el!.style['top'], 10) -
removedHeight -
16

instances[i].vnode.component!.props.offset = pos
}
}

const createMessage = (
{ appendTo, ...options }: MessageParamsNormalized,
context?: AppContext | null
): MessageQueueItem => {
const { nextZIndex } = useZIndex()

const id = `message_${seed++}`
const userOnClose = options.onClose

let verticalOffset = options.offset
instances.forEach(({ vnode: vm }) => {
verticalOffset += (vm.el?.offsetHeight || 0) + 16
})
verticalOffset += 16

const container = document.createElement('div')

const props = {
...options,
zIndex: options.zIndex ?? nextZIndex(),
offset: verticalOffset,
id,
onClose: () => {
userOnClose?.()
closeMessage(instance)
},

// clean message element preventing mem leak
vm.props!.onDestroy = () => {
render(null, container)
onDestroy: () => {
// since the element is destroy, then the VNode should be collected by GC as well
// we do not want cause any mem leak because we have returned vm as a reference to users
// so that we manually set it to false.
}

render(vm, container)
// instances will remove this item when close function gets called. So we do not need to worry about it.
instances.push({ vm })
appendTo.appendChild(container.firstElementChild!)

return {
// instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
// for out component, so that all closing steps will not be skipped.
close: () =>
((
vm.component!.proxy as ComponentPublicInstance<{ visible: boolean }>
).visible = false),
}
render(null, container)
},
}
const vnode = createVNode(
MessageConstructor,
props,
isFunction(props.message) || isVNode(props.message)
? { default: props.message }
: null
)
vnode.appContext = context || message._context

render(vnode, container)
// instances will remove this item when close function gets called. So we do not need to worry about it.
appendTo.appendChild(container.firstElementChild!)

const vm = vnode.component!.proxy as MessageInstance
const handler: MessageHandler = {
// instead of calling the onClose function directly, setting this value so that we can have the full lifecycle
// for out component, so that all closing steps will not be skipped.
close: () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore `visible` from defineExpose
vm.visible = false
},
}

messageTypes.forEach((type) => {
message[type] = (options = {}, appContext?: AppContext | null) => {
if (isString(options) || isVNode(options)) {
options = {
message: options,
}
}
return message(
{
...options,
type,
},
appContext
)
const instance = {
vnode,
vm,
handler,
}
})

export function close(id: string, userOnClose?: (vm: VNode) => void): void {
const idx = instances.findIndex(({ vm }) => id === vm.component!.props.id)
if (idx === -1) return
return instance
}

const { vm } = instances[idx]
if (!vm) return
userOnClose?.(vm)
const message: MessageFn &
Partial<Message> & { _context: AppContext | null } = (
options = {},
context
) => {
if (!isClient) return { close: () => undefined }

const removedHeight = vm.el!.offsetHeight
instances.splice(idx, 1)
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
return { close: () => undefined }
}

// adjust other instances vertical offset
const len = instances.length
if (len < 1) return
for (let i = idx; i < len; i++) {
const pos =
Number.parseInt(instances[i].vm.el!.style['top'], 10) - removedHeight - 16
const normalized = normalizeOptions(options)

instances[i].vm.component!.props.offset = pos
if (normalized.grouping && instances.length) {
const instance = instances.find(
({ vnode: vm }) => vm.props?.message === normalized.message
)
if (instance) {
;(instance.vnode.component as any).props.repeatNum += 1
;(instance.vnode.component as any).props.type = normalized.type
return instance.handler
}
}

const instance = createMessage(normalized, context)

instances.push(instance)
return instance.handler
}

messageTypes.forEach((type) => {
message[type] = (options = {}, appContext) => {
const normalized = normalizeOptions(options)
return message({ ...normalized, type }, appContext)
}
})

export function closeAll(): void {
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].vm.component
;(instance?.proxy as any)?.close()
for (const instance of instances) {
instance.handler.close()
}
}

Expand Down

0 comments on commit 786360b

Please sign in to comment.