b12fc7411d
- Create useFormFieldNavigation composable for Tab-like Enter behavior - KsTextField: Enter -> next field - KsTextArea: Ctrl+Enter for newline, Enter -> next field - KsSelect: Enter -> next field after selection - Implements standard form navigation pattern across input components
230 lines
6.8 KiB
Vue
230 lines
6.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { AgGridVue } from 'ag-grid-vue3'
|
|
import {
|
|
ClientSideRowModelModule,
|
|
ColumnAutoSizeModule,
|
|
CellStyleModule,
|
|
ValidationModule,
|
|
TextFilterModule,
|
|
TextEditorModule,
|
|
NumberEditorModule,
|
|
SelectEditorModule,
|
|
UndoRedoEditModule,
|
|
RowStyleModule,
|
|
RenderApiModule,
|
|
themeQuartz,
|
|
ModuleRegistry,
|
|
RowSelectionModule,
|
|
type ColDef,
|
|
type RowClickedEvent,
|
|
type CellValueChangedEvent,
|
|
type RowClassParams
|
|
} from 'ag-grid-community'
|
|
import type { UiGridColumn } from '../contracts'
|
|
|
|
ModuleRegistry.registerModules([
|
|
ClientSideRowModelModule,
|
|
ColumnAutoSizeModule,
|
|
CellStyleModule,
|
|
ValidationModule,
|
|
TextFilterModule,
|
|
TextEditorModule,
|
|
NumberEditorModule,
|
|
SelectEditorModule,
|
|
UndoRedoEditModule,
|
|
RowStyleModule,
|
|
RenderApiModule,
|
|
RowSelectionModule
|
|
])
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<{
|
|
rows: unknown[]
|
|
columns: UiGridColumn[]
|
|
loading?: boolean
|
|
height?: string
|
|
rowSelection?: 'single' | 'multiple' | 'none'
|
|
}>(), { loading: false, height: '32rem', rowSelection: 'single' })
|
|
|
|
const emit = defineEmits<{ rowSelected: [row: unknown]; cellValueChanged: [event: CellValueChangedEvent] }>()
|
|
|
|
const columnDefs = computed<ColDef[]>(() => props.columns.map(column => ({
|
|
field: column.field,
|
|
headerName: column.header,
|
|
width: column.width,
|
|
minWidth: column.minWidth ?? 120,
|
|
maxWidth: column.maxWidth,
|
|
|
|
sortable: column.sortable ?? true,
|
|
filter: column.filterable ?? false,
|
|
editable: column.editable ?? false,
|
|
|
|
valueFormatter: column.formatter
|
|
? params => column.formatter?.(params.value, params.data) ?? ''
|
|
: undefined
|
|
})))
|
|
|
|
const rowClassRules = computed(() => ({
|
|
'ks-row-added': (params: RowClassParams) => {
|
|
const data = params.data as Record<string, unknown> | undefined
|
|
if (!data) return false
|
|
return !!(data._isNew || (typeof data.code === 'string' && data.code.startsWith('NEW_')) || (typeof data.groupCode === 'string' && data.groupCode.startsWith('NEW_')))
|
|
},
|
|
'ks-row-modified': (params: RowClassParams) => {
|
|
const data = params.data as Record<string, unknown> | undefined
|
|
return data?._isModified === true
|
|
}
|
|
}))
|
|
|
|
const rowSelectionOptions = computed(() => {
|
|
if (props.rowSelection === 'none') return undefined
|
|
if (props.rowSelection === 'multiple') return { mode: 'multiRow' } as const
|
|
return { mode: 'singleRow', checkboxes: false, enableClickSelection: true } as const
|
|
})
|
|
|
|
|
|
function onRowClicked(event: RowClickedEvent): void {
|
|
if (event.data) emit('rowSelected', event.data)
|
|
}
|
|
|
|
function onCellValueChanged(event: CellValueChangedEvent): void {
|
|
if (event.data) {
|
|
(event.data as Record<string, unknown>)._isModified = true
|
|
event.api.redrawRows()
|
|
emit('cellValueChanged', event)
|
|
}
|
|
}
|
|
|
|
import type { NavigateToNextCellParams, CellPosition, CellEditingStoppedEvent, CellKeyDownEvent } from 'ag-grid-community'
|
|
|
|
let isEnterKeyPressed = false
|
|
|
|
function onCellKeyDown(event: CellKeyDownEvent): void {
|
|
const keyboardEvent = event.event as KeyboardEvent
|
|
if (keyboardEvent && (keyboardEvent.key === 'Enter' || String(keyboardEvent.key) === '13')) {
|
|
isEnterKeyPressed = true
|
|
}
|
|
}
|
|
|
|
function onCellEditingStopped(event: CellEditingStoppedEvent): void {
|
|
if (isEnterKeyPressed && event.api) {
|
|
isEnterKeyPressed = false
|
|
const api = event.api
|
|
const rowIndex = event.rowIndex
|
|
const colKey = event.column.getColId()
|
|
|
|
if (rowIndex === null || rowIndex === undefined) return
|
|
|
|
const allCols = api.getAllGridColumns()
|
|
const editableCols = allCols.filter(c => {
|
|
const colDef = c.getColDef()
|
|
return colDef.editable === true || typeof colDef.editable === 'function'
|
|
})
|
|
|
|
const currIdx = editableCols.findIndex(c => c.getColId() === colKey)
|
|
|
|
if (currIdx >= 0 && currIdx < editableCols.length - 1) {
|
|
const nextColId = editableCols[currIdx + 1].getColId()
|
|
setTimeout(() => {
|
|
api.setFocusedCell(rowIndex, nextColId)
|
|
api.startEditingCell({ rowIndex, colKey: nextColId })
|
|
}, 40)
|
|
} else {
|
|
const nextRowIdx = rowIndex + 1
|
|
const firstColId = editableCols[0]?.getColId()
|
|
if (firstColId && nextRowIdx < api.getDisplayedRowCount()) {
|
|
setTimeout(() => {
|
|
api.ensureIndexVisible(nextRowIdx)
|
|
api.setFocusedCell(nextRowIdx, firstColId)
|
|
api.startEditingCell({ rowIndex: nextRowIdx, colKey: firstColId })
|
|
}, 40)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function navigateToNextCell(params: NavigateToNextCellParams): CellPosition | null {
|
|
const previousCell = params.previousCellPosition
|
|
const suggestedNextCell = params.nextCellPosition
|
|
|
|
if (params.key === 'Enter' || String(params.key) === '13') {
|
|
const allColumns = params.api.getAllGridColumns()
|
|
const editableCols = allColumns.filter(c => {
|
|
const colDef = c.getColDef()
|
|
return colDef.editable === true || typeof colDef.editable === 'function'
|
|
})
|
|
|
|
if (editableCols.length > 0) {
|
|
const currentColId = previousCell.column.getColId()
|
|
const currentIndex = editableCols.findIndex(c => c.getColId() === currentColId)
|
|
|
|
if (currentIndex >= 0 && currentIndex < editableCols.length - 1) {
|
|
const nextCol = editableCols[currentIndex + 1]
|
|
const colId = nextCol.getColId()
|
|
setTimeout(() => {
|
|
params.api.startEditingCell({ rowIndex: previousCell.rowIndex, colKey: colId })
|
|
}, 30)
|
|
return {
|
|
rowIndex: previousCell.rowIndex,
|
|
column: nextCol,
|
|
rowPinned: previousCell.rowPinned
|
|
}
|
|
} else {
|
|
const nextRowIndex = previousCell.rowIndex + 1
|
|
const firstCol = editableCols[0]
|
|
const colId = firstCol.getColId()
|
|
setTimeout(() => {
|
|
params.api.startEditingCell({ rowIndex: nextRowIndex, colKey: colId })
|
|
}, 30)
|
|
return {
|
|
rowIndex: nextRowIndex,
|
|
column: firstCol,
|
|
rowPinned: previousCell.rowPinned
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return suggestedNextCell
|
|
}
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="ks-grid" :style="{ height }" :aria-busy="loading">
|
|
<AgGridVue
|
|
style="height: 100%; width: 100%"
|
|
:theme="themeQuartz"
|
|
:row-data="rows"
|
|
:column-defs="columnDefs"
|
|
:row-selection="rowSelectionOptions"
|
|
:row-class-rules="rowClassRules"
|
|
:enter-navigates-to-next-cell="true"
|
|
:enter-navigates-to-next-cell-after-edit="true"
|
|
:navigate-to-next-cell="navigateToNextCell"
|
|
:stop-editing-when-cells-lose-focus="true"
|
|
:loading="loading"
|
|
:suppress-no-rows-overlay="rows.length > 0"
|
|
@row-clicked="onRowClicked"
|
|
@cell-value-changed="onCellValueChanged"
|
|
@cell-key-down="onCellKeyDown"
|
|
@cell-editing-stopped="onCellEditingStopped"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
.ks-grid {
|
|
min-height: 12rem;
|
|
border: 1px solid var(--ks-color-neutral-200);
|
|
border-radius: var(--ks-radius-md);
|
|
overflow: hidden;
|
|
background: #fff;
|
|
}
|
|
</style>
|