11<template >
2- <div class =" permissions" >
2+ <div
3+ class =" permissions"
4+ :class =" { 'drop-at-end': dropAtEnd }"
5+ >
36 <h3 >Permissions</h3 >
47 <div
5- v-for =" (permission, id) of internals.permissions"
8+ v-for =" (permission, id, index ) of internals.permissions"
69 :key =" id"
710 class =" permission"
11+ draggable =" true"
12+ :class =" {
13+ dragging: draggingPermissionId === id,
14+ 'drop-before': dropBeforePermissionId === id && draggingPermissionId !== id
15+ }"
16+ @dragstart =" onPermissionDragStart($event, id)"
17+ @dragend =" onPermissionDragEnd"
18+ @dragover.prevent =" onPermissionDragOver($event, index)"
19+ @drop.prevent =" onPermissionDrop"
820 >
21+ <span
22+ class =" drag-handle"
23+ title =" Drag to reorder"
24+ >
25+ ::
26+ </span >
927 <LInput
1028 borderless
1129 disabled
2038 @click =" deletePermission (id )"
2139 />
2240 </div >
23- <div class =" permission" >
41+ <div class =" permission new-permission " >
2442 <LInput
2543 v-model =" newPermission "
2644 borderless
@@ -49,6 +67,9 @@ import { ref } from "vue";
4967import { addPermission , internals } from " @/internals" ;
5068
5169const newPermission = ref (" " );
70+ const draggingPermissionId = ref <string | null >(null );
71+ const dropBeforePermissionId = ref <string | null >(null );
72+ const dropAtEnd = ref (false );
5273
5374function addNewPermission() {
5475 addPermission ({
@@ -64,6 +85,84 @@ function deletePermission(permissionId: string) {
6485
6586 delete internals .permissions [permissionId ];
6687}
88+
89+ function onPermissionDragStart(event : DragEvent , permissionId : string ) {
90+ draggingPermissionId .value = permissionId ;
91+
92+ const dataTransfer = event .dataTransfer ;
93+ if (! dataTransfer ) {
94+ return ;
95+ }
96+
97+ dataTransfer .setData (" text/plain" , permissionId );
98+ dataTransfer .setDragImage (event .currentTarget as Element , 16 , 16 );
99+ dataTransfer .dropEffect = " move" ;
100+ dataTransfer .effectAllowed = " move" ;
101+ }
102+
103+ function onPermissionDragEnd() {
104+ draggingPermissionId .value = null ;
105+ dropBeforePermissionId .value = null ;
106+ dropAtEnd .value = false ;
107+ }
108+
109+ function onPermissionDragOver(event : DragEvent , targetIndex : number ) {
110+ const permissionIds = Object .keys (internals .permissions );
111+ const target = event .currentTarget as HTMLElement | null ;
112+
113+ if (! target || ! permissionIds .length ) {
114+ return ;
115+ }
116+
117+ const { height, top } = target .getBoundingClientRect ();
118+ const insertAfter = event .clientY > top + height / 2 ;
119+ const insertionIndex = insertAfter ? targetIndex + 1 : targetIndex ;
120+ const nextPermissionId = permissionIds [insertionIndex ] ?? null ;
121+
122+ dropBeforePermissionId .value = nextPermissionId ;
123+ dropAtEnd .value = nextPermissionId === null ;
124+ }
125+
126+ function onPermissionDrop() {
127+ if (! draggingPermissionId .value ) {
128+ return ;
129+ }
130+
131+ moveRecordEntryBefore (internals .permissions , draggingPermissionId .value , dropBeforePermissionId .value );
132+ draggingPermissionId .value = null ;
133+ dropBeforePermissionId .value = null ;
134+ dropAtEnd .value = false ;
135+ }
136+
137+ function moveRecordEntryBefore<T extends { id: string }>(
138+ record : Record <string , T >,
139+ sourceId : string ,
140+ beforeId : string | null
141+ ) {
142+ if (sourceId === beforeId ) {
143+ return ;
144+ }
145+
146+ const entries = Object .entries (record );
147+ const sourceIndex = entries .findIndex (([id ]) => id === sourceId );
148+ if (sourceIndex === - 1 ) {
149+ return ;
150+ }
151+
152+ const [sourceEntry] = entries .splice (sourceIndex , 1 );
153+ const insertionIndex = beforeId ? entries .findIndex (([id ]) => id === beforeId ) : entries .length ;
154+ const safeInsertionIndex = insertionIndex === - 1 ? entries .length : insertionIndex ;
155+
156+ entries .splice (safeInsertionIndex , 0 , sourceEntry );
157+
158+ for (const id of Object .keys (record )) {
159+ delete record [id ];
160+ }
161+
162+ for (const [id, value] of entries ) {
163+ record [id ] = value ;
164+ }
165+ }
67166 </script >
68167
69168<style scoped>
@@ -85,10 +184,65 @@ function deletePermission(permissionId: string) {
85184 .permission {
86185 display : flex ;
87186 gap : var (--length-xs );
187+ cursor : grab ;
188+ position : relative ;
189+ border-radius : var (--length-radius-xs );
190+ transition : background-color 0.12s ease ;
191+
192+ &.dragging {
193+ opacity : 0.45 ;
194+ }
195+
196+ &.drop-before {
197+ background-color : color-mix(in srgb , #3f8cff 10% , transparent );
198+
199+ &::before {
200+ content : " " ;
201+ position : absolute ;
202+ left : 0 ;
203+ right : 0 ;
204+ top : -4px ;
205+ height : 3px ;
206+ border-radius : 999px ;
207+ background : linear-gradient (90deg , #3f8cff 0% , #66a3ff 100% );
208+ }
209+ }
210+
211+ .drag-handle {
212+ align-items : center ;
213+ color : var (--color-content-softer );
214+ display : inline-flex ;
215+ font-size : var (--font-size-s );
216+ justify-content : center ;
217+ min-width : 16px ;
218+ user-select : none ;
219+ }
220+
221+ &.new-permission {
222+ cursor : default ;
223+ position : relative ;
224+
225+ .drag-handle {
226+ display : none ;
227+ }
228+ }
88229
89230 &:deep(.input ) {
90231 flex : 1 1 0 ;
91232 }
92233 }
234+
235+ &.drop-at-end {
236+ .new-permission ::before {
237+ content : " " ;
238+ position : absolute ;
239+ left : 0 ;
240+ right : 0 ;
241+ top : -4px ;
242+ height : 3px ;
243+ border-radius : 999px ;
244+ background : linear-gradient (90deg , #3f8cff 0% , #66a3ff 100% );
245+ }
246+ }
93247}
94248 </style >
0 commit comments