1111 :force-icon-text =" true"
1212 :title =" titleForSection(index, section)"
1313 :aria-description =" ariaForSection(section)"
14- @click.native =" onClick(section.to)" >
14+ @click.native =" onClick(section.to)"
15+ @dragover.native =" onDragOver($event, section.dir)"
16+ @drop =" onDrop($event, section.dir)" >
1517 <template v-if =" index === 0 " #icon >
1618 <NcIconSvgWrapper :size =" 20"
1719 :svg =" viewIcon" />
2527 </NcBreadcrumbs >
2628</template >
2729
28- <script >
30+ <script lang="ts">
31+ import type { Node } from ' @nextcloud/files'
32+
2933import { basename } from ' path'
30- import { translate as t } from ' @nextcloud/l10n'
31- import homeSvg from ' @mdi/svg/svg/home.svg?raw'
34+ import { defineComponent } from ' vue'
35+ import { Permission } from ' @nextcloud/files'
36+ import { translate as t } from ' @nextcloud/l10n'
37+ import HomeSvg from ' @mdi/svg/svg/home.svg?raw'
3238import NcBreadcrumb from ' @nextcloud/vue/dist/Components/NcBreadcrumb.js'
3339import NcBreadcrumbs from ' @nextcloud/vue/dist/Components/NcBreadcrumbs.js'
3440import NcIconSvgWrapper from ' @nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
35- import Vue from ' vue'
3641
42+ import { onDropInternalFiles , dataTransferToFileTree , onDropExternalFiles } from ' ../services/DropService'
43+ import { showError } from ' @nextcloud/dialogs'
44+ import { useDragAndDropStore } from ' ../store/dragging.ts'
3745import { useFilesStore } from ' ../store/files.ts'
3846import { usePathsStore } from ' ../store/paths.ts'
47+ import { useSelectionStore } from ' ../store/selection.ts'
48+ import filesListWidthMixin from ' ../mixins/filesListWidth.ts'
49+ import logger from ' ../logger'
3950
40- export default Vue . extend ({
51+ export default defineComponent ({
4152 name: ' BreadCrumbs' ,
4253
4354 components: {
@@ -46,6 +57,10 @@ export default Vue.extend({
4657 NcIconSvgWrapper ,
4758 },
4859
60+ mixins: [
61+ filesListWidthMixin ,
62+ ],
63+
4964 props: {
5065 path: {
5166 type: String ,
@@ -54,11 +69,15 @@ export default Vue.extend({
5469 },
5570
5671 setup() {
72+ const draggingStore = useDragAndDropStore ()
5773 const filesStore = useFilesStore ()
5874 const pathsStore = usePathsStore ()
75+ const selectionStore = useSelectionStore ()
5976 return {
77+ draggingStore ,
6078 filesStore ,
6179 pathsStore ,
80+ selectionStore ,
6281 }
6382 },
6483
@@ -76,22 +95,32 @@ export default Vue.extend({
7695 },
7796
7897 sections() {
79- return this .dirs .map (dir => {
98+ return this .dirs .map (( dir : string , index : number ) => {
8099 const fileid = this .getFileIdFromPath (dir )
81100 const to = { ... this .$route , params: { fileid }, query: { dir } }
82101 return {
83102 dir ,
84103 exact: true ,
85104 name: this .getDirDisplayName (dir ),
86105 to ,
106+ // disable drop on current directory
107+ disableDrop: index === this .dirs .length - 1 ,
87108 }
88109 })
89110 },
90111
91112 // used to show the views icon for the first breadcrumb
92113 viewIcon() {
93- return this .currentView ? .icon ?? homeSvg
94- }
114+ return this .currentView ?.icon ?? HomeSvg
115+ },
116+
117+ selectedFiles() {
118+ return this .selectionStore .selected
119+ },
120+
121+ draggingFiles() {
122+ return this .draggingStore .dragging
123+ },
95124 },
96125
97126 methods: {
@@ -117,6 +146,77 @@ export default Vue.extend({
117146 }
118147 },
119148
149+ onDragOver(event : DragEvent , path : string ) {
150+ // Cannot drop on the current directory
151+ if (path === this .dirs [this .dirs .length - 1 ]) {
152+ event .dataTransfer .dropEffect = ' none'
153+ return
154+ }
155+
156+ // Handle copy/move drag and drop
157+ if (event .ctrlKey ) {
158+ event .dataTransfer .dropEffect = ' copy'
159+ } else {
160+ event .dataTransfer .dropEffect = ' move'
161+ }
162+ },
163+
164+ async onDrop(event : DragEvent , path : string ) {
165+ // skip if native drop like text drag and drop from files names
166+ if (! this .draggingFiles && ! event .dataTransfer ?.items ?.length ) {
167+ return
168+ }
169+
170+ // Do not stop propagation, so the main content
171+ // drop event can be triggered too and clear the
172+ // dragover state on the DragAndDropNotice component.
173+ event .preventDefault ()
174+
175+ // Caching the selection
176+ const selection = this .draggingFiles
177+ const items = [... event .dataTransfer ?.items || []] as DataTransferItem []
178+
179+ // We need to process the dataTransfer ASAP before the
180+ // browser clears it. This is why we cache the items too.
181+ const fileTree = await dataTransferToFileTree (items )
182+
183+ // We might not have the target directory fetched yet
184+ const contents = await this .currentView ?.getContents (path )
185+ const folder = contents ?.folder
186+ if (! folder ) {
187+ showError (this .t (' files' , ' Target folder does not exist any more' ))
188+ return
189+ }
190+
191+ const canDrop = (folder .permissions & Permission .CREATE ) !== 0
192+ const isCopy = event .ctrlKey
193+
194+ // If another button is pressed, cancel it. This
195+ // allows cancelling the drag with the right click.
196+ if (! canDrop || event .button !== 0 ) {
197+ return
198+ }
199+
200+ logger .debug (' Dropped' , { event , folder , selection , fileTree })
201+
202+ // Check whether we're uploading files
203+ if (fileTree .contents .length > 0 ) {
204+ await onDropExternalFiles (fileTree , folder , contents .contents )
205+ return
206+ }
207+
208+ // Else we're moving/copying files
209+ const nodes = selection .map (fileid => this .filesStore .getNode (fileid )) as Node []
210+ await onDropInternalFiles (nodes , folder , contents .contents , isCopy )
211+
212+ // Reset selection after we dropped the files
213+ // if the dropped files are within the selection
214+ if (selection .some (fileid => this .selectedFiles .includes (fileid ))) {
215+ logger .debug (' Dropped selection, resetting select store...' )
216+ this .selectionStore .reset ()
217+ }
218+ },
219+
120220 titleForSection(index , section ) {
121221 if (section ?.to ?.query ?.dir === this .$route .query .dir ) {
122222 return t (' files' , ' Reload current directory' )
0 commit comments