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] use a single Pinia store to manage table data #232

Closed
wants to merge 1 commit into from
Closed
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
51 changes: 27 additions & 24 deletions atable/src/components/ACell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,44 @@ import { KeypressHandlers, defaultKeypressHandlers, useKeyboardNav } from '@ston
import { useElementBounding } from '@vueuse/core'
import { computed, type CSSProperties, ref, useTemplateRef } from 'vue'

import { createTableStore } from '../stores/table'
import { useTableStore } from '../stores/table'
import { isHtmlString } from '../utils'

const {
colIndex,
rowIndex,
store,
addNavigation = true,
tabIndex = 0,
pinned,
addNavigation = true,
tableId,
} = defineProps<{
colIndex: number
rowIndex: number
store: ReturnType<typeof createTableStore>
addNavigation?: boolean | KeypressHandlers
tabIndex?: number
pinned?: boolean
addNavigation?: boolean | KeypressHandlers
tableId: string
}>()

const store = useTableStore()

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 originalData = store.getCellData(tableId, colIndex, rowIndex)
const currentData = ref('')
const cellModified = ref(false)

const column = store.columns[colIndex]
const row = store.rows[rowIndex]
const column = store.columns[tableId][colIndex]
const row = store.rows[tableId][rowIndex]

const textAlign = column.align || 'center'
const cellWidth = column.width || '40ch'

