Skip to content

Commit

Permalink
feat: make possible select multiple files
Browse files Browse the repository at this point in the history
Signed-off-by: Vitor Mattos <[email protected]>
  • Loading branch information
vitormattos committed Nov 23, 2024
1 parent f358a51 commit 03c5b24
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/store/keyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* SPDX-FileCopyrightText: 2024 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { defineStore } from 'pinia'
import { set } from 'vue'

/**
* Observe various events and save the current
* special keys states. Useful for checking the
* current status of a key when executing a method.
* @param {...any} args properties
*/
export const useKeyboardStore = function(...args) {
const store = defineStore('keyboard', {
state: () => ({
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
}),

actions: {
onEvent(event) {
if (!event) {
event = window.event
}
set(this, 'altKey', !!event.altKey)
set(this, 'ctrlKey', !!event.ctrlKey)
set(this, 'metaKey', !!event.metaKey)
set(this, 'shiftKey', !!event.shiftKey)
},
},
})

const keyboardStore = store(...args)
// Make sure we only register the listeners once
if (!keyboardStore._initialized) {
window.addEventListener('keydown', keyboardStore.onEvent)
window.addEventListener('keyup', keyboardStore.onEvent)
window.addEventListener('mousemove', keyboardStore.onEvent)

keyboardStore._initialized = true
}

return keyboardStore
}
57 changes: 57 additions & 0 deletions src/store/selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* SPDX-FileCopyrightText: 2024 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { defineStore } from 'pinia'
import { set } from 'vue'

import { subscribe } from '@nextcloud/event-bus'

export const useSelectionStore = function(...args) {
const store = defineStore('selection', {
state: () => ({
selected: [],
lastSelection: [],
lastSelectedIndex: null,
}),

actions: {
/**
* Set the selection of fileIds
* @param {Array} selection Selected files
*/
set(selection = []) {
set(this, 'selected', [...new Set(selection)])
},

/**
* Set the last selected index
* @param {number | null} lastSelectedIndex Position of last selected file
*/
setLastIndex(lastSelectedIndex = null) {
// Update the last selection if we provided a new selection starting point
set(this, 'lastSelection', lastSelectedIndex ? this.selected : [])
set(this, 'lastSelectedIndex', lastSelectedIndex)
},

/**
* Reset the selection
*/
reset() {
set(this, 'selected', [])
set(this, 'lastSelection', [])
set(this, 'lastSelectedIndex', null)
},
},
})

const selectionStore = store(...args)

// Make sure we only register the listeners once
if (!selectionStore._initialized) {
subscribe('libresign:filters:update', selectionStore.reset)
selectionStore._initialized = true
}

return selectionStore
}
6 changes: 6 additions & 0 deletions src/views/FilesList/FileEntry/FileEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<template>
<tr class="files-list__row"
@contextmenu="onRightClick">
<!-- Checkbox -->
<FileEntryCheckbox :is-loading="filesStore.loading"
:source="source" />

