LazyContainer.vue
4.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<template>
<transition-group
class="h-full w-full"
v-bind="$attrs"
ref="elRef"
:name="transitionName"
:tag="tag"
mode="out-in"
>
<div key="component" v-if="isInit">
<slot :loading="loading"></slot>
</div>
<div key="skeleton" v-else>
<slot name="skeleton" v-if="$slots.skeleton"></slot>
<Skeleton v-else />
</div>
</transition-group>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent, reactive, onMounted, ref, toRef, toRefs } from 'vue';
import { Skeleton } from 'ant-design-vue';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
interface State {
isInit: boolean;
loading: boolean;
intersectionObserverInstance: IntersectionObserver | null;
}
const props = {
/**
* Waiting time, if the time is specified, whether visible or not, it will be automatically loaded after the specified time
*/
timeout: { type: Number },
/**
* The viewport where the component is located.
* If the component is scrolling in the page container, the viewport is the container
*/
viewport: {
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
default: () => null,
},
/**
* Preload threshold, css unit
*/
threshold: { type: String, default: '0px' },
/**
* The scroll direction of the viewport, vertical represents the vertical direction, horizontal represents the horizontal direction
*/
direction: {
type: String,
default: 'vertical',
validator: (v) => ['vertical', 'horizontal'].includes(v),
},
/**
* The label name of the outer container that wraps the component
*/
tag: { type: String, default: 'div' },
maxWaitingTime: { type: Number, default: 80 },
/**
* transition name
*/
transitionName: { type: String, default: 'lazy-container' },
};
export default defineComponent({
name: 'LazyContainer',
components: { Skeleton },
inheritAttrs: false,
props,
emits: ['init'],
setup(props, { emit }) {
const elRef = ref();
const state = reactive<State>({
isInit: false,
loading: false,
intersectionObserverInstance: null,
});
onMounted(() => {
immediateInit();
initIntersectionObserver();
});
// If there is a set delay time, it will be executed immediately
function immediateInit() {
const { timeout } = props;
timeout &&
useTimeoutFn(() => {
init();
}, timeout);
}
function init() {
state.loading = true;
useTimeoutFn(() => {
if (state.isInit) return;
state.isInit = true;
emit('init');
}, props.maxWaitingTime || 80);
}
function initIntersectionObserver() {
const { timeout, direction, threshold } = props;
if (timeout) return;
// According to the scrolling direction to construct the viewport margin, used to load in advance
let rootMargin = '0px';
switch (direction) {
case 'vertical':
rootMargin = `${threshold} 0px`;
break;
case 'horizontal':
rootMargin = `0px ${threshold}`;
break;
}
try {
const { stop, observer } = useIntersectionObserver({
rootMargin,
target: toRef(elRef.value, '$el'),
onIntersect: (entries: any[]) => {
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
if (isIntersecting) {
init();
if (observer) {
stop();
}
}
},
root: toRef(props, 'viewport'),
});
} catch (e) {
init();
}
}
return {
elRef,
...toRefs(state),
};
},
});
</script>