@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.
@@ -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>