11<script setup lang="ts">
22import type { DragPosition , DragSetupParams } from ' ./utils/useDragSetup'
3- import { onMounted , useTemplateRef , watch } from ' vue'
3+ import { onBeforeUnmount , onMounted , useTemplateRef , watch } from ' vue'
44import ApproveIcon from ' ./components/icons/ApproveIcon.vue'
55import RejectIcon from ' ./components/icons/RejectIcon.vue'
66import { SwipeAction , useDragSetup } from ' ./utils/useDragSetup'
7+ import { useGhostAnimation } from ' ./utils/useGhostAnimation'
78
89export interface FlashCardProps extends DragSetupParams {
910 // Completely disable dragging feature
@@ -117,10 +118,56 @@ watch(() => params.disableDrag, () => {
117118 setupInteract ()
118119})
119120
121+ // Ghost animation composable
122+ const { isAnimating : isGhostAnimating, createGhost, cleanup : cleanupGhost } = useGhostAnimation (el )
123+
124+ // Helper to trigger ghost animation
125+ function triggerGhostAnimation() {
126+ if (! animation || ! el .value )
127+ return
128+
129+ requestAnimationFrame (() => {
130+ createGhost (
131+ {
132+ animationType: animation .type ,
133+ isRestoring: animation .isRestoring ,
134+ swipeDirection: params .swipeDirection ,
135+ initialPosition: animation .initialPosition ,
136+ getTransformStyle ,
137+ },
138+ () => emit (' animationend' ),
139+ )
140+ })
141+ }
142+
143+ // Watch for animation prop changes (serialize to detect deep changes)
144+ watch (() => JSON .stringify (animation ), (newAnimationStr , oldAnimationStr ) => {
145+ // Skip if element is not mounted yet
146+ if (! el .value ) {
147+ return
148+ }
149+
150+ // If animation changed to a new one while old is still running, cleanup first
151+ if (oldAnimationStr && newAnimationStr && oldAnimationStr !== newAnimationStr ) {
152+ cleanupGhost ()
153+ }
154+
155+ triggerGhostAnimation ()
156+ })
157+
120158onMounted (() => {
121159 if (el .value ?.offsetHeight ) {
122160 emit (' mounted' , el .value ?.offsetHeight )
123161 }
162+
163+ // If animation is set, trigger ghost creation after mount
164+ if (animation ) {
165+ triggerGhostAnimation ()
166+ }
167+ })
168+
169+ onBeforeUnmount (() => {
170+ cleanupGhost ()
124171})
125172
126173defineExpose ({
@@ -135,17 +182,12 @@ defineExpose({
135182 :class =" {
136183 'flash-card--dragging': isDragging,
137184 'flash-card--drag-disabled': params.disableDrag,
185+ 'flash-card--hidden': isGhostAnimating,
138186 }"
139187 :style =" { transform: `translate3D(${position.x}px, ${position.y}px, 0)` }"
140188 >
141189 <div
142190 class =" flash-card__animation-wrapper"
143- :class =" {
144- [`flash-card-animation--${animation?.type}`]: animation?.type,
145- [`flash-card-animation--${animation?.type}-restore`]: animation?.isRestoring,
146- [`flash-card-animation--${params.swipeDirection}`]: animation?.type,
147- }"
148- @animationend =" emit('animationend')"
149191 >
150192 <div class =" flash-card__transform" :style =" getTransformStyle(position)" >
151193 <slot :is-dragging =" isDragging" />
@@ -199,6 +241,10 @@ defineExpose({
199241 pointer-events : none ;
200242}
201243
244+ .flash-card--hidden {
245+ visibility : hidden ;
246+ }
247+
202248/* Base animations (horizontal by default) */
203249.flash-card-animation--approve { animation : approve-horizontal 0.4s cubic-bezier (0.4 ,0 ,0.2 ,1 ) forwards ; }
204250.flash-card-animation--reject { animation : reject-horizontal 0.4s cubic-bezier (0.4 ,0 ,0.2 ,1 ) forwards ; }
@@ -218,14 +264,14 @@ defineExpose({
218264.flash-card-animation--vertical.flash-card-animation--reject-restore { animation : restore-reject-vertical 0.4s cubic-bezier (0.4 ,0 ,0.2 ,1 ) forwards ; }
219265
220266/* Horizontal keyframes */
221- @keyframes approve-horizontal { 0% { opacity : 1 ;} 100% {transform :translateX (320px ) rotate (15deg );opacity :0 ;} }
222- @keyframes reject-horizontal { 0% { opacity : 1 ;} 100% {transform :translateX (-320px ) rotate (-15deg );opacity :0 ;} }
223- @keyframes restore-approve-horizontal { 0% {transform :translateX (320px ) rotate (15deg );opacity :0 ;} 100% {transform :translateX (0 ) rotate (0deg );opacity :1 ;} }
224- @keyframes restore-reject-horizontal { 0% {transform :translateX (-320px ) rotate (-15deg );opacity :0 ;} 100% {transform :translateX (0 ) rotate (0deg );opacity :1 ;} }
267+ @keyframes approve-horizontal { to {transform :translateX (320px ) rotate (15deg );opacity :0 ;} }
268+ @keyframes reject-horizontal { to {transform :translateX (-320px ) rotate (-15deg );opacity :0 ;} }
269+ @keyframes restore-approve-horizontal { from {transform :translateX (320px ) rotate (15deg );opacity :0 ;} to {transform :translateX (0 ) rotate (0deg );opacity :1 ;} }
270+ @keyframes restore-reject-horizontal { from {transform :translateX (-320px ) rotate (-15deg );opacity :0 ;} to {transform :translateX (0 ) rotate (0deg );opacity :1 ;} }
225271
226272/* Vertical keyframes */
227- @keyframes approve-vertical { 0% { opacity : 1 ;} 100% {transform :translateY (-320px );opacity :0 ;} }
228- @keyframes reject-vertical { 0% { opacity : 1 ;} 100% {transform :translateY (320px );opacity :0 ;} }
229- @keyframes restore-approve-vertical { 0% {transform :translateY (-320px );opacity :0 ;} 100% {transform :translateY (0 );opacity :1 ;} }
230- @keyframes restore-reject-vertical { 0% {transform :translateY (320px );opacity :0 ;} 100% {transform :translateY (0 );opacity :1 ;} }
273+ @keyframes approve-vertical { to {transform :translateY (-320px );opacity :0 ;} }
274+ @keyframes reject-vertical { to {transform :translateY (320px );opacity :0 ;} }
275+ @keyframes restore-approve-vertical { from {transform :translateY (-320px );opacity :0 ;} to {transform :translateY (0 );opacity :1 ;} }
276+ @keyframes restore-reject-vertical { from {transform :translateY (320px );opacity :0 ;} to {transform :translateY (0 );opacity :1 ;} }
231277 </style >
0 commit comments