<td class="files-list__row-name"
@click="openDetailsIfAvailable">
<FileEntryPreview :source="source" />
Expand Down Expand Up @@ -40,6 +44,7 @@
import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
import FileEntryActions from './FileEntryActions.vue'
import FileEntryCheckbox from './FileEntryCheckbox.vue'
import FileEntryName from './FileEntryName.vue'
import FileEntryPreview from './FileEntryPreview.vue'
import FileEntryStatus from './FileEntryStatus.vue'
Expand All @@ -53,6 +58,7 @@ export default {
components: {
NcDateTime,
FileEntryActions,
FileEntryCheckbox,
FileEntryName,
FileEntryPreview,
FileEntryStatus,
Expand Down
113 changes: 113 additions & 0 deletions src/views/FilesList/FileEntry/FileEntryCheckbox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!--
- SPDX-FileCopyrightText: 2024 LibreCode coop and 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"
@update:checked="onSelectionChange" />
</td>
</template>

<script>
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import logger from '../../../logger.js'
import { useFilesStore } from '../../../store/files.js'
import { useKeyboardStore } from '../../../store/keyboard.js'
import { useSelectionStore } from '../../../store/selection.js'
export default {
name: 'FileEntryCheckbox',
components: {
NcCheckboxRadioSwitch,
NcLoadingIcon,
},
props: {
isLoading: {
type: Boolean,
default: false,
},
source: {
type: Object,
required: true,
},
},
setup() {
const filesStore = useFilesStore()
const keyboardStore = useKeyboardStore()
const selectionStore = useSelectionStore()
return {
filesStore,
keyboardStore,
selectionStore,
}
},
computed: {
selectedFiles() {
return this.selectionStore.selected
},
isSelected() {
return this.selectedFiles.includes(this.source.nodeId)
},
index() {
return this.filesStore.ordered.findIndex(nodeId => Number(nodeId) === this.source.nodeId)
},
ariaLabel() {
return t('libresign', 'Toggle selection for file "{displayName}"', { displayName: this.source.basename })
},
loadingLabel() {
return t('libresign', 'File is loading')
},
},
methods: {
onSelectionChange(selected) {
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.nodeId)
const start = Math.min(newSelectedIndex, lastSelectedIndex)
const end = Math.max(lastSelectedIndex, newSelectedIndex)
const lastSelection = this.selectionStore.lastSelection
const filesToSelect = this.filesStore.ordered
.slice(start, end + 1)
// If already selected, update the new selection _without_ the current file
const selection = [...new Set([...lastSelection, ...filesToSelect])]
.filter(nodeId => !isAlreadySelected || nodeId !== this.source.nodeId)
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.nodeId]
: this.selectedFiles.filter(nodeId => nodeId !== this.source.nodeId)
logger.debug('Updating selection', { selection })
this.selectionStore.set(selection)
this.selectionStore.setLastIndex(newSelectedIndex)
},
resetSelection() {
this.selectionStore.reset()
},
},
}
</script>
6 changes: 6 additions & 0 deletions src/views/FilesList/FileEntry/FileEntryGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
:extension="'.pdf'" />
</td>

<!-- Checkbox -->
<FileEntryCheckbox :is-loading="filesStore.loading"
:source="source" />

<!-- Status -->
<td class="files-list__row-status"
@click="openDetailsIfAvailable">
Expand Down Expand Up @@ -40,6 +44,7 @@
import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
import FileEntryActions from './FileEntryActions.vue'
import FileEntryCheckbox from './FileEntryCheckbox.vue'
import FileEntryName from './FileEntryName.vue'
import FileEntryPreview from './FileEntryPreview.vue'
import FileEntryStatus from './FileEntryStatus.vue'
Expand All @@ -53,6 +58,7 @@ export default {
components: {
NcDateTime,
FileEntryActions,
FileEntryCheckbox,
FileEntryName,
FileEntryPreview,
FileEntryStatus,
Expand Down
57 changes: 57 additions & 0 deletions src/views/FilesList/FilesListTableHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
-->
<template>
<tr class="files-list__row-head">
<th class="files-list__column files-list__row-checkbox"
@keyup.esc.exact="resetSelection">
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
</th>

<!-- Columns display -->

<!-- Link to file -->
Expand Down Expand Up @@ -33,12 +38,19 @@
</template>

<script>
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import logger from '../../logger.js'
import { useFilesStore } from '../../store/files.js'
import { useSelectionStore } from '../../store/selection.js'
export default {
name: 'FilesListTableHeader',
components: {
NcCheckboxRadioSwitch,
FilesListTableHeaderButton,
},
Expand All @@ -48,6 +60,14 @@ export default {
required: true,
},
},
setup() {
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
return {
filesStore,
selectionStore,
}
},
data() {
return {
isAscSorting: false,
Expand All @@ -66,6 +86,29 @@ export default {
],
}
},
computed: {
selectAllBind() {
const label = t('libresign', 'Toggle selection for all files')
return {
'aria-label': label,
checked: this.isAllSelected,
indeterminate: this.isSomeSelected,
title: label,
}
},
selectedNodes() {
return this.selectionStore.selected
},
isAllSelected() {
return this.selectedNodes.length === Object.keys(this.filesStore.files).length
},
isNoneSelected() {
return this.selectedNodes.length === 0
},
isSomeSelected() {
return !this.isAllSelected && !this.isNoneSelected
},
},
methods: {
ariaSortForMode(mode) {
if (this.sortingMode === mode) {
Expand All @@ -81,6 +124,20 @@ export default {
[`files-list__row-${column.id}`]: true,
}
},
onToggleAll(selected) {
if (selected) {
const selection = this.filesStore.ordered
logger.debug('Added all nodes to selection', { selection })
this.selectionStore.setLastIndex(null)
this.selectionStore.set(selection)
} else {
logger.debug('Cleared selection')
this.selectionStore.reset()
}
},
resetSelection() {
this.selectionStore.reset()
},
},
}
</script>
Expand Down

0 comments on commit 03c5b24

Please sign in to comment.