@stonecrop/atable 0.8.1 → 0.8.3
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/dist/assets/index.css +1 -1
- package/dist/atable.d.ts +1233 -1
- package/dist/atable.js +1602 -1269
- package/dist/atable.js.map +1 -1
- package/dist/atable.umd.cjs +46 -2
- package/dist/atable.umd.cjs.map +1 -1
- package/dist/icons/index.js +31 -0
- package/dist/icons/stonecrop-ui-icon-add.svg +5 -0
- package/dist/icons/stonecrop-ui-icon-insert-above.svg +11 -11
- package/dist/icons/stonecrop-ui-icon-insert-below.svg +11 -11
- package/dist/index.js +5 -1
- package/dist/src/icons/index.d.ts +19 -0
- package/dist/src/icons/index.d.ts.map +1 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/stores/table.d.ts +1075 -1
- package/dist/src/stores/table.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +122 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/stores/table.js +178 -0
- package/package.json +3 -3
- package/src/components/ARow.vue +53 -1
- package/src/components/ARowActions.vue +376 -0
- package/src/components/ATable.vue +83 -2
- package/src/components/ATableHeader.vue +49 -19
- package/src/icons/index.ts +34 -0
- package/src/icons/stonecrop-ui-icon-add.svg +5 -0
- package/src/icons/stonecrop-ui-icon-insert-above.svg +11 -11
- package/src/icons/stonecrop-ui-icon-insert-below.svg +11 -11
- package/src/index.ts +6 -0
- package/src/stores/table.ts +203 -0
- package/src/types/index.ts +138 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<td
|
|
3
|
+
ref="actionsCell"
|
|
4
|
+
class="atable-row-actions"
|
|
5
|
+
:class="{ 'sticky-column': position === 'before-index', 'dropdown-active': dropdownOpen }">
|
|
6
|
+
<!-- Dropdown mode -->
|
|
7
|
+
<div v-if="showDropdown" class="row-actions-dropdown">
|
|
8
|
+
<button
|
|
9
|
+
ref="toggleButton"
|
|
10
|
+
type="button"
|
|
11
|
+
class="row-actions-toggle"
|
|
12
|
+
:aria-expanded="dropdownOpen"
|
|
13
|
+
aria-haspopup="true"
|
|
14
|
+
@click.stop="toggleDropdown">
|
|
15
|
+
<span class="dropdown-icon">⋮</span>
|
|
16
|
+
</button>
|
|
17
|
+
<div
|
|
18
|
+
v-show="dropdownOpen"
|
|
19
|
+
class="row-actions-menu"
|
|
20
|
+
:class="{ 'menu-flipped': dropdownFlipped }"
|
|
21
|
+
:style="menuStyle"
|
|
22
|
+
role="menu">
|
|
23
|
+
<button
|
|
24
|
+
v-for="action in enabledActions"
|
|
25
|
+
:key="action.type"
|
|
26
|
+
type="button"
|
|
27
|
+
class="row-action-menu-item"
|
|
28
|
+
role="menuitem"
|
|
29
|
+
@click.stop="executeAction(action.type)">
|
|
30
|
+
<span class="action-icon" v-html="action.icon" />
|
|
31
|
+
<span class="action-label">{{ action.label }}</span>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Icon mode -->
|
|
37
|
+
<div v-else class="row-actions-icons">
|
|
38
|
+
<button
|
|
39
|
+
v-for="action in enabledActions"
|
|
40
|
+
:key="action.type"
|
|
41
|
+
type="button"
|
|
42
|
+
class="row-action-btn"
|
|
43
|
+
:title="action.label"
|
|
44
|
+
:aria-label="action.label"
|
|
45
|
+
@click.stop="executeAction(action.type)">
|
|
46
|
+
<span class="action-icon" v-html="action.icon" />
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</td>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { useResizeObserver, onClickOutside } from '@vueuse/core'
|
|
54
|
+
import { computed, ref, useTemplateRef } from 'vue'
|
|
55
|
+
|
|
56
|
+
import { actionIcons } from '../icons'
|
|
57
|
+
import { createTableStore } from '../stores/table'
|
|
58
|
+
import type { RowActionsConfig, RowActionType } from '../types'
|
|
59
|
+
|
|
60
|
+
const props = defineProps<{
|
|
61
|
+
rowIndex: number
|
|
62
|
+
store: ReturnType<typeof createTableStore>
|
|
63
|
+
config: RowActionsConfig
|
|
64
|
+
position?: 'before-index' | 'after-index' | 'end'
|
|
65
|
+
}>()
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{
|
|
68
|
+
action: [type: RowActionType, rowIndex: number]
|
|
69
|
+
}>()
|
|
70
|
+
|
|
71
|
+
const actionsCellRef = useTemplateRef<HTMLTableCellElement>('actionsCell')
|
|
72
|
+
const toggleButtonRef = useTemplateRef<HTMLButtonElement>('toggleButton')
|
|
73
|
+
const cellWidth = ref(0)
|
|
74
|
+
const dropdownOpen = ref(false)
|
|
75
|
+
const dropdownFlipped = ref(false)
|
|
76
|
+
const menuPosition = ref({ top: 0, left: 0 })
|
|
77
|
+
|
|
78
|
+
// Default labels for actions
|
|
79
|
+
const defaultLabels: Record<RowActionType, string> = {
|
|
80
|
+
add: 'Add Row',
|
|
81
|
+
delete: 'Delete Row',
|
|
82
|
+
duplicate: 'Duplicate Row',
|
|
83
|
+
insertAbove: 'Insert Above',
|
|
84
|
+
insertBelow: 'Insert Below',
|
|
85
|
+
move: 'Move Row',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Determine which actions are enabled
|
|
89
|
+
const enabledActions = computed(() => {
|
|
90
|
+
const actions: Array<{ type: RowActionType; label: string; icon: string }> = []
|
|
91
|
+
const configActions = props.config.actions || {}
|
|
92
|
+
|
|
93
|
+
const actionTypes: RowActionType[] = ['add', 'delete', 'duplicate', 'insertAbove', 'insertBelow', 'move']
|
|
94
|
+
|
|
95
|
+
for (const type of actionTypes) {
|
|
96
|
+
const actionConfig = configActions[type]
|
|
97
|
+
|
|
98
|
+
// Skip if explicitly disabled
|
|
99
|
+
if (actionConfig === false) continue
|
|
100
|
+
|
|
101
|
+
// Skip if not configured and we're being explicit
|
|
102
|
+
if (actionConfig === undefined) continue
|
|
103
|
+
|
|
104
|
+
let enabled = true
|
|
105
|
+
let label = defaultLabels[type]
|
|
106
|
+
let icon = actionIcons[type]
|
|
107
|
+
|
|
108
|
+
if (typeof actionConfig === 'object') {
|
|
109
|
+
enabled = actionConfig.enabled !== false
|
|
110
|
+
label = actionConfig.label || label
|
|
111
|
+
icon = actionConfig.icon || icon
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (enabled) {
|
|
115
|
+
actions.push({ type, label, icon })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return actions
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Determine if we should show dropdown mode
|
|
123
|
+
const showDropdown = computed(() => {
|
|
124
|
+
if (props.config.forceDropdown) return true
|
|
125
|
+
|
|
126
|
+
const threshold = props.config.dropdownThreshold ?? 150
|
|
127
|
+
if (threshold === 0) return false
|
|
128
|
+
|
|
129
|
+
return cellWidth.value > 0 && cellWidth.value < threshold
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Compute menu style for fixed positioning
|
|
133
|
+
const menuStyle = computed(() => {
|
|
134
|
+
if (!dropdownOpen.value) return {}
|
|
135
|
+
|
|
136
|
+
if (dropdownFlipped.value) {
|
|
137
|
+
return {
|
|
138
|
+
position: 'fixed' as const,
|
|
139
|
+
bottom: `${window.innerHeight - menuPosition.value.top}px`,
|
|
140
|
+
left: `${menuPosition.value.left}px`,
|
|
141
|
+
top: 'auto',
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
position: 'fixed' as const,
|
|
146
|
+
top: `${menuPosition.value.top}px`,
|
|
147
|
+
left: `${menuPosition.value.left}px`,
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Track cell width for responsive behavior
|
|
152
|
+
useResizeObserver(actionsCellRef, entries => {
|
|
153
|
+
const entry = entries[0]
|
|
154
|
+
if (entry) {
|
|
155
|
+
cellWidth.value = entry.contentRect.width
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Toggle dropdown menu
|
|
160
|
+
const toggleDropdown = () => {
|
|
161
|
+
if (!dropdownOpen.value) {
|
|
162
|
+
// Opening - check if we need to flip
|
|
163
|
+
checkDropdownPosition()
|
|
164
|
+
}
|
|
165
|
+
dropdownOpen.value = !dropdownOpen.value
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if dropdown should flip upward and calculate position
|
|
169
|
+
const checkDropdownPosition = () => {
|
|
170
|
+
if (!toggleButtonRef.value) return
|
|
171
|
+
|
|
172
|
+
const buttonRect = toggleButtonRef.value.getBoundingClientRect()
|
|
173
|
+
const viewportHeight = window.innerHeight
|
|
174
|
+
const estimatedMenuHeight = enabledActions.value.length * 40 + 16 // ~40px per item + padding
|
|
175
|
+
|
|
176
|
+
// Check if menu would extend beyond viewport bottom
|
|
177
|
+
const spaceBelow = viewportHeight - buttonRect.bottom
|
|
178
|
+
const spaceAbove = buttonRect.top
|
|
179
|
+
|
|
180
|
+
// Flip if not enough space below but enough space above
|
|
181
|
+
dropdownFlipped.value = spaceBelow < estimatedMenuHeight && spaceAbove > estimatedMenuHeight
|
|
182
|
+
|
|
183
|
+
// Calculate fixed position
|
|
184
|
+
if (dropdownFlipped.value) {
|
|
185
|
+
menuPosition.value = {
|
|
186
|
+
top: buttonRect.top,
|
|
187
|
+
left: buttonRect.left,
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
menuPosition.value = {
|
|
191
|
+
top: buttonRect.bottom,
|
|
192
|
+
left: buttonRect.left,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onClickOutside(actionsCellRef, () => {
|
|
198
|
+
dropdownOpen.value = false
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Execute an action
|
|
202
|
+
const executeAction = (actionType: RowActionType) => {
|
|
203
|
+
dropdownOpen.value = false
|
|
204
|
+
|
|
205
|
+
// Check for custom handler
|
|
206
|
+
const actionConfig = props.config.actions?.[actionType]
|
|
207
|
+
if (typeof actionConfig === 'object' && actionConfig.handler) {
|
|
208
|
+
const result = actionConfig.handler(props.rowIndex, props.store)
|
|
209
|
+
if (result === false) {
|
|
210
|
+
// Handler returned false, don't proceed with default behavior
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Emit the action event for parent to handle
|
|
216
|
+
emit('action', actionType, props.rowIndex)
|
|
217
|
+
}
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<style>
|
|
221
|
+
@import url('@stonecrop/themes/default.css');
|
|
222
|
+
|
|
223
|
+
.atable-row-actions {
|
|
224
|
+
width: 2rem;
|
|
225
|
+
min-width: 2rem;
|
|
226
|
+
padding: 0 0.25rem;
|
|
227
|
+
vertical-align: middle;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
border-top: 1px solid var(--sc-row-border-color);
|
|
230
|
+
background: white;
|
|
231
|
+
user-select: none;
|
|
232
|
+
position: relative;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.atable-row-actions.dropdown-active {
|
|
236
|
+
z-index: 500;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.row-actions-icons {
|
|
240
|
+
display: flex;
|
|
241
|
+
gap: 0.25rem;
|
|
242
|
+
align-items: center;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.row-action-btn {
|
|
247
|
+
display: inline-flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
justify-content: center;
|
|
250
|
+
width: 1.5rem;
|
|
251
|
+
height: 1.5rem;
|
|
252
|
+
padding: 0.125rem;
|
|
253
|
+
border: none;
|
|
254
|
+
background: transparent;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
border-radius: 0.25rem;
|
|
257
|
+
transition: background-color 0.15s ease;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.row-action-btn:hover {
|
|
261
|
+
background-color: var(--sc-gray-10, #e5e5e5);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.row-action-btn:focus {
|
|
265
|
+
outline: 2px solid var(--sc-focus-cell-outline, #3b82f6);
|
|
266
|
+
outline-offset: 1px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.row-action-btn .action-icon {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
justify-content: center;
|
|
273
|
+
width: 1rem;
|
|
274
|
+
height: 1rem;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.row-action-btn .action-icon :deep(svg) {
|
|
278
|
+
width: 100%;
|
|
279
|
+
height: 100%;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Dropdown mode styles */
|
|
283
|
+
.row-actions-dropdown {
|
|
284
|
+
position: relative;
|
|
285
|
+
display: inline-block;
|
|
286
|
+
}
|
|
287
|
+
.row-actions-dropdown:has(button:focus) {
|
|
288
|
+
outline: 2px solid var(--sc-focus-cell-outline);
|
|
289
|
+
outline-offset: -2px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.row-actions-toggle {
|
|
293
|
+
display: inline-flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
justify-content: center;
|
|
296
|
+
width: 1.5rem;
|
|
297
|
+
height: 1.5rem;
|
|
298
|
+
padding: 0;
|
|
299
|
+
border: none;
|
|
300
|
+
background: transparent;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
border-radius: 0.25rem;
|
|
303
|
+
font-size: 1rem;
|
|
304
|
+
font-weight: bold;
|
|
305
|
+
transition: background-color 0.15s ease;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.row-actions-toggle:hover {
|
|
309
|
+
background-color: var(--sc-gray-10, #e5e5e5);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.row-actions-toggle:focus {
|
|
313
|
+
/* outline: 2px solid var(--sc-focus-cell-outline, #3b82f6);
|
|
314
|
+
outline-offset: 1px; */
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.dropdown-icon {
|
|
318
|
+
line-height: 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.row-actions-menu {
|
|
322
|
+
position: fixed;
|
|
323
|
+
z-index: 9999;
|
|
324
|
+
min-width: 10rem;
|
|
325
|
+
padding: 0.25rem 0;
|
|
326
|
+
background: white;
|
|
327
|
+
border: 1px solid var(--sc-row-border-color);
|
|
328
|
+
border-left: 4px solid var(--sc-row-border-color);
|
|
329
|
+
border-radius: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.row-actions-menu.menu-flipped {
|
|
333
|
+
box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.row-action-menu-item {
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
gap: 0.5rem;
|
|
340
|
+
width: 100%;
|
|
341
|
+
padding: 0.5rem 0.75rem;
|
|
342
|
+
border: none;
|
|
343
|
+
background: transparent;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
text-align: left;
|
|
346
|
+
font-size: 0.875rem;
|
|
347
|
+
transition: background-color 0.15s ease;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.row-action-menu-item:hover {
|
|
351
|
+
background-color: var(--sc-gray-10, #f5f5f5);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.row-action-menu-item:focus {
|
|
355
|
+
outline: none;
|
|
356
|
+
background-color: var(--sc-gray-10, #f5f5f5);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.row-action-menu-item .action-icon {
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
justify-content: center;
|
|
363
|
+
width: 1rem;
|
|
364
|
+
height: 1rem;
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.row-action-menu-item .action-icon :deep(svg) {
|
|
369
|
+
width: 100%;
|
|
370
|
+
height: 100%;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.row-action-menu-item .action-label {
|
|
374
|
+
flex: 1;
|
|
375
|
+
}
|
|
376
|
+
</style>
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
:key="`${row.originalIndex}-${filteredIndex}`"
|
|
19
19
|
:row="row"
|
|
20
20
|
:rowIndex="row.originalIndex"
|
|
21
|
-
:store="store"
|
|
21
|
+
:store="store"
|
|
22
|
+
@row:action="handleRowAction">
|
|
22
23
|
<template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
|
|
23
24
|
<component
|
|
24
25
|
:is="column.ganttComponent || 'AGanttCell'"
|
|
@@ -91,7 +92,20 @@ import ARow from './ARow.vue'
|
|
|
91
92
|
import ATableHeader from './ATableHeader.vue'
|
|
92
93
|
import ATableModal from './ATableModal.vue'
|
|
93
94
|
import { createTableStore } from '../stores/table'
|
|
94
|
-
import type {
|
|
95
|
+
import type {
|
|
96
|
+
ConnectionEvent,
|
|
97
|
+
ConnectionPath,
|
|
98
|
+
GanttDragEvent,
|
|
99
|
+
RowActionType,
|
|
100
|
+
RowAddEvent,
|
|
101
|
+
RowDeleteEvent,
|
|
102
|
+
RowDuplicateEvent,
|
|
103
|
+
RowInsertEvent,
|
|
104
|
+
RowMoveEvent,
|
|
105
|
+
TableColumn,
|
|
106
|
+
TableConfig,
|
|
107
|
+
TableRow,
|
|
108
|
+
} from '../types'
|
|
95
109
|
|
|
96
110
|
const rows = defineModel<TableRow[]>('rows', { required: true })
|
|
97
111
|
const columns = defineModel<TableColumn[]>('columns', { required: true })
|
|
@@ -106,6 +120,12 @@ const emit = defineEmits<{
|
|
|
106
120
|
'gantt:drag': [event: GanttDragEvent]
|
|
107
121
|
'connection:event': [event: ConnectionEvent]
|
|
108
122
|
'columns:update': [columns: TableColumn[]]
|
|
123
|
+
'row:add': [event: RowAddEvent]
|
|
124
|
+
'row:delete': [event: RowDeleteEvent]
|
|
125
|
+
'row:duplicate': [event: RowDuplicateEvent]
|
|
126
|
+
'row:insert-above': [event: RowInsertEvent]
|
|
127
|
+
'row:insert-below': [event: RowInsertEvent]
|
|
128
|
+
'row:move': [event: RowMoveEvent]
|
|
109
129
|
}>()
|
|
110
130
|
|
|
111
131
|
const tableRef = useTemplateRef<HTMLTableElement>('table')
|
|
@@ -267,12 +287,73 @@ const handleConnectionDelete = (connection: ConnectionPath) => {
|
|
|
267
287
|
emit('connection:event', { type: 'delete', connection })
|
|
268
288
|
}
|
|
269
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Handle row action events from ARow components.
|
|
292
|
+
* Performs the default action and emits the appropriate event.
|
|
293
|
+
*/
|
|
294
|
+
const handleRowAction = (actionType: RowActionType, rowIndex: number) => {
|
|
295
|
+
switch (actionType) {
|
|
296
|
+
case 'add': {
|
|
297
|
+
// Add a new row after the current row
|
|
298
|
+
const newIndex = store.addRow({}, rowIndex + 1)
|
|
299
|
+
const newRow = store.rows[newIndex]
|
|
300
|
+
rows.value = [...store.rows]
|
|
301
|
+
emit('row:add', { rowIndex: newIndex, row: newRow })
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
case 'delete': {
|
|
305
|
+
const deletedRow = store.deleteRow(rowIndex)
|
|
306
|
+
if (deletedRow) {
|
|
307
|
+
rows.value = [...store.rows]
|
|
308
|
+
emit('row:delete', { rowIndex, row: deletedRow })
|
|
309
|
+
}
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case 'duplicate': {
|
|
313
|
+
const newIndex = store.duplicateRow(rowIndex)
|
|
314
|
+
if (newIndex >= 0) {
|
|
315
|
+
const newRow = store.rows[newIndex]
|
|
316
|
+
rows.value = [...store.rows]
|
|
317
|
+
emit('row:duplicate', { sourceIndex: rowIndex, newIndex, row: newRow })
|
|
318
|
+
}
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
case 'insertAbove': {
|
|
322
|
+
const newIndex = store.insertRowAbove(rowIndex)
|
|
323
|
+
const newRow = store.rows[newIndex]
|
|
324
|
+
rows.value = [...store.rows]
|
|
325
|
+
emit('row:insert-above', { targetIndex: rowIndex, newIndex, row: newRow })
|
|
326
|
+
break
|
|
327
|
+
}
|
|
328
|
+
case 'insertBelow': {
|
|
329
|
+
const newIndex = store.insertRowBelow(rowIndex)
|
|
330
|
+
const newRow = store.rows[newIndex]
|
|
331
|
+
rows.value = [...store.rows]
|
|
332
|
+
emit('row:insert-below', { targetIndex: rowIndex, newIndex, row: newRow })
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
case 'move': {
|
|
336
|
+
// Move action requires a target index - for now, emit an event
|
|
337
|
+
// The consumer should handle showing a UI for selecting the target
|
|
338
|
+
emit('row:move', { fromIndex: rowIndex, toIndex: -1 })
|
|
339
|
+
break
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
270
344
|
defineExpose({
|
|
271
345
|
store,
|
|
272
346
|
createConnection: store.createConnection,
|
|
273
347
|
deleteConnection: store.deleteConnection,
|
|
274
348
|
getConnectionsForBar: store.getConnectionsForBar,
|
|
275
349
|
getHandlesForBar: store.getHandlesForBar,
|
|
350
|
+
// Row action methods
|
|
351
|
+
addRow: store.addRow,
|
|
352
|
+
deleteRow: store.deleteRow,
|
|
353
|
+
duplicateRow: store.duplicateRow,
|
|
354
|
+
insertRowAbove: store.insertRowAbove,
|
|
355
|
+
insertRowBelow: store.insertRowBelow,
|
|
356
|
+
moveRow: store.moveRow,
|
|
276
357
|
})
|
|
277
358
|
</script>
|
|
278
359
|
|
|
@@ -1,45 +1,63 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<thead v-if="columns.length">
|
|
2
|
+
<thead v-if="props.columns.length">
|
|
3
3
|
<!-- Header row -->
|
|
4
4
|
<tr class="atable-header-row" tabindex="-1">
|
|
5
|
+
<!-- Row actions header cell (before-index position) -->
|
|
5
6
|
<th
|
|
6
|
-
v-if="
|
|
7
|
+
v-if="showRowActionsHeader && rowActionsPosition === 'before-index'"
|
|
8
|
+
class="row-actions-header"
|
|
9
|
+
:class="{ 'sticky-column': rowActionsPosition === 'before-index' }" />
|
|
10
|
+
<th
|
|
11
|
+
v-if="props.store.zeroColumn"
|
|
7
12
|
id="header-index"
|
|
8
13
|
:class="[
|
|
9
|
-
store.hasPinnedColumns ? 'sticky-index' : '',
|
|
10
|
-
store.isTreeView ? 'tree-index' : '',
|
|
11
|
-
store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
14
|
+
props.store.hasPinnedColumns ? 'sticky-index' : '',
|
|
15
|
+
props.store.isTreeView ? 'tree-index' : '',
|
|
16
|
+
props.store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
12
17
|
]"
|
|
13
18
|
class="list-index" />
|
|
19
|
+
<!-- Row actions header cell (after-index position) -->
|
|
20
|
+
<th v-if="showRowActionsHeader && rowActionsPosition === 'after-index'" class="row-actions-header" />
|
|
14
21
|
<th
|
|
15
|
-
v-for="(column, colKey) in columns"
|
|
22
|
+
v-for="(column, colKey) in props.columns"
|
|
16
23
|
:key="column.name"
|
|
17
24
|
v-resize-observer="onResize"
|
|
18
25
|
:data-colindex="colKey"
|
|
19
26
|
tabindex="-1"
|
|
20
|
-
:style="store.getHeaderCellStyle(column)"
|
|
27
|
+
:style="props.store.getHeaderCellStyle(column)"
|
|
21
28
|
:class="`${column.pinned ? 'sticky-column' : ''} ${column.sortable === false ? '' : 'cursor-pointer'}`"
|
|
22
29
|
@click="column.sortable !== false ? handleSort(colKey) : undefined">
|
|
23
30
|
<slot>{{ column.label || String.fromCharCode(colKey + 97).toUpperCase() }}</slot>
|
|
24
31
|
</th>
|
|
32
|
+
<!-- Row actions header cell (end position) -->
|
|
33
|
+
<th v-if="showRowActionsHeader && rowActionsPosition === 'end'" class="row-actions-header" />
|
|
25
34
|
</tr>
|
|
26
35
|
<!-- Filters row -->
|
|
27
36
|
<tr v-if="filterableColumns.length > 0" class="atable-filters-row">
|
|
37
|
+
<!-- Row actions filter cell (before-index position) -->
|
|
38
|
+
<th
|
|
39
|
+
v-if="showRowActionsHeader && rowActionsPosition === 'before-index'"
|
|
40
|
+
class="row-actions-header"
|
|
41
|
+
:class="{ 'sticky-column': rowActionsPosition === 'before-index' }" />
|
|
28
42
|
<th
|
|
29
|
-
v-if="store.zeroColumn"
|
|
43
|
+
v-if="props.store.zeroColumn"
|
|
30
44
|
:class="[
|
|
31
|
-
store.hasPinnedColumns ? 'sticky-index' : '',
|
|
32
|
-
store.isTreeView ? 'tree-index' : '',
|
|
33
|
-
store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
45
|
+
props.store.hasPinnedColumns ? 'sticky-index' : '',
|
|
46
|
+
props.store.isTreeView ? 'tree-index' : '',
|
|
47
|
+
props.store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
34
48
|
]"
|
|
35
49
|
class="list-index" />
|
|
50
|
+
<!-- Row actions filter cell (after-index position) -->
|
|
51
|
+
<th v-if="showRowActionsHeader && rowActionsPosition === 'after-index'" class="row-actions-header" />
|
|
36
52
|
<th
|
|
37
|
-
v-for="(column, colKey) in columns"
|
|
53
|
+
v-for="(column, colKey) in props.columns"
|
|
38
54
|
:key="`filter-${column.name}`"
|
|
39
55
|
:class="`${column.pinned ? 'sticky-column' : ''}`"
|
|
40
|
-
:style="store.getHeaderCellStyle(column)">
|
|
41
|
-
<ATableColumnFilter v-if="column.filterable" :column="column" :col-index="colKey" :store="store" />
|
|
56
|
+
:style="props.store.getHeaderCellStyle(column)">
|
|
57
|
+
<ATableColumnFilter v-if="column.filterable" :column="column" :col-index="colKey" :store="props.store" />
|
|
42
58
|
</th>
|
|
59
|
+
<!-- Row actions filter cell (end position) -->
|
|
60
|
+
<th v-if="showRowActionsHeader && rowActionsPosition === 'end'" class="row-actions-header" />
|
|
43
61
|
</tr>
|
|
44
62
|
</thead>
|
|
45
63
|
</template>
|
|
@@ -51,14 +69,18 @@ import ATableColumnFilter from './ATableColumnFilter.vue'
|
|
|
51
69
|
import { createTableStore } from '../stores/table'
|
|
52
70
|
import type { TableColumn } from '../types'
|
|
53
71
|
|
|
54
|
-
const
|
|
72
|
+
const props = defineProps<{
|
|
55
73
|
columns: TableColumn[]
|
|
56
74
|
store: ReturnType<typeof createTableStore>
|
|
57
75
|
}>()
|
|
58
76
|
|
|
59
|
-
const filterableColumns = computed(() => columns.filter(column => column.filterable))
|
|
77
|
+
const filterableColumns = computed(() => props.columns.filter(column => column.filterable))
|
|
78
|
+
|
|
79
|
+
// Row actions header support
|
|
80
|
+
const showRowActionsHeader = computed(() => props.store.config.value?.rowActions?.enabled ?? false)
|
|
81
|
+
const rowActionsPosition = computed(() => props.store.config.value?.rowActions?.position ?? 'before-index')
|
|
60
82
|
|
|
61
|
-
const handleSort = (colIndex: number) => store.sortByColumn(colIndex)
|
|
83
|
+
const handleSort = (colIndex: number) => props.store.sortByColumn(colIndex)
|
|
62
84
|
|
|
63
85
|
const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
|
|
64
86
|
for (const entry of entries) {
|
|
@@ -66,10 +88,10 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
|
|
|
66
88
|
const observedCell = entry.borderBoxSize[0]
|
|
67
89
|
const observedWidth = observedCell.inlineSize
|
|
68
90
|
const colIndex = Number((entry.target as HTMLElement).dataset.colindex)
|
|
69
|
-
const currentWidth = store.columns[colIndex]?.width
|
|
91
|
+
const currentWidth = props.store.columns[colIndex]?.width
|
|
70
92
|
|
|
71
93
|
if (typeof currentWidth === 'number' && currentWidth !== observedWidth) {
|
|
72
|
-
store.resizeColumn(colIndex, observedWidth)
|
|
94
|
+
props.store.resizeColumn(colIndex, observedWidth)
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
}
|
|
@@ -81,6 +103,7 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
|
|
|
81
103
|
.atable-header-row th {
|
|
82
104
|
padding-left: 0.5ch !important;
|
|
83
105
|
font-weight: 700;
|
|
106
|
+
min-width: 3ch;
|
|
84
107
|
padding-top: var(--sc-atable-row-padding);
|
|
85
108
|
padding-bottom: var(--sc-atable-row-padding);
|
|
86
109
|
box-sizing: border-box;
|
|
@@ -90,6 +113,7 @@ const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
|
|
|
90
113
|
#header-index {
|
|
91
114
|
padding-left: var(--sc-atable-row-padding);
|
|
92
115
|
box-sizing: border-box;
|
|
116
|
+
border-top: none;
|
|
93
117
|
}
|
|
94
118
|
.tree-index {
|
|
95
119
|
padding-right: 0;
|
|
@@ -110,4 +134,10 @@ th {
|
|
|
110
134
|
padding: 0.25rem 0.5ch;
|
|
111
135
|
vertical-align: top;
|
|
112
136
|
}
|
|
137
|
+
|
|
138
|
+
.row-actions-header {
|
|
139
|
+
width: 2rem;
|
|
140
|
+
min-width: 2rem;
|
|
141
|
+
padding: 0 0.25rem;
|
|
142
|
+
}
|
|
113
143
|
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon exports for ATable row actions.
|
|
3
|
+
* Icons are exported as raw SVG strings for flexibility in rendering.
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// @ts-expect-error Vite raw import
|
|
8
|
+
import AddIcon from './stonecrop-ui-icon-add.svg?raw'
|
|
9
|
+
// @ts-expect-error Vite raw import
|
|
10
|
+
import DeleteIcon from './stonecrop-ui-icon-delete.svg?raw'
|
|
11
|
+
// @ts-expect-error Vite raw import
|
|
12
|
+
import DuplicateIcon from './stonecrop-ui-icon-duplicate.svg?raw'
|
|
13
|
+
// @ts-expect-error Vite raw import
|
|
14
|
+
import InsertAboveIcon from './stonecrop-ui-icon-insert-above.svg?raw'
|
|
15
|
+
// @ts-expect-error Vite raw import
|
|
16
|
+
import InsertBelowIcon from './stonecrop-ui-icon-insert-below.svg?raw'
|
|
17
|
+
// @ts-expect-error Vite raw import
|
|
18
|
+
import MoveIcon from './stonecrop-ui-icon-move.svg?raw'
|
|
19
|
+
|
|
20
|
+
export { AddIcon, DeleteIcon, DuplicateIcon, InsertAboveIcon, InsertBelowIcon, MoveIcon }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map of action types to their default icons.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export const actionIcons: Record<string, string> = {
|
|
28
|
+
add: AddIcon as string,
|
|
29
|
+
delete: DeleteIcon as string,
|
|
30
|
+
duplicate: DuplicateIcon as string,
|
|
31
|
+
insertAbove: InsertAboveIcon as string,
|
|
32
|
+
insertBelow: InsertBelowIcon as string,
|
|
33
|
+
move: MoveIcon as string,
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
3
|
+
<path d="M32,64C14.35,64,0,49.65,0,32S14.35,0,32,0s32,14.35,32,32-14.35,32-32,32ZM32,4c-15.44,0-28,12.56-28,28s12.56,28,28,28,28-12.56,28-28S47.44,4,32,4Z" style="fill: #000; stroke-width: 0px;"/>
|
|
4
|
+
<polygon points="34 18 30 18 30 30 18 30 18 34 30 34 30 46 34 46 34 34 46 34 46 30 34 30 34 18" style="fill: #000; stroke-width: 0px;"/>
|
|
5
|
+
</svg>
|