aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/DragAndDropNotice.vue
blob: 23ebf7cd2966ae416c23214b21920677c9907ded (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<!--
  - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
	<div v-show="dragover"
		data-cy-files-drag-drop-area
		class="files-list__drag-drop-notice"
		@drop="onDrop">
		<div class="files-list__drag-drop-notice-wrapper">
			<template v-if="canUpload && !isQuotaExceeded">
				<TrayArrowDownIcon :size="48" />
				<h3 class="files-list-drag-drop-notice__title">
					{{ t('files', 'Drag and drop files here to upload') }}
				</h3>
			</template>

			<!-- Not permitted to drop files here -->
			<template v-else>
				<h3 class="files-list-drag-drop-notice__title">
					{{ cantUploadLabel }}
				</h3>
			</template>
		</div>
	</div>
</template>

<script lang="ts">
import type { Folder } from '@nextcloud/files'

import { Permission } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { UploadStatus } from '@nextcloud/upload'
import { defineComponent, type PropType } from 'vue'
import debounce from 'debounce'

import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'

import { useNavigation } from '../composables/useNavigation'
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import logger from '../logger.ts'
import type { RawLocation } from 'vue-router'

export default defineComponent({
	name: 'DragAndDropNotice',

	components: {
		TrayArrowDownIcon,
	},

	props: {
		currentFolder: {
			type: Object as PropType<Folder>,
			required: true,
		},
	},

	setup() {
		const { currentView } = useNavigation()

		return {
			currentView,
		}
	},

	data() {
		return {
			dragover: false,
		}
	},

	computed: {
		/**
		 * Check if the current folder has create permissions
		 */
		canUpload() {
			return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
		},
		isQuotaExceeded() {
			return this.currentFolder?.attributes?.['quota-available-bytes'] === 0
		},

		cantUploadLabel() {
			if (this.isQuotaExceeded) {
				return this.t('files', 'Your have used your space quota and cannot upload files anymore')
			} else if (!this.canUpload) {
				return this.t('files', 'You don’t have permission to upload or create files here')
			}
			return null
		},

		/**
		 * Debounced function to reset the drag over state
		 * Required as Firefox has a bug where no dragleave is emitted:
		 * https://bugzilla.mozilla.org/show_bug.cgi?id=656164
		 */
		resetDragOver() {
			return debounce(() => {
				this.dragover = false
			}, 3000)
		},
	},

	mounted() {
		// Add events on parent to cover both the table and DragAndDrop notice
		const mainContent = window.document.getElementById('app-content-vue') as HTMLElement
		mainContent.addEventListener('dragover', this.onDragOver)
		mainContent.addEventListener('dragleave', this.onDragLeave)
		mainContent.addEventListener('drop', this.onContentDrop)
	},

	beforeDestroy() {
		const mainContent = window.document.getElementById('app-content-vue') as HTMLElement
		mainContent.removeEventListener('dragover', this.onDragOver)
		mainContent.removeEventListener('dragleave', this.onDragLeave)
		mainContent.removeEventListener('drop', this.onContentDrop)
	},

	methods: {
		onDragOver(event: DragEvent) {
			// Needed to keep the drag/drop events chain working
			event.preventDefault()

			const isForeignFile = event.dataTransfer?.types.includes('Files')
			if (isForeignFile) {
				// Only handle uploading of outside files (not Nextcloud files)
				this.dragover = true
				this.resetDragOver()
			}
		},

		onDragLeave(event: DragEvent) {
			// Counter bubbling, make sure we're ending the drag
			// only when we're leaving the current element
			// Avoid flickering
			const currentTarget = event.currentTarget as HTMLElement
			if (currentTarget?.contains((event.relatedTarget ?? event.target) as HTMLElement)) {
				return
			}

			if (this.dragover) {
				this.dragover = false
				this.resetDragOver.clear()
			}
		},

		onContentDrop(event: DragEvent) {
			logger.debug('Drag and drop cancelled, dropped on empty space', { event })
			event.preventDefault()
			if (this.dragover) {
				this.dragover = false
				this.resetDragOver.clear()
			}
		},

		async onDrop(event: DragEvent) {
			// cantUploadLabel is null if we can upload
			if (this.cantUploadLabel) {
				showError(this.cantUploadLabel)
				return
			}

			if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
				return
			}

			event.preventDefault()
			event.stopPropagation()

			// Caching the selection
			const items: DataTransferItem[] = [...event.dataTransfer?.items || []]

			// We need to process the dataTransfer ASAP before the
			// browser clears it. This is why we cache the items too.
			const fileTree = await dataTransferToFileTree(items)

			// We might not have the target directory fetched yet
			const contents = await this.currentView?.getContents(this.currentFolder.path)
			const folder = contents?.folder
			if (!folder) {
				showError(this.t('files', 'Target folder does not exist any more'))
				return
			}

			// If another button is pressed, cancel it. This
			// allows cancelling the drag with the right click.
			if (event.button) {
				return
			}

			logger.debug('Dropped', { event, folder, fileTree })

			// Check whether we're uploading files
			const uploads = await onDropExternalFiles(fileTree, folder, contents.contents)

			// Scroll to last successful upload in current directory if terminated
			const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
				&& !upload.file.webkitRelativePath.includes('/')
				&& upload.response?.headers?.['oc-fileid']
				// Only use the last ID if it's in the current folder
				&& upload.source.replace(folder.source, '').split('/').length === 2)

			if (lastUpload !== undefined) {
				logger.debug('Scrolling to last upload in current folder', { lastUpload })
				const location: RawLocation = {
					path: this.$route.path,
					// Keep params but change file id
					params: {
						...this.$route.params,
						fileid: String(lastUpload.response!.headers['oc-fileid']),
					},
					query: {
						...this.$route.query,
					},
				}
				// Remove open file from query
				delete location.query.openfile
				this.$router.push(location)
			}

			this.dragover = false
			this.resetDragOver.clear()
		},

		t,
	},
})
</script>

<style lang="scss" scoped>
.files-list__drag-drop-notice {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	// Breadcrumbs height + row thead height
	min-height: calc(58px + 55px);
	margin: 0;
	user-select: none;
	color: var(--color-text-maxcontrast);
	background-color: var(--color-main-background);
	border-color: black;

	h3 {
		margin-inline-start: 16px;
		color: inherit;
	}

	&-wrapper {
		display: flex;
		align-items: center;
		justify-content: center;
		height: 15vh;
		max-height: 70%;
		padding: 0 5vw;
		border: 2px var(--color-border-dark) dashed;
		border-radius: var(--border-radius-large);
	}
}

</style>