diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-09-27 10:30:55 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-10-10 15:28:52 +0200 |
commit | 35aed73edeffebb9b924cdd13e8b9881f1cd07ab (patch) | |
tree | 3956665427f1b98fd5c84ef1920361366a5fdf85 /apps/files/src | |
parent | 9de246d74f72e290197efd0335aacc6f854cbc9a (diff) | |
download | nextcloud-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.vue | 155 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 78 | ||||
-rw-r--r-- | apps/files/src/components/FilesListFooter.vue | 1 | ||||
-rw-r--r-- | apps/files/src/components/FilesListTableHeaderButton.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 194 | ||||
-rw-r--r-- | apps/files/src/components/VirtualList.vue | 5 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 1 |
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; |