Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[atable] emit cell update in columns with format #225

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions atable/src/components/ACell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
:style="cellStyle"
@focus="onFocus"
@paste="updateCellData"
@blur="updateCellData"
@input="updateCellData"
@click="showModal"
class="atable-cell"
Expand All @@ -28,7 +27,7 @@
<script setup lang="ts">
import { KeypressHandlers, defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
import { useElementBounding } from '@vueuse/core'
import { computed, CSSProperties, ref, useTemplateRef, watch } from 'vue'
import { computed, type CSSProperties, ref, useTemplateRef } from 'vue'

import { createTableStore } from '../stores/table'
import { isHtmlString } from '../utils'
Expand All @@ -48,12 +47,13 @@ const {
pinned?: boolean
}>()

const emit = defineEmits<{ cellInput: [colIndex: number, rowIndex: number, newValue: string, oldValue: string] }>()

const cellRef = useTemplateRef<HTMLTableCellElement>('cell')
const { width, height } = useElementBounding(cellRef)

// keep a shallow copy of the original cell value for comparison
const originalData = store.getCellData(colIndex, rowIndex)
const displayValue = ref(store.getCellDisplayValue(colIndex, rowIndex))
const currentData = ref('')
const cellModified = ref(false)

Expand All @@ -63,13 +63,7 @@ const row = store.rows[rowIndex]
const textAlign = column.align || 'center'
const cellWidth = column.width || '40ch'

watch(
() => store.getCellData(colIndex, rowIndex),
cellData => {
displayValue.value = store.getFormattedValue(colIndex, rowIndex, cellData)
}
)

const displayValue = computed(() => store.getCellDisplayValue(colIndex, rowIndex))
const isHtmlValue = computed(() => {
// TODO: check if display value is a native DOM element
return typeof displayValue.value === 'string' ? isHtmlString(displayValue.value) : false
Expand Down Expand Up @@ -156,23 +150,23 @@ const onFocus = () => {
}
}

const updateCellData = () => {
if (cellRef.value) {
// only apply changes if the cell value has changed after being mounted
if (column.format) {
cellModified.value = cellRef.value.textContent !== store.getFormattedValue(colIndex, rowIndex, originalData)
} else {
cellModified.value = cellRef.value.textContent !== originalData
}
const updateCellData = (payload: Event) => {
const target = payload.target as HTMLTableCellElement
if (target.textContent === currentData.value) {
return
}

if (cellRef.value.textContent !== currentData.value) {
currentData.value = cellRef.value.textContent
cellRef.value.dispatchEvent(new Event('change'))
if (!column.format) {
// TODO: need to setup reverse format function
store.setCellData(colIndex, rowIndex, currentData.value)
}
}
emit('cellInput', colIndex, rowIndex, target.textContent, currentData.value)
currentData.value = target.textContent

// only apply changes if the cell value has changed after being mounted
if (column.format) {
cellModified.value = target.textContent !== store.getFormattedValue(colIndex, rowIndex, originalData)
// TODO: need to setup reverse format function?
store.setCellText(colIndex, rowIndex, target.textContent)
} else {
cellModified.value = target.textContent !== originalData
store.setCellData(colIndex, rowIndex, target.textContent)
}
}
</script>
Expand Down
62 changes: 11 additions & 51 deletions atable/src/components/ATable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
textAlign: col?.align || 'center',
minWidth: col?.width || '40ch',
width: store.config.fullWidth ? 'auto' : null,
}" />
}"
@cellInput="emitInput" />
</ARow>
</slot>
</tbody>
Expand Down Expand Up @@ -87,11 +88,14 @@ const tableRef = useTemplateRef<HTMLTableElement>('table')
const rowsValue = modelValue ? modelValue : rows
const store = createTableStore({ columns, rows: rowsValue, id, config })

store.$onAction(({ name, store, args }) => {
store.$onAction(({ name, store, args, after }) => {
if (name === 'setCellData') {
const [colIndex, rowIndex, newCellValue] = args
const prevCellValue = store.getCellData(colIndex, rowIndex)
emit('cellUpdate', [colIndex, rowIndex, newCellValue, prevCellValue])

after(() => {
emit('cellUpdate', colIndex, rowIndex, newCellValue, prevCellValue)
})
}
})

Expand All @@ -114,6 +118,10 @@ onMounted(() => {
}
})

const emitInput = (colIndex: number, rowIndex: number, newCellValue: any, prevCellValue: any) => {
emit('cellUpdate', colIndex, rowIndex, newCellValue, prevCellValue)
}

const assignStickyCellWidths = () => {
const table = tableRef.value

Expand Down Expand Up @@ -148,54 +156,6 @@ const assignStickyCellWidths = () => {
}
}

// const formatCell = (event?: KeyboardEvent, column?: TableColumn, cellData?: any) => {
// let colIndex: number
// const target = event?.target as HTMLTableCellElement
// if (event) {
// colIndex = target.cellIndex
// } else if (column && cellData) {
// colIndex = store.columns.indexOf(column)
// }

// if (!column && 'format' in store.columns[colIndex]) {
// // TODO: (utils) create helper to extract format from string
// const format = store.columns[colIndex].format
// if (typeof format === 'function') {
// return format(target.innerHTML)
// } else if (typeof format === 'string') {
// // parse format function from string
// // eslint-disable-next-line @typescript-eslint/no-implied-eval
// const formatFn: (args: any) => any = Function(`"use strict";return (${format})`)()
// return formatFn(target.innerHTML)
// } else {
// return target.innerHTML
// }
// } else if (cellData && 'format' in column) {
// const format = column.format
// if (typeof format === 'function') {
// return format(cellData)
// } else if (typeof format === 'string') {
// // parse format function from string
// // eslint-disable-next-line @typescript-eslint/no-implied-eval
// const formatFn: (args: any) => any = Function(`"use strict";return (${format})`)()
// return formatFn(cellData)
// } else {
// return cellData
// }
// } else if (cellData && column.type.toLowerCase() in ['int', 'decimal', 'float', 'number', 'percent']) {
// return cellData
// // TODO: number formatting
// } else {
// return cellData
// }
// }

// const moveCursorToEnd = (target: HTMLElement) => {
// target.focus()
// document.execCommand('selectAll', false, null)
// document.getSelection().collapseToEnd()
// }

window.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (store.modal.visible) {
Expand Down
10 changes: 9 additions & 1 deletion atable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import ATable from './components/ATable.vue'
import ATableHeader from './components/ATableHeader.vue'
import ATableModal from './components/ATableModal.vue'
export { createTableStore } from './stores/table'
export type { CellContext, TableColumn, TableConfig, TableDisplay, TableRow, TableModal } from './types'
export type {
CellContext,
TableColumn,
TableConfig,
TableDisplay,
TableModal,
TableModalProps,
TableRow,
} from './types'

/**
* Install all ATable components
Expand Down
16 changes: 14 additions & 2 deletions atable/src/stores/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const createTableStore = (initData: {
const table = ref(initData.table || createTableObject())
const display = ref(createDisplayObject(initData.display))
const modal = ref(initData.modal || { visible: false })
const updates = ref<Record<string, string>>({})

// getters
const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned))
Expand All @@ -100,6 +101,15 @@ export const createTableStore = (initData: {
rows.value[rowIndex][col.name] = value
}

const setCellText = (colIndex: number, rowIndex: number, value: string) => {
const index = `${colIndex}:${rowIndex}`

if (table.value[index] !== value) {
display.value[rowIndex].rowModified = true
updates.value[index] = value
}
}

const getHeaderCellStyle = (column: TableColumn): CSSProperties => ({
minWidth: column.width || '40ch',
textAlign: column.align || 'center',
Expand Down Expand Up @@ -185,11 +195,12 @@ export const createTableStore = (initData: {
return {
// state
columns,
rows,
config,
table,
display,
modal,
rows,
table,
updates,

// getters
hasPinnedColumns,
Expand All @@ -206,6 +217,7 @@ export const createTableStore = (initData: {
getRowExpandSymbol,
isRowVisible,
setCellData,
setCellText,
toggleRowExpand,
}
})
Expand Down
13 changes: 13 additions & 0 deletions atable/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createTableStore } from '../stores/table'

/**
* Table column definition.
* @public
Expand Down Expand Up @@ -104,3 +106,14 @@ export type TableModal = {
component?: string
componentProps?: Record<string, any>
}

/**
* Table modal props definition.
* @public
*/
export type TableModalProps = {
[key: string]: any
colIndex: number
rowIndex: number
store: ReturnType<typeof createTableStore>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@stonecrop/atable",
"comment": "emit cell update in columns with format",
"type": "patch"
}
],
"packageName": "@stonecrop/atable"
}
Loading
Loading