v-bind="section"
dir="auto"
:to="section.to"
- :force-icon-text="index === 0 && filesListWidth >= 486"
+ :force-icon-text="index === 0 && fileListWidth >= 486"
:title="titleForSection(index, section)"
:aria-description="ariaForSection(section)"
@click.native="onClick(section.to)"
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import { useNavigation } from '../composables/useNavigation'
-import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
+import { useNavigation } from '../composables/useNavigation.ts'
+import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
export default defineComponent({
NcIconSvgWrapper,
},
- mixins: [
- filesListWidthMixin,
- ],
-
props: {
path: {
type: String,
const pathsStore = usePathsStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
+ const fileListWidth = useFileListWidth()
const { currentView, views } = useNavigation()
return {
uploaderStore,
currentView,
+ fileListWidth,
views,
}
},
wrapUploadProgressBar(): boolean {
// if an upload is ongoing, and on small screens / mobile, then
// show the progress bar for the upload below breadcrumbs
- return this.isUploadInProgress && this.filesListWidth < 512
+ return this.isUploadInProgress && this.fileListWidth < 512
},
// used to show the views icon for the first breadcrumb
<FileEntryName ref="name"
:basename="basename"
:extension="extension"
- :files-list-width="filesListWidth"
:nodes="nodes"
:source="source"
@auxclick.native="execDefaultAction"
<FileEntryActions v-show="!isRenamingSmallScreen"
ref="actions"
:class="`files-list__row-actions-${uniqueId}`"
- :files-list-width="filesListWidth"
:loading.sync="loading"
:opened.sync="openedMenu"
:source="source" />
import moment from '@nextcloud/moment'
import { useNavigation } from '../composables/useNavigation.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
+ const filesListWidth = useFileListWidth()
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
const {
currentDir,
currentFileId,
currentView,
+ filesListWidth,
}
},
import CustomElementRender from '../CustomElementRender.vue'
import { useNavigation } from '../../composables/useNavigation'
+import { useFileListWidth } from '../../composables/useFileListWidth.ts'
import logger from '../../logger.ts'
export default defineComponent({
},
props: {
- filesListWidth: {
- type: Number,
- required: true,
- },
loading: {
type: String,
required: true,
setup() {
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
+
+ const filesListWidth = useFileListWidth()
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
return {
currentView,
enabledFileActions,
+ filesListWidth,
}
},
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { useNavigation } from '../../composables/useNavigation'
+import { useFileListWidth } from '../../composables/useFileListWidth.ts'
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
import { useRenamingStore } from '../../store/renaming.ts'
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
type: String,
required: true,
},
- filesListWidth: {
- type: Number,
- required: true,
- },
nodes: {
type: Array as PropType<Node[]>,
required: true,
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
const { currentView } = useNavigation(true)
const { directory } = useRouteParameters()
+ const filesListWidth = useFileListWidth()
const renamingStore = useRenamingStore()
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
currentView,
defaultFileAction,
directory,
+ filesListWidth,
renamingStore,
}
<FileEntryName ref="name"
:basename="basename"
:extension="extension"
- :files-list-width="filesListWidth"
:grid-mode="true"
:nodes="nodes"
:source="source"
<!-- Actions -->
<FileEntryActions ref="actions"
:class="`files-list__row-actions-${uniqueId}`"
- :files-list-width="filesListWidth"
:grid-mode="true"
:loading.sync="loading"
:opened.sync="openedMenu"
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.ts'
// The registered actions list
NcLoadingIcon,
},
- mixins: [
- filesListWidthMixin,
- ],
-
props: {
currentView: {
type: Object as PropType<View>,
const actionsMenuStore = useActionsMenuStore()
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
+ const fileListWidth = useFileListWidth()
const { directory } = useRouteParameters()
return {
directory,
+ fileListWidth,
actionsMenuStore,
filesStore,
},
inlineActions() {
- if (this.filesListWidth < 512) {
+ if (this.fileListWidth < 512) {
return 0
}
- if (this.filesListWidth < 768) {
+ if (this.fileListWidth < 768) {
return 1
}
- if (this.filesListWidth < 1024) {
+ if (this.fileListWidth < 1024) {
return 2
}
return 3
isMtimeAvailable,
isSizeAvailable,
nodes,
- filesListWidth,
+ fileListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
<template #header>
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
- :files-list-width="filesListWidth"
+ :files-list-width="fileListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
<!-- Tfoot-->
<template #footer>
<FilesListTableFooter :current-view="currentView"
- :files-list-width="filesListWidth"
+ :files-list-width="fileListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
import { defineComponent } from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { getSummaryFor } from '../utils/fileUtils'
import { useSelectionStore } from '../store/selection.js'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
import VirtualList from './VirtualList.vue'
import logger from '../logger.ts'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
FilesListTableHeaderActions,
},
- mixins: [
- filesListWidthMixin,
- ],
-
props: {
currentView: {
type: View,
setup() {
const userConfigStore = useUserConfigStore()
const selectionStore = useSelectionStore()
+ const fileListWidth = useFileListWidth()
const { fileId, openFile } = useRouteParameters()
return {
fileId,
+ fileListWidth,
openFile,
userConfigStore,
isMtimeAvailable() {
// Hide mtime column on narrow screens
- if (this.filesListWidth < 768) {
+ if (this.fileListWidth < 768) {
return false
}
return this.nodes.some(node => node.mtime !== undefined)
},
isSizeAvailable() {
// Hide size column on narrow screens
- if (this.filesListWidth < 768) {
+ if (this.fileListWidth < 768) {
return false
}
return this.nodes.some(node => node.size !== undefined)
import type { File, Folder, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
+import { defineComponent } from 'vue'
import debounce from 'debounce'
-import Vue from 'vue'
-
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.ts'
interface RecycledPoolItem {
type DataSourceKey = keyof DataSource
-export default Vue.extend({
+export default defineComponent({
name: 'VirtualList',
- mixins: [filesListWidthMixin],
-
props: {
dataComponent: {
type: [Object, Function],
default: false,
},
/**
- * Visually hidden caption for the table accesibility
+ * Visually hidden caption for the table accessibility
*/
caption: {
type: String,
},
},
+ setup() {
+ const fileListWidth = useFileListWidth()
+
+ return {
+ fileListWidth,
+ }
+ },
+
data() {
return {
index: this.scrollToIndex,
if (!this.gridMode) {
return 1
}
- return Math.floor(this.filesListWidth / this.itemWidth)
+ return Math.floor(this.fileListWidth / this.itemWidth)
},
/**
--- /dev/null
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { defineComponent } from 'vue'
+import { useFileListWidth } from './useFileListWidth.ts'
+
+const ComponentMock = defineComponent({
+ template: '<div id="test-component" style="width: 100%;background: white;">{{ fileListWidth }}</div>',
+ setup() {
+ return {
+ fileListWidth: useFileListWidth(),
+ }
+ },
+})
+const FileListMock = defineComponent({
+ template: '<main id="app-content-vue" style="width: 100%;"><component-mock /></main>',
+ components: {
+ ComponentMock,
+ },
+})
+
+describe('composable: fileListWidth', () => {
+
+ it('Has initial value', () => {
+ cy.viewport(600, 400)
+
+ cy.mount(FileListMock, {})
+ cy.get('#app-content-vue')
+ .should('be.visible')
+ .and('contain.text', '600')
+ })
+
+ it('Is reactive to size change', () => {
+ cy.viewport(600, 400)
+ cy.mount(FileListMock)
+ cy.get('#app-content-vue').should('contain.text', '600')
+
+ cy.viewport(800, 400)
+ cy.screenshot()
+ cy.get('#app-content-vue').should('contain.text', '800')
+ })
+
+ it('Is reactive to style changes', () => {
+ cy.viewport(600, 400)
+ cy.mount(FileListMock)
+ cy.get('#app-content-vue')
+ .should('be.visible')
+ .and('contain.text', '600')
+ .invoke('attr', 'style', 'width: 100px')
+
+ cy.get('#app-content-vue')
+ .should('contain.text', '100')
+ })
+})
--- /dev/null
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { Ref } from 'vue'
+import { onMounted, readonly, ref } from 'vue'
+
+/** The element we observe */
+let element: HTMLElement | undefined
+
+/** The current width of the element */
+const width = ref(0)
+
+const observer = new ResizeObserver((elements) => {
+ if (elements[0].contentBoxSize) {
+ // use the newer `contentBoxSize` property if available
+ width.value = elements[0].contentBoxSize[0].inlineSize
+ } else {
+ // fall back to `contentRect`
+ width.value = elements[0].contentRect.width
+ }
+})
+
+/**
+ * Update the observed element if needed and reconfigure the observer
+ */
+function updateObserver() {
+ const el = document.querySelector<HTMLElement>('#app-content-vue') ?? document.body
+ if (el !== element) {
+ // if already observing: stop observing the old element
+ if (element) {
+ observer.unobserve(element)
+ }
+ // observe the new element if needed
+ observer.observe(el)
+ element = el
+ }
+}
+
+/**
+ * Get the reactive width of the file list
+ */
+export function useFileListWidth(): Readonly<Ref<number>> {
+ // Update the observer when the component is mounted (e.g. because this is the files app)
+ onMounted(updateObserver)
+ // Update the observer also in setup context, so we already have an initial value
+ updateObserver()
+
+ return readonly(width)
+}
+++ /dev/null
-/**
- * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { defineComponent } from 'vue'
-
-export default defineComponent({
- data() {
- return {
- filesListWidth: 0,
- }
- },
-
- mounted() {
- const fileListEl = document.querySelector('#app-content-vue')
- this.filesListWidth = fileListEl?.clientWidth ?? 0
-
- // @ts-expect-error The resize observer is just now attached to the object
- this.$resizeObserver = new ResizeObserver((entries) => {
- if (entries.length > 0 && entries[0].target === fileListEl) {
- this.filesListWidth = entries[0].contentRect.width
- }
- })
- // @ts-expect-error The resize observer was attached right before to the this object
- this.$resizeObserver.observe(fileListEl as Element)
- },
-
- beforeDestroy() {
- // @ts-expect-error mounted must have been called before the destroy, so the resize
- this.$resizeObserver.disconnect()
- },
-})
<BreadCrumbs :path="directory" @reload="fetchContent">
<template #actions>
<!-- Sharing button -->
- <NcButton v-if="canShare && filesListWidth >= 512"
+ <NcButton v-if="canShare && fileListWidth >= 512"
:aria-label="shareButtonLabel"
:class="{ 'files-list__header-share-button--shared': shareButtonType }"
:title="shareButtonLabel"
<!-- Secondary loading indicator -->
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
- <NcButton v-if="filesListWidth >= 512 && enableGridView"
+ <NcButton v-if="fileListWidth >= 512 && enableGridView"
:aria-label="gridViewButtonLabel"
:title="gridViewButtonLabel"
class="files-list__header-grid-button"
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { useNavigation } from '../composables/useNavigation.ts'
+import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useFilesStore } from '../store/files.ts'
import { useFiltersStore } from '../store/filters.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
import BreadCrumbs from '../components/BreadCrumbs.vue'
import FilesListVirtual from '../components/FilesListVirtual.vue'
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.ts'
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
},
mixins: [
- filesListWidthMixin,
filesSortingMixin,
],
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
const { currentView } = useNavigation()
+ const fileListWidth = useFileListWidth()
const { directory, fileId } = useRouteParameters()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
currentView,
directory,
fileId,
+ fileListWidth,
t,
filesStore,