@stonecrop/atable 0.4.22 → 0.4.24
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 +511 -25
- package/dist/atable.js +1327 -1060
- package/dist/atable.js.map +1 -1
- package/dist/atable.umd.cjs +2 -2
- package/dist/atable.umd.cjs.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/stores/table.d.ts +370 -16
- package/dist/src/stores/table.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +138 -10
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/stores/table.js +116 -14
- package/package.json +3 -3
- package/src/components/AGanttCell.vue +444 -170
- package/src/components/AGanttConnection.vue +164 -0
- package/src/components/ARow.vue +1 -1
- package/src/components/ATable.vue +107 -77
- package/src/components/ATableHeader.vue +1 -1
- package/src/index.ts +4 -0
- package/src/stores/table.ts +134 -14
- package/src/types/index.ts +159 -10
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="gantt-connection-overlay">
|
|
3
|
+
<svg
|
|
4
|
+
class="connection-svg"
|
|
5
|
+
:style="{
|
|
6
|
+
position: 'absolute',
|
|
7
|
+
top: 0,
|
|
8
|
+
left: 0,
|
|
9
|
+
width: '100%',
|
|
10
|
+
height: '100%',
|
|
11
|
+
pointerEvents: 'none',
|
|
12
|
+
zIndex: 1,
|
|
13
|
+
}">
|
|
14
|
+
<defs>
|
|
15
|
+
<!-- Define arrowhead marker for connections -->
|
|
16
|
+
<path id="arrowhead" d="M 0 -7 L 20 0 L 0 7Z" stroke="black" stroke-width="1" fill="currentColor"></path>
|
|
17
|
+
<marker
|
|
18
|
+
id="arrowhead-marker"
|
|
19
|
+
markerWidth="10"
|
|
20
|
+
markerHeight="7"
|
|
21
|
+
refX="5"
|
|
22
|
+
refY="3.5"
|
|
23
|
+
orient="auto"
|
|
24
|
+
markerUnits="strokeWidth">
|
|
25
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="currentColor" />
|
|
26
|
+
</marker>
|
|
27
|
+
</defs>
|
|
28
|
+
|
|
29
|
+
<!-- Invisible wider path for easier double-click interaction -->
|
|
30
|
+
<path
|
|
31
|
+
v-for="connection in visibleConnections"
|
|
32
|
+
:key="`${connection.id}-hitbox`"
|
|
33
|
+
:d="getPathData(connection)"
|
|
34
|
+
stroke="transparent"
|
|
35
|
+
:stroke-width="(connection.style?.width || 2) + 10"
|
|
36
|
+
fill="none"
|
|
37
|
+
class="connection-hitbox"
|
|
38
|
+
@dblclick="handleConnectionDelete(connection)" />
|
|
39
|
+
|
|
40
|
+
<!-- Visible connection path -->
|
|
41
|
+
<path
|
|
42
|
+
v-for="connection in visibleConnections"
|
|
43
|
+
:key="connection.id"
|
|
44
|
+
:d="getPathData(connection)"
|
|
45
|
+
:stroke="connection.style?.color || '#666'"
|
|
46
|
+
:stroke-width="connection.style?.width || 2"
|
|
47
|
+
fill="none"
|
|
48
|
+
marker-mid="url(#arrowhead-marker)"
|
|
49
|
+
:id="connection.id"
|
|
50
|
+
class="connection-path animated-path"
|
|
51
|
+
@dblclick="handleConnectionDelete(connection)" />
|
|
52
|
+
</svg>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
import { computed } from 'vue'
|
|
58
|
+
|
|
59
|
+
import { createTableStore } from '../stores/table'
|
|
60
|
+
import type { ConnectionPath } from '../types'
|
|
61
|
+
|
|
62
|
+
const { store } = defineProps<{
|
|
63
|
+
store: ReturnType<typeof createTableStore>
|
|
64
|
+
}>()
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
'connection:delete': [connection: ConnectionPath]
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
const BEZIER_CURVE_FACTOR = 0.25 // Control point offset factor for bezier curves
|
|
71
|
+
const CONNECTION_HANDLE_SIZE = 16 // Width of the connection handles; this should match the handle size in the AGanttCell component
|
|
72
|
+
|
|
73
|
+
const visibleConnections = computed(() => {
|
|
74
|
+
return store.connectionPaths.filter(connection => {
|
|
75
|
+
const fromBar = store.ganttBars.find(bar => bar.id === connection.from.barId)
|
|
76
|
+
const toBar = store.ganttBars.find(bar => bar.id === connection.to.barId)
|
|
77
|
+
return fromBar && toBar
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const getPathData = (connection: ConnectionPath, isMarker: Boolean = false) => {
|
|
82
|
+
const fromHandle = store.connectionHandles.find(
|
|
83
|
+
handle => handle.barId === connection.from.barId && handle.side === connection.from.side
|
|
84
|
+
)
|
|
85
|
+
const toHandle = store.connectionHandles.find(
|
|
86
|
+
handle => handle.barId === connection.to.barId && handle.side === connection.to.side
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (!fromHandle || !toHandle) return ''
|
|
90
|
+
|
|
91
|
+
const fromX = fromHandle.position.x + CONNECTION_HANDLE_SIZE / 2 // Center of the handle
|
|
92
|
+
const fromY = fromHandle.position.y + CONNECTION_HANDLE_SIZE / 2
|
|
93
|
+
const toX = toHandle.position.x + CONNECTION_HANDLE_SIZE / 2
|
|
94
|
+
const toY = toHandle.position.y + CONNECTION_HANDLE_SIZE / 2
|
|
95
|
+
|
|
96
|
+
// Calculate control points for smooth bezier curve
|
|
97
|
+
const deltaX = Math.abs(toX - fromX)
|
|
98
|
+
const controlPointOffset = Math.max(deltaX * BEZIER_CURVE_FACTOR, 50) // Minimum offset for better curves
|
|
99
|
+
const cp1X = fromX + (connection.from.side === 'left' ? -controlPointOffset : controlPointOffset)
|
|
100
|
+
const cp2X = toX + (connection.to.side === 'left' ? -controlPointOffset : controlPointOffset)
|
|
101
|
+
|
|
102
|
+
// Use cubic bezier curve for smooth connections
|
|
103
|
+
|
|
104
|
+
//calculate the mid point of the curve
|
|
105
|
+
const m0 = { x: 0.5 * fromX + 0.5 * cp1X, y: 0.5 * fromY + 0.5 * fromY }
|
|
106
|
+
const m1 = { x: 0.5 * cp1X + 0.5 * cp2X, y: 0.5 * fromY + 0.5 * toY }
|
|
107
|
+
const m2 = { x: 0.5 * cp2X + 0.5 * toX, y: 0.5 * toY + 0.5 * toY }
|
|
108
|
+
const m3 = { x: 0.5 * m0.x + 0.5 * m1.x, y: 0.5 * m0.y + 0.5 * m1.y }
|
|
109
|
+
const m4 = { x: 0.5 * m1.x + 0.5 * m2.x, y: 0.5 * m1.y + 0.5 * m2.y }
|
|
110
|
+
const midpoint = { x: 0.5 * m3.x + 0.5 * m4.x, y: 0.5 * m3.y + 0.5 * m4.y }
|
|
111
|
+
|
|
112
|
+
// Calculate the bezier curve using two arcs
|
|
113
|
+
return `M ${fromX} ${fromY} Q ${cp1X} ${fromY}, ${midpoint.x} ${midpoint.y} Q ${cp2X} ${toY}, ${toX} ${toY}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleConnectionDelete = (connection: ConnectionPath) => {
|
|
117
|
+
if (store.deleteConnection(connection.id)) {
|
|
118
|
+
emit('connection:delete', connection)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<style scoped>
|
|
124
|
+
.gantt-connection-overlay {
|
|
125
|
+
position: absolute;
|
|
126
|
+
top: 0;
|
|
127
|
+
left: 0;
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 100%;
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
z-index: 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.connection-path {
|
|
135
|
+
transition: stroke-width 0.2s ease;
|
|
136
|
+
pointer-events: auto;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
stroke-dasharray: 5px;
|
|
139
|
+
stroke: var(--sc-cell-text-color);
|
|
140
|
+
}
|
|
141
|
+
#arrowhead-marker polygon {
|
|
142
|
+
fill: var(--sc-cell-text-color);
|
|
143
|
+
}
|
|
144
|
+
.animated-path {
|
|
145
|
+
animation: animated-dash infinite 1.5s linear;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.connection-path:hover {
|
|
149
|
+
stroke-width: 3px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.connection-hitbox {
|
|
153
|
+
pointer-events: auto;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
}
|
|
156
|
+
@keyframes animated-dash {
|
|
157
|
+
0% {
|
|
158
|
+
stroke-dashoffset: 0px;
|
|
159
|
+
}
|
|
160
|
+
100% {
|
|
161
|
+
stroke-dashoffset: -10px;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</style>
|
package/src/components/ARow.vue
CHANGED
|
@@ -1,110 +1,119 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
2
|
+
<div class="atable-container" style="position: relative">
|
|
3
|
+
<!-- Main table view -->
|
|
4
|
+
<table
|
|
5
|
+
ref="table"
|
|
6
|
+
class="atable"
|
|
7
|
+
:style="{
|
|
8
|
+
width: store.config.fullWidth ? '100%' : 'auto',
|
|
9
|
+
}"
|
|
10
|
+
v-on-click-outside="store.closeModal">
|
|
11
|
+
<slot name="header" :data="store">
|
|
12
|
+
<ATableHeader :columns="store.columns" :store="store" />
|
|
13
|
+
</slot>
|
|
14
|
+
|
|
15
|
+
<tbody>
|
|
16
|
+
<slot name="body" :data="store">
|
|
17
|
+
<ARow v-for="(row, rowIndex) in store.rows" :key="row.id" :row="row" :rowIndex="rowIndex" :store="store">
|
|
18
|
+
<template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
|
|
19
|
+
<component
|
|
20
|
+
v-if="column.isGantt"
|
|
21
|
+
:is="column.ganttComponent || 'AGanttCell'"
|
|
22
|
+
:store="store"
|
|
23
|
+
:columnsCount="store.columns.length - pinnedColumnCount"
|
|
24
|
+
:color="row.gantt?.color"
|
|
25
|
+
:start="row.gantt?.startIndex"
|
|
26
|
+
:end="row.gantt?.endIndex"
|
|
27
|
+
:colspan="column.colspan"
|
|
28
|
+
:pinned="column.pinned"
|
|
29
|
+
:rowIndex="rowIndex"
|
|
30
|
+
:colIndex="column.originalIndex ?? colIndex"
|
|
31
|
+
:style="{
|
|
32
|
+
textAlign: column?.align || 'center',
|
|
33
|
+
minWidth: column?.width || '40ch',
|
|
34
|
+
width: store.config.fullWidth ? 'auto' : null,
|
|
35
|
+
}"
|
|
36
|
+
@connection:create="handleConnectionCreate" />
|
|
37
|
+
<component
|
|
38
|
+
v-else
|
|
39
|
+
:is="column.cellComponent || 'ACell'"
|
|
40
|
+
:store="store"
|
|
41
|
+
:pinned="column.pinned"
|
|
42
|
+
:rowIndex="rowIndex"
|
|
43
|
+
:colIndex="colIndex"
|
|
44
|
+
:style="{
|
|
45
|
+
textAlign: column?.align || 'center',
|
|
46
|
+
width: store.config.fullWidth ? 'auto' : null,
|
|
47
|
+
}"
|
|
48
|
+
spellcheck="false" />
|
|
49
|
+
</template>
|
|
50
|
+
</ARow>
|
|
51
|
+
</slot>
|
|
52
|
+
</tbody>
|
|
53
|
+
|
|
54
|
+
<slot name="footer" :data="store" />
|
|
55
|
+
|
|
56
|
+
<!-- Modal overlay -->
|
|
57
|
+
<slot name="modal" :data="store">
|
|
58
|
+
<ATableModal v-show="store.modal.visible" :store="store">
|
|
59
|
+
<template #default>
|
|
34
60
|
<component
|
|
35
|
-
|
|
36
|
-
:is="
|
|
61
|
+
:key="`${store.modal.rowIndex}:${store.modal.colIndex}`"
|
|
62
|
+
:is="store.modal.component"
|
|
63
|
+
:colIndex="store.modal.colIndex"
|
|
64
|
+
:rowIndex="store.modal.rowIndex"
|
|
37
65
|
:store="store"
|
|
38
|
-
|
|
39
|
-
:rowIndex="rowIndex"
|
|
40
|
-
:colIndex="colIndex"
|
|
41
|
-
:style="{
|
|
42
|
-
textAlign: column?.align || 'center',
|
|
43
|
-
width: store.config.fullWidth ? 'auto' : null,
|
|
44
|
-
}"
|
|
45
|
-
spellcheck="false" />
|
|
66
|
+
v-bind="store.modal.componentProps" />
|
|
46
67
|
</template>
|
|
47
|
-
</
|
|
68
|
+
</ATableModal>
|
|
48
69
|
</slot>
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
<template #default>
|
|
55
|
-
<component
|
|
56
|
-
:key="`${store.modal.rowIndex}:${store.modal.colIndex}`"
|
|
57
|
-
:is="store.modal.component"
|
|
58
|
-
:colIndex="store.modal.colIndex"
|
|
59
|
-
:rowIndex="store.modal.rowIndex"
|
|
60
|
-
:store="store"
|
|
61
|
-
v-bind="store.modal.componentProps" />
|
|
62
|
-
</template>
|
|
63
|
-
</ATableModal>
|
|
64
|
-
</slot>
|
|
65
|
-
</table>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
<!-- Connection overlay for gantt connections -->
|
|
73
|
+
<AGanttConnection v-if="store.isGanttView" :store="store" @connection:delete="handleConnectionDelete" />
|
|
74
|
+
</div>
|
|
66
75
|
</template>
|
|
67
76
|
|
|
68
77
|
<script setup lang="ts">
|
|
69
78
|
import { vOnClickOutside } from '@vueuse/components'
|
|
70
79
|
import { useMutationObserver } from '@vueuse/core'
|
|
71
|
-
import {
|
|
80
|
+
import { computed, nextTick, onMounted, useTemplateRef, watch } from 'vue'
|
|
72
81
|
|
|
82
|
+
import AGanttConnection from './AGanttConnection.vue'
|
|
73
83
|
import ARow from './ARow.vue'
|
|
74
84
|
import ATableHeader from './ATableHeader.vue'
|
|
75
85
|
import ATableModal from './ATableModal.vue'
|
|
76
86
|
import { createTableStore } from '../stores/table'
|
|
77
|
-
import type { GanttDragEvent, TableColumn, TableConfig, TableRow } from '../types'
|
|
87
|
+
import type { ConnectionEvent, ConnectionPath, GanttDragEvent, TableColumn, TableConfig, TableRow } from '../types'
|
|
88
|
+
|
|
89
|
+
const modelValue = defineModel<TableRow[]>({ required: true })
|
|
78
90
|
|
|
79
91
|
const {
|
|
80
92
|
id,
|
|
81
|
-
modelValue,
|
|
82
93
|
columns,
|
|
83
|
-
rows = [],
|
|
84
94
|
config = new Object(),
|
|
85
95
|
} = defineProps<{
|
|
86
96
|
id?: string
|
|
87
|
-
modelValue: TableRow[]
|
|
88
97
|
columns: TableColumn[]
|
|
89
|
-
rows?: TableRow[]
|
|
90
98
|
config?: TableConfig
|
|
91
99
|
}>()
|
|
92
100
|
|
|
93
101
|
const emit = defineEmits<{
|
|
94
|
-
'update:modelValue': [value: TableRow[]]
|
|
95
102
|
cellUpdate: [{ colIndex: number; rowIndex: number; newValue: any; oldValue: any }]
|
|
96
103
|
'gantt:drag': [event: GanttDragEvent]
|
|
104
|
+
'connection:event': [event: ConnectionEvent]
|
|
97
105
|
}>()
|
|
98
106
|
|
|
99
107
|
const tableRef = useTemplateRef<HTMLTableElement>('table')
|
|
100
|
-
const
|
|
101
|
-
const store = createTableStore({ columns, rows: rowsValue, id, config })
|
|
108
|
+
const store = createTableStore({ columns, rows: modelValue.value, id, config })
|
|
102
109
|
|
|
103
110
|
store.$onAction(({ name, store, args, after }) => {
|
|
104
111
|
if (name === 'setCellData' || name === 'setCellText') {
|
|
105
112
|
const [colIndex, rowIndex, newValue] = args
|
|
106
113
|
const oldValue = store.getCellData(colIndex, rowIndex)
|
|
107
114
|
after(() => {
|
|
115
|
+
// Update modelValue to trigger update:modelValue event
|
|
116
|
+
modelValue.value = [...store.rows]
|
|
108
117
|
emit('cellUpdate', { colIndex, rowIndex, newValue, oldValue })
|
|
109
118
|
})
|
|
110
119
|
} else if (name === 'updateGanttBar') {
|
|
@@ -124,10 +133,14 @@ store.$onAction(({ name, store, args, after }) => {
|
|
|
124
133
|
}
|
|
125
134
|
})
|
|
126
135
|
|
|
136
|
+
// Watch for external changes to modelValue and sync to store
|
|
127
137
|
watch(
|
|
128
|
-
() =>
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
() => modelValue.value,
|
|
139
|
+
newRows => {
|
|
140
|
+
// Only update if the rows have actually changed (avoid infinite loops)
|
|
141
|
+
if (JSON.stringify(newRows) !== JSON.stringify(store.rows)) {
|
|
142
|
+
store.rows = [...newRows]
|
|
143
|
+
}
|
|
131
144
|
},
|
|
132
145
|
{ deep: true }
|
|
133
146
|
)
|
|
@@ -136,8 +149,8 @@ onMounted(() => {
|
|
|
136
149
|
if (columns.some(column => column.pinned)) {
|
|
137
150
|
assignStickyCellWidths()
|
|
138
151
|
|
|
139
|
-
// in tree
|
|
140
|
-
if (store.
|
|
152
|
+
// in tree views, also add a mutation observer to capture and adjust expanded rows
|
|
153
|
+
if (store.isTreeView) {
|
|
141
154
|
useMutationObserver(tableRef, assignStickyCellWidths, { childList: true, subtree: true })
|
|
142
155
|
}
|
|
143
156
|
}
|
|
@@ -199,8 +212,7 @@ window.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
|
199
212
|
})
|
|
200
213
|
|
|
201
214
|
const getProcessedColumnsForRow = (row: TableRow) => {
|
|
202
|
-
|
|
203
|
-
if (!isGanttRow || pinnedColumnCount.value === 0) {
|
|
215
|
+
if (!row.gantt || pinnedColumnCount.value === 0) {
|
|
204
216
|
return store.columns
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -224,10 +236,28 @@ const getProcessedColumnsForRow = (row: TableRow) => {
|
|
|
224
236
|
return result
|
|
225
237
|
}
|
|
226
238
|
|
|
227
|
-
|
|
239
|
+
const handleConnectionCreate = (connection: ConnectionPath) => {
|
|
240
|
+
emit('connection:event', { type: 'create', connection })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const handleConnectionDelete = (connection: ConnectionPath) => {
|
|
244
|
+
emit('connection:event', { type: 'delete', connection })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
defineExpose({
|
|
248
|
+
store,
|
|
249
|
+
createConnection: store.createConnection,
|
|
250
|
+
deleteConnection: store.deleteConnection,
|
|
251
|
+
getConnectionsForBar: store.getConnectionsForBar,
|
|
252
|
+
getHandlesForBar: store.getHandlesForBar,
|
|
253
|
+
})
|
|
228
254
|
</script>
|
|
229
255
|
|
|
230
256
|
<style>
|
|
257
|
+
.atable-container {
|
|
258
|
+
position: relative;
|
|
259
|
+
}
|
|
260
|
+
|
|
231
261
|
.sticky-index {
|
|
232
262
|
position: sticky;
|
|
233
263
|
left: 0px;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
id="header-index"
|
|
7
7
|
:class="[
|
|
8
8
|
store.hasPinnedColumns ? 'sticky-index' : '',
|
|
9
|
-
store.
|
|
9
|
+
store.isTreeView ? 'tree-index' : '',
|
|
10
10
|
store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
11
11
|
]"
|
|
12
12
|
class="list-index" />
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ import ATableModal from './components/ATableModal.vue'
|
|
|
12
12
|
export { createTableStore } from './stores/table'
|
|
13
13
|
export type {
|
|
14
14
|
CellContext,
|
|
15
|
+
ConnectionEvent,
|
|
16
|
+
ConnectionHandle,
|
|
17
|
+
ConnectionPath,
|
|
18
|
+
GanttBarInfo,
|
|
15
19
|
GanttDragEvent,
|
|
16
20
|
GanttOptions,
|
|
17
21
|
TableColumn,
|