Files
tolaria/src/components/sidebar/useSidebarViewItemInteractions.ts
2026-05-01 06:34:25 +02:00

234 lines
6.9 KiB
TypeScript

import {
useCallback, useRef, useState, type Dispatch, type KeyboardEvent, type MouseEvent, type RefObject,
type SetStateAction,
} from 'react'
import type { ViewDefinition, ViewFile } from '../../types'
import { getElementMenuPosition, useOutsideClick, useSidebarContextMenu } from './sidebarHooks'
import type { MenuPosition, ViewDefinitionPatchHandler } from './SidebarViewActions'
interface SidebarViewItemInteractionInput {
view: ViewFile
onSelect: () => void
onEditView?: (filename: string) => void
onDeleteView?: (filename: string) => void
onUpdateViewDefinition?: ViewDefinitionPatchHandler
}
type RowKeyboardAction = 'select' | 'rename' | 'menu'
function getRowKeyboardAction(event: KeyboardEvent<HTMLDivElement>): RowKeyboardAction | null {
if (event.key === 'Enter' || event.key === ' ') return 'select'
if (event.key === 'F2') return 'rename'
if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) return 'menu'
return null
}
function commitViewRename(
view: ViewFile,
nextName: string,
onUpdateViewDefinition?: (filename: string, patch: Partial<ViewDefinition>) => void,
) {
const trimmed = nextName.trim()
if (trimmed && trimmed !== view.definition.name) {
onUpdateViewDefinition?.(view.filename, { name: trimmed })
}
}
function useViewInteractionState() {
const [customizePos, setCustomizePos] = useState<MenuPosition | null>(null)
const [isRenaming, setIsRenaming] = useState(false)
const customizeRef = useRef<HTMLDivElement>(null)
const rowRef = useRef<HTMLDivElement>(null)
const {
closeContextMenu,
contextMenu,
contextMenuRef,
openContextMenuAt,
openContextMenuFromPointer,
} = useSidebarContextMenu<string>()
const closeCustomize = useCallback(() => setCustomizePos(null), [])
useOutsideClick(customizeRef, !!customizePos, closeCustomize)
return {
closeContextMenu,
closeCustomize,
contextMenuPos: contextMenu?.pos ?? null,
contextMenuRef,
customizePos,
customizeRef,
isRenaming,
rowRef,
setCustomizePos,
setIsRenaming,
openContextMenuAt,
openContextMenuFromPointer,
}
}
function useViewRenameActions({
closeContextMenu,
closeCustomize,
onUpdateViewDefinition,
setIsRenaming,
view,
}: {
closeContextMenu: () => void
closeCustomize: () => void
onUpdateViewDefinition?: ViewDefinitionPatchHandler
setIsRenaming: Dispatch<SetStateAction<boolean>>
view: ViewFile
}) {
const startRename = useCallback(() => {
closeContextMenu()
closeCustomize()
setIsRenaming(true)
}, [closeContextMenu, closeCustomize, setIsRenaming])
const handleRenameSubmit = useCallback((nextName: string) => {
setIsRenaming(false)
commitViewRename(view, nextName, onUpdateViewDefinition)
}, [onUpdateViewDefinition, setIsRenaming, view])
return { handleRenameSubmit, startRename }
}
function useViewMenuActions({
view,
onEditView,
onDeleteView,
onUpdateViewDefinition,
closeContextMenu,
contextMenuPos,
openContextMenuAt,
openContextMenuFromPointer,
rowRef,
setCustomizePos,
}: SidebarViewItemInteractionInput & {
closeContextMenu: () => void
contextMenuPos: MenuPosition | null
openContextMenuAt: (target: string, pos: MenuPosition) => void
openContextMenuFromPointer: (target: string, event: MouseEvent) => void
rowRef: RefObject<HTMLDivElement | null>
setCustomizePos: Dispatch<SetStateAction<MenuPosition | null>>
}) {
const hasMenuActions = !!(onEditView || onDeleteView || onUpdateViewDefinition)
const handleContextMenu = useCallback((event: MouseEvent) => {
if (!hasMenuActions) return
setCustomizePos(null)
openContextMenuFromPointer(view.filename, event)
}, [hasMenuActions, openContextMenuFromPointer, setCustomizePos, view.filename])
const openKeyboardContextMenu = useCallback(() => {
setCustomizePos(null)
openContextMenuAt(view.filename, getElementMenuPosition(rowRef.current))
}, [openContextMenuAt, rowRef, setCustomizePos, view.filename])
const handleEdit = useCallback(() => {
closeContextMenu()
onEditView?.(view.filename)
}, [closeContextMenu, onEditView, view.filename])
const handleDelete = useCallback(() => {
closeContextMenu()
onDeleteView?.(view.filename)
}, [closeContextMenu, onDeleteView, view.filename])
const handleCustomize = useCallback(() => {
setCustomizePos(contextMenuPos ?? { x: 20, y: 100 })
closeContextMenu()
}, [closeContextMenu, contextMenuPos, setCustomizePos])
return {
handleContextMenu,
handleCustomize,
handleDelete,
handleEdit,
openKeyboardContextMenu,
}
}
function useViewRowKeyboardActions({
isRenaming,
onSelect,
openKeyboardContextMenu,
startRename,
}: {
isRenaming: boolean
onSelect: () => void
openKeyboardContextMenu: () => void
startRename: () => void
}) {
const runKeyboardAction = useCallback((action: RowKeyboardAction) => {
const actions: Record<RowKeyboardAction, () => void> = {
select: onSelect,
rename: startRename,
menu: openKeyboardContextMenu,
}
actions[action]()
}, [onSelect, openKeyboardContextMenu, startRename])
return useCallback((event: KeyboardEvent<HTMLDivElement>) => {
if (isRenaming) return
const action = getRowKeyboardAction(event)
if (!action) return
event.preventDefault()
runKeyboardAction(action)
}, [isRenaming, runKeyboardAction])
}
export function useSidebarViewItemInteractions({
view,
onSelect,
onEditView,
onDeleteView,
onUpdateViewDefinition,
}: SidebarViewItemInteractionInput) {
const state = useViewInteractionState()
const rename = useViewRenameActions({
closeContextMenu: state.closeContextMenu,
closeCustomize: state.closeCustomize,
onUpdateViewDefinition,
setIsRenaming: state.setIsRenaming,
view,
})
const menu = useViewMenuActions({
view,
onSelect,
onEditView,
onDeleteView,
onUpdateViewDefinition,
closeContextMenu: state.closeContextMenu,
contextMenuPos: state.contextMenuPos,
openContextMenuAt: state.openContextMenuAt,
openContextMenuFromPointer: state.openContextMenuFromPointer,
rowRef: state.rowRef,
setCustomizePos: state.setCustomizePos,
})
const handleRowKeyDown = useViewRowKeyboardActions({
isRenaming: state.isRenaming,
onSelect,
openKeyboardContextMenu: menu.openKeyboardContextMenu,
startRename: rename.startRename,
})
return {
closeCustomize: state.closeCustomize,
contextMenuPos: state.contextMenuPos,
contextMenuRef: state.contextMenuRef,
customizePos: state.customizePos,
customizeRef: state.customizeRef,
handleContextMenu: menu.handleContextMenu,
handleCustomize: menu.handleCustomize,
handleDelete: menu.handleDelete,
handleEdit: menu.handleEdit,
handleRenameSubmit: rename.handleRenameSubmit,
handleRowKeyDown,
isRenaming: state.isRenaming,
rowRef: state.rowRef,
setIsRenaming: state.setIsRenaming,
startRename: rename.startRename,
}
}