Skip to content

Commit 39b0843

Browse files
Implement drag-and-drop functionality for permissions and roles management
1 parent 3ac14d4 commit 39b0843

2 files changed

Lines changed: 314 additions & 6 deletions

File tree

src/components/general/panels/access/LAccessPermissions.vue

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
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
@@ -20,7 +38,7 @@
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";
4967
import { addPermission, internals } from "@/internals";
5068
5169
const newPermission = ref("");
70+
const draggingPermissionId = ref<string | null>(null);
71+
const dropBeforePermissionId = ref<string | null>(null);
72+
const dropAtEnd = ref(false);
5273
5374
function 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

Comments
 (0)