|
|
|
|
@ -3,23 +3,39 @@ |
|
|
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later |
|
|
|
|
--> |
|
|
|
|
<template> |
|
|
|
|
<tr class="files-list__row-head"> |
|
|
|
|
<tr |
|
|
|
|
class="files-list__row-head" |
|
|
|
|
:class="{ 'files-list__row-head--selected': !isNoneSelected }"> |
|
|
|
|
<th |
|
|
|
|
class="files-list__column files-list__row-checkbox" |
|
|
|
|
@keyup.esc.exact="resetSelection"> |
|
|
|
|
<NcCheckboxRadioSwitch |
|
|
|
|
v-bind="selectAllBind" |
|
|
|
|
:id="FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID" |
|
|
|
|
data-cy-files-list-selection-checkbox |
|
|
|
|
@update:model-value="onToggleAll" /> |
|
|
|
|
|
|
|
|
|
<!-- Batch selection UI lives in the select-all cell so column headers |
|
|
|
|
stay valid and Tab order stays in document flow |
|
|
|
|
(select-all → actions → rows). --> |
|
|
|
|
<template v-if="!isNoneSelected"> |
|
|
|
|
<span class="files-list__selected"> |
|
|
|
|
{{ n('files', '{count} selected', '{count} selected', selectedNodes.length, { count: selectedNodes.length }) }} |
|
|
|
|
</span> |
|
|
|
|
<FilesListTableHeaderActions |
|
|
|
|
:current-view="currentView" |
|
|
|
|
:selected-nodes="selectedNodes" /> |
|
|
|
|
</template> |
|
|
|
|
</th> |
|
|
|
|
|
|
|
|
|
<!-- Columns display --> |
|
|
|
|
<!-- Columns display (always present so headers match body cells; |
|
|
|
|
visually hidden while a selection is active) --> |
|
|
|
|
|
|
|
|
|
<!-- Link to file --> |
|
|
|
|
<th |
|
|
|
|
class="files-list__column files-list__row-name files-list__column--sortable" |
|
|
|
|
:aria-sort="ariaSortForMode('basename')"> |
|
|
|
|
:class="{ 'hidden-visually': !isNoneSelected }" |
|
|
|
|
:aria-sort="ariaSortForMode('basename')" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<!-- Icon or preview --> |
|
|
|
|
<span class="files-list__row-icon" /> |
|
|
|
|
|
|
|
|
|
@ -28,7 +44,10 @@ |
|
|
|
|
</th> |
|
|
|
|
|
|
|
|
|
<!-- Actions --> |
|
|
|
|
<th class="files-list__row-actions"> |
|
|
|
|
<th |
|
|
|
|
class="files-list__row-actions" |
|
|
|
|
:class="{ 'hidden-visually': !isNoneSelected }" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<span class="hidden-visually"> |
|
|
|
|
{{ t('files', 'Actions') }} |
|
|
|
|
</span> |
|
|
|
|
@ -38,8 +57,12 @@ |
|
|
|
|
<th |
|
|
|
|
v-if="isMimeAvailable" |
|
|
|
|
class="files-list__column files-list__row-mime" |
|
|
|
|
:class="{ 'files-list__column--sortable': isMimeAvailable }" |
|
|
|
|
:aria-sort="ariaSortForMode('mime')"> |
|
|
|
|
:class="{ |
|
|
|
|
'files-list__column--sortable': isMimeAvailable, |
|
|
|
|
'hidden-visually': !isNoneSelected, |
|
|
|
|
}" |
|
|
|
|
:aria-sort="ariaSortForMode('mime')" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<FilesListTableHeaderButton :name="t('files', 'File type')" mode="mime" /> |
|
|
|
|
</th> |
|
|
|
|
|
|
|
|
|
@ -47,8 +70,12 @@ |
|
|
|
|
<th |
|
|
|
|
v-if="isSizeAvailable" |
|
|
|
|
class="files-list__column files-list__row-size" |
|
|
|
|
:class="{ 'files-list__column--sortable': isSizeAvailable }" |
|
|
|
|
:aria-sort="ariaSortForMode('size')"> |
|
|
|
|
:class="{ |
|
|
|
|
'files-list__column--sortable': isSizeAvailable, |
|
|
|
|
'hidden-visually': !isNoneSelected, |
|
|
|
|
}" |
|
|
|
|
:aria-sort="ariaSortForMode('size')" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<FilesListTableHeaderButton :name="t('files', 'Size')" mode="size" /> |
|
|
|
|
</th> |
|
|
|
|
|
|
|
|
|
@ -56,8 +83,12 @@ |
|
|
|
|
<th |
|
|
|
|
v-if="isMtimeAvailable" |
|
|
|
|
class="files-list__column files-list__row-mtime" |
|
|
|
|
:class="{ 'files-list__column--sortable': isMtimeAvailable }" |
|
|
|
|
:aria-sort="ariaSortForMode('mtime')"> |
|
|
|
|
:class="{ |
|
|
|
|
'files-list__column--sortable': isMtimeAvailable, |
|
|
|
|
'hidden-visually': !isNoneSelected, |
|
|
|
|
}" |
|
|
|
|
:aria-sort="ariaSortForMode('mtime')" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<FilesListTableHeaderButton :name="t('files', 'Modified')" mode="mtime" /> |
|
|
|
|
</th> |
|
|
|
|
|
|
|
|
|
@ -66,7 +97,8 @@ |
|
|
|
|
v-for="column in columns" |
|
|
|
|
:key="column.id" |
|
|
|
|
:class="classForColumn(column)" |
|
|
|
|
:aria-sort="ariaSortForMode(column.id)"> |
|
|
|
|
:aria-sort="ariaSortForMode(column.id)" |
|
|
|
|
:inert="!isNoneSelected"> |
|
|
|
|
<FilesListTableHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" /> |
|
|
|
|
<span v-else> |
|
|
|
|
{{ column.title }} |
|
|
|
|
@ -76,15 +108,15 @@ |
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
|
import type { Node } from '@nextcloud/files' |
|
|
|
|
import type { Node, View } from '@nextcloud/files' |
|
|
|
|
import type { PropType } from 'vue' |
|
|
|
|
import type { FileSource } from '../types.ts' |
|
|
|
|
|
|
|
|
|
import { t } from '@nextcloud/l10n' |
|
|
|
|
import { n, t } from '@nextcloud/l10n' |
|
|
|
|
import { useHotKey } from '@nextcloud/vue/composables/useHotKey' |
|
|
|
|
import { defineComponent } from 'vue' |
|
|
|
|
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' |
|
|
|
|
import { FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID } from './FilesListTableHeaderActions.vue' |
|
|
|
|
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' |
|
|
|
|
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue' |
|
|
|
|
import { useFileListWidth } from '../composables/useFileListWidth.ts' |
|
|
|
|
import { useRouteParameters } from '../composables/useRouteParameters.ts' |
|
|
|
|
@ -94,12 +126,11 @@ import { useFilesStore } from '../store/files.ts' |
|
|
|
|
import { useSelectionStore } from '../store/selection.ts' |
|
|
|
|
import { logger } from '../utils/logger.ts' |
|
|
|
|
|
|
|
|
|
export const FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID = 'files-list-header-select-all-checkbox' |
|
|
|
|
|
|
|
|
|
export default defineComponent({ |
|
|
|
|
name: 'FilesListTableHeader', |
|
|
|
|
|
|
|
|
|
components: { |
|
|
|
|
FilesListTableHeaderActions, |
|
|
|
|
FilesListTableHeaderButton, |
|
|
|
|
NcCheckboxRadioSwitch, |
|
|
|
|
}, |
|
|
|
|
@ -109,6 +140,11 @@ export default defineComponent({ |
|
|
|
|
], |
|
|
|
|
|
|
|
|
|
props: { |
|
|
|
|
currentView: { |
|
|
|
|
type: Object as PropType<View>, |
|
|
|
|
required: true, |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
isMimeAvailable: { |
|
|
|
|
type: Boolean, |
|
|
|
|
default: false, |
|
|
|
|
@ -145,8 +181,6 @@ export default defineComponent({ |
|
|
|
|
|
|
|
|
|
directory, |
|
|
|
|
isNarrow, |
|
|
|
|
|
|
|
|
|
FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID, |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
@ -206,11 +240,6 @@ export default defineComponent({ |
|
|
|
|
}) |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
mounted() { |
|
|
|
|
const selectAllCheckbox = document.getElementById(FILES_LIST_HEADER_SELECT_ALL_CHECKBOX_ID) |
|
|
|
|
selectAllCheckbox?.addEventListener('keydown', this.onSelectAllCheckboxFocusOut) |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
methods: { |
|
|
|
|
ariaSortForMode(mode: string): 'ascending' | 'descending' | undefined { |
|
|
|
|
if (this.sortingMode === mode) { |
|
|
|
|
@ -224,6 +253,7 @@ export default defineComponent({ |
|
|
|
|
'files-list__column--sortable': !!column.sort, |
|
|
|
|
'files-list__row-column-custom': true, |
|
|
|
|
[`files-list__row-${this.activeStore.activeView?.id}-${column.id}`]: true, |
|
|
|
|
'hidden-visually': !this.isNoneSelected, |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
@ -246,18 +276,7 @@ export default defineComponent({ |
|
|
|
|
this.selectionStore.reset() |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
onSelectAllCheckboxFocusOut(event: KeyboardEvent) { |
|
|
|
|
// If the user tabbed further and we have a batch action to tab to |
|
|
|
|
const firstBatchActionButton = document.getElementById(FILE_LIST_HEAD_FIRST_BATCH_ACTION_ID) |
|
|
|
|
if (event.code === 'Tab' && !event.shiftKey && !event.metaKey && firstBatchActionButton) { |
|
|
|
|
event.preventDefault() |
|
|
|
|
event.stopPropagation() |
|
|
|
|
|
|
|
|
|
firstBatchActionButton.focus() |
|
|
|
|
logger.debug('Focusing first batch action button') |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
n, |
|
|
|
|
t, |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
@ -274,4 +293,11 @@ export default defineComponent({ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.files-list__selected { |
|
|
|
|
padding-inline-end: 12px; |
|
|
|
|
white-space: nowrap; |
|
|
|
|
font-variant-numeric: tabular-nums; |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
color: var(--color-main-text); |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|