173 lines
4.9 KiB
TypeScript
173 lines
4.9 KiB
TypeScript
import type { ClicksContext, NormalizedRangeClickValue, NormalizedSingleClickValue, RawAtValue, RawSingleAtValue, SlideRoute } from '@slidev/types'
|
|
import type { MaybeRefOrGetter, Ref } from 'vue'
|
|
import { clamp, sum } from '@antfu/utils'
|
|
import { computed, isReadonly, onMounted, onUnmounted, ref, shallowReactive, toValue } from 'vue'
|
|
|
|
export function normalizeSingleAtValue(at: RawSingleAtValue): NormalizedSingleClickValue {
|
|
if (at === false || at === 'false')
|
|
return null
|
|
if (at == null || at === true || at === 'true')
|
|
return '+1'
|
|
if (typeof at === 'string' && '+-'.includes(at[0]))
|
|
return at
|
|
const v = +at
|
|
if (Number.isNaN(v)) {
|
|
console.error(`Invalid "at" prop value: ${at}`)
|
|
return null
|
|
}
|
|
return v
|
|
}
|
|
|
|
export function normalizeRangeAtValue(at: RawAtValue): NormalizedRangeClickValue {
|
|
if (Array.isArray(at))
|
|
return [normalizeSingleAtValue(at[0])!, normalizeSingleAtValue(at[1])!]
|
|
return null
|
|
}
|
|
|
|
export function createClicksContextBase(
|
|
current: Ref<number>,
|
|
clicksStart = 0,
|
|
clicksTotalOverrides?: number,
|
|
): ClicksContext {
|
|
const isMounted = ref(false)
|
|
let relativeSizeMap: ClicksContext['relativeSizeMap'] = new Map()
|
|
let maxMap: ClicksContext['maxMap'] = new Map()
|
|
const context: ClicksContext = {
|
|
get current() {
|
|
return clamp(+current.value, clicksStart, context.total)
|
|
},
|
|
set current(value) {
|
|
current.value = isMounted.value
|
|
? clamp(value, clicksStart, context.total)
|
|
: value /* context.total is not available yet */
|
|
},
|
|
clicksStart,
|
|
get relativeSizeMap() {
|
|
if (__DEV__ && isMounted.value)
|
|
console.warn('[slidev] ClicksContext: Unexpected access to relativeSizeMap after mounted')
|
|
return relativeSizeMap
|
|
},
|
|
get maxMap() {
|
|
return maxMap
|
|
},
|
|
get isMounted() {
|
|
return isMounted.value
|
|
},
|
|
setup() {
|
|
onMounted(() => {
|
|
isMounted.value = true
|
|
// Convert maxMap to reactive
|
|
maxMap = shallowReactive(maxMap)
|
|
// Make sure the query is not greater than the total
|
|
if (!isReadonly(current))
|
|
context.current = current.value
|
|
})
|
|
onUnmounted(() => {
|
|
isMounted.value = false
|
|
relativeSizeMap = new Map()
|
|
maxMap = new Map()
|
|
})
|
|
},
|
|
calculateSince(rawAt, size = 1) {
|
|
const at = normalizeSingleAtValue(rawAt)
|
|
if (at == null)
|
|
return null
|
|
let start: number, max: number, delta: number
|
|
if (typeof at === 'string') {
|
|
const offset = context.currentOffset
|
|
const value = +at
|
|
start = offset + value
|
|
max = offset + value + size - 1
|
|
delta = value + size - 1
|
|
}
|
|
else {
|
|
start = at
|
|
max = at + size - 1
|
|
delta = 0
|
|
}
|
|
return {
|
|
start,
|
|
end: +Number.POSITIVE_INFINITY,
|
|
max,
|
|
delta,
|
|
currentOffset: computed(() => context.current - start),
|
|
isCurrent: computed(() => context.current === start),
|
|
isActive: computed(() => context.current >= start),
|
|
}
|
|
},
|
|
calculateRange(rawAt) {
|
|
const at = normalizeRangeAtValue(rawAt)
|
|
if (at == null)
|
|
return null
|
|
const [a, b] = at
|
|
let start: number, end: number, delta: number
|
|
if (typeof a === 'string') {
|
|
const offset = context.currentOffset
|
|
start = offset + +a
|
|
delta = +a
|
|
}
|
|
else {
|
|
start = a
|
|
delta = 0
|
|
}
|
|
if (typeof b === 'string') {
|
|
end = start + +b
|
|
delta += +b
|
|
}
|
|
else {
|
|
end = b
|
|
}
|
|
return {
|
|
start,
|
|
end,
|
|
max: end,
|
|
delta,
|
|
currentOffset: computed(() => context.current - start),
|
|
isCurrent: computed(() => context.current === start),
|
|
isActive: computed(() => start <= context.current && context.current < end),
|
|
}
|
|
},
|
|
calculate(at) {
|
|
if (Array.isArray(at))
|
|
return context.calculateRange(at)
|
|
return context.calculateSince(at)
|
|
},
|
|
register(el, info) {
|
|
if (!info)
|
|
return
|
|
if (__DEV__ && isMounted.value)
|
|
console.warn('[slidev] ClicksContext: Unexpected register after mounted')
|
|
const { delta, max } = info
|
|
relativeSizeMap.set(el, delta)
|
|
maxMap.set(el, max)
|
|
},
|
|
unregister(el) {
|
|
relativeSizeMap.delete(el)
|
|
maxMap.delete(el)
|
|
},
|
|
get currentOffset() {
|
|
return sum(...relativeSizeMap.values())
|
|
},
|
|
get total() {
|
|
return clicksTotalOverrides
|
|
?? (isMounted.value
|
|
? Math.max(0, ...maxMap.values())
|
|
: 0 /* fallback value */
|
|
)
|
|
},
|
|
}
|
|
return context
|
|
}
|
|
|
|
export function createFixedClicks(
|
|
route?: SlideRoute | undefined,
|
|
currentInit: MaybeRefOrGetter<number> = 0,
|
|
): ClicksContext {
|
|
const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
|
|
return createClicksContextBase(
|
|
computed(() => Math.max(toValue(currentInit), clicksStart)),
|
|
clicksStart,
|
|
route?.meta?.clicks,
|
|
)
|
|
}
|