mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 10:08:52 +08:00
282 lines
7.3 KiB
Vue
282 lines
7.3 KiB
Vue
<template>
|
|
<div>
|
|
<div class="rounded-bg">
|
|
<div class="header-container" ref="header-container">
|
|
<div>
|
|
<div
|
|
v-for="(th, index) in header"
|
|
class="fixed-header"
|
|
:style="{ width: `${th.width}px` }"
|
|
:key="index"
|
|
:title="th.name"
|
|
>
|
|
{{ th.name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="table-container"
|
|
ref="table-container"
|
|
@scroll="onScrollTable"
|
|
>
|
|
<table
|
|
ref="table"
|
|
class="sqliteviz-table"
|
|
tabindex="0"
|
|
@keydown="onTableKeydown"
|
|
>
|
|
<thead>
|
|
<tr>
|
|
<th v-for="(th, index) in columns" :key="index" ref="th">
|
|
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
|
<td
|
|
v-for="(col, colIndex) in columns"
|
|
:data-col="colIndex"
|
|
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
|
|
:data-isNull="isNull(getCellValue(col, rowIndex))"
|
|
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
|
|
:key="colIndex"
|
|
:aria-selected="false"
|
|
@click="onCellClick"
|
|
>
|
|
<div class="cell-data" :style="cellStyle">
|
|
{{ getCellText(col, rowIndex) }}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="table-footer">
|
|
<div class="table-footer-count">
|
|
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
|
|
<span v-if="preview">for preview</span>
|
|
<span v-if="time">in {{ time }}</span>
|
|
</div>
|
|
<pager
|
|
v-show="pageCount > 1"
|
|
:page-count="pageCount"
|
|
v-model="currentPage"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Pager from './Pager.vue'
|
|
|
|
export default {
|
|
name: 'SqlTable',
|
|
components: { Pager },
|
|
props: {
|
|
dataSet: Object,
|
|
time: [String, Number],
|
|
pageSize: {
|
|
type: Number,
|
|
default: 20
|
|
},
|
|
page: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
preview: Boolean,
|
|
selectedCellCoordinates: Object
|
|
},
|
|
emits: ['updateSelectedCell'],
|
|
data() {
|
|
return {
|
|
header: null,
|
|
tableWidth: null,
|
|
currentPage: this.page,
|
|
resizeObserver: null,
|
|
selectedCellElement: null
|
|
}
|
|
},
|
|
computed: {
|
|
columns() {
|
|
return this.dataSet.columns
|
|
},
|
|
rowCount() {
|
|
return this.dataSet.values[this.columns[0]].length
|
|
},
|
|
cellStyle() {
|
|
const eq = this.tableWidth / this.columns.length
|
|
return { maxWidth: `${Math.max(eq, 100)}px` }
|
|
},
|
|
pageCount() {
|
|
return Math.ceil(this.rowCount / this.pageSize)
|
|
},
|
|
currentPageData() {
|
|
const start = (this.currentPage - 1) * this.pageSize
|
|
let end = start + this.pageSize
|
|
if (end > this.rowCount - 1) {
|
|
end = this.rowCount - 1
|
|
}
|
|
return {
|
|
start,
|
|
end,
|
|
count: end - start + 1
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
|
this.resizeObserver.observe(this.$refs.table)
|
|
this.calculateHeadersWidth()
|
|
|
|
if (this.selectedCellCoordinates) {
|
|
const { row, col } = this.selectedCellCoordinates
|
|
const cell = this.$refs.table.querySelector(
|
|
`td[data-col="${col}"][data-row="${row}"]`
|
|
)
|
|
if (cell) {
|
|
this.selectCell(cell)
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
isBlob(value) {
|
|
return value && ArrayBuffer.isView(value)
|
|
},
|
|
isNull(value) {
|
|
return value === null
|
|
},
|
|
getCellValue(col, rowIndex) {
|
|
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
|
|
},
|
|
getCellText(col, rowIndex) {
|
|
const value = this.getCellValue(col, rowIndex)
|
|
if (this.isNull(value)) {
|
|
return 'NULL'
|
|
}
|
|
if (this.isBlob(value)) {
|
|
return 'BLOB'
|
|
}
|
|
return value
|
|
},
|
|
calculateHeadersWidth() {
|
|
this.tableWidth = this.$refs['table-container'].offsetWidth
|
|
this.$nextTick(() => {
|
|
this.header = this.$refs.th.map(th => {
|
|
return { name: th.innerText, width: th.getBoundingClientRect().width }
|
|
})
|
|
})
|
|
},
|
|
onScrollTable() {
|
|
this.$refs['header-container'].scrollLeft =
|
|
this.$refs['table-container'].scrollLeft
|
|
},
|
|
onTableKeydown(e) {
|
|
const keyCodeMap = {
|
|
37: 'left',
|
|
39: 'right',
|
|
38: 'up',
|
|
40: 'down'
|
|
}
|
|
|
|
if (
|
|
!this.selectedCellElement ||
|
|
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
|
) {
|
|
return
|
|
}
|
|
e.preventDefault()
|
|
|
|
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
|
},
|
|
onCellClick(e) {
|
|
this.selectCell(e.target.closest('td'), false)
|
|
},
|
|
selectCell(cell, scrollTo = true) {
|
|
if (!cell) {
|
|
if (this.selectedCellElement) {
|
|
this.selectedCellElement.ariaSelected = 'false'
|
|
}
|
|
this.selectedCellElement = cell
|
|
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
|
if (this.selectedCellElement) {
|
|
this.selectedCellElement.ariaSelected = 'false'
|
|
}
|
|
cell.ariaSelected = 'true'
|
|
this.selectedCellElement = cell
|
|
} else {
|
|
cell.ariaSelected = 'false'
|
|
this.selectedCellElement = null
|
|
}
|
|
|
|
if (this.selectedCellElement && scrollTo) {
|
|
this.selectedCellElement.scrollIntoView()
|
|
}
|
|
|
|
this.$emit('updateSelectedCell', this.selectedCellElement)
|
|
},
|
|
moveFocusInTable(initialCell, direction) {
|
|
const currentRowIndex = +initialCell.dataset.row
|
|
const currentColIndex = +initialCell.dataset.col
|
|
let newRowIndex, newColIndex
|
|
|
|
if (direction === 'right') {
|
|
if (currentColIndex === this.columns.length - 1) {
|
|
newRowIndex = currentRowIndex + 1
|
|
newColIndex = 0
|
|
} else {
|
|
newRowIndex = currentRowIndex
|
|
newColIndex = currentColIndex + 1
|
|
}
|
|
} else if (direction === 'left') {
|
|
if (currentColIndex === 0) {
|
|
newRowIndex = currentRowIndex - 1
|
|
newColIndex = this.columns.length - 1
|
|
} else {
|
|
newRowIndex = currentRowIndex
|
|
newColIndex = currentColIndex - 1
|
|
}
|
|
} else if (direction === 'up') {
|
|
newRowIndex = currentRowIndex - 1
|
|
newColIndex = currentColIndex
|
|
} else if (direction === 'down') {
|
|
newRowIndex = currentRowIndex + 1
|
|
newColIndex = currentColIndex
|
|
}
|
|
|
|
const newCell = this.$refs.table.querySelector(
|
|
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
|
|
)
|
|
if (newCell) {
|
|
this.selectCell(newCell)
|
|
}
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
this.resizeObserver.unobserve(this.$refs.table)
|
|
},
|
|
watch: {
|
|
currentPageData() {
|
|
this.calculateHeadersWidth()
|
|
this.selectCell(null)
|
|
},
|
|
dataSet() {
|
|
this.currentPage = 1
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
table.sqliteviz-table:focus {
|
|
outline: none;
|
|
}
|
|
.sqliteviz-table tbody td:hover {
|
|
background-color: var(--color-bg-light-3);
|
|
}
|
|
.sqliteviz-table tbody td[aria-selected='true'] {
|
|
box-shadow: inset 0 0 0 1px var(--color-accent);
|
|
}
|
|
</style>
|