Drag and Drop operation on MusicCard
This commit is contained in:
42
app/components/action/Draggable.vue
Normal file
42
app/components/action/Draggable.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDraggable } from '@vue-dnd-kit/core';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const { index, source } = defineProps<{
|
||||||
|
index: number;
|
||||||
|
source: any[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { elementRef, handleDragStart, isOvered, isDragging } = useDraggable({
|
||||||
|
data: computed(() => ({
|
||||||
|
index,
|
||||||
|
source,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="elementRef" @pointerdown="handleDragStart" :class="{
|
||||||
|
'is-over': isOvered,
|
||||||
|
'is-dragging': isDragging,
|
||||||
|
}" class="draggable">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.draggable {
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-dragging {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-over {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -2,9 +2,11 @@
|
|||||||
import { Search } from 'lucide-vue-next'
|
import { Search } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { InputWithIcon } from '@/components/ui/input'
|
import { InputWithIcon } from '@/components/ui/input'
|
||||||
import MusicCard from '@/components/ui/musiccard/MusicCard.vue'
|
|
||||||
import { Outline } from '@/components/ui/outline'
|
import { Outline } from '@/components/ui/outline'
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
|
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
|
||||||
|
import { DnDOperations, useDroppable } from '@vue-dnd-kit/core'
|
||||||
|
import Draggable from '~/components/action/Draggable.vue'
|
||||||
|
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
|
|
||||||
@ -66,6 +68,15 @@ const selectTrack = (trackId: number) => {
|
|||||||
track.selected = track.id === trackId
|
track.selected = track.id === trackId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { elementRef: tracksRef } = useDroppable({
|
||||||
|
data: {
|
||||||
|
source: tracks.value,
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
onDrop: DnDOperations.applyTransfer,
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -88,9 +99,16 @@ const selectTrack = (trackId: number) => {
|
|||||||
<InputWithIcon v-model="searchValue" :icon="Search" placeholder="Search..." type="search" icon-size="5"
|
<InputWithIcon v-model="searchValue" :icon="Search" placeholder="Search..." type="search" icon-size="5"
|
||||||
id="user-search" class="w-full" />
|
id="user-search" class="w-full" />
|
||||||
|
|
||||||
<MusicCard v-for="track in filteredTracks" :key="track.id" :title="track.title" :author="track.author"
|
<div ref="tracksRef" class="space-y-2">
|
||||||
:authorLabel="track.authorLabel" :badges="track.badges" :imageUrl="track.imageUrl"
|
<TransitionGroup name="list" ref="tracksRef">
|
||||||
:date="track.date" :selected="track.selected" @click="selectTrack(track.id)" />
|
<Draggable v-for="(track, index) in filteredTracks" :key="track.id" :index="index"
|
||||||
|
:source="tracks">
|
||||||
|
<MusicCard :key="track.id" :title="track.title" :author="track.author"
|
||||||
|
:authorLabel="track.authorLabel" :badges="track.badges" :imageUrl="track.imageUrl"
|
||||||
|
:date="track.date" :selected="track.selected" @click="selectTrack(track.id)" />
|
||||||
|
</Draggable>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<Outline padding="none" class="h-full" side="left">
|
<Outline padding="none" class="h-full" side="left">
|
||||||
@ -104,3 +122,25 @@ const selectTrack = (trackId: number) => {
|
|||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-move {
|
||||||
|
transition: 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
6
app/plugins/vue-dnd-kit.client.ts
Normal file
6
app/plugins/vue-dnd-kit.client.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// ~/plugins/vue-dnd-kit.client.ts
|
||||||
|
import VueDnDKitPlugin from '@vue-dnd-kit/core';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.vueApp.use(VueDnDKitPlugin);
|
||||||
|
});
|
||||||
3
bun.lock
3
bun.lock
@ -11,6 +11,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tanstack/vue-query": "^5.90.7",
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
|
"@vue-dnd-kit/core": "^1.7.0",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@ -611,6 +612,8 @@
|
|||||||
|
|
||||||
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
||||||
|
|
||||||
|
"@vue-dnd-kit/core": ["@vue-dnd-kit/core@1.7.0", "", { "peerDependencies": { "@vueuse/core": "^13.1.0", "vue": "^3.5.13" } }, "sha512-6Otpo/9Fp/cX5EiUoN6XmIPtm/mOreAXi8clr9HgzVvDHD0sfkgVOFxCUiVbH46yAzr6KBU1Q1tjSWLBqua+5g=="],
|
||||||
|
|
||||||
"@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="],
|
"@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="],
|
||||||
|
|
||||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tanstack/vue-query": "^5.90.7",
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
|
"@vue-dnd-kit/core": "^1.7.0",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user