mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
refactor(components): refactor loading (#4750)
This commit is contained in:
parent
b6c077e2c1
commit
3d019cfbac
@ -1,9 +1,9 @@
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { sleep } from '@element-plus/test-utils'
|
import { sleep } from '@element-plus/test-utils'
|
||||||
import Loading from '../src/index'
|
import { Loading } from '../src/service'
|
||||||
|
import { vLoading } from '../src/directive'
|
||||||
import ElInput from '../../input'
|
import ElInput from '../../input'
|
||||||
import vLoading from '../src/directive'
|
|
||||||
|
|
||||||
function destroyLoadingInstance(loadingInstance) {
|
function destroyLoadingInstance(loadingInstance) {
|
||||||
if (!loadingInstance) return
|
if (!loadingInstance) return
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import Loading from './src'
|
import { Loading } from './src/service'
|
||||||
import vLoading from './src/directive'
|
import { vLoading } from './src/directive'
|
||||||
|
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
|
|
||||||
// installer and everything in all
|
// installer and everything in all
|
||||||
const ElLoading = {
|
export const ElLoading = {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
app.directive('loading', vLoading)
|
app.directive('loading', vLoading)
|
||||||
app.config.globalProperties.$loading = Loading
|
app.config.globalProperties.$loading = Loading
|
||||||
@ -15,9 +15,7 @@ const ElLoading = {
|
|||||||
|
|
||||||
export default ElLoading
|
export default ElLoading
|
||||||
|
|
||||||
export { ElLoading }
|
|
||||||
|
|
||||||
export const ElLoadingDirective = vLoading
|
export const ElLoadingDirective = vLoading
|
||||||
export const ElLoadingService = Loading
|
export const ElLoadingService = Loading
|
||||||
|
|
||||||
export * from './src/loading.type'
|
export * from './src/types'
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
import {
|
|
||||||
createVNode,
|
|
||||||
h,
|
|
||||||
reactive,
|
|
||||||
ref,
|
|
||||||
render,
|
|
||||||
toRefs,
|
|
||||||
Transition,
|
|
||||||
vShow,
|
|
||||||
withCtx,
|
|
||||||
withDirectives,
|
|
||||||
} from 'vue'
|
|
||||||
import { removeClass } from '@element-plus/utils/dom'
|
|
||||||
|
|
||||||
import type { VNode } from 'vue'
|
|
||||||
import type { Nullable } from '@element-plus/utils/types'
|
|
||||||
import type {
|
|
||||||
ILoadingCreateComponentParams,
|
|
||||||
ILoadingInstance,
|
|
||||||
} from './loading.type'
|
|
||||||
|
|
||||||
export function createLoadingComponent({
|
|
||||||
options,
|
|
||||||
globalLoadingOption,
|
|
||||||
}: ILoadingCreateComponentParams): ILoadingInstance {
|
|
||||||
let vm: VNode = null
|
|
||||||
let afterLeaveTimer: Nullable<number> = null
|
|
||||||
|
|
||||||
const afterLeaveFlag = ref(false)
|
|
||||||
const data = reactive({
|
|
||||||
...options,
|
|
||||||
originalPosition: '',
|
|
||||||
originalOverflow: '',
|
|
||||||
visible: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
function setText(text: string) {
|
|
||||||
data.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroySelf() {
|
|
||||||
const target = data.parent
|
|
||||||
if (!target.vLoadingAddClassList) {
|
|
||||||
let loadingNumber: number | string = target.getAttribute('loading-number')
|
|
||||||
loadingNumber = Number.parseInt(loadingNumber) - 1
|
|
||||||
if (!loadingNumber) {
|
|
||||||
removeClass(target, 'el-loading-parent--relative')
|
|
||||||
target.removeAttribute('loading-number')
|
|
||||||
} else {
|
|
||||||
target.setAttribute('loading-number', loadingNumber.toString())
|
|
||||||
}
|
|
||||||
removeClass(target, 'el-loading-parent--hidden')
|
|
||||||
}
|
|
||||||
if (vm.el && vm.el.parentNode) {
|
|
||||||
vm.el.parentNode.removeChild(vm.el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
const target = data.parent
|
|
||||||
target.vLoadingAddClassList = null
|
|
||||||
if (data.fullscreen) {
|
|
||||||
globalLoadingOption.fullscreenLoading = undefined
|
|
||||||
}
|
|
||||||
afterLeaveFlag.value = true
|
|
||||||
clearTimeout(afterLeaveTimer)
|
|
||||||
|
|
||||||
afterLeaveTimer = window.setTimeout(() => {
|
|
||||||
if (afterLeaveFlag.value) {
|
|
||||||
afterLeaveFlag.value = false
|
|
||||||
destroySelf()
|
|
||||||
}
|
|
||||||
}, 400)
|
|
||||||
data.visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAfterLeave() {
|
|
||||||
if (!afterLeaveFlag.value) return
|
|
||||||
afterLeaveFlag.value = false
|
|
||||||
destroySelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentSetupConfig = {
|
|
||||||
...toRefs(data),
|
|
||||||
setText,
|
|
||||||
close,
|
|
||||||
handleAfterLeave,
|
|
||||||
}
|
|
||||||
|
|
||||||
const elLoadingComponent = {
|
|
||||||
name: 'ElLoading',
|
|
||||||
setup() {
|
|
||||||
return componentSetupConfig
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const svg = this.spinner || this.svg
|
|
||||||
const spinner = h(
|
|
||||||
'svg',
|
|
||||||
{
|
|
||||||
class: 'circular',
|
|
||||||
viewBox: this.svgViewBox ? this.svgViewBox : '25 25 50 50',
|
|
||||||
...(svg ? { innerHTML: svg } : {}),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h('circle', {
|
|
||||||
class: 'path',
|
|
||||||
cx: '50',
|
|
||||||
cy: '50',
|
|
||||||
r: '20',
|
|
||||||
fill: 'none',
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const spinnerText = h('p', { class: 'el-loading-text' }, [this.text])
|
|
||||||
|
|
||||||
return h(
|
|
||||||
Transition,
|
|
||||||
{
|
|
||||||
name: 'el-loading-fade',
|
|
||||||
onAfterLeave: this.handleAfterLeave,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: withCtx(() => [
|
|
||||||
withDirectives(
|
|
||||||
createVNode(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
backgroundColor: this.background || '',
|
|
||||||
},
|
|
||||||
class: [
|
|
||||||
'el-loading-mask',
|
|
||||||
this.customClass,
|
|
||||||
this.fullscreen ? 'is-fullscreen' : '',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'el-loading-spinner',
|
|
||||||
},
|
|
||||||
[spinner, this.text ? spinnerText : null]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
[[vShow, this.visible]]
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
vm = createVNode(elLoadingComponent)
|
|
||||||
|
|
||||||
render(vm, document.createElement('div'))
|
|
||||||
|
|
||||||
return {
|
|
||||||
...componentSetupConfig,
|
|
||||||
vm,
|
|
||||||
get $el() {
|
|
||||||
return vm.el as HTMLElement
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +1,94 @@
|
|||||||
import Loading from './index'
|
import { isRef, ref } from 'vue'
|
||||||
|
import { isObject, isString, hyphenate } from '@vue/shared'
|
||||||
|
import { Loading } from './service'
|
||||||
|
import type { Directive, DirectiveBinding, UnwrapRef } from 'vue'
|
||||||
|
import type { LoadingOptions } from './types'
|
||||||
|
import type { LoadingInstance } from './loading'
|
||||||
|
|
||||||
import type { DirectiveBinding } from 'vue'
|
const INSTANCE_KEY = Symbol('ElLoading')
|
||||||
import type { ILoadingInstance } from './loading.type'
|
|
||||||
|
|
||||||
const INSTANCE_NAME = 'ElLoading'
|
|
||||||
|
|
||||||
|
export type LoadingBinding = boolean | UnwrapRef<LoadingOptions>
|
||||||
export interface ElementLoading extends HTMLElement {
|
export interface ElementLoading extends HTMLElement {
|
||||||
[INSTANCE_NAME]?: ILoadingInstance
|
[INSTANCE_KEY]?: {
|
||||||
|
instance: LoadingInstance
|
||||||
|
options: LoadingOptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createInstance = (el: ElementLoading, binding: DirectiveBinding) => {
|
const createInstance = (
|
||||||
const textExr = el.getAttribute('element-loading-text')
|
el: ElementLoading,
|
||||||
const spinnerExr = el.getAttribute('element-loading-spinner')
|
binding: DirectiveBinding<LoadingBinding>
|
||||||
const svgExr = el.getAttribute('element-loading-svg')
|
) => {
|
||||||
const svgViewBoxExr = el.getAttribute('element-loading-svg-view-box')
|
|
||||||
const backgroundExr = el.getAttribute('element-loading-background')
|
|
||||||
const customClassExr = el.getAttribute('element-loading-custom-class')
|
|
||||||
const vm = binding.instance
|
const vm = binding.instance
|
||||||
el[INSTANCE_NAME] = Loading({
|
|
||||||
text: (vm && vm[textExr]) || textExr,
|
const getBindingProp = <K extends keyof LoadingOptions>(
|
||||||
svg: (vm && vm[svgExr]) || svgExr,
|
key: K
|
||||||
svgViewBox: (vm && vm[svgViewBoxExr]) || svgViewBoxExr,
|
): LoadingOptions[K] =>
|
||||||
spinner: (vm && vm[spinnerExr]) || spinnerExr,
|
isObject(binding.value) ? binding.value[key] : undefined
|
||||||
background: (vm && vm[backgroundExr]) || backgroundExr,
|
|
||||||
customClass: (vm && vm[customClassExr]) || customClassExr,
|
const resolveExpression = (key: any) => {
|
||||||
fullscreen: !!binding.modifiers.fullscreen,
|
const data = (isString(key) && vm?.[key]) || key
|
||||||
target: binding.modifiers.fullscreen ? null : el,
|
if (data) return ref(data)
|
||||||
body: !!binding.modifiers.body,
|
else return data
|
||||||
visible: true,
|
}
|
||||||
lock: !!binding.modifiers.lock,
|
|
||||||
})
|
const getProp = <K extends keyof LoadingOptions>(name: K) =>
|
||||||
|
resolveExpression(
|
||||||
|
getBindingProp(name) ||
|
||||||
|
el.getAttribute(`element-loading-${hyphenate(name)}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const fullscreen =
|
||||||
|
getBindingProp('fullscreen') ?? binding.modifiers.fullscreen
|
||||||
|
|
||||||
|
const options: LoadingOptions = {
|
||||||
|
text: getProp('text'),
|
||||||
|
svg: getProp('svg'),
|
||||||
|
svgViewBox: getProp('svgViewBox'),
|
||||||
|
spinner: getProp('spinner'),
|
||||||
|
background: getProp('background'),
|
||||||
|
customClass: getProp('customClass'),
|
||||||
|
fullscreen,
|
||||||
|
target: getBindingProp('target') ?? (fullscreen ? undefined : el),
|
||||||
|
body: getBindingProp('body') ?? binding.modifiers.body,
|
||||||
|
lock: getBindingProp('lock') ?? binding.modifiers.lock,
|
||||||
|
}
|
||||||
|
el[INSTANCE_KEY] = {
|
||||||
|
options,
|
||||||
|
instance: Loading(options),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const vLoading = {
|
const updateOptions = (
|
||||||
mounted(el: ElementLoading, binding: DirectiveBinding) {
|
newOptions: UnwrapRef<LoadingOptions>,
|
||||||
|
originalOptions: LoadingOptions
|
||||||
|
) => {
|
||||||
|
for (const key of Object.keys(originalOptions)) {
|
||||||
|
if (isRef(originalOptions[key]))
|
||||||
|
originalOptions[key].value = newOptions[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vLoading: Directive<ElementLoading, LoadingBinding> = {
|
||||||
|
mounted(el, binding) {
|
||||||
if (binding.value) {
|
if (binding.value) {
|
||||||
createInstance(el, binding)
|
createInstance(el, binding)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated(el: ElementLoading, binding: DirectiveBinding) {
|
updated(el, binding) {
|
||||||
const instance = el[INSTANCE_NAME]
|
const instance = el[INSTANCE_KEY]
|
||||||
if (binding.oldValue !== binding.value) {
|
if (binding.oldValue !== binding.value) {
|
||||||
if (binding.value) {
|
if (binding.value && !binding.oldValue) {
|
||||||
createInstance(el, binding)
|
createInstance(el, binding)
|
||||||
|
} else if (binding.value && binding.oldValue) {
|
||||||
|
if (isObject(binding.value))
|
||||||
|
updateOptions(binding.value, instance!.options)
|
||||||
} else {
|
} else {
|
||||||
instance?.close()
|
instance?.instance.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
unmounted(el: ElementLoading) {
|
unmounted(el) {
|
||||||
el[INSTANCE_NAME]?.close()
|
el[INSTANCE_KEY]?.instance.close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vLoading
|
|
||||||
|
@ -1,158 +0,0 @@
|
|||||||
import { nextTick } from 'vue'
|
|
||||||
import { hasOwn } from '@vue/shared'
|
|
||||||
import { addClass, getStyle, removeClass } from '@element-plus/utils/dom'
|
|
||||||
import PopupManager from '@element-plus/utils/popup-manager'
|
|
||||||
import isServer from '@element-plus/utils/isServer'
|
|
||||||
import { createLoadingComponent } from './createLoadingComponent'
|
|
||||||
|
|
||||||
import type { CSSProperties } from 'vue'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ILoadingGlobalConfig,
|
|
||||||
ILoadingInstance,
|
|
||||||
ILoadingOptions,
|
|
||||||
} from './loading.type'
|
|
||||||
|
|
||||||
const defaults: ILoadingOptions = {
|
|
||||||
parent: null,
|
|
||||||
background: '',
|
|
||||||
svg: null,
|
|
||||||
svgViewBox: null,
|
|
||||||
spinner: false,
|
|
||||||
text: null,
|
|
||||||
fullscreen: true,
|
|
||||||
body: false,
|
|
||||||
lock: false,
|
|
||||||
customClass: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalLoadingOption: ILoadingGlobalConfig = {
|
|
||||||
fullscreenLoading: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const addStyle = async (
|
|
||||||
options: ILoadingOptions,
|
|
||||||
parent: HTMLElement,
|
|
||||||
instance: ILoadingInstance
|
|
||||||
) => {
|
|
||||||
const maskStyle: Partial<CSSProperties> = {}
|
|
||||||
if (options.fullscreen) {
|
|
||||||
instance.originalPosition.value = getStyle(document.body, 'position')
|
|
||||||
instance.originalOverflow.value = getStyle(document.body, 'overflow')
|
|
||||||
maskStyle.zIndex = PopupManager.nextZIndex()
|
|
||||||
} else if (options.body) {
|
|
||||||
instance.originalPosition.value = getStyle(document.body, 'position')
|
|
||||||
/**
|
|
||||||
* await dom render when visible is true in init,
|
|
||||||
* because some component's height maybe 0.
|
|
||||||
* e.g. el-table.
|
|
||||||
*/
|
|
||||||
await nextTick()
|
|
||||||
;['top', 'left'].forEach((property) => {
|
|
||||||
const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft'
|
|
||||||
maskStyle[property] = `${
|
|
||||||
(options.target as HTMLElement).getBoundingClientRect()[property] +
|
|
||||||
document.body[scroll] +
|
|
||||||
document.documentElement[scroll] -
|
|
||||||
parseInt(getStyle(document.body, `margin-${property}`), 10)
|
|
||||||
}px`
|
|
||||||
})
|
|
||||||
;['height', 'width'].forEach((property) => {
|
|
||||||
maskStyle[property] = `${
|
|
||||||
(options.target as HTMLElement).getBoundingClientRect()[property]
|
|
||||||
}px`
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
instance.originalPosition.value = getStyle(parent, 'position')
|
|
||||||
}
|
|
||||||
Object.keys(maskStyle).forEach((property) => {
|
|
||||||
instance.$el.style[property] = maskStyle[property]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addClassList = (
|
|
||||||
options: ILoadingOptions,
|
|
||||||
parent: HTMLElement,
|
|
||||||
instance: ILoadingInstance
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
instance.originalPosition.value !== 'absolute' &&
|
|
||||||
instance.originalPosition.value !== 'fixed'
|
|
||||||
) {
|
|
||||||
addClass(parent, 'el-loading-parent--relative')
|
|
||||||
} else {
|
|
||||||
removeClass(parent, 'el-loading-parent--relative')
|
|
||||||
}
|
|
||||||
if (options.fullscreen && options.lock) {
|
|
||||||
addClass(parent, 'el-loading-parent--hidden')
|
|
||||||
} else {
|
|
||||||
removeClass(parent, 'el-loading-parent--hidden')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Loading = function (options: ILoadingOptions = {}): ILoadingInstance {
|
|
||||||
if (isServer) return
|
|
||||||
options = {
|
|
||||||
...defaults,
|
|
||||||
...options,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof options.target === 'string') {
|
|
||||||
options.target = document.querySelector(options.target) as HTMLElement
|
|
||||||
}
|
|
||||||
options.target = options.target || document.body
|
|
||||||
if (options.target !== document.body) {
|
|
||||||
options.fullscreen = false
|
|
||||||
} else {
|
|
||||||
options.body = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.fullscreen && globalLoadingOption.fullscreenLoading) {
|
|
||||||
globalLoadingOption.fullscreenLoading.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = options.body ? document.body : options.target
|
|
||||||
options.parent = parent
|
|
||||||
|
|
||||||
const instance = createLoadingComponent({
|
|
||||||
options,
|
|
||||||
globalLoadingOption,
|
|
||||||
})
|
|
||||||
|
|
||||||
addStyle(options, parent, instance)
|
|
||||||
addClassList(options, parent, instance)
|
|
||||||
|
|
||||||
options.parent.vLoadingAddClassList = () => {
|
|
||||||
addClassList(options, parent, instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add loading-number to parent.
|
|
||||||
* because if a fullscreen loading is triggered when somewhere
|
|
||||||
* a v-loading.body was triggered before and it's parent is
|
|
||||||
* document.body which with a margin , the fullscreen loading's
|
|
||||||
* destroySelf function will remove 'el-loading-parent--relative',
|
|
||||||
* and then the position of v-loading.body will be error.
|
|
||||||
*/
|
|
||||||
let loadingNumber: number | string = parent.getAttribute('loading-number')
|
|
||||||
if (!loadingNumber) {
|
|
||||||
loadingNumber = 1
|
|
||||||
} else {
|
|
||||||
loadingNumber = Number.parseInt(loadingNumber) + 1
|
|
||||||
}
|
|
||||||
parent.setAttribute('loading-number', loadingNumber.toString())
|
|
||||||
|
|
||||||
parent.appendChild(instance.$el)
|
|
||||||
|
|
||||||
// after instance render, then modify visible to trigger transition
|
|
||||||
nextTick().then(() => {
|
|
||||||
instance.visible.value = hasOwn(options, 'visible') ? options.visible : true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (options.fullscreen) {
|
|
||||||
globalLoadingOption.fullscreenLoading = instance
|
|
||||||
}
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Loading
|
|
155
packages/components/loading/src/loading.ts
Normal file
155
packages/components/loading/src/loading.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
createApp,
|
||||||
|
h,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
createVNode,
|
||||||
|
toRefs,
|
||||||
|
Transition,
|
||||||
|
vShow,
|
||||||
|
withCtx,
|
||||||
|
withDirectives,
|
||||||
|
} from 'vue'
|
||||||
|
import { removeClass } from '@element-plus/utils/dom'
|
||||||
|
|
||||||
|
import type { LoadingOptionsResolved } from './types'
|
||||||
|
|
||||||
|
export function createLoadingComponent(options: LoadingOptionsResolved) {
|
||||||
|
let afterLeaveTimer: number
|
||||||
|
|
||||||
|
const afterLeaveFlag = ref(false)
|
||||||
|
const data = reactive({
|
||||||
|
...options,
|
||||||
|
originalPosition: '',
|
||||||
|
originalOverflow: '',
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function setText(text: string) {
|
||||||
|
data.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroySelf() {
|
||||||
|
const target = data.parent
|
||||||
|
if (!target.vLoadingAddClassList) {
|
||||||
|
let loadingNumber: number | string | null =
|
||||||
|
target.getAttribute('loading-number')
|
||||||
|
loadingNumber = Number.parseInt(loadingNumber as any) - 1
|
||||||
|
if (!loadingNumber) {
|
||||||
|
removeClass(target, 'el-loading-parent--relative')
|
||||||
|
target.removeAttribute('loading-number')
|
||||||
|
} else {
|
||||||
|
target.setAttribute('loading-number', loadingNumber.toString())
|
||||||
|
}
|
||||||
|
removeClass(target, 'el-loading-parent--hidden')
|
||||||
|
}
|
||||||
|
vm.$el?.parentNode?.removeChild(vm.$el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (options.beforeClose && !options.beforeClose()) return
|
||||||
|
|
||||||
|
const target = data.parent
|
||||||
|
target.vLoadingAddClassList = undefined
|
||||||
|
afterLeaveFlag.value = true
|
||||||
|
clearTimeout(afterLeaveTimer)
|
||||||
|
|
||||||
|
afterLeaveTimer = window.setTimeout(() => {
|
||||||
|
if (afterLeaveFlag.value) {
|
||||||
|
afterLeaveFlag.value = false
|
||||||
|
destroySelf()
|
||||||
|
}
|
||||||
|
}, 400)
|
||||||
|
data.visible = false
|
||||||
|
|
||||||
|
options.closed?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAfterLeave() {
|
||||||
|
if (!afterLeaveFlag.value) return
|
||||||
|
afterLeaveFlag.value = false
|
||||||
|
destroySelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
const elLoadingComponent = {
|
||||||
|
name: 'ElLoading',
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
const svg = data.spinner || data.svg
|
||||||
|
const spinner = h(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
class: 'circular',
|
||||||
|
viewBox: data.svgViewBox ? data.svgViewBox : '25 25 50 50',
|
||||||
|
...(svg ? { innerHTML: svg } : {}),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('circle', {
|
||||||
|
class: 'path',
|
||||||
|
cx: '50',
|
||||||
|
cy: '50',
|
||||||
|
r: '20',
|
||||||
|
fill: 'none',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const spinnerText = data.text
|
||||||
|
? h('p', { class: 'el-loading-text' }, [data.text])
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return h(
|
||||||
|
Transition,
|
||||||
|
{
|
||||||
|
name: 'el-loading-fade',
|
||||||
|
onAfterLeave: handleAfterLeave,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: withCtx(() => [
|
||||||
|
withDirectives(
|
||||||
|
createVNode(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
backgroundColor: data.background || '',
|
||||||
|
},
|
||||||
|
class: [
|
||||||
|
'el-loading-mask',
|
||||||
|
data.customClass,
|
||||||
|
data.fullscreen ? 'is-fullscreen' : '',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'el-loading-spinner',
|
||||||
|
},
|
||||||
|
[spinner, spinnerText]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
[[vShow, data.visible]]
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const vm = createApp(elLoadingComponent).mount(document.createElement('div'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(data),
|
||||||
|
setText,
|
||||||
|
close,
|
||||||
|
handleAfterLeave,
|
||||||
|
vm,
|
||||||
|
get $el(): HTMLElement {
|
||||||
|
return vm.$el
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoadingInstance = ReturnType<typeof createLoadingComponent>
|
@ -1,49 +0,0 @@
|
|||||||
import type { Ref, VNode } from 'vue'
|
|
||||||
|
|
||||||
export type ILoadingOptions = {
|
|
||||||
parent?: ILoadingParentElement
|
|
||||||
background?: string
|
|
||||||
svg?: string
|
|
||||||
svgViewBox?: string
|
|
||||||
spinner?: boolean | string
|
|
||||||
text?: string
|
|
||||||
fullscreen?: boolean
|
|
||||||
body?: boolean
|
|
||||||
lock?: boolean
|
|
||||||
customClass?: string
|
|
||||||
visible?: boolean
|
|
||||||
target?: string | HTMLElement
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ILoadingInstance = {
|
|
||||||
parent?: Ref<ILoadingParentElement>
|
|
||||||
background?: Ref<string>
|
|
||||||
spinner?: Ref<boolean | string>
|
|
||||||
text?: Ref<string>
|
|
||||||
fullscreen?: Ref<boolean>
|
|
||||||
body?: Ref<boolean>
|
|
||||||
lock?: Ref<boolean>
|
|
||||||
customClass?: Ref<string>
|
|
||||||
visible?: Ref<boolean>
|
|
||||||
target?: Ref<string | HTMLElement>
|
|
||||||
originalPosition?: Ref<string>
|
|
||||||
originalOverflow?: Ref<string>
|
|
||||||
setText: (text: string) => void
|
|
||||||
close: () => void
|
|
||||||
handleAfterLeave: () => void
|
|
||||||
vm: VNode
|
|
||||||
$el: HTMLElement
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ILoadingGlobalConfig = {
|
|
||||||
fullscreenLoading: ILoadingInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ILoadingCreateComponentParams = {
|
|
||||||
options: ILoadingOptions
|
|
||||||
globalLoadingOption: ILoadingGlobalConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILoadingParentElement extends HTMLElement {
|
|
||||||
vLoadingAddClassList?: () => void
|
|
||||||
}
|
|
148
packages/components/loading/src/service.ts
Normal file
148
packages/components/loading/src/service.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { isString } from '@vue/shared'
|
||||||
|
import { isClient } from '@vueuse/core'
|
||||||
|
import { addClass, getStyle, removeClass } from '@element-plus/utils/dom'
|
||||||
|
import PopupManager from '@element-plus/utils/popup-manager'
|
||||||
|
import { createLoadingComponent } from './loading'
|
||||||
|
import type { LoadingInstance } from './loading'
|
||||||
|
import type { LoadingOptionsResolved } from '..'
|
||||||
|
import type { LoadingOptions } from './types'
|
||||||
|
import type { CSSProperties } from 'vue'
|
||||||
|
|
||||||
|
let fullscreenInstance: LoadingInstance | undefined = undefined
|
||||||
|
|
||||||
|
export const Loading = function (
|
||||||
|
options: LoadingOptions = {}
|
||||||
|
): LoadingInstance {
|
||||||
|
if (!isClient) return undefined as any
|
||||||
|
|
||||||
|
const resolved = resolveOptions(options)
|
||||||
|
|
||||||
|
if (resolved.fullscreen && fullscreenInstance) {
|
||||||
|
fullscreenInstance.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = createLoadingComponent({
|
||||||
|
...resolved,
|
||||||
|
closed: () => {
|
||||||
|
resolved.closed?.()
|
||||||
|
if (resolved.fullscreen) fullscreenInstance = undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
addStyle(resolved, resolved.parent, instance)
|
||||||
|
addClassList(resolved, resolved.parent, instance)
|
||||||
|
|
||||||
|
resolved.parent.vLoadingAddClassList = () =>
|
||||||
|
addClassList(resolved, resolved.parent, instance)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add loading-number to parent.
|
||||||
|
* because if a fullscreen loading is triggered when somewhere
|
||||||
|
* a v-loading.body was triggered before and it's parent is
|
||||||
|
* document.body which with a margin , the fullscreen loading's
|
||||||
|
* destroySelf function will remove 'el-loading-parent--relative',
|
||||||
|
* and then the position of v-loading.body will be error.
|
||||||
|
*/
|
||||||
|
let loadingNumber: string | null =
|
||||||
|
resolved.parent.getAttribute('loading-number')
|
||||||
|
if (!loadingNumber) {
|
||||||
|
loadingNumber = '1'
|
||||||
|
} else {
|
||||||
|
loadingNumber = `${Number.parseInt(loadingNumber) + 1}`
|
||||||
|
}
|
||||||
|
resolved.parent.setAttribute('loading-number', loadingNumber)
|
||||||
|
|
||||||
|
resolved.parent.appendChild(instance.$el)
|
||||||
|
|
||||||
|
// after instance render, then modify visible to trigger transition
|
||||||
|
nextTick(() => (instance.visible.value = resolved.visible))
|
||||||
|
|
||||||
|
if (resolved.fullscreen) {
|
||||||
|
fullscreenInstance = instance
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveOptions = (options: LoadingOptions): LoadingOptionsResolved => {
|
||||||
|
let target: HTMLElement
|
||||||
|
if (isString(options.target)) {
|
||||||
|
target =
|
||||||
|
document.querySelector<HTMLElement>(options.target) ?? document.body
|
||||||
|
} else {
|
||||||
|
target = options.target || document.body
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
parent: target === document.body || options.body ? document.body : target,
|
||||||
|
background: options.background || '',
|
||||||
|
svg: options.svg || '',
|
||||||
|
svgViewBox: options.svgViewBox || '',
|
||||||
|
spinner: options.spinner || false,
|
||||||
|
text: options.text || '',
|
||||||
|
fullscreen: target === document.body && (options.fullscreen ?? true),
|
||||||
|
lock: options.lock ?? false,
|
||||||
|
customClass: options.customClass || '',
|
||||||
|
visible: options.visible ?? true,
|
||||||
|
target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addStyle = async (
|
||||||
|
options: LoadingOptionsResolved,
|
||||||
|
parent: HTMLElement,
|
||||||
|
instance: LoadingInstance
|
||||||
|
) => {
|
||||||
|
const maskStyle: CSSProperties = {}
|
||||||
|
if (options.fullscreen) {
|
||||||
|
instance.originalPosition.value = getStyle(document.body, 'position')
|
||||||
|
instance.originalOverflow.value = getStyle(document.body, 'overflow')
|
||||||
|
maskStyle.zIndex = PopupManager.nextZIndex()
|
||||||
|
} else if (options.parent === document.body) {
|
||||||
|
instance.originalPosition.value = getStyle(document.body, 'position')
|
||||||
|
/**
|
||||||
|
* await dom render when visible is true in init,
|
||||||
|
* because some component's height maybe 0.
|
||||||
|
* e.g. el-table.
|
||||||
|
*/
|
||||||
|
await nextTick()
|
||||||
|
for (const property of ['top', 'left']) {
|
||||||
|
const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft'
|
||||||
|
maskStyle[property] = `${
|
||||||
|
(options.target as HTMLElement).getBoundingClientRect()[property] +
|
||||||
|
document.body[scroll] +
|
||||||
|
document.documentElement[scroll] -
|
||||||
|
parseInt(getStyle(document.body, `margin-${property}`), 10)
|
||||||
|
}px`
|
||||||
|
}
|
||||||
|
for (const property of ['height', 'width']) {
|
||||||
|
maskStyle[property] = `${
|
||||||
|
(options.target as HTMLElement).getBoundingClientRect()[property]
|
||||||
|
}px`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
instance.originalPosition.value = getStyle(parent, 'position')
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(maskStyle)) {
|
||||||
|
instance.$el.style[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addClassList = (
|
||||||
|
options: LoadingOptions,
|
||||||
|
parent: HTMLElement,
|
||||||
|
instance: LoadingInstance
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
instance.originalPosition.value !== 'absolute' &&
|
||||||
|
instance.originalPosition.value !== 'fixed'
|
||||||
|
) {
|
||||||
|
addClass(parent, 'el-loading-parent--relative')
|
||||||
|
} else {
|
||||||
|
removeClass(parent, 'el-loading-parent--relative')
|
||||||
|
}
|
||||||
|
if (options.fullscreen && options.lock) {
|
||||||
|
addClass(parent, 'el-loading-parent--hidden')
|
||||||
|
} else {
|
||||||
|
removeClass(parent, 'el-loading-parent--hidden')
|
||||||
|
}
|
||||||
|
}
|
27
packages/components/loading/src/types.ts
Normal file
27
packages/components/loading/src/types.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { MaybeRef } from '@vueuse/core'
|
||||||
|
|
||||||
|
export type LoadingOptionsResolved = {
|
||||||
|
parent: LoadingParentElement
|
||||||
|
background: MaybeRef<string>
|
||||||
|
svg: MaybeRef<string>
|
||||||
|
svgViewBox: MaybeRef<string>
|
||||||
|
spinner: MaybeRef<boolean | string>
|
||||||
|
text: MaybeRef<string>
|
||||||
|
fullscreen: boolean
|
||||||
|
lock: boolean
|
||||||
|
customClass: MaybeRef<string>
|
||||||
|
visible: boolean
|
||||||
|
target: HTMLElement
|
||||||
|
beforeClose?: () => boolean
|
||||||
|
closed?: () => void
|
||||||
|
}
|
||||||
|
export type LoadingOptions = Partial<
|
||||||
|
Omit<LoadingOptionsResolved, 'parent' | 'target'> & {
|
||||||
|
target: HTMLElement | string
|
||||||
|
body: boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface LoadingParentElement extends HTMLElement {
|
||||||
|
vLoadingAddClassList?: () => void
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user