/
loading-indicator.ts
177 lines (156 loc) · 4.54 KB
/
loading-indicator.ts
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { computed, getCurrentScope, onScopeDispose, ref } from 'vue'
import type { Ref } from 'vue'
import { useNuxtApp } from '#app/nuxt'
export type LoadingIndicatorOpts = {
/** @default 2000 */
duration: number
/** @default 200 */
throttle: number
/** @default 500 */
hideDelay: number
/** @default 400 */
resetDelay: number
/**
* You can provide a custom function to customize the progress estimation,
* which is a function that receives the duration of the loading bar (above)
* and the elapsed time. It should return a value between 0 and 100.
*/
estimatedProgress?: (duration: number, elapsed: number) => number
}
export type LoadingIndicator = {
_cleanup: () => void
progress: Ref<number>
isLoading: Ref<boolean>
start: () => void
set: (value: number) => void
finish: (opts?: { force?: boolean }) => void
clear: () => void
}
function defaultEstimatedProgress (duration: number, elapsed: number): number {
const completionPercentage = elapsed / duration * 100
return (2 / Math.PI * 100) * Math.atan(completionPercentage / 50)
}
function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
const { duration = 2000, throttle = 200, hideDelay = 500, resetDelay = 400 } = opts
const getProgress = opts.estimatedProgress || defaultEstimatedProgress
const nuxtApp = useNuxtApp()
const progress = ref(0)
const isLoading = ref(false)
let done = false
let rafId: number
let throttleTimeout: number | NodeJS.Timeout
let hideTimeout: number | NodeJS.Timeout
let resetTimeout: number | NodeJS.Timeout
const start = () => set(0)
function set (at = 0) {
if (nuxtApp.isHydrating) {
return
}
if (at >= 100) { return finish() }
clear()
progress.value = at < 0 ? 0 : at
if (throttle && import.meta.client) {
throttleTimeout = setTimeout(() => {
isLoading.value = true
_startProgress()
}, throttle)
} else {
isLoading.value = true
_startProgress()
}
}
function _hide () {
if (import.meta.client) {
hideTimeout = setTimeout(() => {
isLoading.value = false
resetTimeout = setTimeout(() => { progress.value = 0 }, resetDelay)
}, hideDelay)
}
}
function finish (opts: { force?: boolean } = {}) {
progress.value = 100
done = true
clear()
_clearTimeouts()
if (opts.force) {
progress.value = 0
isLoading.value = false
} else {
_hide()
}
}
function _clearTimeouts () {
if (import.meta.client) {
clearTimeout(hideTimeout)
clearTimeout(resetTimeout)
}
}
function clear () {
if (import.meta.client) {
clearTimeout(throttleTimeout)
cancelAnimationFrame(rafId)
}
}
function _startProgress () {
done = false
let startTimeStamp: number
function step (timeStamp: number): void {
if (done) { return }
startTimeStamp ??= timeStamp
const elapsed = timeStamp - startTimeStamp
progress.value = Math.max(0, Math.min(100, getProgress(duration, elapsed)))
if (import.meta.client) {
rafId = requestAnimationFrame(step)
}
}
if (import.meta.client) {
rafId = requestAnimationFrame(step)
}
}
let _cleanup = () => {}
if (import.meta.client) {
const unsubLoadingStartHook = nuxtApp.hook('page:loading:start', () => {
start()
})
const unsubLoadingFinishHook = nuxtApp.hook('page:loading:end', () => {
finish()
})
const unsubError = nuxtApp.hook('vue:error', () => finish())
_cleanup = () => {
unsubError()
unsubLoadingStartHook()
unsubLoadingFinishHook()
clear()
}
}
return {
_cleanup,
progress: computed(() => progress.value),
isLoading: computed(() => isLoading.value),
start,
set,
finish,
clear,
}
}
/**
* composable to handle the loading state of the page
* @since 3.9.0
*/
export function useLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}): Omit<LoadingIndicator, '_cleanup'> {
const nuxtApp = useNuxtApp()
// Initialise global loading indicator if it doesn't exist already
const indicator = nuxtApp._loadingIndicator = nuxtApp._loadingIndicator || createLoadingIndicator(opts)
if (import.meta.client && getCurrentScope()) {
nuxtApp._loadingIndicatorDeps = nuxtApp._loadingIndicatorDeps || 0
nuxtApp._loadingIndicatorDeps++
onScopeDispose(() => {
nuxtApp._loadingIndicatorDeps!--
if (nuxtApp._loadingIndicatorDeps === 0) {
indicator._cleanup()
delete nuxtApp._loadingIndicator
}
})
}
return indicator
}