Skip to content

Commit

Permalink
Keyboard Actions composables in Search Results (#9447)
Browse files Browse the repository at this point in the history
* Keyboard Actions composables in Search Results

* key params rewritten

* List removing duplication, KeyboardAction improving modifier readability
  • Loading branch information
jacob-nv authored and Jan committed Aug 15, 2023
1 parent 64ddac5 commit 7f5b941
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 1 deletion.
8 changes: 7 additions & 1 deletion packages/web-app-files/src/components/Search/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ import { eventBus, useCapabilityFilesFullTextSearch } from 'web-pkg'
import ItemFilter from 'web-pkg/src/components/ItemFilter.vue'
import { isLocationCommonActive } from 'web-app-files/src/router'
import ItemFilterToggle from 'web-pkg/src/components/ItemFilterToggle.vue'
import { useKeyboardActions } from 'web-pkg/src/composables/keyboardActions'
import { useKeyboardActionsSearchTable } from 'web-app-files/src/composables/keyboardActions'
const visibilityObserver = new VisibilityObserver()
Expand Down Expand Up @@ -194,6 +196,10 @@ export default defineComponent({
const scopeQuery = useRouteQuery('scope')
const doUseScope = useRouteQuery('useScope')
const resourcesView = useResourcesViewDefaults<Resource, any, any[]>()
const keyActions = useKeyboardActions('files-view')
useKeyboardActionsSearchTable(keyActions, resourcesView.paginatedResources)
const searchTerm = computed(() => {
return queryItemAsString(unref(searchTermQuery))
})
Expand Down Expand Up @@ -295,7 +301,7 @@ export default defineComponent({
return {
...useFileActions({ store }),
...useResourcesViewDefaults<Resource, any, any[]>(),
...resourcesView,
loadAvailableTagsTask,
fileListHeaderY,
fullTextSearchEnabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useKeyboardActionsSearchTable'
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { computed, onBeforeUnmount, onMounted, ref, unref } from 'vue'
import { useStore } from 'web-pkg/src/composables'
import { useScrollTo } from 'web-app-files/src/composables/scrollTo'
import { Key, ModifierKey } from 'web-pkg/src/composables/keyboardActions'
import { eventBus } from 'web-pkg'
import { useGettext } from 'vue3-gettext'

export const useKeyboardActionsSearchTable = (keyActions, paginatedResources) => {
const store = useStore()
const { scrollToResource } = useScrollTo()
const selectionCursor = ref(0)
const latestSelectedId = computed(() => store.state.Files.latestSelectedId)
const language = useGettext()

let fileListClickedEvent
let fileListClickedMetaEvent
let fileListClickedShiftEvent

keyActions.bindKeyAction({ primary: Key.ArrowUp }, () => handleNavigateAction(true))

keyActions.bindKeyAction({ primary: Key.ArrowDown }, () => handleNavigateAction())

keyActions.bindKeyAction({ primary: Key.Esc }, () => {
resetSelectionCursor()
store.dispatch('Files/resetFileSelection')
})

keyActions.bindKeyAction({ primary: Key.Space }, () => {
store.dispatch('Files/toggleFileSelection', { id: unref(latestSelectedId) })
})

keyActions.bindKeyAction({ modifier: ModifierKey.Shift, primary: Key.ArrowUp }, () =>
handleShiftUpAction()
)

keyActions.bindKeyAction({ modifier: ModifierKey.Shift, primary: Key.ArrowDown }, () =>
handleShiftDownAction()
)

keyActions.bindKeyAction({ modifier: ModifierKey.Ctrl, primary: Key.A }, () =>
handleSelectAllAction()
)

keyActions.bindKeyAction({ modifier: ModifierKey.Ctrl, primary: Key.C }, () => {
store.dispatch('Files/copySelectedFiles', {
...language,
resources: store.getters['Files/selectedFiles']
})
})

const handleNavigateAction = async (up = false) => {
const nextId = !unref(latestSelectedId) ? getFirstResourceId() : getNextResourceId(up)
if (nextId === -1) {
return
}
resetSelectionCursor()
await store.dispatch('Files/resetFileSelection')
store.commit('Files/ADD_FILE_SELECTION', { id: nextId })
scrollToResource({ id: nextId } as any)
}

const getNextResourceId = (previous = false) => {
const latestSelectedResourceIndex = paginatedResources.value.findIndex(
(resource) => resource.id === latestSelectedId.value
)
if (latestSelectedResourceIndex === -1) {
return -1
}
const nextResourceIndex = latestSelectedResourceIndex + (previous ? -1 : 1)
if (nextResourceIndex < 0 || nextResourceIndex >= paginatedResources.value.length) {
return -1
}
return paginatedResources.value[nextResourceIndex].id
}

const getFirstResourceId = () => {
return paginatedResources.value.length ? paginatedResources.value[0].id : -1
}

const resetSelectionCursor = () => {
selectionCursor.value = 0
}

const handleSelectAllAction = () => {
resetSelectionCursor()
store.commit('Files/SET_FILE_SELECTION', paginatedResources.value)
}

const handleShiftUpAction = async () => {
const nextResourceId = getNextResourceId(true)
if (nextResourceId === -1) {
return
}
if (unref(selectionCursor) > 0) {
// deselect
await store.dispatch('Files/toggleFileSelection', { id: unref(latestSelectedId) })
store.commit('Files/SET_LATEST_SELECTED_FILE_ID', nextResourceId)
} else {
// select
store.commit('Files/ADD_FILE_SELECTION', { id: nextResourceId })
}
scrollToResource({ id: nextResourceId } as any)
selectionCursor.value = unref(selectionCursor) - 1
}
const handleShiftDownAction = () => {
const nextResourceId = getNextResourceId()
if (nextResourceId === -1) {
return
}
if (unref(selectionCursor) < 0) {
// deselect
store.dispatch('Files/toggleFileSelection', { id: unref(latestSelectedId) })
store.commit('Files/SET_LATEST_SELECTED_FILE_ID', nextResourceId)
} else {
// select
store.commit('Files/ADD_FILE_SELECTION', { id: nextResourceId })
}
scrollToResource({ id: nextResourceId } as any)
selectionCursor.value = unref(selectionCursor) + 1
}

const handleCtrlClickAction = async (resource) => {
await store.dispatch('Files/toggleFileSelection', { id: resource.id })
}

const handleShiftClickAction = ({ resource, skipTargetSelection }) => {
const parent = document.querySelectorAll(`[data-item-id='${resource.id}']`)[0]
const resourceNodes = Object.values(parent.parentNode.children)
const latestNode = resourceNodes.find(
(r) => r.getAttribute('data-item-id') === unref(latestSelectedId)
)
const clickedNode = resourceNodes.find((r) => r.getAttribute('data-item-id') === resource.id)

let latestNodeIndex = resourceNodes.indexOf(latestNode)
latestNodeIndex = latestNodeIndex === -1 ? 0 : latestNodeIndex

const clickedNodeIndex = resourceNodes.indexOf(clickedNode)
const minIndex = Math.min(latestNodeIndex, clickedNodeIndex)
const maxIndex = Math.max(latestNodeIndex, clickedNodeIndex)

for (let i = minIndex; i <= maxIndex; i++) {
const nodeId = resourceNodes[i].getAttribute('data-item-id')
if (skipTargetSelection && nodeId === resource.id) {
continue
}
store.commit('Files/ADD_FILE_SELECTION', { id: nodeId })
}
store.commit('Files/SET_LATEST_SELECTED_FILE_ID', resource.id)
}

onMounted(() => {
fileListClickedEvent = eventBus.subscribe('app.files.list.clicked', resetSelectionCursor)
fileListClickedMetaEvent = eventBus.subscribe(
'app.files.list.clicked.meta',
handleCtrlClickAction
)
fileListClickedShiftEvent = eventBus.subscribe(
'app.files.list.clicked.shift',
handleShiftClickAction
)
})

onBeforeUnmount(() => {
eventBus.unsubscribe('app.files.list.clicked', fileListClickedEvent)
eventBus.unsubscribe('app.files.list.clicked.meta', fileListClickedMetaEvent)
eventBus.unsubscribe('app.files.list.clicked.shift', fileListClickedShiftEvent)
})
}
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/keyboardActions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useKeyboardActions'
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { onBeforeUnmount, onMounted, ref } from 'vue'

export enum Key {
C = 'c',
V = 'v',
A = 'a',
Space = ' ',
ArrowUp = 'ArrowUp',
ArrowDown = 'ArrowDown',
Esc = 'Escape'
}

export enum ModifierKey {
Ctrl = 'Control',
Shift = 'Shift'
}

export const useKeyboardActions = (keyBindOnElementId: string | null = null) => {
const actions = ref([])
const listener = (event: KeyboardEvent): void => {
event.preventDefault()
const { key, ctrlKey, metaKey, shiftKey } = event
let modifier = null
if (metaKey || ctrlKey) {
modifier = ModifierKey.Ctrl
} else if (shiftKey) {
modifier = ModifierKey.Shift
}
const action = actions.value
.filter((action) => action !== null)
.find((action) => {
return action.primary === key && action.modifier === modifier
})
if (action) {
action.callback(event)
}
}
const bindKeyAction = (
keys: { primary: Key; modifier?: ModifierKey },
callback: () => void
): number => {
return (
actions.value.push({
...keys,
modifier: keys.modifier ?? null,
callback
}) - 1
)
}

const removeKeyAction = (index): void => {
actions.value[index] = null
}

onMounted(() => {
let element = null
if (keyBindOnElementId) {
element = document.getElementById(keyBindOnElementId)
}
element
? element.addEventListener('keydown', listener)
: document.addEventListener('keydown', listener)
})

onBeforeUnmount(() => {
let element = null
if (keyBindOnElementId) {
element = document.getElementById(keyBindOnElementId)
}
element
? element.removeEventListener('keydown', listener)
: document.removeEventListener('keydown', listener)
})

return {
bindKeyAction,
removeKeyAction
}
}

0 comments on commit 7f5b941

Please sign in to comment.