Skip to content

Commit

Permalink
feat: add VSwitch component (#1562)
Browse files Browse the repository at this point in the history
Co-authored-by: _Kerman <kermanx@qq.com>
  • Loading branch information
xiaodong2008 and KermanX committed May 2, 2024
1 parent 81e2a05 commit e145f1a
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 37 deletions.
10 changes: 10 additions & 0 deletions docs/builtin/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,16 @@ Parameters:

See https://sli.dev/guide/animations.html

### `VSwitch`

Switch between multiple slots based on clicks.

See https://sli.dev/guide/animations.html#enter-leave

- If the `unmount` prop is set to `true`, the previous slot will be unmounted when switching to the next slot. Default is `false`.
- Use the `tag` and `childTag` props to change the default tag of the component and its children. Default is `div`.
- Use the `transition` prop to change the transition effect. Default is `false` (disabled).

### `VDrag`

See https://sli.dev/guide/draggable.html
Expand Down
13 changes: 12 additions & 1 deletion docs/guide/animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,24 @@ You can also specify the enter and leave index for the `v-click` directive by pa
<div v-click.hide="[2, 4]">
This will be hidden at click 2 and 3.
</div>

<div v-click />
<div v-click="'[+1, +1]'">
This will be shown at click 3, and hidden since click 4.
</div>
```

You can also use `v-switch` to achieve the same effect:

```md
<v-switch>
<template #1> show at click 1, hide at click 2. </template>
<template #2> show at click 2, hide at click 5. </template>
<template #5-7> show at click 5, hide at click 7. </template>
</v-switch>
```

See [`VSwitch` Component](/builtin/components#vswitch) for more details.

### Custom Total Clicks Count

By default, Slidev counts how many steps are needed before going to the next slide. You can override this setting by passing the `clicks` frontmatter option:
Expand Down
4 changes: 2 additions & 2 deletions packages/client/builtin/CodeBlockWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import type { PropType } from 'vue'
import { configs } from '../env'
import { makeId, updateCodeHighlightRange } from '../logic/utils'
import { CLASS_VCLICK_HIDDEN } from '../constants'
import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
import { useSlideContext } from '../context'
const props = defineProps({
Expand Down Expand Up @@ -66,7 +66,7 @@ onMounted(() => {
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
clicks.register(id, clicksInfo)
const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
const finallyRange = computed(() => {
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
Expand Down
4 changes: 2 additions & 2 deletions packages/client/builtin/KaTexBlockWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Learn more: https://sli.dev/guide/syntax.html#latex-line-highlighting
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import type { PropType } from 'vue'
import { parseRangeString } from '@slidev/parser'
import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET } from '../constants'
import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
import { makeId } from '../logic/utils'
import { useSlideContext } from '../context'
Expand Down Expand Up @@ -61,7 +61,7 @@ onMounted(() => {
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
clicks.register(id, clicksInfo)
const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
const finallyRange = computed(() => {
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
Expand Down
3 changes: 2 additions & 1 deletion packages/client/builtin/ShikiMagicMove.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import lz from 'lz-string'
import { useSlideContext } from '../context'
import { makeId, updateCodeHighlightRange } from '../logic/utils'
import { useNav } from '../composables/useNav'
import { CLICKS_MAX } from '../constants'
const props = defineProps<{
at?: string | number
Expand Down Expand Up @@ -43,7 +44,7 @@ onMounted(() => {
() => clicks.current,
() => {
// Calculate the step and rangeStr based on the current click count
const clickCount = clicks.current - clickInfo.start
const clickCount = clickInfo ? clicks.current - clickInfo.start : CLICKS_MAX
let step = steps.length - 1
let currentClickSum = 0
let rangeStr = 'all'
Expand Down
6 changes: 3 additions & 3 deletions packages/client/builtin/VClicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { toArray } from '@antfu/utils'
import type { VNode, VNodeArrayChildren } from 'vue'
import { Comment, createVNode, defineComponent, h, isVNode, resolveDirective, withDirectives } from 'vue'
import { normalizeAtValue } from '../composables/useClicks'
import { normalizeSingleAtValue } from '../composables/useClicks'
import VClickGap from './VClickGap.vue'

const listTags = ['ul', 'ol']
Expand Down Expand Up @@ -37,9 +37,9 @@ export default defineComponent({
},
render() {
const every = +this.every
const at = normalizeAtValue(this.at)
const at = normalizeSingleAtValue(this.at)
const isRelative = typeof at === 'string'
if (typeof at !== 'string' && typeof at !== 'number') {
if (!at) {
console.warn('[slidev] Invalid at prop for v-clicks component:', at)
return
}
Expand Down
114 changes: 114 additions & 0 deletions packages/client/builtin/VSwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { PropType, Ref, Slot, TransitionGroupProps, VNode } from 'vue'
import { TransitionGroup, defineComponent, h, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { recomputeAllPoppers } from 'floating-vue'
import { useSlideContext } from '../context'
import { makeId } from '../logic/utils'
import { resolveTransition } from '../logic/transition'
import { skipTransition } from '../logic/hmr'
import { CLASS_VCLICK_CURRENT, CLASS_VCLICK_DISPLAY_NONE, CLASS_VCLICK_PRIOR, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'

export default defineComponent({
props: {
at: {
type: [Number, String],
default: '+1',
},
/**
* unmount or hide the content when it's not visible
*/
unmount: {
type: Boolean,
default: false,
},
transition: {
type: [Object, String, Boolean] as PropType<TransitionGroupProps | string | false>,
default: false,
},
tag: {
type: String,
default: 'div',
},
childTag: {
type: String,
default: 'div',
},
},
setup({ at, unmount, transition, tag, childTag }, { slots }) {
const slotEntries = Object.entries(slots).sort((a, b) => -a[0].split('-')[0] + +b[0].split('-')[0])
const contents: [start: number, end: number, slot: Slot<any> | undefined, elRef: Ref<HTMLElement | undefined>][] = []

let lastStart: number | undefined
for (const [range, slot] of slotEntries) {
const elRef = ref<HTMLElement>()
if (Number.isFinite(+range)) {
contents.push([+range, lastStart ?? +range + 1, slot, elRef])
lastStart = +range
}
else {
const [start, end] = range.split('-').map(Number)
if (!Number.isFinite(start) || !Number.isFinite(end))
throw new Error(`Invalid range for v-switch: ${range}`)
contents.push([start, end, slot, elRef])
lastStart = start
}
}

const size = Math.max(...contents.map(c => c[1])) - 1
const id = makeId()
const offset = ref(0)

const { $clicksContext: clicks, $nav: nav } = useSlideContext()

onMounted(() => {
const clicksInfo = clicks.calculateSince(at, size)
if (!clicksInfo) {
offset.value = CLICKS_MAX
return
}
clicks.register(id, clicksInfo)
watchEffect(() => {
offset.value = clicksInfo.currentOffset.value + 1
})
})

onUnmounted(() => {
clicks.unregister(id)
})

function onAfterLeave() {
// Refer to SlidesShow.vue
skipTransition.value = true
recomputeAllPoppers()
}
const transitionProps = transition && {
...resolveTransition(transition, nav.value.navDirection < 0),
tag,
onAfterLeave,
}

return () => {
const children: VNode[] = []
for (let i = contents.length - 1; i >= 0; i--) {
const [start, end, slot, ref] = contents[i]
const visible = start <= offset.value && offset.value < end
if (unmount && !visible)
continue
children.push(h(childTag, {
'key': i,
ref,
'class': [
CLASS_VCLICK_TARGET,
offset.value === start && CLASS_VCLICK_CURRENT,
offset.value >= end && CLASS_VCLICK_PRIOR,
!visible && CLASS_VCLICK_DISPLAY_NONE,
].filter(Boolean),
'data-slidev-clicks-start': start,
'data-slidev-clicks-end': end,
}, slot?.()))
}
return transitionProps
? h(TransitionGroup, skipTransition.value ? {} : transitionProps, () => children)
: h(tag, children)
}
},
})
39 changes: 29 additions & 10 deletions packages/client/composables/useClicks.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { clamp, sum } from '@antfu/utils'
import type { ClicksContext, NormalizedAtValue, RawAtValue, SlideRoute } from '@slidev/types'
import type { ClicksContext, NormalizedRangeClickValue, NormalizedSingleClickValue, RawAtValue, RawSingleAtValue, SlideRoute } from '@slidev/types'
import type { Ref } from 'vue'
import { computed, ref, shallowReactive } from 'vue'
import { routeForceRefresh } from '../logic/route'

export function normalizeAtValue(at: RawAtValue): NormalizedAtValue {
export function normalizeSingleAtValue(at: RawSingleAtValue): NormalizedSingleClickValue {
if (at === false || at === 'false')
return null
if (at == null || at === true || at === 'true')
return '+1'
if (Array.isArray(at))
return [+at[0], +at[1]]
if (typeof at === 'string' && '+-'.includes(at[0]))
return at
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(
Expand All @@ -33,7 +42,10 @@ export function createClicksContextBase(
relativeOffsets: new Map(),
maxMap: shallowReactive(new Map()),
onMounted() { },
calculateSince(at, size = 1) {
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
Expand All @@ -52,11 +64,16 @@ export function createClicksContextBase(
end: +Number.POSITIVE_INFINITY,
max,
delta,
currentOffset: computed(() => context.current - start),
isCurrent: computed(() => context.current === start),
isActive: computed(() => context.current >= start),
}
},
calculateRange([a, b]) {
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
Expand All @@ -79,18 +96,20 @@ export function createClicksContextBase(
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 (at == null)
return null
if (Array.isArray(at))
return context.calculateRange(at)
return context.calculateSince(at)
},
register(el, { delta, max }) {
register(el, info) {
if (!info)
return
const { delta, max } = info
context.relativeOffsets.set(el, delta)
context.maxMap.set(el, max)
},
Expand Down
1 change: 1 addition & 0 deletions packages/client/composables/useNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
return v
},
set(v) {
skipTransition.value = false
queryClicksRaw.value = v.toString()
},
})
Expand Down
1 change: 1 addition & 0 deletions packages/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const CLASS_VCLICK_GONE = 'slidev-vclick-gone'
export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
export const CLASS_VCLICK_DISPLAY_NONE = 'slidev-vclick-display-none'

export const CLICKS_MAX = 999999

Expand Down
4 changes: 1 addition & 3 deletions packages/client/internals/CodeRunner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useSlideContext } from '../context'
import setupCodeRunners from '../setup/code-runners'
import { useNav } from '../composables/useNav'
import { makeId } from '../logic/utils'
import { normalizeAtValue } from '../composables/useClicks'
import IconButton from './IconButton.vue'
import DomElement from './DomElement.vue'
Expand Down Expand Up @@ -40,8 +39,7 @@ const hidden = ref(props.showOutputAt)
if (props.showOutputAt) {
const id = makeId()
onMounted(() => {
const at = normalizeAtValue(props.showOutputAt)
const info = $clicksContext.calculate(at)
const info = $clicksContext.calculate(props.showOutputAt)
if (info) {
$clicksContext.register(id, info)
watchSyncEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/logic/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const transitionResolveMap: Record<string, string | undefined> = {
'slide-down': 'slide-down | slide-up',
}

function resolveTransition(transition?: string | TransitionGroupProps, isBackward = false): TransitionGroupProps | undefined {
export function resolveTransition(transition?: string | TransitionGroupProps, isBackward = false): TransitionGroupProps | undefined {
if (!transition)
return undefined
if (typeof transition === 'string') {
Expand Down
4 changes: 1 addition & 3 deletions packages/client/modules/v-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
injectionClicksContext,
} from '../constants'
import { directiveInject } from '../utils'
import { normalizeAtValue } from '../composables/useClicks'

export function createVClickDirectives() {
return {
Expand Down Expand Up @@ -126,8 +125,7 @@ export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, v
const flagHide = explicitHide || (dir.modifiers.hide !== false && dir.modifiers.hide != null)
const flagFade = dir.modifiers.fade !== false && dir.modifiers.fade != null

const at = normalizeAtValue(value)
const info = ctx.calculate(at)
const info = ctx.calculate(value)
if (!info)
return null

Expand Down
4 changes: 4 additions & 0 deletions packages/client/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ html {
user-select: none !important;
}

.slidev-vclick-display-none {
display: none !important;
}

.slidev-vclick-fade {
opacity: 0.5;
}
Expand Down

0 comments on commit e145f1a

Please sign in to comment.