@stonecrop/atable 0.4.23 → 0.4.25
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 +518 -86
- package/dist/atable.js +1340 -1044
- 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 +376 -77
- 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 +186 -41
- 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 +96 -71
- package/src/components/ATableHeader.vue +1 -1
- package/src/index.ts +4 -0
- package/src/stores/table.ts +213 -45
- 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,103 +1,111 @@
|
|
|
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 { nextTick, onMounted, useTemplateRef,
|
|
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'
|
|
78
88
|
|
|
79
89
|
const modelValue = defineModel<TableRow[]>({ required: true })
|
|
80
90
|
|
|
81
91
|
const {
|
|
82
92
|
id,
|
|
83
93
|
columns,
|
|
84
|
-
rows = [],
|
|
85
94
|
config = new Object(),
|
|
86
95
|
} = defineProps<{
|
|
87
96
|
id?: string
|
|
88
97
|
columns: TableColumn[]
|
|
89
|
-
rows?: TableRow[]
|
|
90
98
|
config?: TableConfig
|
|
91
99
|
}>()
|
|
92
100
|
|
|
93
101
|
const emit = defineEmits<{
|
|
94
102
|
cellUpdate: [{ colIndex: number; rowIndex: number; newValue: any; oldValue: any }]
|
|
95
103
|
'gantt:drag': [event: GanttDragEvent]
|
|
104
|
+
'connection:event': [event: ConnectionEvent]
|
|
96
105
|
}>()
|
|
97
106
|
|
|
98
107
|
const tableRef = useTemplateRef<HTMLTableElement>('table')
|
|
99
|
-
const
|
|
100
|
-
const store = createTableStore({ columns, rows: rowsValue, id, config })
|
|
108
|
+
const store = createTableStore({ columns, rows: modelValue.value, id, config })
|
|
101
109
|
|
|
102
110
|
store.$onAction(({ name, store, args, after }) => {
|
|
103
111
|
if (name === 'setCellData' || name === 'setCellText') {
|
|
@@ -141,8 +149,8 @@ onMounted(() => {
|
|
|
141
149
|
if (columns.some(column => column.pinned)) {
|
|
142
150
|
assignStickyCellWidths()
|
|
143
151
|
|
|
144
|
-
// in tree
|
|
145
|
-
if (store.
|
|
152
|
+
// in tree views, also add a mutation observer to capture and adjust expanded rows
|
|
153
|
+
if (store.isTreeView) {
|
|
146
154
|
useMutationObserver(tableRef, assignStickyCellWidths, { childList: true, subtree: true })
|
|
147
155
|
}
|
|
148
156
|
}
|
|
@@ -204,8 +212,7 @@ window.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
|
204
212
|
})
|
|
205
213
|
|
|
206
214
|
const getProcessedColumnsForRow = (row: TableRow) => {
|
|
207
|
-
|
|
208
|
-
if (!isGanttRow || pinnedColumnCount.value === 0) {
|
|
215
|
+
if (!row.gantt || pinnedColumnCount.value === 0) {
|
|
209
216
|
return store.columns
|
|
210
217
|
}
|
|
211
218
|
|
|
@@ -229,10 +236,28 @@ const getProcessedColumnsForRow = (row: TableRow) => {
|
|
|
229
236
|
return result
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
|
|
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
|
+
})
|
|
233
254
|
</script>
|
|
234
255
|
|
|
235
256
|
<style>
|
|
257
|
+
.atable-container {
|
|
258
|
+
position: relative;
|
|
259
|
+
}
|
|
260
|
+
|
|
236
261
|
.sticky-index {
|
|
237
262
|
position: sticky;
|
|
238
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,
|