Skip to content

Commit

Permalink
fix: v-motion (#1481)
Browse files Browse the repository at this point in the history
  • Loading branch information
KermanX committed Apr 5, 2024
1 parent 0ef4fdd commit 179a313
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 53 deletions.
17 changes: 9 additions & 8 deletions demo/starter/slides.md
Expand Up @@ -373,8 +373,6 @@ also allows you to add

</div>

---
preload: false
---

# Motions
Expand All @@ -385,34 +383,37 @@ Motion animations are powered by [@vueuse/motion](https://motion.vueuse.org/), t
<div
v-motion
:initial="{ x: -80 }"
:enter="{ x: 0 }">
:enter="{ x: 0 }"
:click-3="{ x: 80 }"
:leave="{ x: 1000 }"
>
Slidev
</div>
```

<div class="w-60 relative mt-6">
<div class="w-60 relative">
<div class="relative w-40 h-40">
<img
v-motion
:initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
class="absolute inset-0"
src="https://sli.dev/logo-square.png"
alt=""
/>
<img
v-motion
:initial="{ y: 500, x: -100, scale: 2 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
class="absolute inset-0"
src="https://sli.dev/logo-circle.png"
alt=""
/>
<img
v-motion
:initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
class="absolute inset-0"
src="https://sli.dev/logo-triangle.png"
alt=""
/>
Expand Down Expand Up @@ -445,7 +446,7 @@ const final = {

<div
v-motion
:initial="{ x:35, y: 40, opacity: 0}"
:initial="{ x:35, y: 30, opacity: 0}"
:enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">

[Learn More](https://sli.dev/guide/animations.html#motion)
Expand Down
72 changes: 51 additions & 21 deletions docs/guide/animations.md
Expand Up @@ -338,34 +338,64 @@ Slidev has [@vueuse/motion](https://motion.vueuse.org/) built-in. You can use th
<div
v-motion
:initial="{ x: -80 }"
:enter="{ x: 0 }">
:enter="{ x: 0 }"
:leave="{ x: 80 }"
>
Slidev
</div>
```

The text `Slidev` will move from `-80px` to its original position on initialization.
The text `Slidev` will move from `-80px` to its original position when entering the slide. When leaving, it will move to `80px`.

> Note: Slidev preloads the next slide for performance, which means the animations might start before you navigate to the page. To get it works properly, you can disable the preloading for the particular slide
>
> ```md
> ---
> preload: false
> ---
> ```
> Before v0.48.9, you need to add `preload: false` to the slide's frontmatter to enable motion.
### Motion with Clicks

> Available since v0.48.9
You can also trigger the motion by clicks. For example

```html
<div
v-motion
:initial="{ x: -80 }"
:enter="{ x: 0, y: 0 }"
:click-1="{ x: 0, y: 30 }"
:click-2="{ y: 60 }"
:click-2-4="{ x: 40 }"
:leave="{ y: 0, x: 80 }"
>
> Or control the element life-cycle with `v-if` to have fine-grained controls
Slidev
</div>
```

Or combine `v-click` with `v-motion`:

```html
<div v-click="[2, 4]" v-motion
:initial="{ x: -50 }"
:enter="{ x: 0 }"
:leave="{ x: 50 }"
>
> ```html
> <div
> v-if="$slidev.nav.currentPage === 7"
> v-motion
> :initial="{ x: -80 }"
> :enter="{ x: 0 }">
> Slidev
> </div>
> ```
Learn mode: [Demo](https://sli.dev/demo/starter/7) | [@vueuse/motion](https://motion.vueuse.org/) | [v-motion](https://motion.vueuse.org/features/directive-usage) | [Presets](https://motion.vueuse.org/features/presets)
Shown at click 2 and hidden at click 4.
</div>
```

The meanings of variants:

- `initial`: When `currentPage < thisPage`, or `v-click` hides the current element because `$clicks` is too small.
- `enter`: When `currentPage === thisPage`, and `v-click` shows the element. _Priority: lowest_
- `click-x`: `x` is a number representing the **absolute** click num. The variant will take effect if `$clicks >= x`. _Priority: `x`_
- `click-x-y`: The variant will take effect if `x <= $clicks < y`. _Priority: `x`_
- `leave`: `currentPage > thisPage`, or `v-click` hides the current element because `$clicks` is too large.

The variants will be combined according to the priority defined above.

::: warning
Due to a Vue internal [bug](https://github.com/vuejs/core/issues/10295), currently **only** `v-click` to the same element of `v-motion` can control the motion animation. As a workaround, you can use something like `v-if="3 < $clicks"` to achieve the same effect.
:::

Learn mode: [Demo](https://sli.dev/demo/starter/10) | [@vueuse/motion](https://motion.vueuse.org/) | [v-motion](https://motion.vueuse.org/features/directive-usage) | [Presets](https://motion.vueuse.org/features/presets)

## Slide Transitions

Expand Down
1 change: 1 addition & 0 deletions packages/client/constants.ts
Expand Up @@ -13,6 +13,7 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionKey<ComputedRef<number>>
export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>

export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
Expand Down
61 changes: 40 additions & 21 deletions packages/client/modules/v-click.ts
@@ -1,5 +1,5 @@
import type { ResolvedClicksInfo } from '@slidev/types'
import type { App, DirectiveBinding, InjectionKey } from 'vue'
import type { App, DirectiveBinding } from 'vue'
import { computed, watchEffect } from 'vue'
import {
CLASS_VCLICK_CURRENT,
Expand All @@ -8,14 +8,12 @@ import {
CLASS_VCLICK_HIDDEN_EXP,
CLASS_VCLICK_PRIOR,
CLASS_VCLICK_TARGET,
injectionClickVisibility,
injectionClicksContext,
} from '../constants'
import { directiveInject, directiveProvide } from '../utils'

export type VClickValue = string | [string | number, string | number] | boolean

export function dirInject<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, defaultValue?: T): T | undefined {
return (dir.instance?.$ as any).provides[key as any] ?? defaultValue
}
export type VClickValue = undefined | string | number | [string | number, string | number] | boolean

export function createVClickDirectives() {
return {
Expand All @@ -25,7 +23,7 @@ export function createVClickDirectives() {
name: 'v-click',

mounted(el, dir) {
const resolved = resolveClick(el, dir, dir.value)
const resolved = resolveClick(el, dir, dir.value, true)
if (resolved == null)
return

Expand All @@ -37,7 +35,8 @@ export function createVClickDirectives() {
if (clicks[1] != null)
el.dataset.slidevClicksEnd = String(clicks[1])

watchEffect(() => {
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
Expand All @@ -62,13 +61,14 @@ export function createVClickDirectives() {
name: 'v-after',

mounted(el, dir) {
const resolved = resolveClick(el, dir, dir.value, true)
const resolved = resolveClick(el, dir, dir.value, true, true)
if (resolved == null)
return

el.classList.toggle(CLASS_VCLICK_TARGET, true)

watchEffect(() => {
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
Expand All @@ -93,13 +93,14 @@ export function createVClickDirectives() {
name: 'v-click-hide',

mounted(el, dir) {
const resolved = resolveClick(el, dir, dir.value, false, true)
const resolved = resolveClick(el, dir, dir.value, true, false, true)
if (resolved == null)
return

el.classList.toggle(CLASS_VCLICK_TARGET, true)

watchEffect(() => {
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
Expand All @@ -117,20 +118,20 @@ export function createVClickDirectives() {
}
}

function isActive(thisClick: number | [number, number], clicks: number) {
function isClickActive(thisClick: number | [number, number], clicks: number) {
return Array.isArray(thisClick)
? thisClick[0] <= clicks && clicks < thisClick[1]
: thisClick <= clicks
}

function isCurrent(thisClick: number | [number, number], clicks: number) {
function isClickCurrent(thisClick: number | [number, number], clicks: number) {
return Array.isArray(thisClick)
? thisClick[0] === clicks
: thisClick === clicks
}

export function resolveClick(el: Element, dir: DirectiveBinding<any>, value: VClickValue, clickAfter = false, flagHide = false): ResolvedClicksInfo | null {
const ctx = dirInject(dir, injectionClicksContext)?.value
export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: VClickValue, provideVisibility = false, clickAfter = false, flagHide = false): ResolvedClicksInfo | null {
const ctx = directiveInject(dir, injectionClicksContext)?.value

if (!el || !ctx)
return null
Expand All @@ -152,29 +153,47 @@ export function resolveClick(el: Element, dir: DirectiveBinding<any>, value: VCl
if (Array.isArray(value)) {
// range (absolute)
delta = 0
thisClick = value as [number, number]
thisClick = [+value[0], +value[1]]
maxClick = +value[1]
}
else {
({ start: thisClick, end: maxClick, delta } = ctx.resolve(value))
}

const isActive = computed(() => isClickActive(thisClick, ctx.current))
const isCurrent = computed(() => isClickCurrent(thisClick, ctx.current))
const isShown = computed(() => flagHide ? !isActive.value : isActive.value)

const resolved: ResolvedClicksInfo = {
max: maxClick,
clicks: thisClick,
delta,
isActive: computed(() => isActive(thisClick, ctx.current)),
isCurrent: computed(() => isCurrent(thisClick, ctx.current)),
isShown: computed(() => flagHide ? !isActive(thisClick, ctx.current) : isActive(thisClick, ctx.current)),
isActive,
isCurrent,
isShown,
flagFade,
flagHide,
}
ctx.register(el, resolved)

if (provideVisibility) {
directiveProvide(dir, injectionClickVisibility, computed(() => {
if (isShown.value)
return true
if (Array.isArray(thisClick))
return ctx.current < thisClick[0] ? 'before' : 'after'
else
return flagHide ? 'after' : 'before'
}))
}

return resolved
}

function unmounted(el: HTMLElement, dir: DirectiveBinding<any>) {
el.classList.toggle(CLASS_VCLICK_TARGET, false)
const ctx = dirInject(dir, injectionClicksContext)?.value
const ctx = directiveInject(dir, injectionClicksContext)?.value
ctx?.unregister(el)
// @ts-expect-error extra prop
el.watchStopHandle?.()
}
8 changes: 7 additions & 1 deletion packages/client/modules/v-mark.ts
Expand Up @@ -121,7 +121,8 @@ export function createVMarkDirective() {
return
}

watchEffect(() => {
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
let shouldShow: boolean | undefined

if (options.value.class)
Expand All @@ -147,6 +148,11 @@ export function createVMarkDirective() {
annotation.hide()
})
},

unmounted: (el) => {
// @ts-expect-error extra prop
el.watchStopHandle?.()
},
})
},
}
Expand Down

0 comments on commit 179a313

Please sign in to comment.