22import type { DocksContext } from ' @vitejs/devtools-kit/client'
33import type { SharedState } from ' @vitejs/devtools-kit/utils/shared-state'
44import type { DevToolsDocksUserSettings } from ' ../../state/dock-settings'
5- import { computed } from ' vue'
5+ import { useDraggable } from ' @vueuse/core'
6+ import { computed , ref , useTemplateRef } from ' vue'
67import { docksGroupByCategories } from ' ../../state/dock-settings'
78import { sharedStateToRef } from ' ../../state/docks'
89import HashBadge from ' ../display/HashBadge.vue'
@@ -19,6 +20,102 @@ const categories = computed(() => {
1920 return docksGroupByCategories (props .context .docks .entries , props .settingsStore .value (), { includeHidden: true })
2021})
2122
23+ const sortContainerEl = useTemplateRef <HTMLElement >(' sortContainer' )
24+ const entryEls = new Map <string , { el: HTMLElement , category: string }>()
25+ const draggingId = ref <string | null >(null )
26+ const draggingCategory = ref <string | null >(null )
27+ const dragOverId = ref <string | null >(null )
28+ const hasMoved = ref (false )
29+ let startY = 0
30+ const DRAG_THRESHOLD = 4
31+
32+ function findEntryFromEvent(event : PointerEvent ): { dockId: string | null , category: string | null } {
33+ let el = event .target as HTMLElement | null
34+ while (el && el !== sortContainerEl .value ) {
35+ if (el .dataset .dockId )
36+ return { dockId: el .dataset .dockId , category: el .dataset .category ?? null }
37+ el = el .parentElement
38+ }
39+ return { dockId: null , category: null }
40+ }
41+
42+ function isInteractiveElement(el : HTMLElement | null ): boolean {
43+ while (el && el !== sortContainerEl .value ) {
44+ if (el .tagName === ' BUTTON' || el .tagName === ' A' || el .tagName === ' INPUT' )
45+ return true
46+ el = el .parentElement
47+ }
48+ return false
49+ }
50+
51+ useDraggable (sortContainerEl , {
52+ onStart(_ , event ) {
53+ if (event .button !== 0 )
54+ return false
55+ if (isInteractiveElement (event .target as HTMLElement ))
56+ return false
57+ const { dockId, category } = findEntryFromEvent (event )
58+ if (! dockId || ! category )
59+ return false
60+ draggingId .value = dockId
61+ draggingCategory .value = category
62+ hasMoved .value = false
63+ startY = event .clientY
64+ },
65+ onMove(_ , event ) {
66+ if (! draggingId .value )
67+ return
68+ if (! hasMoved .value ) {
69+ if (Math .abs (event .clientY - startY ) < DRAG_THRESHOLD )
70+ return
71+ hasMoved .value = true
72+ }
73+ let target: string | null = null
74+ for (const [id, { el, category }] of entryEls ) {
75+ if (id === draggingId .value )
76+ continue
77+ if (category !== draggingCategory .value )
78+ continue
79+ const rect = el .getBoundingClientRect ()
80+ if (event .clientY >= rect .top && event .clientY <= rect .bottom ) {
81+ target = id
82+ break
83+ }
84+ }
85+ dragOverId .value = target
86+ },
87+ onEnd() {
88+ if (draggingId .value && hasMoved .value && dragOverId .value && draggingCategory .value ) {
89+ const categoryEntries = categories .value .find (([cat ]) => cat === draggingCategory .value )
90+ if (categoryEntries ) {
91+ const items = [... categoryEntries [1 ]]
92+ const fromIndex = items .findIndex (item => item .id === draggingId .value )
93+ const toIndex = items .findIndex (item => item .id === dragOverId .value )
94+ if (fromIndex !== - 1 && toIndex !== - 1 ) {
95+ items .splice (toIndex , 0 , items .splice (fromIndex , 1 )[0 ]! )
96+ categoryEntries [1 ] = items
97+ props .settingsStore .mutate ((state ) => {
98+ items .forEach ((item , index ) => {
99+ state .docksCustomOrder [item .id ] = index
100+ })
101+ })
102+ }
103+ }
104+ }
105+ draggingId .value = null
106+ draggingCategory .value = null
107+ dragOverId .value = null
108+ hasMoved .value = false
109+ },
110+ })
111+
112+ function setEntryRef(el : any , dockId : string , category : string ) {
113+ if (el )
114+ entryEls .set (dockId , { el: el as HTMLElement , category })
115+ else
116+ entryEls .delete (dockId )
117+ }
118+
22119function getCategoryLabel(category : string ): string {
23120 const labels: Record <string , string > = {
24121 ' ~viteplus' : ' Vite+' ,
@@ -132,7 +229,7 @@ function resetCustomOrderForCategory(category: string) {
132229 Manage visibility and order of dock entries. Hidden entries will not appear in the dock bar.
133230 </p >
134231
135- <div class =" flex flex-col gap-4" >
232+ <div ref = " sortContainer " class =" flex flex-col gap-4" >
136233 <template v-for =" [category , entries ] of categories " :key =" category " >
137234 <div
138235 class =" border border-base rounded-lg overflow-hidden transition-opacity"
@@ -173,9 +270,23 @@ function resetCustomOrderForCategory(category: string) {
173270 <div
174271 v-for =" (dock, index) of entries"
175272 :key =" dock.id"
176- class =" flex items-center gap-3 px-4 py-2.5 hover:bg-gray/5 transition-colors group border-b border-base border-t-0"
177- :class =" settings.docksHidden.includes(dock.id) ? 'op40' : ''"
273+ :ref =" (el: any) => setEntryRef(el, dock.id, category)"
274+ :data-dock-id =" dock.id"
275+ :data-category =" category"
276+ class =" flex items-center gap-3 px-2 py-2.5 hover:bg-gray/5 transition-all group border-b border-base border-t-0"
277+ :class =" [
278+ settings.docksHidden.includes(dock.id) ? 'op40' : '',
279+ hasMoved && draggingId === dock.id ? 'op30 bg-gray/10' : '',
280+ dragOverId === dock.id ? 'ring-1.5 ring-purple/50 rounded' : '',
281+ hasMoved ? 'select-none' : '',
282+ ]"
178283 >
284+ <!-- drag icon -->
285+ <div
286+ class =" i-ph-dots-six-vertical w-4 h-4 shrink-0 op25 group-hover:op50 transition-opacity cursor-grab"
287+ :style =" hasMoved && draggingId === dock.id ? 'cursor: grabbing' : ''"
288+ />
289+
179290 <!-- Visibility toggle -->
180291 <button
181292 class =" w-6 h-6 flex items-center justify-center rounded border border-transparent hover:border-base transition-colors shrink-0"
0 commit comments