@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.
@@ -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>
@@ -10,7 +10,7 @@
10
10
  {{ rowIndex + 1 }}
11
11
  </td>
12
12
  <td
13
- v-else-if="store.config.view === 'tree'"
13
+ v-else-if="store.isTreeView"
14
14
  :tabIndex="-1"
15
15
  class="tree-index"
16
16
  :class="store.hasPinnedColumns ? 'sticky-index' : ''"
@@ -1,103 +1,111 @@
1
1
  <template>
2
- <table
3
- ref="table"
4
- class="atable"
5
- :style="{
6
- width: store.config.fullWidth ? '100%' : 'auto',
7
- }"
8
- v-on-click-outside="store.closeModal">
9
- <slot name="header" :data="store">
10
- <ATableHeader :columns="store.columns" :store="store" />
11
- </slot>
12
-
13
- <tbody>
14
- <slot name="body" :data="store">
15
- <ARow v-for="(row, rowIndex) in store.rows" :key="row.id" :row="row" :rowIndex="rowIndex" :store="store">
16
- <template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
17
- <component
18
- v-if="column.isGantt"
19
- :is="column.ganttComponent || 'AGanttCell'"
20
- :store="store"
21
- :columnsCount="store.columns.length - pinnedColumnCount"
22
- :color="row.gantt?.color"
23
- :start="row.gantt?.startIndex"
24
- :end="row.gantt?.endIndex"
25
- :colspan="column.colspan"
26
- :pinned="column.pinned"
27
- :rowIndex="rowIndex"
28
- :colIndex="column.originalIndex ?? colIndex"
29
- :style="{
30
- textAlign: column?.align || 'center',
31
- minWidth: column?.width || '40ch',
32
- width: store.config.fullWidth ? 'auto' : null,
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
- v-else
36
- :is="column.cellComponent || 'ACell'"
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
- :pinned="column.pinned"
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
- </ARow>
68
+ </ATableModal>
48
69
  </slot>
49
- </tbody>
50
-
51
- <slot name="footer" :data="store" />
52
- <slot name="modal" :data="store">
53
- <ATableModal v-show="store.modal.visible" :store="store">
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, computed, watch } from 'vue'
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 rowsValue = modelValue.value.length > 0 ? modelValue.value : rows
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 view, also add a mutation observer to capture and adjust expanded rows
145
- if (store.config.view === 'tree') {
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
- const isGanttRow = row.indent === 0
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
- defineExpose({ store })
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.config.view === 'tree' ? 'tree-index' : '',
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,