aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-09-27 10:30:55 +0200
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-10-10 15:28:52 +0200
commit35aed73edeffebb9b924cdd13e8b9881f1cd07ab (patch)
tree3956665427f1b98fd5c84ef1920361366a5fdf85 /apps/files/src
parent9de246d74f72e290197efd0335aacc6f854cbc9a (diff)
downloadnextcloud-server-35aed73edeffebb9b924cdd13e8b9881f1cd07ab.tar.gz
nextcloud-server-35aed73edeffebb9b924cdd13e8b9881f1cd07ab.zip
feat: allow external drop and add dropzone
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r--apps/files/src/components/DragAndDropNotice.vue155
-rw-r--r--apps/files/src/components/FileEntry.vue78
-rw-r--r--apps/files/src/components/FilesListFooter.vue1
-rw-r--r--apps/files/src/components/FilesListTableHeaderButton.vue2
-rw-r--r--apps/files/src/components/FilesListVirtual.vue194
-rw-r--r--apps/files/src/components/VirtualList.vue5
-rw-r--r--apps/files/src/views/FilesList.vue1
7 files changed, 338 insertions, 98 deletions
diff --git a/apps/files/src/components/DragAndDropNotice.vue b/apps/files/src/components/DragAndDropNotice.vue
new file mode 100644
index 00000000000..d5f93dac256
--- /dev/null
+++ b/apps/files/src/components/DragAndDropNotice.vue
@@ -0,0 +1,155 @@
+<!--
+ - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+<template>
+ <div class="files-list__drag-drop-notice"
+ :class="{ 'files-list__drag-drop-notice--dragover': dragover }"
+ @drop="onDrop">
+ <div class="files-list__drag-drop-notice-wrapper">
+ <TrayArrowDownIcon :size="48" />
+ <h3 class="files-list-drag-drop-notice__title">
+ {{ t('files', 'Drag and drop files here to upload') }}
+ </h3>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import type { Upload } from '@nextcloud/upload'
+import { join } from 'path'
+import { showSuccess } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+import { getUploader } from '@nextcloud/upload'
+import Vue from 'vue'
+
+import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
+
+import logger from '../logger.js'
+
+export default Vue.extend({
+ name: 'DragAndDropNotice',
+
+ components: {
+ TrayArrowDownIcon,
+ },
+
+ props: {
+ currentFolder: {
+ type: Object,
+ required: true,
+ },
+ dragover: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ methods: {
+ onDrop(event: DragEvent) {
+ this.$emit('update:dragover', false)
+
+ if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ if (event.dataTransfer && event.dataTransfer.files?.length > 0) {
+ const uploader = getUploader()
+ uploader.destination = this.currentFolder
+
+ // Start upload
+ logger.debug(`Uploading files to ${this.currentFolder.path}`)
+ const promises = [...event.dataTransfer.files].map((file: File) => {
+ return uploader.upload(file.name, file) as Promise<Upload>
+ })
+
+ // Process finished uploads
+ Promise.all(promises).then((uploads) => {
+ logger.debug('Upload terminated', { uploads })
+ showSuccess(t('files', 'Upload successful'))
+
+ // Scroll to last upload if terminated
+ const lastUpload = uploads[uploads.length - 1]
+ if (lastUpload?.response?.headers?.['oc-fileid']) {
+ this.$router.push(Object.assign({}, this.$route, {
+ params: {
+ // Remove instanceid from header response
+ fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
+ },
+ }))
+ }
+ })
+ }
+ },
+ t,
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+.files-list__drag-drop-notice {
+ position: absolute;
+ z-index: 9999;
+ top: 0;
+ right: 0;
+ left: 0;
+ display: none;
+ 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);
+
+ &--dragover {
+ display: flex;
+ border-color: black;
+ }
+
+ h3 {
+ margin-left: 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);
+ }
+
+ &__close {
+ position: absolute !important;
+ top: 10px;
+ right: 10px;
+ }
+}
+
+</style>
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index e6592f7ba0c..ff71eaeff9a 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -189,12 +189,13 @@
<script lang="ts">
import type { PropType } from 'vue'
-import { emit } from '@nextcloud/event-bus'
-import { extname } from 'path'
+import { emit, subscribe } from '@nextcloud/event-bus'
+import { extname, join } from 'path'
import { generateUrl } from '@nextcloud/router'
-import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File, FileAction, NodeStatus, Node } from '@nextcloud/files'
+import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File as NcFile, FileAction, NodeStatus, Node } from '@nextcloud/files'
+import { getUploader } from '@nextcloud/upload'
import { showError, showSuccess } from '@nextcloud/dialogs'
-import { translate } from '@nextcloud/l10n'
+import { translate as t } from '@nextcloud/l10n'
import { Type as ShareType } from '@nextcloud/sharing'
import { vOnClickOutside } from '@vueuse/components'
import axios from '@nextcloud/axios'
@@ -278,7 +279,7 @@ export default Vue.extend({
default: false,
},
source: {
- type: [Folder, File, Node] as PropType<Node>,
+ type: [Folder, NcFile, Node] as PropType<Node>,
required: true,
},
index: {
@@ -369,7 +370,7 @@ export default Vue.extend({
size() {
const size = parseInt(this.source.size, 10) || 0
if (typeof size !== 'number' || size < 0) {
- return this.t('files', 'Pending')
+ return t('files', 'Pending')
}
return formatFileSize(size, true)
},
@@ -391,7 +392,7 @@ export default Vue.extend({
if (this.source.mtime) {
return moment(this.source.mtime).fromNow()
}
- return this.t('files_trashbin', 'A long time ago')
+ return t('files_trashbin', 'A long time ago')
},
mtimeOpacity() {
const maxOpacityTime = 31 * 24 * 60 * 60 * 1000 // 31 days
@@ -457,7 +458,7 @@ export default Vue.extend({
linkTo() {
if (this.source.attributes.failed) {
return {
- title: this.t('files', 'This node is unavailable'),
+ title: t('files', 'This node is unavailable'),
is: 'span',
}
}
@@ -475,7 +476,7 @@ export default Vue.extend({
return {
download: this.source.basename,
href: this.source.source,
- title: this.t('files', 'Download file {name}', { name: this.displayName }),
+ title: t('files', 'Download file {name}', { name: this.displayName }),
}
}
@@ -508,7 +509,7 @@ export default Vue.extend({
try {
const previewUrl = this.source.attributes.previewUrl
- || generateUrl('/core/preview?fileid={fileid}', {
+ || generateUrl('/core/preview?fileId={fileid}', {
fileid: this.fileid,
})
const url = new URL(window.location.origin + previewUrl)
@@ -699,13 +700,13 @@ export default Vue.extend({
}
if (success) {
- showSuccess(this.t('files', '"{displayName}" action executed successfully', { displayName }))
+ showSuccess(t('files', '"{displayName}" action executed successfully', { displayName }))
return
}
- showError(this.t('files', '"{displayName}" action failed', { displayName }))
+ showError(t('files', '"{displayName}" action failed', { displayName }))
} catch (e) {
logger.error('Error while executing action', { action, e })
- showError(this.t('files', '"{displayName}" action failed', { displayName }))
+ showError(t('files', '"{displayName}" action failed', { displayName }))
} finally {
// Reset the loading marker
this.loading = ''
@@ -803,15 +804,15 @@ export default Vue.extend({
isFileNameValid(name) {
const trimmedName = name.trim()
if (trimmedName === '.' || trimmedName === '..') {
- throw new Error(this.t('files', '"{name}" is an invalid file name.', { name }))
+ throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
} else if (trimmedName.length === 0) {
- throw new Error(this.t('files', 'File name cannot be empty.'))
+ throw new Error(t('files', 'File name cannot be empty.'))
} else if (trimmedName.indexOf('/') !== -1) {
- throw new Error(this.t('files', '"/" is not allowed inside a file name.'))
+ throw new Error(t('files', '"/" is not allowed inside a file name.'))
} else if (trimmedName.match(OC.config.blacklist_files_regex)) {
- throw new Error(this.t('files', '"{name}" is not an allowed filetype.', { name }))
+ throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
} else if (this.checkIfNodeExists(name)) {
- throw new Error(this.t('files', '{newName} already exists.', { newName: name }))
+ throw new Error(t('files', '{newName} already exists.', { newName: name }))
}
const toCheck = trimmedName.split('')
@@ -859,7 +860,7 @@ export default Vue.extend({
const oldEncodedSource = this.source.encodedSource
const newName = this.newName.trim?.() || ''
if (newName === '') {
- showError(this.t('files', 'Name cannot be empty'))
+ showError(t('files', 'Name cannot be empty'))
return
}
@@ -870,7 +871,7 @@ export default Vue.extend({
// Checking if already exists
if (this.checkIfNodeExists(newName)) {
- showError(this.t('files', 'Another entry with the same name already exists'))
+ showError(t('files', 'Another entry with the same name already exists'))
return
}
@@ -894,7 +895,7 @@ export default Vue.extend({
// Success 🎉
emit('files:node:updated', this.source)
emit('files:node:renamed', this.source)
- showSuccess(this.t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
+ showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
// Reset the renaming store
this.stopRenaming()
@@ -908,15 +909,15 @@ export default Vue.extend({
// TODO: 409 means current folder does not exist, redirect ?
if (error?.response?.status === 404) {
- showError(this.t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
+ showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
return
} else if (error?.response?.status === 412) {
- showError(this.t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
+ showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
return
}
// Unknown error
- showError(this.t('files', 'Could not rename "{oldName}"', { oldName }))
+ showError(t('files', 'Could not rename "{oldName}"', { oldName }))
} finally {
this.loading = false
Vue.set(this.source, 'status', undefined)
@@ -945,8 +946,6 @@ export default Vue.extend({
onDragOver(event: DragEvent) {
this.dragover = this.canDrop
if (!this.canDrop) {
- event.preventDefault()
- event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
return
}
@@ -959,9 +958,13 @@ export default Vue.extend({
}
},
onDragLeave(event: DragEvent) {
- if (this.$el.contains(event.target) && event.target !== this.$el) {
+ // Counter bubbling, make sure we're ending the drag
+ // only when we're leaving the current element
+ const currentTarget = event.currentTarget as HTMLElement
+ if (currentTarget?.contains(event.relatedTarget as HTMLElement)) {
return
}
+
this.dragover = false
},
@@ -990,7 +993,7 @@ export default Vue.extend({
.map(fileid => this.filesStore.getNode(fileid)) as Node[]
const image = await getDragAndDropPreview(nodes)
- event.dataTransfer.setDragImage(image, -10, -10)
+ event.dataTransfer?.setDragImage(image, -10, -10)
},
onDragEnd() {
this.draggingStore.reset()
@@ -999,6 +1002,9 @@ export default Vue.extend({
},
async onDrop(event) {
+ event.preventDefault()
+ event.stopPropagation()
+
// If another button is pressed, cancel it
// This allows cancelling the drag with the right click
if (!this.canDrop || event.button !== 0) {
@@ -1010,6 +1016,16 @@ export default Vue.extend({
logger.debug('Dropped', { event, selection: this.draggingFiles })
+ // Check whether we're uploading files
+ if (event.dataTransfer?.files?.length > 0) {
+ const uploader = getUploader()
+ event.dataTransfer.files.forEach((file: File) => {
+ uploader.upload(join(this.source.path, file.name), file)
+ })
+ logger.debug(`Uploading files to ${this.source.path}`)
+ return
+ }
+
const nodes = this.draggingFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
nodes.forEach(async (node: Node) => {
Vue.set(node, 'status', NodeStatus.LOADING)
@@ -1019,9 +1035,9 @@ export default Vue.extend({
} catch (error) {
logger.error('Error while moving file', { error })
if (isCopy) {
- showError(this.t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
+ showError(t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
} else {
- showError(this.t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
+ showError(t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
}
} finally {
Vue.set(node, 'status', undefined)
@@ -1036,7 +1052,7 @@ export default Vue.extend({
}
},
- t: translate,
+ t,
formatFileSize,
},
})
diff --git a/apps/files/src/components/FilesListFooter.vue b/apps/files/src/components/FilesListFooter.vue
index 3a89970a26d..51b04179b8c 100644
--- a/apps/files/src/components/FilesListFooter.vue
+++ b/apps/files/src/components/FilesListFooter.vue
@@ -159,7 +159,6 @@ export default Vue.extend({
<style scoped lang="scss">
// Scoped row
tr {
- padding-bottom: 300px;
border-top: 1px solid var(--color-border);
// Prevent hover effect on the whole row
background-color: transparent !important;
diff --git a/apps/files/src/components/FilesListTableHeaderButton.vue b/apps/files/src/components/FilesListTableHeaderButton.vue
index c8dcf3cfd66..11d7e63f772 100644
--- a/apps/files/src/components/FilesListTableHeaderButton.vue
+++ b/apps/files/src/components/FilesListTableHeaderButton.vue
@@ -22,7 +22,7 @@
<template>
<NcButton :aria-label="sortAriaLabel(name)"
:class="{'files-list__column-sort-button--active': sortingMode === mode}"
- :alignment="mode !== 'size' ? 'start-reverse' : ''"
+ :alignment="mode !== 'size' ? 'start-reverse' : 'center'"
class="files-list__column-sort-button"
type="tertiary"
@click.stop.prevent="toggleSortBy(mode)">
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index b1bc010423b..438a9d04ca7 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -20,62 +20,76 @@
-
-->
<template>
- <VirtualList :data-component="FileEntry"
- :data-key="'source'"
- :data-sources="nodes"
- :item-height="56"
- :extra-props="{
- isMtimeAvailable,
- isSizeAvailable,
- nodes,
- filesListWidth,
- }"
- :scroll-to-index="scrollToIndex">
- <!-- Accessibility description and headers -->
- <template #before>
- <!-- Accessibility description -->
- <caption class="hidden-visually">
- {{ currentView.caption || t('files', 'List of files and folders.') }}
- {{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
- </caption>
-
- <!-- Headers -->
- <FilesListHeader v-for="header in sortedHeaders"
- :key="header.id"
- :current-folder="currentFolder"
- :current-view="currentView"
- :header="header" />
- </template>
-
- <!-- Thead-->
- <template #header>
- <FilesListTableHeader :files-list-width="filesListWidth"
- :is-mtime-available="isMtimeAvailable"
- :is-size-available="isSizeAvailable"
- :nodes="nodes" />
- </template>
-
- <!-- Tfoot-->
- <template #footer>
- <FilesListTableFooter :files-list-width="filesListWidth"
- :is-mtime-available="isMtimeAvailable"
- :is-size-available="isSizeAvailable"
- :nodes="nodes"
- :summary="summary" />
- </template>
- </VirtualList>
+ <Fragment>
+ <!-- Drag and drop notice -->
+ <DragAndDropNotice v-if="canUpload && filesListWidth >= 512"
+ :current-folder="currentFolder"
+ :dragover.sync="dragover"
+ :style="{ height: dndNoticeHeight }" />
+
+ <VirtualList ref="table"
+ :data-component="FileEntry"
+ :data-key="'source'"
+ :data-sources="nodes"
+ :item-height="56"
+ :extra-props="{
+ isMtimeAvailable,
+ isSizeAvailable,
+ nodes,
+ filesListWidth,
+ }"
+ :scroll-to-index="scrollToIndex"
+ @scroll="onScroll">
+ <!-- Accessibility description and headers -->
+ <template #before>
+ <!-- Accessibility description -->
+ <caption class="hidden-visually">
+ {{ currentView.caption || t('files', 'List of files and folders.') }}
+ {{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
+ </caption>
+
+ <!-- Headers -->
+ <FilesListHeader v-for="header in sortedHeaders"
+ :key="header.id"
+ :current-folder="currentFolder"
+ :current-view="currentView"
+ :header="header" />
+ </template>
+
+ <!-- Thead-->
+ <template #header>
+ <!-- Table header and sort buttons -->
+ <FilesListTableHeader ref="thead"
+ :files-list-width="filesListWidth"
+ :is-mtime-available="isMtimeAvailable"
+ :is-size-available="isSizeAvailable"
+ :nodes="nodes" />
+ </template>
+
+ <!-- Tfoot-->
+ <template #footer>
+ <FilesListTableFooter :files-list-width="filesListWidth"
+ :is-mtime-available="isMtimeAvailable"
+ :is-size-available="isSizeAvailable"
+ :nodes="nodes"
+ :summary="summary" />
+ </template>
+ </VirtualList>
+ </Fragment>
</template>
<script lang="ts">
import type { PropType } from 'vue'
-import type { Node } from '@nextcloud/files'
+import type { Node as NcNode } from '@nextcloud/files'
-import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import { getFileListHeaders, Folder, View } from '@nextcloud/files'
+import { Fragment } from 'vue-frag'
+import { getFileListHeaders, Folder, View, Permission } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import DragAndDropNotice from './DragAndDropNotice.vue'
import FileEntry from './FileEntry.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
@@ -88,9 +102,11 @@ export default Vue.extend({
name: 'FilesListVirtual',
components: {
+ DragAndDropNotice,
FilesListHeader,
- FilesListTableHeader,
FilesListTableFooter,
+ FilesListTableHeader,
+ Fragment,
VirtualList,
},
@@ -108,7 +124,7 @@ export default Vue.extend({
required: true,
},
nodes: {
- type: Array as PropType<Node[]>,
+ type: Array as PropType<NcNode[]>,
required: true,
},
},
@@ -118,6 +134,8 @@ export default Vue.extend({
FileEntry,
headers: getFileListHeaders(),
scrollToIndex: 0,
+ dragover: false,
+ dndNoticeHeight: 0,
}
},
@@ -163,9 +181,18 @@ export default Vue.extend({
return [...this.headers].sort((a, b) => a.order - b.order)
},
+
+ canUpload() {
+ return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
+ },
},
mounted() {
+ // Add events on parent to cover both the table and DragAndDrop notice
+ const mainContent = window.document.querySelector('main.app-content') as HTMLElement
+ mainContent.addEventListener('dragover', this.onDragOver)
+ mainContent.addEventListener('dragleave', this.onDragLeave)
+
// Scroll to the file if it's in the url
if (this.fileId) {
const index = this.nodes.findIndex(node => node.fileid === this.fileId)
@@ -176,15 +203,11 @@ export default Vue.extend({
}
// Open the file sidebar if we have the room for it
- if (document.documentElement.clientWidth > 1024) {
- // Don't open the sidebar for the current folder
- if (this.currentFolder.fileid === this.fileId) {
- return
- }
-
+ // but don't open the sidebar for the current folder
+ if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== this.fileId) {
// Open the sidebar for the given URL fileid
// iif we just loaded the app.
- const node = this.nodes.find(n => n.fileid === this.fileId) as Node
+ const node = this.nodes.find(n => n.fileid === this.fileId) as NcNode
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
logger.debug('Opening sidebar on file ' + node.path, { node })
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
@@ -197,6 +220,49 @@ export default Vue.extend({
return node.fileid
},
+ onDragOver(event: DragEvent) {
+ // Detect if we're only dragging existing files or not
+ const isForeignFile = event.dataTransfer?.types.includes('Files')
+ if (isForeignFile) {
+ this.dragover = true
+ } else {
+ this.dragover = false
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ // If reaching top, scroll up
+ const firstVisible = this.$refs.table?.$el?.querySelector('.files-list__row--visible') as HTMLElement
+ const firstSibling = firstVisible?.previousElementSibling as HTMLElement
+ if ([firstVisible, firstSibling].some(elmt => elmt?.contains(event.target as Node))) {
+ this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop - 25
+ return
+ }
+
+ // If reaching bottom, scroll down
+ const lastVisible = [...(this.$refs.table?.$el?.querySelectorAll('.files-list__row--visible') || [])].pop() as HTMLElement
+ const nextSibling = lastVisible?.nextElementSibling as HTMLElement
+ if ([lastVisible, nextSibling].some(elmt => elmt?.contains(event.target as Node))) {
+ this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop + 25
+ }
+ },
+ onDragLeave(event: DragEvent) {
+ // Counter bubbling, make sure we're ending the drag
+ // only when we're leaving the current element
+ const currentTarget = event.currentTarget as HTMLElement
+ if (currentTarget?.contains(event.relatedTarget as HTMLElement)) {
+ return
+ }
+
+ this.dragover = false
+ },
+
+ onScroll() {
+ // Update the sticky position of the thead to adapt to the scroll
+ this.dndNoticeHeight = (this.$refs.thead.$el?.getBoundingClientRect?.()?.top ?? 0) + 'px'
+ },
+
t,
},
})
@@ -232,6 +298,15 @@ export default Vue.extend({
flex-direction: column;
}
+ .files-list__thead,
+ .files-list__tfoot {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ background-color: var(--color-main-background);
+
+ }
+
// Table header
.files-list__thead {
// Pinned on top when scrolling
@@ -240,12 +315,9 @@ export default Vue.extend({
top: 0;
}
- .files-list__thead,
+ // Table footer
.files-list__tfoot {
- display: flex;
- width: 100%;
- background-color: var(--color-main-background);
-
+ min-height: 300px;
}
tr {
diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue
index 511053b2fa1..ef824d7ba91 100644
--- a/apps/files/src/components/VirtualList.vue
+++ b/apps/files/src/components/VirtualList.vue
@@ -152,11 +152,8 @@ export default Vue.extend({
onScroll() {
// Max 0 to prevent negative index
this.index = Math.max(0, Math.round((this.$el.scrollTop - this.beforeHeight) / this.itemHeight))
+ this.$emit('scroll')
},
},
})
</script>
-
-<style scoped>
-
-</style>
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index d43a2432dff..03ddafb7346 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -437,6 +437,7 @@ export default Vue.extend({
overflow: hidden;
flex-direction: column;
max-height: 100%;
+ position: relative;
}
$margin: 4px;