Skip to content

Commit 45f2226

Browse files
authored
Merge pull request #1707 from aronovgj/feat/drag-drop-categories
feat (UI): drag & drop notes to categories
2 parents f966831 + ee1f0cc commit 45f2226

3 files changed

Lines changed: 163 additions & 3 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- Felix Nüsse <felix.nuesse@t-online.de>
3131
- Ferdinand Mütsch <mail@ferdinand-muetsch.de>
3232
- Florian Hülsmann <fh@cbix.de>
33+
- Grigorij Aronov <aronovgj@gmx.net>
3334
- Hendrik Leppelsack <hendrik@leppelsack.de>
3435
- Holger Hees <holger.hees@gmail.com>
3536
- Holly <holly@r00t.li>

src/components/CategoriesList.vue

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
<Fragment>
88
<NcAppNavigationItem
99
:name="t('notes', 'All notes')"
10-
:class="{ active: selectedCategory === null }"
10+
:class="{
11+
active: selectedCategory === null,
12+
'drop-over': dragOverAllNotes,
13+
}"
1114
@click.prevent.stop="onSelectCategory(null)"
15+
@dragover.native="onAllNotesDragOver($event)"
16+
@dragleave.native="onAllNotesDragLeave($event)"
17+
@drop.native="onAllNotesDrop($event)"
1218
>
1319
<template #icon>
1420
<HistoryIcon :size="20" />
@@ -26,8 +32,14 @@
2632
:key="category.name"
2733
:name="categoryTitle(category.name)"
2834
:icon="category.name === '' ? 'icon-emptyfolder' : 'icon-files'"
29-
:class="{ active: category.name === selectedCategory }"
35+
:class="{
36+
active: category.name === selectedCategory,
37+
'drop-over': category.name === dragOverCategory,
38+
}"
3039
@click.prevent.stop="onSelectCategory(category.name)"
40+
@dragover.native="onCategoryDragOver(category.name, $event)"
41+
@dragleave.native="onCategoryDragLeave(category.name, $event)"
42+
@drop.native="onCategoryDrop(category.name, $event)"
3143
>
3244
<template #icon>
3345
<FolderOutlineIcon v-if="category.name === ''" :size="20" />
@@ -52,7 +64,7 @@ import FolderIcon from 'vue-material-design-icons/Folder.vue'
5264
import FolderOutlineIcon from 'vue-material-design-icons/FolderOutline.vue'
5365
import HistoryIcon from 'vue-material-design-icons/History.vue'
5466
55-
import { getCategories } from '../NotesService.js'
67+
import { getCategories, setCategory } from '../NotesService.js'
5668
import { categoryLabel } from '../Util.js'
5769
import store from '../store.js'
5870
@@ -69,6 +81,13 @@ export default {
6981
HistoryIcon,
7082
},
7183
84+
data() {
85+
return {
86+
dragOverCategory: null,
87+
dragOverAllNotes: false,
88+
}
89+
},
90+
7291
computed: {
7392
numNotes() {
7493
return store.getters.numNotes()
@@ -88,6 +107,118 @@ export default {
88107
return categoryLabel(category)
89108
},
90109
110+
getDraggedNoteId(event) {
111+
const dt = event?.dataTransfer
112+
if (!dt) {
113+
return null
114+
}
115+
116+
let raw = ''
117+
try {
118+
raw = dt.getData('application/x-nextcloud-notes-note-id')
119+
} catch {
120+
// Some browsers only allow specific mime types.
121+
}
122+
if (!raw) {
123+
raw = dt.getData('text/plain') || dt.getData('text/uri-list') || dt.getData('text/x-moz-url') || ''
124+
}
125+
126+
const match = /\/note\/(\d+)(?:[/?#\s]|$)/.exec(raw) || /^\s*(\d+)\s*$/.exec(raw)
127+
const noteId = match ? Number.parseInt(match[1], 10) : Number.NaN
128+
if (!Number.isFinite(noteId)) {
129+
return null
130+
}
131+
const note = store.getters.getNote(noteId)
132+
if (!note || note.readonly) {
133+
return null
134+
}
135+
136+
return noteId
137+
},
138+
139+
onCategoryDragOver(category, event) {
140+
event.preventDefault()
141+
if (event.dataTransfer) {
142+
event.dataTransfer.dropEffect = 'move'
143+
}
144+
this.dragOverAllNotes = false
145+
this.dragOverCategory = category
146+
},
147+
148+
onAllNotesDragOver(event) {
149+
event.preventDefault()
150+
if (event.dataTransfer) {
151+
event.dataTransfer.dropEffect = 'move'
152+
}
153+
this.dragOverCategory = null
154+
this.dragOverAllNotes = true
155+
},
156+
157+
onAllNotesDragLeave(event) {
158+
if (!this.dragOverAllNotes) {
159+
return
160+
}
161+
162+
const currentTarget = event.currentTarget
163+
const relatedTarget = event.relatedTarget
164+
if (currentTarget && relatedTarget && currentTarget.contains(relatedTarget)) {
165+
return
166+
}
167+
168+
this.dragOverAllNotes = false
169+
},
170+
171+
onCategoryDragLeave(category, event) {
172+
if (this.dragOverCategory !== category) {
173+
return
174+
}
175+
176+
const currentTarget = event.currentTarget
177+
const relatedTarget = event.relatedTarget
178+
if (currentTarget && relatedTarget && currentTarget.contains(relatedTarget)) {
179+
return
180+
}
181+
182+
this.dragOverCategory = null
183+
},
184+
185+
async onAllNotesDrop(event) {
186+
event.preventDefault()
187+
event.stopPropagation()
188+
189+
this.dragOverAllNotes = false
190+
const noteId = this.getDraggedNoteId(event)
191+
if (noteId === null) {
192+
return
193+
}
194+
195+
const note = store.getters.getNote(noteId)
196+
if (!note || note.category === '') {
197+
return
198+
}
199+
200+
await setCategory(noteId, '')
201+
},
202+
203+
async onCategoryDrop(category, event) {
204+
event.preventDefault()
205+
event.stopPropagation()
206+
207+
const noteId = this.getDraggedNoteId(event)
208+
this.dragOverCategory = null
209+
this.dragOverAllNotes = false
210+
if (noteId === null) {
211+
return
212+
}
213+
214+
const note = store.getters.getNote(noteId)
215+
if (!note || note.category === category) {
216+
return
217+
}
218+
219+
await setCategory(noteId, category)
220+
},
221+
91222
onSelectCategory(category) {
92223
store.commit('setSelectedCategory', category)
93224
},
@@ -98,4 +229,10 @@ export default {
98229
.app-navigation-entry-wrapper.active:deep(.app-navigation-entry) {
99230
background-color: var(--color-primary-element-light) !important;
100231
}
232+
233+
.app-navigation-entry-wrapper.drop-over:deep(.app-navigation-entry) {
234+
background-color: var(--color-primary-element-light) !important;
235+
outline: 2px dashed var(--color-primary-element);
236+
outline-offset: -2px;
237+
}
101238
</style>

src/components/NoteItem.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
:name="title"
99
:active="isSelected"
1010
:to="{ name: 'note', params: { noteId: note.id.toString() } }"
11+
:draggable="isDraggable"
1112
one-line
1213
@update:menuOpen="onMenuChange"
1314
@click="onNoteSelected(note.id)"
15+
@dragstart.native="onDragStart"
1416
>
1517
<template #subtitle>
1618
{{ categoryTitle }}
@@ -152,6 +154,10 @@ export default {
152154
},
153155
154156
computed: {
157+
isDraggable() {
158+
return !this.note.readonly
159+
},
160+
155161
isSelected() {
156162
return this.$store.getters.getSelectedNote() === this.note.id
157163
},
@@ -206,6 +212,22 @@ export default {
206212
},
207213
208214
methods: {
215+
onDragStart(event) {
216+
if (!this.isDraggable) {
217+
event.preventDefault()
218+
return
219+
}
220+
221+
const noteId = this.note.id.toString()
222+
event.dataTransfer.effectAllowed = 'move'
223+
try {
224+
event.dataTransfer.setData('application/x-nextcloud-notes-note-id', noteId)
225+
} catch {
226+
// Some browsers only allow specific mime types.
227+
}
228+
event.dataTransfer.setData('text/plain', noteId)
229+
},
230+
209231
onMenuChange(state) {
210232
this.actionsOpen = state
211233
this.showCategorySelect = false

0 commit comments

Comments
 (0)