@stonecrop/atable 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/atable.js +775 -0
- package/dist/atable.js.map +1 -0
- package/dist/atable.umd.cjs +2 -0
- package/dist/atable.umd.cjs.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +66 -0
- package/src/components/ACell.vue +213 -0
- package/src/components/AExpansionRow.vue +88 -0
- package/src/components/ARow.vue +116 -0
- package/src/components/ATable.vue +210 -0
- package/src/components/ATableHeader.vue +60 -0
- package/src/components/ATableModal.vue +50 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<td
|
|
3
|
+
ref="cell"
|
|
4
|
+
:data-colindex="colIndex"
|
|
5
|
+
:data-rowindex="rowIndex"
|
|
6
|
+
:data-editable="tableData.columns[colIndex].edit"
|
|
7
|
+
:contenteditable="tableData.columns[colIndex].edit"
|
|
8
|
+
:tabindex="tabIndex"
|
|
9
|
+
:spellcheck="false"
|
|
10
|
+
:style="cellStyle"
|
|
11
|
+
@focus="onFocus"
|
|
12
|
+
@paste="onChange"
|
|
13
|
+
@blur="onChange"
|
|
14
|
+
@input="onChange"
|
|
15
|
+
@click="handleInput"
|
|
16
|
+
@mousedown="handleInput">
|
|
17
|
+
<component
|
|
18
|
+
v-if="tableData.columns[colIndex].cellComponent"
|
|
19
|
+
:is="tableData.columns[colIndex].cellComponent"
|
|
20
|
+
:value="displayValue"
|
|
21
|
+
v-bind="tableData.columns[colIndex].cellComponentProps">
|
|
22
|
+
</component>
|
|
23
|
+
<span v-else>{{ displayValue }}</span>
|
|
24
|
+
</td>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { computed, CSSProperties, inject, ref } from 'vue'
|
|
29
|
+
|
|
30
|
+
import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
|
|
31
|
+
import TableDataStore from '.'
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(
|
|
34
|
+
defineProps<{
|
|
35
|
+
colIndex: number
|
|
36
|
+
rowIndex: number
|
|
37
|
+
tableid: string
|
|
38
|
+
addNavigation?: boolean | object
|
|
39
|
+
tabIndex?: number
|
|
40
|
+
clickHandler?: (event: MouseEvent) => void
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
tabIndex: 0,
|
|
44
|
+
addNavigation: true,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const tableData = inject<TableDataStore>(props.tableid)
|
|
49
|
+
const cell = ref<HTMLTableCellElement>(null)
|
|
50
|
+
|
|
51
|
+
let cellModified = ref(false)
|
|
52
|
+
const displayValue = computed(() => {
|
|
53
|
+
const data = tableData.cellData<any>(props.colIndex, props.rowIndex)
|
|
54
|
+
if (tableData.columns[props.colIndex].format) {
|
|
55
|
+
const format = tableData.columns[props.colIndex].format
|
|
56
|
+
if (typeof format === 'function') {
|
|
57
|
+
return format(data)
|
|
58
|
+
} else if (typeof format === 'string') {
|
|
59
|
+
// parse format function from string
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
61
|
+
const formatFn: (args: any) => any = Function(`"use strict";return (${format})`)()
|
|
62
|
+
return formatFn(data)
|
|
63
|
+
} else {
|
|
64
|
+
return data
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
return data
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const handleInput = (event: MouseEvent) => {
|
|
72
|
+
// Not sure if click handler is needed anymore?
|
|
73
|
+
if (props.clickHandler) {
|
|
74
|
+
props.clickHandler(event)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (tableData.columns[props.colIndex].mask) {
|
|
79
|
+
// TODO: add masking to cell values
|
|
80
|
+
// tableData.columns[props.colIndex].mask(event)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tableData.columns[props.colIndex].modalComponent) {
|
|
84
|
+
const domRect = cell.value.getBoundingClientRect()
|
|
85
|
+
tableData.modal.visible = true
|
|
86
|
+
tableData.modal.colIndex = props.colIndex
|
|
87
|
+
tableData.modal.rowIndex = props.rowIndex
|
|
88
|
+
tableData.modal.parent = cell.value
|
|
89
|
+
tableData.modal.top = domRect.top + domRect.height
|
|
90
|
+
tableData.modal.left = domRect.left
|
|
91
|
+
tableData.modal.width = cellWidth.value
|
|
92
|
+
tableData.modal.component = tableData.columns[props.colIndex].modalComponent
|
|
93
|
+
tableData.modal.componentProps = tableData.columns[props.colIndex].modalComponentProps
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (props.addNavigation) {
|
|
98
|
+
let handlers = {
|
|
99
|
+
...defaultKeypressHandlers,
|
|
100
|
+
...{
|
|
101
|
+
'keydown.f2': handleInput,
|
|
102
|
+
'keydown.alt.up': handleInput,
|
|
103
|
+
'keydown.alt.down': handleInput,
|
|
104
|
+
'keydown.alt.left': handleInput,
|
|
105
|
+
'keydown.alt.right': handleInput,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof props.addNavigation === 'object') {
|
|
110
|
+
handlers = {
|
|
111
|
+
...handlers,
|
|
112
|
+
...props.addNavigation,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
useKeyboardNav([
|
|
117
|
+
{
|
|
118
|
+
selectors: cell,
|
|
119
|
+
handlers: handlers,
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// const updateData = (event: Event) => {
|
|
125
|
+
// if (event) {
|
|
126
|
+
// // custom components need to handle their own updateData, this is the default
|
|
127
|
+
// if (!tableData.columns[props.colIndex].component) {
|
|
128
|
+
// tableData.setCellData(props.rowIndex, props.colIndex, cell.value.innerHTML)
|
|
129
|
+
// }
|
|
130
|
+
// cellModified.value = true
|
|
131
|
+
// }
|
|
132
|
+
// }
|
|
133
|
+
|
|
134
|
+
const textAlign = computed(() => {
|
|
135
|
+
return tableData.columns[props.colIndex].align || 'center'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const cellWidth = computed(() => {
|
|
139
|
+
return tableData.columns[props.colIndex].width || '40ch'
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
let currentData = ''
|
|
143
|
+
const onFocus = () => {
|
|
144
|
+
if (cell.value) {
|
|
145
|
+
currentData = cell.value.innerText
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const onChange = () => {
|
|
150
|
+
if (cell.value) {
|
|
151
|
+
if (cell.value.innerHTML !== currentData) {
|
|
152
|
+
currentData = cell.value.innerText
|
|
153
|
+
cell.value.dispatchEvent(new Event('change'))
|
|
154
|
+
cellModified.value = true // set display instead
|
|
155
|
+
if (!tableData.columns[props.colIndex].format) {
|
|
156
|
+
// TODO: need to setup reverse format function
|
|
157
|
+
tableData.setCellData(props.rowIndex, props.colIndex, currentData)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const getIndent = (colKey: number, indent: number) => {
|
|
164
|
+
if (indent && colKey === 0 && indent > 0) {
|
|
165
|
+
return `${indent}ch`
|
|
166
|
+
} else {
|
|
167
|
+
return 'inherit'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cellStyle: CSSProperties = {
|
|
172
|
+
textAlign: textAlign.value,
|
|
173
|
+
width: cellWidth.value,
|
|
174
|
+
backgroundColor: !cellModified.value ? 'inherit' : 'var(--cell-modified-color)',
|
|
175
|
+
fontWeight: !cellModified.value ? 'inherit' : 'bold',
|
|
176
|
+
paddingLeft: getIndent(props.colIndex, tableData.display[props.rowIndex]?.indent),
|
|
177
|
+
}
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<style scoped>
|
|
181
|
+
@import url('@stonecrop/themes/default/default.css');
|
|
182
|
+
td {
|
|
183
|
+
border-radius: 0px;
|
|
184
|
+
box-sizing: border-box;
|
|
185
|
+
margin: 0px;
|
|
186
|
+
outline: none;
|
|
187
|
+
box-shadow: none;
|
|
188
|
+
color: var(--cell-text-color);
|
|
189
|
+
text-overflow: ellipsis;
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
padding-left: 0.5ch !important;
|
|
192
|
+
padding-right: 0.5ch;
|
|
193
|
+
|
|
194
|
+
padding-top: var(--atable-row-padding);
|
|
195
|
+
padding-bottom: var(--atable-row-padding);
|
|
196
|
+
|
|
197
|
+
border-spacing: 0px;
|
|
198
|
+
border-collapse: collapse;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
td:focus,
|
|
202
|
+
td:focus-within {
|
|
203
|
+
background-color: var(--focus-cell-background);
|
|
204
|
+
outline-width: 2px;
|
|
205
|
+
outline-style: solid;
|
|
206
|
+
outline-color: var(--focus-cell-outline);
|
|
207
|
+
box-shadow: none;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
min-height: 1.15em;
|
|
210
|
+
max-height: 1.15em;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr v-bind="$attrs" ref="rowEl" :tabindex="tabIndex" class="expandable-row">
|
|
3
|
+
<td :tabIndex="-1" @click="tableData.toggleRowExpand(rowIndex)" class="row-index">
|
|
4
|
+
{{ getRowExpandSymbol() }}
|
|
5
|
+
</td>
|
|
6
|
+
<slot name="row" />
|
|
7
|
+
</tr>
|
|
8
|
+
<tr v-if="tableData.display[props.rowIndex].expanded" ref="rowExpanded" :tabindex="tabIndex" class="expanded-row">
|
|
9
|
+
<td :tabIndex="-1" :colspan="tableData.columns.length + 1" class="expanded-row-content">
|
|
10
|
+
<slot name="content" />
|
|
11
|
+
</td>
|
|
12
|
+
</tr>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { TableRow } from 'types'
|
|
17
|
+
import { inject, ref } from 'vue'
|
|
18
|
+
|
|
19
|
+
import { useKeyboardNav } from '@stonecrop/utilities'
|
|
20
|
+
|
|
21
|
+
import TableDataStore from '.'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(
|
|
24
|
+
defineProps<{
|
|
25
|
+
row: TableRow
|
|
26
|
+
rowIndex: number
|
|
27
|
+
tableid: string
|
|
28
|
+
tabIndex?: number
|
|
29
|
+
addNavigation?: {
|
|
30
|
+
[key: string]: (ev: KeyboardEvent) => any
|
|
31
|
+
}
|
|
32
|
+
}>(),
|
|
33
|
+
{
|
|
34
|
+
tabIndex: -1,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const tableData = inject<TableDataStore>(props.tableid)
|
|
39
|
+
const rowEl = ref<HTMLTableRowElement>(null)
|
|
40
|
+
const rowExpanded = ref<HTMLDivElement>(null)
|
|
41
|
+
|
|
42
|
+
const getRowExpandSymbol = () => {
|
|
43
|
+
return tableData.display[props.rowIndex].expanded ? '▼' : '►'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (props.addNavigation !== undefined) {
|
|
47
|
+
const keyboardNav = Object.assign({}, props.addNavigation)
|
|
48
|
+
keyboardNav['keydown.control.g'] = (event: KeyboardEvent) => {
|
|
49
|
+
event.stopPropagation()
|
|
50
|
+
event.preventDefault()
|
|
51
|
+
tableData.toggleRowExpand(props.rowIndex)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useKeyboardNav([
|
|
55
|
+
{
|
|
56
|
+
selectors: rowEl,
|
|
57
|
+
handlers: keyboardNav,
|
|
58
|
+
},
|
|
59
|
+
])
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
@import url('@stonecrop/themes/default/default.css');
|
|
65
|
+
.row-index {
|
|
66
|
+
color: var(--header-text-color);
|
|
67
|
+
font-weight: bold;
|
|
68
|
+
text-align: center;
|
|
69
|
+
user-select: none;
|
|
70
|
+
width: 2ch;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.expandable-row {
|
|
74
|
+
border-top: 1px solid var(--row-border-color);
|
|
75
|
+
height: var(--atable-row-height);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.expanded-row {
|
|
79
|
+
border-bottom: 1px solid var(--row-border-color);
|
|
80
|
+
border-top: 1px solid var(--row-border-color);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.expanded-row-content {
|
|
84
|
+
border-bottom: 1px solid var(--row-border-color);
|
|
85
|
+
border-top: 1px solid var(--row-border-color);
|
|
86
|
+
padding: 1.5rem;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr ref="rowEl" :tabindex="tabIndex" v-show="rowVisible()" class="table-row">
|
|
3
|
+
<!-- render numbered/tree view index -->
|
|
4
|
+
<td v-if="tableData.config.view === 'list'" :tabIndex="-1" class="list-index">
|
|
5
|
+
{{ rowIndex + 1 }}
|
|
6
|
+
</td>
|
|
7
|
+
<td
|
|
8
|
+
v-else-if="tableData.config.view === 'tree'"
|
|
9
|
+
:tabIndex="-1"
|
|
10
|
+
class="tree-index"
|
|
11
|
+
@click="toggleRowExpand(rowIndex)">
|
|
12
|
+
{{ getRowExpandSymbol() }}
|
|
13
|
+
</td>
|
|
14
|
+
<slot v-else name="indexCell"></slot>
|
|
15
|
+
|
|
16
|
+
<!-- render cell content -->
|
|
17
|
+
<slot></slot>
|
|
18
|
+
</tr>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { TableRow } from 'types'
|
|
23
|
+
import { inject, ref } from 'vue'
|
|
24
|
+
import { useKeyboardNav } from '@stonecrop/utilities'
|
|
25
|
+
|
|
26
|
+
import TableDataStore from '.'
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(
|
|
29
|
+
defineProps<{
|
|
30
|
+
row: TableRow
|
|
31
|
+
rowIndex: number
|
|
32
|
+
tableid: string
|
|
33
|
+
tabIndex?: number
|
|
34
|
+
addNavigation?: object
|
|
35
|
+
}>(),
|
|
36
|
+
{
|
|
37
|
+
tabIndex: -1,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const tableData = inject<TableDataStore>(props.tableid)
|
|
42
|
+
const rowEl = ref<HTMLTableRowElement>(null)
|
|
43
|
+
const numberedRowWidth = tableData.numberedRowWidth.value
|
|
44
|
+
|
|
45
|
+
const getRowExpandSymbol = () => {
|
|
46
|
+
if (tableData.config.view !== 'tree') {
|
|
47
|
+
return ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (tableData.display[props.rowIndex].isRoot) {
|
|
51
|
+
if (tableData.display[props.rowIndex].childrenOpen) {
|
|
52
|
+
return '-'
|
|
53
|
+
} else {
|
|
54
|
+
return '+'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (tableData.display[props.rowIndex].isParent) {
|
|
59
|
+
if (tableData.display[props.rowIndex].childrenOpen) {
|
|
60
|
+
return '-'
|
|
61
|
+
} else {
|
|
62
|
+
return '+'
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
return ''
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rowVisible = () => {
|
|
70
|
+
return (
|
|
71
|
+
tableData.config.view !== 'tree' ||
|
|
72
|
+
tableData.display[props.rowIndex].isRoot ||
|
|
73
|
+
tableData.display[props.rowIndex].open
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const toggleRowExpand = (rowIndex: number) => {
|
|
78
|
+
tableData.toggleRowExpand(rowIndex)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (props.addNavigation) {
|
|
82
|
+
useKeyboardNav([
|
|
83
|
+
{
|
|
84
|
+
selectors: rowEl,
|
|
85
|
+
handlers: props.addNavigation,
|
|
86
|
+
},
|
|
87
|
+
])
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
@import url('@stonecrop/themes/default/default.css');
|
|
93
|
+
.table-row {
|
|
94
|
+
border-top: 1px solid var(--row-border-color);
|
|
95
|
+
height: var(--atable-row-height);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.list-index {
|
|
99
|
+
color: var(--header-text-color);
|
|
100
|
+
font-weight: bold;
|
|
101
|
+
padding-left: var(--atable-row-padding);
|
|
102
|
+
padding-right: 1em;
|
|
103
|
+
text-align: center;
|
|
104
|
+
user-select: none;
|
|
105
|
+
width: v-bind(numberedRowWidth);
|
|
106
|
+
max-width: v-bind(numberedRowWidth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tree-index {
|
|
110
|
+
color: var(--header-text-color);
|
|
111
|
+
font-weight: bold;
|
|
112
|
+
text-align: center;
|
|
113
|
+
user-select: none;
|
|
114
|
+
width: 2ch;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<table class="atable" :style="{ width: tableData.config.fullWidth ? '100%' : 'auto' }">
|
|
3
|
+
<slot name="header" :data="tableData">
|
|
4
|
+
<ATableHeader :columns="tableData.columns" :config="tableData.config" :tableid="tableData.id" />
|
|
5
|
+
</slot>
|
|
6
|
+
|
|
7
|
+
<tbody>
|
|
8
|
+
<slot name="body" :data="tableData">
|
|
9
|
+
<ARow
|
|
10
|
+
v-for="(row, rowIndex) in tableData.rows"
|
|
11
|
+
:key="row.id || v4()"
|
|
12
|
+
:row="row"
|
|
13
|
+
:rowIndex="rowIndex"
|
|
14
|
+
:tableid="tableData.id">
|
|
15
|
+
<ACell
|
|
16
|
+
v-for="(col, colIndex) in tableData.columns"
|
|
17
|
+
:key="`${colIndex}:${rowIndex}`"
|
|
18
|
+
:tableid="tableData.id"
|
|
19
|
+
:col="col"
|
|
20
|
+
spellcheck="false"
|
|
21
|
+
:rowIndex="rowIndex"
|
|
22
|
+
:colIndex="colIndex + (tableData.zeroColumn ? 0 : -1)"
|
|
23
|
+
:component="col.cellComponent"
|
|
24
|
+
:style="{
|
|
25
|
+
textAlign: col?.align || 'center',
|
|
26
|
+
minWidth: col?.width || '40ch',
|
|
27
|
+
width: tableData.config.fullWidth ? 'auto' : null,
|
|
28
|
+
}" />
|
|
29
|
+
</ARow>
|
|
30
|
+
</slot>
|
|
31
|
+
</tbody>
|
|
32
|
+
|
|
33
|
+
<slot name="footer" :data="tableData" />
|
|
34
|
+
<slot name="modal" :data="tableData">
|
|
35
|
+
<ATableModal
|
|
36
|
+
v-show="tableData.modal.visible"
|
|
37
|
+
:colIndex="tableData.modal.colIndex"
|
|
38
|
+
:rowIndex="tableData.modal.rowIndex"
|
|
39
|
+
:tableid="tableData.id"
|
|
40
|
+
:style="{
|
|
41
|
+
left: tableData.modal.left + 'px',
|
|
42
|
+
top: tableData.modal.top + 'px',
|
|
43
|
+
maxWidth: tableData.modal.width + 'px',
|
|
44
|
+
}">
|
|
45
|
+
<template #default>
|
|
46
|
+
<component
|
|
47
|
+
:key="`${tableData.modal.rowIndex}:${tableData.modal.colIndex}`"
|
|
48
|
+
:is="tableData.modal.component"
|
|
49
|
+
:colIndex="tableData.modal.colIndex"
|
|
50
|
+
:rowIndex="tableData.modal.rowIndex"
|
|
51
|
+
:tableid="tableData.id"
|
|
52
|
+
v-bind="tableData.modal.componentProps" />
|
|
53
|
+
</template>
|
|
54
|
+
</ATableModal>
|
|
55
|
+
</slot>
|
|
56
|
+
</table>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { v4 } from 'uuid'
|
|
61
|
+
import { nextTick, provide, watch } from 'vue'
|
|
62
|
+
|
|
63
|
+
import { TableColumn, TableConfig, TableRow } from 'types'
|
|
64
|
+
import TableDataStore from '.'
|
|
65
|
+
import ACell from '@/components/ACell.vue'
|
|
66
|
+
import ARow from '@/components/ARow.vue'
|
|
67
|
+
import ATableHeader from '@/components/ATableHeader.vue'
|
|
68
|
+
import ATableModal from '@/components/ATableModal.vue'
|
|
69
|
+
|
|
70
|
+
const props = withDefaults(
|
|
71
|
+
defineProps<{
|
|
72
|
+
id?: string
|
|
73
|
+
modelValue: TableRow[]
|
|
74
|
+
columns: TableColumn[]
|
|
75
|
+
rows?: TableRow[]
|
|
76
|
+
config?: TableConfig
|
|
77
|
+
tableid?: string
|
|
78
|
+
}>(),
|
|
79
|
+
{
|
|
80
|
+
rows: () => [],
|
|
81
|
+
config: () => new Object(),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const emit = defineEmits(['update:modelValue'])
|
|
86
|
+
|
|
87
|
+
let rows = props.modelValue ? props.modelValue : props.rows
|
|
88
|
+
|
|
89
|
+
let tableData = new TableDataStore(props.id, props.columns, rows, props.config)
|
|
90
|
+
provide(tableData.id, tableData)
|
|
91
|
+
|
|
92
|
+
watch(
|
|
93
|
+
() => tableData.rows,
|
|
94
|
+
(newValue, oldValue) => {
|
|
95
|
+
emit('update:modelValue', newValue)
|
|
96
|
+
},
|
|
97
|
+
{ deep: true }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const formatCell = (event?: KeyboardEvent, column?: TableColumn, cellData?: any) => {
|
|
101
|
+
let colIndex: number
|
|
102
|
+
const target = event?.target as HTMLTableCellElement
|
|
103
|
+
if (event) {
|
|
104
|
+
colIndex = target.cellIndex + (tableData.zeroColumn ? -1 : 0)
|
|
105
|
+
} else if (column && cellData) {
|
|
106
|
+
colIndex = tableData.columns.indexOf(column)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!column && 'format' in tableData.columns[colIndex]) {
|
|
110
|
+
// TODO: (utils) create helper to extract format from string
|
|
111
|
+
const format = tableData.columns[colIndex].format
|
|
112
|
+
if (typeof format === 'function') {
|
|
113
|
+
return format(target.innerHTML)
|
|
114
|
+
} else if (typeof format === 'string') {
|
|
115
|
+
// parse format function from string
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
117
|
+
const formatFn: (args: any) => any = Function(`"use strict";return (${format})`)()
|
|
118
|
+
return formatFn(target.innerHTML)
|
|
119
|
+
} else {
|
|
120
|
+
return target.innerHTML
|
|
121
|
+
}
|
|
122
|
+
} else if (cellData && 'format' in column) {
|
|
123
|
+
const format = column.format
|
|
124
|
+
if (typeof format === 'function') {
|
|
125
|
+
return format(cellData)
|
|
126
|
+
} else if (typeof format === 'string') {
|
|
127
|
+
// parse format function from string
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
129
|
+
const formatFn: (args: any) => any = Function(`"use strict";return (${format})`)()
|
|
130
|
+
return formatFn(cellData)
|
|
131
|
+
} else {
|
|
132
|
+
return cellData
|
|
133
|
+
}
|
|
134
|
+
} else if (cellData && column.type.toLowerCase() in ['int', 'decimal', 'float', 'number', 'percent']) {
|
|
135
|
+
return cellData
|
|
136
|
+
// TODO: number formatting
|
|
137
|
+
} else {
|
|
138
|
+
return cellData
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const moveCursorToEnd = (target: HTMLElement) => {
|
|
143
|
+
target.focus()
|
|
144
|
+
document.execCommand('selectAll', false, null)
|
|
145
|
+
document.getSelection().collapseToEnd()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const clickOutside = (event: MouseEvent) => {
|
|
149
|
+
if (!tableData.modal.parent?.contains(event.target as HTMLElement)) {
|
|
150
|
+
if (tableData.modal.visible) {
|
|
151
|
+
// call set data
|
|
152
|
+
tableData.modal.visible = false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
window.addEventListener('click', clickOutside)
|
|
158
|
+
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
159
|
+
if (event.key === 'Escape') {
|
|
160
|
+
if (tableData.modal.visible) {
|
|
161
|
+
tableData.modal.visible = false
|
|
162
|
+
|
|
163
|
+
// focus on the parent cell again
|
|
164
|
+
const $parent = tableData.modal.parent
|
|
165
|
+
if ($parent) {
|
|
166
|
+
// wait for the modal to close
|
|
167
|
+
void nextTick().then(() => {
|
|
168
|
+
// for some reason, the parent is not immediately visible in the DOM;
|
|
169
|
+
// re-fetching the cell to add focus instead
|
|
170
|
+
const rowIndex = $parent.dataset.rowindex
|
|
171
|
+
const colIndex = $parent.dataset.colindex
|
|
172
|
+
const $parentCell = document.querySelectorAll(`[data-rowindex='${rowIndex}'][data-colindex='${colIndex}']`)
|
|
173
|
+
if ($parentCell) {
|
|
174
|
+
;($parentCell[0] as HTMLTableCellElement).focus()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<style scoped>
|
|
184
|
+
@import url('@stonecrop/themes/default/default.css');
|
|
185
|
+
|
|
186
|
+
table {
|
|
187
|
+
display: table;
|
|
188
|
+
border-collapse: collapse;
|
|
189
|
+
caret-color: var(--brand-color);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
table.atable,
|
|
193
|
+
.atable {
|
|
194
|
+
font-family: var(--atable-font-family);
|
|
195
|
+
-webkit-font-smoothing: antialiased;
|
|
196
|
+
-moz-osx-font-smoothing: grayscale;
|
|
197
|
+
font-size: var(--table-font-size);
|
|
198
|
+
border-collapse: collapse;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
th {
|
|
202
|
+
box-sizing: border-box;
|
|
203
|
+
background-color: var(--brand-color);
|
|
204
|
+
border-width: 1px;
|
|
205
|
+
border-style: solid;
|
|
206
|
+
border-color: var(--header-border-color);
|
|
207
|
+
border-radius: 0px;
|
|
208
|
+
color: var(--header-text-color);
|
|
209
|
+
}
|
|
210
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<thead v-if="columns.length">
|
|
3
|
+
<tr class="atable-header-row" tabindex="-1">
|
|
4
|
+
<th v-if="tableData.zeroColumn" id="header-index" />
|
|
5
|
+
<th v-for="(column, colKey) in columns" :key="colKey" tabindex="-1" :style="getHeaderCellStyle(column)">
|
|
6
|
+
<slot>{{ column.label || String.fromCharCode(colKey + 97).toUpperCase() }}</slot>
|
|
7
|
+
</th>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { CSSProperties, inject } from 'vue'
|
|
14
|
+
|
|
15
|
+
import { TableColumn, TableConfig } from 'types'
|
|
16
|
+
import TableDataStore from '.'
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
columns: TableColumn[]
|
|
20
|
+
config?: TableConfig
|
|
21
|
+
tableid?: string
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const tableData = inject<TableDataStore>(props.tableid)
|
|
25
|
+
|
|
26
|
+
const numberedRowWidth = tableData.numberedRowWidth.value
|
|
27
|
+
const getHeaderCellStyle = (column: TableColumn): CSSProperties => ({
|
|
28
|
+
minWidth: column.width || '40ch',
|
|
29
|
+
textAlign: column.align || 'center',
|
|
30
|
+
width: tableData.config.fullWidth ? 'auto' : null,
|
|
31
|
+
})
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
@import url('@stonecrop/themes/default/default.css');
|
|
36
|
+
thead {
|
|
37
|
+
background-color: var(--gray-5);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#header-index {
|
|
41
|
+
width: v-bind(numberedRowWidth);
|
|
42
|
+
max-width: v-bind(numberedRowWidth);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
th {
|
|
46
|
+
border-width: 0px;
|
|
47
|
+
border-style: solid;
|
|
48
|
+
border-radius: 0px;
|
|
49
|
+
padding-left: 0.5ch;
|
|
50
|
+
padding-right: 0.5ch;
|
|
51
|
+
padding-top: var(--atable-row-padding);
|
|
52
|
+
padding-bottom: var(--atable-row-padding);
|
|
53
|
+
color: var(--gray-60);
|
|
54
|
+
height: var(--atable-row-height);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
th:focus {
|
|
58
|
+
outline: none;
|
|
59
|
+
}
|
|
60
|
+
</style>
|