aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FileEntry/FileEntryCheckbox.vue')
-rw-r--r--apps/files/src/components/FileEntry/FileEntryCheckbox.vue173
1 files changed, 173 insertions, 0 deletions
diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
new file mode 100644
index 00000000000..5b80a971118
--- /dev/null
+++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
@@ -0,0 +1,173 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <td class="files-list__row-checkbox"
+ @keyup.esc.exact="resetSelection">
+ <NcLoadingIcon v-if="isLoading" :name="loadingLabel" />
+ <NcCheckboxRadioSwitch v-else
+ :aria-label="ariaLabel"
+ :checked="isSelected"
+ data-cy-files-list-row-checkbox
+ @update:checked="onSelectionChange" />
+ </td>
+</template>
+
+<script lang="ts">
+import type { Node } from '@nextcloud/files'
+import type { PropType } from 'vue'
+import type { FileSource } from '../../types.ts'
+
+import { FileType } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
+import { defineComponent } from 'vue'
+
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+import { useActiveStore } from '../../store/active.ts'
+import { useKeyboardStore } from '../../store/keyboard.ts'
+import { useSelectionStore } from '../../store/selection.ts'
+import logger from '../../logger.ts'
+
+export default defineComponent({
+ name: 'FileEntryCheckbox',
+
+ components: {
+ NcCheckboxRadioSwitch,
+ NcLoadingIcon,
+ },
+
+ props: {
+ fileid: {
+ type: Number,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ default: false,
+ },
+ nodes: {
+ type: Array as PropType<Node[]>,
+ required: true,
+ },
+ source: {
+ type: Object as PropType<Node>,
+ required: true,
+ },
+ },
+
+ setup() {
+ const selectionStore = useSelectionStore()
+ const keyboardStore = useKeyboardStore()
+ const activeStore = useActiveStore()
+
+ return {
+ activeStore,
+ keyboardStore,
+ selectionStore,
+ t,
+ }
+ },
+
+ computed: {
+ isActive() {
+ return this.activeStore.activeNode?.source === this.source.source
+ },
+
+ selectedFiles() {
+ return this.selectionStore.selected
+ },
+ isSelected() {
+ return this.selectedFiles.includes(this.source.source)
+ },
+ index() {
+ return this.nodes.findIndex((node: Node) => node.source === this.source.source)
+ },
+ isFile() {
+ return this.source.type === FileType.File
+ },
+ ariaLabel() {
+ return this.isFile
+ ? t('files', 'Toggle selection for file "{displayName}"', { displayName: this.source.basename })
+ : t('files', 'Toggle selection for folder "{displayName}"', { displayName: this.source.basename })
+ },
+ loadingLabel() {
+ return this.isFile
+ ? t('files', 'File is loading')
+ : t('files', 'Folder is loading')
+ },
+ },
+
+ created() {
+ // ctrl+space toggle selection
+ useHotKey(' ', this.onToggleSelect, {
+ stop: true,
+ prevent: true,
+ ctrl: true,
+ })
+
+ // ctrl+shift+space toggle range selection
+ useHotKey(' ', this.onToggleSelect, {
+ stop: true,
+ prevent: true,
+ ctrl: true,
+ shift: true,
+ })
+ },
+
+ methods: {
+ onSelectionChange(selected: boolean) {
+ const newSelectedIndex = this.index
+ const lastSelectedIndex = this.selectionStore.lastSelectedIndex
+
+ // Get the last selected and select all files in between
+ if (this.keyboardStore?.shiftKey && lastSelectedIndex !== null) {
+ const isAlreadySelected = this.selectedFiles.includes(this.source.source)
+
+ const start = Math.min(newSelectedIndex, lastSelectedIndex)
+ const end = Math.max(lastSelectedIndex, newSelectedIndex)
+
+ const lastSelection = this.selectionStore.lastSelection
+ const filesToSelect = this.nodes
+ .map(file => file.source)
+ .slice(start, end + 1)
+ .filter(Boolean) as FileSource[]
+
+ // If already selected, update the new selection _without_ the current file
+ const selection = [...lastSelection, ...filesToSelect]
+ .filter(source => !isAlreadySelected || source !== this.source.source)
+
+ logger.debug('Shift key pressed, selecting all files in between', { start, end, filesToSelect, isAlreadySelected })
+ // Keep previous lastSelectedIndex to be use for further shift selections
+ this.selectionStore.set(selection)
+ return
+ }
+
+ const selection = selected
+ ? [...this.selectedFiles, this.source.source]
+ : this.selectedFiles.filter(source => source !== this.source.source)
+
+ logger.debug('Updating selection', { selection })
+ this.selectionStore.set(selection)
+ this.selectionStore.setLastIndex(newSelectedIndex)
+ },
+
+ resetSelection() {
+ this.selectionStore.reset()
+ },
+
+ onToggleSelect() {
+ // Don't react if the node is not active
+ if (!this.isActive) {
+ return
+ }
+
+ logger.debug('Toggling selection for file', { source: this.source })
+ this.onSelectionChange(!this.isSelected)
+ },
+ },
+})
+</script>