Loading
@ep_src_vue_directives_loading_code_Example(./loading_code/Example.vue)
vue
<script setup lang="ts">
import { getCurrentInstance, ref } from 'vue'
import { vLoading, service } from './loading'
const loading = ref(false)
service.appContext = getCurrentInstance()?.appContext
const handleFullScreenLoading = () => {
const instance = service.create({ fullscreen: true, lock: true })
setTimeout(() => {
instance.close()
}, 3000)
}
</script>
<template>
<button class="native" @click="loading = true">loading</button>
<button class="native" @click="loading = false">close loading</button>
<button class="native" @click="handleFullScreenLoading">
fullscreen loading
</button>
<div
style="
width: 100px;
height: 100px;
margin-top: 20px;
background-color: pink;
"
v-loading="loading"
></div>
</template>
ts
import './loading.css'
import LoadingContainer from './LoadingContainer.vue'
import LoadingContent from './LoadingContent.vue'
import { createVNode, render } from 'vue'
import type {
AppContext,
Directive,
UnwrapRef,
Plugin,
DirectiveBinding,
} from 'vue'
interface LoadingOptions {
target?: HTMLElement | string
fullscreen?: boolean
lock?: boolean
background?: string
className?: string
zIndex?: number
appContext?: AppContext
beforeClose?: () => void | false
onClosed?: () => void
}
type ResolvedLoadingOptions = LoadingOptions & { target: HTMLElement }
export interface LoadingMeta {
close: () => void
}
export type LoadingBinding = boolean | UnwrapRef<LoadingOptions>
class LoadingService {
private readonly map = new WeakMap<HTMLElement, LoadingMeta>()
appContext?: AppContext
create(options?: LoadingOptions) {
const resolved = resolveOptions(options)
const _meta = this.map.get(resolved.target)
if (_meta) {
_meta.close()
}
let container: HTMLDivElement | null = document.createElement('div')
container.dataset.loading = ''
const props = {
background: resolved.background,
className: resolved.className,
zIndex: resolved.zIndex,
onClose: () => {
this.map.delete(resolved.target)
},
onClosed: () => {
resolved.onClosed?.()
if (!this.map.has(resolved.target)) {
resolved.target.classList.remove('loading-target', 'scroll-lock')
}
render(null, container!)
container = null
},
}
const vnode = createVNode(LoadingContainer, props, {
default: () => createVNode(LoadingContent),
})
vnode.appContext = resolved.appContext || this.appContext || null
render(vnode, container)
resolved.target.appendChild(container.firstElementChild!)
addClassName(resolved)
const meta = {
close: () => {
if (resolved.beforeClose && !resolved.beforeClose()) return
vnode!.component!.exposed!.visible.value = false
},
}
this.map.set(resolved.target, meta)
return meta
}
createFromBinding(
el: HTMLElement,
binding: DirectiveBinding<LoadingBinding, string, string>,
) {
const options = isObject(binding.value) ? binding.value : {}
binding.modifiers.fullscreen && (options.fullscreen = true)
binding.modifiers.lock && (options.lock = true)
options.target = el
service.create(options)
}
update(el: HTMLElement, options: LoadingOptions) {
// TODO: implement update
}
close(target?: HTMLElement | string) {
if (typeof target === 'string') {
const el = document.querySelector<HTMLElement>(target)
if (!el) return
target = el
} else if (!target) {
target = document.body
}
this.map.get(target)?.close()
}
}
const isObject = (val: any): val is Record<any, any> =>
val !== null && typeof val === 'object'
const resolveOptions = (options: LoadingOptions = {}) => {
if (options.fullscreen) {
options.target = document.body
} else if (typeof options.target === 'string') {
const el = document.querySelector<HTMLElement>(options.target)
if (!el) {
throw new Error(`[Loading] target ${options.target} not found`)
}
options.target = el
} else if (!options.target) {
options.target = document.body
}
const { loadingBackground, loadingClassName } = options.target.dataset
if (loadingBackground && !options.background) {
options.background = loadingBackground
}
if (loadingClassName && !options.className) {
options.className = loadingClassName
}
return options as ResolvedLoadingOptions
}
const addClassName = (resolved: ResolvedLoadingOptions) => {
resolved.target.classList.remove('loading-target', 'scroll-lock')
resolved.target.classList.add('loading-target')
resolved.lock && resolved.target.classList.add('scroll-lock')
}
export const service = new LoadingService()
export const vLoading: Directive<HTMLElement, LoadingBinding> = {
mounted(el, binding) {
if (!binding.value) return
service.createFromBinding(el, binding)
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
if (binding.value && !binding.oldValue) {
service.createFromBinding(el, binding)
} else if (binding.value && binding.oldValue) {
if (isObject(binding.value)) {
service.update(el, binding.value)
}
} else {
service.close(el)
}
}
},
unmounted(el) {
service.close(el)
},
}
export const loadingPlugin: Plugin = {
install(app) {
app.directive('loading', vLoading)
service.appContext = app._context
},
}vue
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
background?: string
className?: string
zIndex?: number
}
defineProps<Props>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'closed'): void
}>()
const visible = ref(true)
defineExpose({
visible,
})
</script>
<template>
<Transition
appear
name="loading"
@before-leave="emit('close')"
@after-leave="emit('closed')"
>
<div
v-show="visible"
:class="['loading-container', className]"
:style="{ zIndex, backgroundColor: background }"
>
<slot></slot>
</div>
</Transition>
</template>
<style scoped>
.loading-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.loading-enter-active,
.loading-leave-active {
transition: opacity 0.3s;
}
.loading-enter-from,
.loading-leave-to {
opacity: 0;
}
</style>vue
<template>
<span class="loading-content" style="color: #fff;">Loading...</span>
</template>css
.loading-target {
position: relative;
}
.scroll-lock {
overflow: hidden;
}