mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-04 04:08:34 +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 { mount } from '@vue/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 vLoading from '../src/directive'
|
||||
|
||||
function destroyLoadingInstance(loadingInstance) {
|
||||
if (!loadingInstance) return
|
||||
|
@ -1,10 +1,10 @@
|
||||
import Loading from './src'
|
||||
import vLoading from './src/directive'
|
||||
import { Loading } from './src/service'
|
||||
import { vLoading } from './src/directive'
|
||||
|
||||
import type { App } from 'vue'
|
||||
|
||||
// installer and everything in all
|
||||
const ElLoading = {
|
||||
export const ElLoading = {
|
||||
install(app: App) {
|
||||
app.directive('loading', vLoading)
|
||||
app.config.globalProperties.$loading = Loading
|
||||
@ -15,9 +15,7 @@ const ElLoading = {
|
||||
|
||||
export default ElLoading
|
||||
|
||||
export { ElLoading }
|
||||
|
||||
export const ElLoadingDirective = vLoading
|
||||
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'
|
||||
import type { ILoadingInstance } from './loading.type'
|
||||
|
||||
const INSTANCE_NAME = 'ElLoading'
|
||||
const INSTANCE_KEY = Symbol('ElLoading')
|
||||
|
||||
export type LoadingBinding = boolean | UnwrapRef<LoadingOptions>
|
||||
export interface ElementLoading extends HTMLElement {
|
||||
[INSTANCE_NAME]?: ILoadingInstance
|
||||
[INSTANCE_KEY]?: {
|
||||
instance: LoadingInstance
|
||||
options: LoadingOptions
|
||||
}
|
||||
}
|
||||
|
||||
const createInstance = (el: ElementLoading, binding: DirectiveBinding) => {
|
||||
const textExr = el.getAttribute('element-loading-text')
|
||||
const spinnerExr = el.getAttribute('element-loading-spinner')
|
||||
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 createInstance = (
|
||||
el: ElementLoading,
|
||||
binding: DirectiveBinding<LoadingBinding>
|
||||
) => {
|
||||
const vm = binding.instance
|
||||
el[INSTANCE_NAME] = Loading({
|
||||
text: (vm && vm[textExr]) || textExr,
|
||||
svg: (vm && vm[svgExr]) || svgExr,
|
||||
svgViewBox: (vm && vm[svgViewBoxExr]) || svgViewBoxExr,
|
||||
spinner: (vm && vm[spinnerExr]) || spinnerExr,
|
||||
background: (vm && vm[backgroundExr]) || backgroundExr,
|
||||
customClass: (vm && vm[customClassExr]) || customClassExr,
|
||||
fullscreen: !!binding.modifiers.fullscreen,
|
||||
target: binding.modifiers.fullscreen ? null : el,
|
||||
body: !!binding.modifiers.body,
|
||||
visible: true,
|
||||
lock: !!binding.modifiers.lock,
|
||||
})
|
||||
|
||||
const getBindingProp = <K extends keyof LoadingOptions>(
|
||||
key: K
|
||||
): LoadingOptions[K] =>
|
||||
isObject(binding.value) ? binding.value[key] : undefined
|
||||
|
||||
const resolveExpression = (key: any) => {
|
||||
const data = (isString(key) && vm?.[key]) || key
|
||||
if (data) return ref(data)
|
||||
else return data
|
||||
}
|
||||
|
||||
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 = {
|
||||
mounted(el: ElementLoading, binding: DirectiveBinding) {
|
||||
const updateOptions = (
|
||||
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) {
|
||||
createInstance(el, binding)
|
||||
}
|
||||
},
|
||||
updated(el: ElementLoading, binding: DirectiveBinding) {
|
||||
const instance = el[INSTANCE_NAME]
|
||||
updated(el, binding) {
|
||||
const instance = el[INSTANCE_KEY]
|
||||
if (binding.oldValue !== binding.value) {
|
||||
if (binding.value) {
|
||||
if (binding.value && !binding.oldValue) {
|
||||
createInstance(el, binding)
|
||||
} else if (binding.value && binding.oldValue) {
|
||||
if (isObject(binding.value))
|
||||
updateOptions(binding.value, instance!.options)
|
||||
} else {
|
||||
instance?.close()
|
||||
instance?.instance.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
unmounted(el: ElementLoading) {
|
||||
el[INSTANCE_NAME]?.close()
|
||||
unmounted(el) {
|
||||
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