Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: v-motion #1481

Merged
merged 9 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 9 additions & 8 deletions demo/starter/slides.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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