返回 slidev
useClicks.ts
根目录 / packages / client / composables / useClicks.ts
1 import type { ClicksContext, NormalizedRangeClickValue, NormalizedSingleClickValue, RawAtValue, RawSingleAtValue, SlideRoute } from '@slidev/types'
2 import type { MaybeRefOrGetter, Ref } from 'vue'
3 import { clamp, sum } from '@antfu/utils'
4 import { computed, isReadonly, onMounted, onUnmounted, ref, shallowReactive, toValue, watch } from 'vue'
5
6 export function normalizeSingleAtValue(at: RawSingleAtValue): NormalizedSingleClickValue {
7 if (at === false || at === 'false')
8 return null
9 if (at == null || at === true || at === 'true')
10 return '+1'
11 if (typeof at === 'string' && '+-'.includes(at[0]))
12 return at
13 const v = +at
14 if (Number.isNaN(v)) {
15 console.error(`Invalid "at" prop value: ${at}`)
16 return null
17 }
18 if (v <= 0) {
19 console.warn(`[Slidev] "at" prop value must be greater than 0, but got ${at}, has been set to 1`)
20 return 1
21 }
22 return v
23 }
24
25 export function normalizeRangeAtValue(at: RawAtValue): NormalizedRangeClickValue {
26 if (Array.isArray(at))
27 return [normalizeSingleAtValue(at[0])!, normalizeSingleAtValue(at[1])!]
28 return null
29 }
30
31 export function createClicksContextBase(
32 current: Ref<number>,
33 clicksStart = 0,
34 clicksTotalOverrides?: number,
35 ): ClicksContext {
36 const isMounted = ref(false)
37 let relativeSizeMap: ClicksContext['relativeSizeMap'] = new Map()
38 let maxMap: ClicksContext['maxMap'] = new Map()
39 const context: ClicksContext = {
40 get current() {
41 return clamp(+current.value, clicksStart, context.total)
42 },
43 set current(value) {
44 current.value = isMounted.value
45 ? clamp(value, clicksStart, context.total)
46 : value /* context.total is not available yet */
47 },
48 clicksStart,
49 get relativeSizeMap() {
50 if (__DEV__ && isMounted.value)
51 console.warn('[slidev] ClicksContext: Unexpected access to relativeSizeMap after mounted')
52 return relativeSizeMap
53 },
54 get maxMap() {
55 return maxMap
56 },
57 get isMounted() {
58 return isMounted.value
59 },
60 setup() {
61 onMounted(() => {
62 isMounted.value = true
63 // Convert maxMap to reactive
64 maxMap = shallowReactive(maxMap)
65 // Make sure the query is not greater than the total
66 if (!isReadonly(current))
67 context.current = current.value
68 })
69 onUnmounted(() => {
70 isMounted.value = false
71 relativeSizeMap = new Map()
72 maxMap = new Map()
73 })
74 },
75 calculateSince(rawAt, size = 1) {
76 const at = normalizeSingleAtValue(rawAt)
77 if (at == null)
78 return null
79 let start: number, max: number, delta: number
80 if (typeof at === 'string') {
81 const offset = context.currentOffset
82 const value = +at
83 start = offset + value
84 max = offset + value + size - 1
85 delta = value + size - 1
86 }
87 else {
88 start = at
89 max = at + size - 1
90 delta = 0
91 }
92 return {
93 start,
94 end: +Number.POSITIVE_INFINITY,
95 max,
96 delta,
97 currentOffset: computed(() => context.current - start),
98 isCurrent: computed(() => context.current === start),
99 isActive: computed(() => context.current >= start),
100 }
101 },
102 calculateRange(rawAt) {
103 const at = normalizeRangeAtValue(rawAt)
104 if (at == null)
105 return null
106 const [a, b] = at
107 let start: number, end: number, delta: number
108 if (typeof a === 'string') {
109 const offset = context.currentOffset
110 start = offset + +a
111 delta = +a
112 }
113 else {
114 start = a
115 delta = 0
116 }
117 if (typeof b === 'string') {
118 end = start + +b
119 delta += +b
120 }
121 else {
122 end = b
123 }
124 return {
125 start,
126 end,
127 max: end,
128 delta,
129 currentOffset: computed(() => context.current - start),
130 isCurrent: computed(() => context.current === start),
131 isActive: computed(() => start <= context.current && context.current < end),
132 }
133 },
134 calculate(at) {
135 if (Array.isArray(at))
136 return context.calculateRange(at)
137 return context.calculateSince(at)
138 },
139 register(el, info) {
140 if (!info)
141 return
142 if (__DEV__ && isMounted.value)
143 console.warn('[slidev] ClicksContext: Unexpected register after mounted')
144 const { delta, max } = info
145 relativeSizeMap.set(el, delta)
146 maxMap.set(el, max)
147 },
148 unregister(el) {
149 relativeSizeMap.delete(el)
150 maxMap.delete(el)
151 },
152 get currentOffset() {
153 return sum(...relativeSizeMap.values())
154 },
155 get total() {
156 return clicksTotalOverrides
157 ?? (isMounted.value
158 ? Math.max(0, ...maxMap.values())
159 : 0 /* fallback value */
160 )
161 },
162 }
163 return context
164 }
165
166 export function createFixedClicks(
167 route?: SlideRoute | undefined,
168 currentInit: MaybeRefOrGetter<number> = 0,
169 ): ClicksContext {
170 const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
171 const clicks = ref(Math.max(toValue(currentInit), clicksStart))
172 watch(() => toValue(currentInit), (v) => {
173 clicks.value = Math.max(v, clicksStart)
174 })
175 return createClicksContextBase(
176 clicks,
177 clicksStart,
178 route?.meta?.clicks,
179 )
180 }
181
181 lines TYPESCRIPT