feat(files): implement shift-select

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ 2023-04-07 11:46:41 +02:00
parent c6645cbc46
commit 8bef77235f
No known key found for this signature in database
GPG key ID: 60C25B8C072916CF
10 changed files with 145 additions and 23 deletions

View file

@ -25,9 +25,10 @@
<td class="files-list__row-checkbox">
<NcCheckboxRadioSwitch v-if="active"
:aria-label="t('files', 'Select the row for {displayName}', { displayName })"
:checked.sync="selectedFiles"
:value="fileid.toString()"
name="selectedFiles" />
:checked="selectedFiles"
:value="fileid"
name="selectedFiles"
@update:checked="onSelectionChange" />
</td>
<!-- Link to file -->
@ -120,6 +121,7 @@ import { getFileActions } from '../services/FileAction.ts'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import { useKeyboardStore } from '../store/keyboard.ts'
import CustomElementRender from './CustomElementRender.vue'
import CustomSvgIconRender from './CustomSvgIconRender.vue'
import logger from '../logger.js'
@ -159,16 +161,22 @@ export default Vue.extend({
type: Number,
required: true,
},
nodes: {
type: Array,
required: true,
},
},
setup() {
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
const userConfigStore = useUserConfigStore()
const keyboardStore = useKeyboardStore()
return {
filesStore,
selectionStore,
userConfigStore,
keyboardStore,
}
},
@ -199,7 +207,7 @@ export default Vue.extend({
},
fileid() {
return this.source.attributes.fileid
return this.source?.fileid?.toString?.()
},
displayName() {
return this.source.attributes.displayName
@ -242,14 +250,8 @@ export default Vue.extend({
}
},
selectedFiles: {
get() {
return this.selectionStore.selected
},
set(selection) {
logger.debug('Changed nodes selection', { selection })
this.selectionStore.set(selection)
},
selectedFiles() {
return this.selectionStore.selected
},
cropPreviews() {
@ -454,6 +456,37 @@ export default Vue.extend({
}
},
onSelectionChange(selection) {
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.fileid)
const start = Math.min(newSelectedIndex, lastSelectedIndex)
const end = Math.max(lastSelectedIndex, newSelectedIndex)
const lastSelection = this.selectionStore.lastSelection
const filesToSelect = this.nodes
.map(file => file.fileid?.toString?.())
.slice(start, end + 1)
// If already selected, update the new selection _without_ the current file
const selection = [...lastSelection, ...filesToSelect]
.filter(fileId => !isAlreadySelected || fileId !== this.fileid)
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
}
logger.debug('Updating selection', { selection })
this.selectionStore.set(selection)
this.selectionStore.setLastIndex(newSelectedIndex)
},
t: translate,
formatFileSize,
},

View file

@ -183,6 +183,7 @@ export default Vue.extend({
if (selected) {
const selection = this.nodes.map(node => node.attributes.fileid.toString())
logger.debug('Added all nodes to selection', { selection })
this.selectionStore.setLastIndex(null)
this.selectionStore.set(selection)
} else {
logger.debug('Cleared selection')

View file

@ -36,6 +36,7 @@
<FileEntry :active="active"
:index="index"
:is-size-available="isSizeAvailable"
:nodes="nodes"
:source="item" />
</template>

View file

@ -27,6 +27,7 @@ import { defineStore } from 'pinia'
import { subscribe } from '@nextcloud/event-bus'
import Vue from 'vue'
import logger from '../logger'
import { FileId } from '../types'
export const useFilesStore = () => {
const store = defineStore('files', {
@ -39,13 +40,13 @@ export const useFilesStore = () => {
/**
* Get a file or folder by id
*/
getNode: (state) => (id: number): Node|undefined => state.files[id],
getNode: (state) => (id: FileId): Node|undefined => state.files[id],
/**
* Get a list of files or folders by their IDs
* Does not return undefined values
*/
getNodes: (state) => (ids: number[]): Node[] => ids
getNodes: (state) => (ids: FileId[]): Node[] => ids
.map(id => state.files[id])
.filter(Boolean),
/**

View file

@ -0,0 +1,64 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* 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/>.
*
*/
/* eslint-disable */
import { defineStore } from 'pinia'
import Vue 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.
*/
export const useKeyboardStore = () => {
const store = defineStore('keyboard', {
state: () => ({
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
}),
actions: {
onEvent(event: MouseEvent | KeyboardEvent) {
if (!event) {
event = window.event as MouseEvent | KeyboardEvent
}
Vue.set(this, 'altKey', !!event.altKey)
Vue.set(this, 'ctrlKey', !!event.ctrlKey)
Vue.set(this, 'metaKey', !!event.metaKey)
Vue.set(this, 'shiftKey', !!event.shiftKey)
},
}
})
const keyboardStore = store()
// 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
}

View file

@ -25,6 +25,7 @@ import type { PathOptions, ServicesState } from '../types.ts'
import { defineStore } from 'pinia'
import Vue from 'vue'
import { subscribe } from '@nextcloud/event-bus'
import { FileId } from '../types'
export const usePathsStore = () => {
const store = defineStore('paths', {
@ -32,7 +33,7 @@ export const usePathsStore = () => {
getters: {
getPath: (state) => {
return (service: string, path: string): number|undefined => {
return (service: string, path: string): FileId|undefined => {
if (!state[service]) {
return undefined
}

View file

@ -22,25 +22,39 @@
/* eslint-disable */
import { defineStore } from 'pinia'
import Vue from 'vue'
import { FileId, SelectionStore } from '../types'
export const useSelectionStore = defineStore('selection', {
state: () => ({
selected: [] as number[]
}),
selected: [],
lastSelection: [],
lastSelectedIndex: null,
} as SelectionStore),
actions: {
/**
* Set the selection of fileIds
*/
set(selection = [] as number[]) {
set(selection = [] as FileId[]) {
Vue.set(this, 'selected', selection)
},
/**
* Set the last selected index
*/
setLastIndex(lastSelectedIndex = null as FileId | null) {
// Update the last selection if we provided a new selection starting point
Vue.set(this, 'lastSelection', lastSelectedIndex ? this.selected : [])
Vue.set(this, 'lastSelectedIndex', lastSelectedIndex)
},
/**
* Reset the selection
*/
reset() {
Vue.set(this, 'selected', [])
Vue.set(this, 'lastSelection', [])
Vue.set(this, 'lastSelectedIndex', null)
}
}
})

View file

@ -25,6 +25,7 @@ import type { Node } from '@nextcloud/files'
// Global definitions
export type Service = string
export type FileId = number
// Files store
export type FilesState = {
@ -33,7 +34,7 @@ export type FilesState = {
}
export type FilesStore = {
[fileid: number]: Node
[fileid: FileId]: Node
}
export type RootsStore = {
@ -57,7 +58,7 @@ export type PathsStore = {
export interface PathOptions {
service: Service
path: string
fileid: number
fileid: FileId
}
// Sorting store
@ -79,3 +80,9 @@ export interface UserConfig {
export interface UserConfigStore {
userConfig: UserConfig
}
export interface SelectionStore {
selected: FileId[]
lastSelection: FileId[]
lastSelectedIndex: number | null
}

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long