const displayValue = computed(() => store.getCellDisplayValue(colIndex, rowIndex))
const displayValue = computed(() => store.getCellDisplayValue(tableId, 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 All @@ -75,7 +78,7 @@ const cellStyle = computed((): CSSProperties => {
width: cellWidth,
backgroundColor: !cellModified.value ? 'inherit' : 'var(--sc-cell-changed-color)',
fontWeight: !cellModified.value ? 'inherit' : 'bold',
paddingLeft: store.getIndent(colIndex, store.display[rowIndex]?.indent),
paddingLeft: store.getIndent(tableId, colIndex, store.display[tableId][rowIndex]?.indent),
}
})

Expand All @@ -87,22 +90,22 @@ const showModal = () => {

if (column.modalComponent) {
store.$patch(state => {
state.modal.visible = true
state.modal.colIndex = colIndex
state.modal.rowIndex = rowIndex
state.modal.parent = cellRef.value
state.modal.top = cellRef.value.offsetTop + cellRef.value.offsetHeight
state.modal.left = cellRef.value.offsetLeft
state.modal.width = width.value
state.modal.height = height.value
state.modal[tableId].visible = true
state.modal[tableId].colIndex = colIndex
state.modal[tableId].rowIndex = rowIndex
state.modal[tableId].parent = cellRef.value
state.modal[tableId].top = cellRef.value.offsetTop + cellRef.value.offsetHeight
state.modal[tableId].left = cellRef.value.offsetLeft
state.modal[tableId].width = width.value
state.modal[tableId].height = height.value

if (typeof column.modalComponent === 'function') {
state.modal.component = column.modalComponent({ table: state.table, row, column })
state.modal[tableId].component = column.modalComponent({ table: state.table[tableId], row, column })
} else {
state.modal.component = column.modalComponent
state.modal[tableId].component = column.modalComponent
}

state.modal.componentProps = column.modalComponentExtraProps
state.modal[tableId].componentProps = column.modalComponentExtraProps
})
}
}
Expand Down Expand Up @@ -161,12 +164,12 @@ const updateCellData = (payload: Event) => {

// only apply changes if the cell value has changed after being mounted
if (column.format) {
cellModified.value = target.textContent !== store.getFormattedValue(colIndex, rowIndex, originalData)
cellModified.value = target.textContent !== store.getFormattedValue(tableId, colIndex, rowIndex, originalData)
// TODO: need to setup reverse format function?
store.setCellText(colIndex, rowIndex, target.textContent)
store.setCellText(tableId, colIndex, rowIndex, target.textContent)
} else {
cellModified.value = target.textContent !== originalData
store.setCellData(colIndex, rowIndex, target.textContent)
store.setCellData(tableId, colIndex, rowIndex, target.textContent)
}
}
</script>
Expand Down
18 changes: 10 additions & 8 deletions atable/src/components/AExpansionRow.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<tr v-bind="$attrs" ref="rowEl" :tabindex="tabIndex" class="expandable-row">
<td :tabIndex="-1" @click="store.toggleRowExpand(rowIndex)" class="row-index">
<td :tabIndex="-1" @click="store.toggleRowExpand(rowIndex, tableId)" class="row-index">
{{ rowExpandSymbol }}
</td>
<slot name="row" />
</tr>
<tr v-if="store.display[rowIndex].expanded" ref="rowExpanded" :tabindex="tabIndex" class="expanded-row">
<td :tabIndex="-1" :colspan="store.columns.length + 1" class="expanded-row-content">
<tr v-if="store.display[tableId][rowIndex].expanded" ref="rowExpanded" :tabindex="tabIndex" class="expanded-row">
<td :tabIndex="-1" :colspan="store.columns[tableId].length + 1" class="expanded-row-content">
<slot name="content" />
</td>
</tr>
Expand All @@ -16,33 +16,35 @@
import { type KeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
import { computed, useTemplateRef } from 'vue'

import { createTableStore } from '../stores/table'
import { useTableStore } from '../stores/table'

const {
rowIndex,
store,
tabIndex = -1,
addNavigation,
tableId,
} = defineProps<{
rowIndex: number
store: ReturnType<typeof createTableStore>
tabIndex?: number
addNavigation?: boolean | KeypressHandlers
tableId: string
}>()

const store = useTableStore()

const rowRef = useTemplateRef<HTMLTableRowElement>('rowEl')
// const expandedRowRef = useTemplateRef<HTMLDivElement>('rowExpanded')

const rowExpandSymbol = computed(() => {
return store.display[rowIndex].expanded ? '▼' : '►'
return store.display[tableId][rowIndex].expanded ? '▼' : '►'
})

if (addNavigation) {
const handlers: KeypressHandlers = {
'keydown.control.g': (event: KeyboardEvent) => {
event.stopPropagation()
event.preventDefault()
store.toggleRowExpand(rowIndex)
store.toggleRowExpand(rowIndex, tableId)
},
}

Expand Down
14 changes: 8 additions & 6 deletions atable/src/components/ARow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
:tabIndex="-1"
class="tree-index"
:class="store.hasPinnedColumns ? 'sticky-index' : ''"
@click="store.toggleRowExpand(rowIndex)">
@click="store.toggleRowExpand(rowIndex, tableId)">
{{ rowExpandSymbol }}
</td>
</slot>
Expand All @@ -28,23 +28,25 @@
import { type KeypressHandlers, useKeyboardNav, defaultKeypressHandlers } from '@stonecrop/utilities'
import { useTemplateRef } from 'vue'

import { createTableStore } from '../stores/table'
import { useTableStore } from '../stores/table'

const {
rowIndex,
store,
tabIndex = -1,
addNavigation = false, // default to allowing cell navigation
tableId,
} = defineProps<{
rowIndex: number
store: ReturnType<typeof createTableStore>
tabIndex?: number
addNavigation?: boolean | KeypressHandlers
tableId: string
}>()

const store = useTableStore()

const rowRef = useTemplateRef<HTMLTableRowElement>('rowEl')
const isRowVisible = store.isRowVisible(rowIndex)
const rowExpandSymbol = store.getRowExpandSymbol(rowIndex)
const isRowVisible = store.isRowVisible(rowIndex, tableId)
const rowExpandSymbol = store.getRowExpandSymbol(rowIndex, tableId)

if (addNavigation) {
let handlers = defaultKeypressHandlers
Expand Down
59 changes: 33 additions & 26 deletions atable/src/components/ATable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@
:style="{ width: store.config.fullWidth ? '100%' : 'auto' }"
v-on-click-outside="store.closeModal">
<slot name="header" :data="store">
<ATableHeader :columns="store.columns" :store="store" />
<ATableHeader :columns="store.columns" :tableId="tableId" />
</slot>

<tbody>
<slot name="body" :data="store">
<ARow v-for="(row, rowIndex) in store.rows" :key="row.id" :row="row" :rowIndex="rowIndex" :store="store">
<ARow v-for="(row, rowIndex) in store.rows[tableId]" :key="row.id" :rowIndex="rowIndex" :tableId="tableId">
<ACell
v-for="(col, colIndex) in store.columns"
v-for="(col, colIndex) in store.columns[tableId]"
:key="col.name"
:store="store"
:col="col"
spellcheck="false"
:pinned="col.pinned"
:rowIndex="rowIndex"
:colIndex="colIndex"
:component="col.cellComponent"
:rowIndex="rowIndex"
:pinned="col.pinned"
:tableId="tableId"
:style="{
textAlign: col?.align || 'center',
minWidth: col?.width || '40ch',
Expand All @@ -34,19 +31,19 @@
<slot name="footer" :data="store" />
<slot name="modal" :data="store">
<ATableModal
v-show="store.modal.visible"
:colIndex="store.modal.colIndex"
:rowIndex="store.modal.rowIndex"
:store="store"
v-show="store.modal[tableId].visible"
:colIndex="store.modal[tableId].colIndex"
:rowIndex="store.modal[tableId].rowIndex"
:tableId="tableId"
:container="tableRef">
<template #default>
<component
:key="`${store.modal.rowIndex}:${store.modal.colIndex}`"
:is="store.modal.component"
:colIndex="store.modal.colIndex"
:rowIndex="store.modal.rowIndex"
:store="store"
v-bind="store.modal.componentProps" />
:key="`${store.modal[tableId].rowIndex}:${store.modal[tableId].colIndex}`"
:is="store.modal[tableId].component"
:colIndex="store.modal[tableId].colIndex"
:rowIndex="store.modal[tableId].rowIndex"
:tableId="tableId"
v-bind="store.modal[tableId].componentProps" />
</template>
</ATableModal>
</slot>
Expand All @@ -62,7 +59,7 @@ import ACell from './ACell.vue'
import ARow from './ARow.vue'
import ATableHeader from './ATableHeader.vue'
import ATableModal from './ATableModal.vue'
import { createTableStore } from '../stores/table'
import { useTableStore } from '../../../stonecrop/src/stores/table'
import type { TableColumn, TableConfig, TableRow } from '../types'

const {
Expand All @@ -86,12 +83,22 @@ const emit = defineEmits<{

const tableRef = useTemplateRef<HTMLTableElement>('table')
const rowsValue = modelValue ? modelValue : rows
const store = createTableStore({ columns, rows: rowsValue, id, config })
const store = useTableStore()
const tableId = id || crypto.randomUUID()

store.$patch(state => {
state.columns[tableId] = columns
state.rows[tableId] = rowsValue
state.config = config
state.table[tableId] = store.createTableObject(columns, rowsValue)
state.display[tableId] = store.createDisplayObject(rowsValue)
state.modal[tableId] = { visible: false }
})

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

after(() => {
emit('cellUpdate', colIndex, rowIndex, newCellValue, prevCellValue)
Expand All @@ -100,7 +107,7 @@ store.$onAction(({ name, store, args, after }) => {
})

watch(
() => store.rows,
() => store.rows[tableId],
newValue => {
emit('update:modelValue', newValue)
},
Expand Down Expand Up @@ -158,11 +165,11 @@ const assignStickyCellWidths = () => {

window.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (store.modal.visible) {
store.modal.visible = false
if (store.modal[tableId].visible) {
store.modal[tableId].visible = false

// focus on the parent cell again
const $parent = store.modal.parent
const $parent = store.modal[tableId].parent
if ($parent) {
// wait for the modal to close before focusing
void nextTick().then(() => {
Expand Down
8 changes: 5 additions & 3 deletions atable/src/components/ATableHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
</template>

<script setup lang="ts">
import { createTableStore } from '../stores/table'
import { useTableStore } from '../stores/table'
import type { TableColumn } from '../types'

const { columns, store } = defineProps<{
const { columns, tableId } = defineProps<{
columns: TableColumn[]
store: ReturnType<typeof createTableStore>
tableId: string
}>()

const store = useTableStore()
</script>

<style>
Expand Down
20 changes: 11 additions & 9 deletions atable/src/components/ATableModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
import { useElementBounding } from '@vueuse/core'
import { useTemplateRef, computed } from 'vue'

import { createTableStore } from '../stores/table'
import { useTableStore } from '../stores/table'

const { store, container } = defineProps<{
const { colIndex, rowIndex, tableId, container } = defineProps<{
colIndex?: number
rowIndex?: number
store?: ReturnType<typeof createTableStore>
tableId: string
container?: HTMLElement
}>()

const store = useTableStore()

const amodalRef = useTemplateRef('amodal')
const { width, height } = useElementBounding(amodalRef)

Expand All @@ -30,14 +32,14 @@ const amodalStyles = computed(() => {

// if this modal would go outside the edge of its container, instead place it above the element (Y) or along the right side (X)
const modalLeft =
store.modal.left + width.value > containerWidth
? store.modal.left - (width.value - store.modal.width)
: store.modal.left
store.modal[tableId].left + width.value > containerWidth
? store.modal[tableId].left - (width.value - store.modal[tableId].width)
: store.modal[tableId].left

const modalTop =
store.modal.top + height.value > containerHeight
? store.modal.top - height.value - store.modal.height
: store.modal.top
store.modal[tableId].top + height.value > containerHeight
? store.modal[tableId].top - height.value - store.modal[tableId].height
: store.modal[tableId].top

return {
left: `${modalLeft}px`,
Expand Down
Loading
Loading