@bagelink/vue 1.10.1 → 1.10.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/components/Spreadsheet/Index.vue.d.ts +3 -22
- package/dist/components/Spreadsheet/Index.vue.d.ts.map +1 -1
- package/dist/components/Spreadsheet/SpreadsheetCell.vue.d.ts +23 -0
- package/dist/components/Spreadsheet/SpreadsheetCell.vue.d.ts.map +1 -0
- package/dist/components/Spreadsheet/SpreadsheetTable.vue.d.ts +2 -15
- package/dist/components/Spreadsheet/SpreadsheetTable.vue.d.ts.map +1 -1
- package/dist/components/Spreadsheet/composables/useSpreadsheetClipboard.d.ts +7 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetClipboard.d.ts.map +1 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetColumns.d.ts +14 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetColumns.d.ts.map +1 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetSelection.d.ts +24 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetSelection.d.ts.map +1 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetSort.d.ts +8 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetSort.d.ts.map +1 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetUndo.d.ts +72 -0
- package/dist/components/Spreadsheet/composables/useSpreadsheetUndo.d.ts.map +1 -0
- package/dist/components/Spreadsheet/types.d.ts +36 -0
- package/dist/components/Spreadsheet/types.d.ts.map +1 -0
- package/dist/components/Spreadsheet/utils.d.ts +6 -0
- package/dist/components/Spreadsheet/utils.d.ts.map +1 -0
- package/dist/index.cjs +90 -90
- package/dist/index.mjs +10165 -10102
- package/dist/style.css +1 -1
- package/package.json +2 -2
- package/src/components/Spreadsheet/Index.vue +127 -619
- package/src/components/Spreadsheet/SpreadsheetCell.vue +56 -0
- package/src/components/Spreadsheet/SpreadsheetTable.vue +79 -77
- package/src/components/Spreadsheet/composables/useSpreadsheetClipboard.ts +71 -0
- package/src/components/Spreadsheet/composables/useSpreadsheetColumns.ts +110 -0
- package/src/components/Spreadsheet/composables/useSpreadsheetSelection.ts +43 -0
- package/src/components/Spreadsheet/composables/useSpreadsheetSort.ts +42 -0
- package/src/components/Spreadsheet/composables/useSpreadsheetUndo.ts +55 -0
- package/src/components/Spreadsheet/types.ts +35 -0
- package/src/components/Spreadsheet/utils.ts +69 -0
|
@@ -1,243 +1,85 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import type { SpreadsheetProps, CellPosition } from './types'
|
|
2
3
|
import { Btn, CheckInput, TextInput, ToggleInput, useLocalStore, ListItem, Dropdown, useI18n } from '@bagelink/vue'
|
|
3
|
-
import { computed, ref, watch, nextTick
|
|
4
|
+
import { computed, ref, watch, nextTick } from 'vue'
|
|
5
|
+
import { useSpreadsheetClipboard } from './composables/useSpreadsheetClipboard'
|
|
6
|
+
import { useSpreadsheetColumns } from './composables/useSpreadsheetColumns'
|
|
7
|
+
import { useSpreadsheetSelection } from './composables/useSpreadsheetSelection'
|
|
8
|
+
import { useSpreadsheetSort } from './composables/useSpreadsheetSort'
|
|
9
|
+
import { useSpreadsheetUndo } from './composables/useSpreadsheetUndo'
|
|
4
10
|
import SpreadsheetTable from './SpreadsheetTable.vue'
|
|
11
|
+
import { flattenObject, unflattenObject, parseValueForFormat } from './utils'
|
|
5
12
|
|
|
6
|
-
const { modelValue, columnConfig, label, allowAddRow = true } = defineProps<
|
|
13
|
+
const { modelValue, columnConfig, label, allowAddRow = true } = defineProps<SpreadsheetProps>()
|
|
7
14
|
|
|
8
15
|
const emit = defineEmits(['update:modelValue'])
|
|
9
|
-
|
|
10
16
|
const { $t } = useI18n()
|
|
17
|
+
const { wrapText } = useLocalStore()
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
interface ColumnConfig {
|
|
16
|
-
key: string
|
|
17
|
-
label?: string
|
|
18
|
-
locked?: boolean
|
|
19
|
-
format?: ColumnFormat
|
|
20
|
-
sortable?: boolean
|
|
21
|
-
width?: string
|
|
22
|
-
fixed?: boolean
|
|
23
|
-
hidden?: boolean
|
|
24
|
-
defaultValue?: any
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Define props interface with column configuration
|
|
28
|
-
interface Props {
|
|
29
|
-
modelValue: Array<{ [key: string]: any }>
|
|
30
|
-
columnConfig?: ColumnConfig[]
|
|
31
|
-
label?: string
|
|
32
|
-
allowAddRow?: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Helper function to flatten an object with dot notation
|
|
36
|
-
function flattenObject(obj: { [key: string]: any }, prefix = ''): { [key: string]: any } {
|
|
37
|
-
const flattened: { [key: string]: any } = {}
|
|
38
|
-
|
|
39
|
-
for (const key in obj) {
|
|
40
|
-
if (Object.hasOwn(obj, key)) {
|
|
41
|
-
const value = obj[key]
|
|
42
|
-
const newKey = prefix ? `${prefix}.${key}` : key
|
|
43
|
-
|
|
44
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
45
|
-
Object.assign(flattened, flattenObject(value, newKey))
|
|
46
|
-
} else {
|
|
47
|
-
flattened[newKey] = value
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return flattened
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Helper function to unflatten an object from dot notation
|
|
56
|
-
function unflattenObject(obj: { [key: string]: any }): { [key: string]: any } {
|
|
57
|
-
const result: { [key: string]: any } = {}
|
|
58
|
-
|
|
59
|
-
for (const key in obj) {
|
|
60
|
-
if (Object.hasOwn(obj, key)) {
|
|
61
|
-
const keys = key.split('.')
|
|
62
|
-
let current = result
|
|
63
|
-
|
|
64
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
65
|
-
const k = keys[i]
|
|
66
|
-
if (!current[k]) {
|
|
67
|
-
current[k] = {}
|
|
68
|
-
}
|
|
69
|
-
current = current[k]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
current[keys[keys.length - 1]] = obj[key]
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return result
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Create local copy of the modelValue with flattened objects
|
|
80
|
-
const localRows = ref<Array<{ [key: string]: any }>>(
|
|
81
|
-
modelValue
|
|
82
|
-
? modelValue.map(row => flattenObject({ ...row }))
|
|
83
|
-
: []
|
|
19
|
+
const localRows = ref(
|
|
20
|
+
modelValue ? modelValue.map(row => flattenObject({ ...row })) : []
|
|
84
21
|
)
|
|
85
22
|
|
|
86
|
-
// Update the watch handler to handle flattening
|
|
87
23
|
watch(() => modelValue, (newVal) => {
|
|
88
|
-
localRows.value = newVal
|
|
89
|
-
? newVal.map(row => flattenObject({ ...row }))
|
|
90
|
-
: []
|
|
24
|
+
localRows.value = newVal ? newVal.map(row => flattenObject({ ...row })) : []
|
|
91
25
|
})
|
|
92
26
|
|
|
93
|
-
// Update the emit to unflatten the data
|
|
94
27
|
function emitUpdate() {
|
|
95
28
|
emit('update:modelValue', localRows.value.map(row => unflattenObject({ ...row })))
|
|
96
29
|
}
|
|
97
30
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Merge configured columns with data keys
|
|
118
|
-
const allKeys = new Set([...dataKeys, ...(columnConfig?.map(c => c.key) || [])])
|
|
119
|
-
|
|
120
|
-
return Array.from(allKeys).map((key) => {
|
|
121
|
-
const configuredColumn = configMap.get(key)
|
|
122
|
-
return {
|
|
123
|
-
key,
|
|
124
|
-
label: configuredColumn?.label ?? key,
|
|
125
|
-
locked: configuredColumn?.locked ?? false,
|
|
126
|
-
sortable: configuredColumn?.sortable ?? true,
|
|
127
|
-
format: configuredColumn?.format ?? 'text',
|
|
128
|
-
width: configuredColumn?.width,
|
|
129
|
-
fixed: configuredColumn?.fixed ?? false,
|
|
130
|
-
hidden: configuredColumn?.hidden ?? false,
|
|
131
|
-
defaultValue: configuredColumn?.defaultValue
|
|
132
|
-
} as ColumnConfig
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Format cell value based on column configuration
|
|
137
|
-
function formatCellValue(value: any, format?: ColumnFormat): string {
|
|
138
|
-
if (value === null || value === undefined) { return '' }
|
|
139
|
-
|
|
140
|
-
switch (format) {
|
|
141
|
-
case 'image':
|
|
142
|
-
return String(value) // Return raw value for images
|
|
143
|
-
case 'boolean':
|
|
144
|
-
return '' // Return empty string since we're using CheckInput component
|
|
145
|
-
case 'number':
|
|
146
|
-
return Number(value).toLocaleString()
|
|
147
|
-
case 'currency':
|
|
148
|
-
return Number(value).toLocaleString(undefined, {
|
|
149
|
-
style: 'currency',
|
|
150
|
-
currency: 'USD'
|
|
151
|
-
})
|
|
152
|
-
case 'date':
|
|
153
|
-
return value ? new Date(value).toLocaleDateString() : ''
|
|
154
|
-
case 'percentage':
|
|
155
|
-
return Number(value).toLocaleString(undefined, {
|
|
156
|
-
style: 'percent',
|
|
157
|
-
minimumFractionDigits: 2
|
|
158
|
-
})
|
|
159
|
-
default:
|
|
160
|
-
return String(value)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Update the parseValueForFormat function to handle type conversions properly
|
|
165
|
-
function parseValueForFormat(value: string | boolean, format?: ColumnFormat): any {
|
|
166
|
-
switch (format) {
|
|
167
|
-
case 'boolean':
|
|
168
|
-
return typeof value === 'boolean' ? value : value === 'true'
|
|
169
|
-
case 'number':
|
|
170
|
-
case 'percentage':
|
|
171
|
-
case 'currency':
|
|
172
|
-
if (typeof value === 'boolean') { return null }
|
|
173
|
-
return value === '' ? null : Number(String(value).replace(/[^0-9.-]/g, ''))
|
|
174
|
-
case 'date':
|
|
175
|
-
if (typeof value === 'boolean') { return null }
|
|
176
|
-
return value === '' ? null : new Date(String(value)).toISOString()
|
|
177
|
-
default:
|
|
178
|
-
return String(value)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Check if a cell is editable
|
|
183
|
-
function isCellEditable(columnKey: string): boolean {
|
|
184
|
-
const column = columns.value.find(col => col.key === columnKey)
|
|
185
|
-
return !(column?.locked ?? false)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Update the sortByColumn function to preserve column visibility
|
|
189
|
-
function sortByColumn(columnKey: string) {
|
|
190
|
-
const column = columns.value.find(col => col.key === columnKey)
|
|
191
|
-
if (!column?.sortable) { return }
|
|
192
|
-
|
|
193
|
-
if (sortColumn.value === columnKey) {
|
|
194
|
-
sortDirection.value = sortDirection.value === 'desc' ? 'asc' : 'desc'
|
|
195
|
-
} else {
|
|
196
|
-
sortColumn.value = columnKey
|
|
197
|
-
sortDirection.value = 'desc'
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const sorted = [...localRows.value].sort((a, b) => {
|
|
201
|
-
const aVal = a[columnKey]
|
|
202
|
-
const bVal = b[columnKey]
|
|
203
|
-
|
|
204
|
-
if (aVal === bVal) { return 0 }
|
|
205
|
-
if (aVal === null || aVal === undefined) { return 1 }
|
|
206
|
-
if (bVal === null || bVal === undefined) { return -1 }
|
|
207
|
-
|
|
208
|
-
const modifier = sortDirection.value === 'desc' ? 1 : -1
|
|
209
|
-
return aVal < bVal ? -modifier : modifier
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// Save the current visibleColumns state
|
|
213
|
-
const currentVisibleColumns = [...visibleColumns.value]
|
|
31
|
+
const {
|
|
32
|
+
columns,
|
|
33
|
+
fixedColumns,
|
|
34
|
+
scrollableColumns,
|
|
35
|
+
columnOptions,
|
|
36
|
+
visibleColumns,
|
|
37
|
+
columnWidths,
|
|
38
|
+
isCellEditable,
|
|
39
|
+
createEmptyRow,
|
|
40
|
+
handleResizeStart,
|
|
41
|
+
} = useSpreadsheetColumns(localRows, () => columnConfig)
|
|
42
|
+
|
|
43
|
+
const { sortColumn, sortDirection, sortByColumn } = useSpreadsheetSort(
|
|
44
|
+
localRows,
|
|
45
|
+
columns,
|
|
46
|
+
visibleColumns,
|
|
47
|
+
emitUpdate
|
|
48
|
+
)
|
|
214
49
|
|
|
215
|
-
|
|
216
|
-
|
|
50
|
+
const {
|
|
51
|
+
selectionStart,
|
|
52
|
+
selectionEnd,
|
|
53
|
+
handleMouseDown,
|
|
54
|
+
handleMouseOver,
|
|
55
|
+
handleMouseUp,
|
|
56
|
+
selectEntireRow,
|
|
57
|
+
} = useSpreadsheetSelection(columns)
|
|
58
|
+
|
|
59
|
+
const { canUndo, canRedo, saveState, undo, redo } = useSpreadsheetUndo(
|
|
60
|
+
localRows,
|
|
61
|
+
selectionStart,
|
|
62
|
+
selectionEnd,
|
|
63
|
+
emitUpdate
|
|
64
|
+
)
|
|
217
65
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
66
|
+
const { copySelection, pasteSelection } = useSpreadsheetClipboard(
|
|
67
|
+
localRows,
|
|
68
|
+
columns,
|
|
69
|
+
selectionStart,
|
|
70
|
+
selectionEnd,
|
|
71
|
+
saveState,
|
|
72
|
+
isCellEditable,
|
|
73
|
+
createEmptyRow,
|
|
74
|
+
emitUpdate
|
|
75
|
+
)
|
|
223
76
|
|
|
224
|
-
//
|
|
225
|
-
interface CellPosition {
|
|
226
|
-
row: number
|
|
227
|
-
col: number
|
|
228
|
-
}
|
|
229
|
-
const isSelecting = ref(false)
|
|
230
|
-
const selectionStart = ref<CellPosition | null>(null)
|
|
231
|
-
const selectionEnd = ref<CellPosition | null>(null)
|
|
232
|
-
const { wrapText } = useLocalStore()
|
|
77
|
+
// ─── Editing ─────────────────────────────────────────────────────────────────
|
|
233
78
|
|
|
234
|
-
// Reactive variable to track the currently editing cell
|
|
235
79
|
const editingCell = ref<CellPosition | null>(null)
|
|
236
|
-
|
|
237
|
-
// Update the editInputRef to use a Map
|
|
80
|
+
const editingOriginalValue = ref<string | null>(null)
|
|
238
81
|
const editInputRef = ref(new Map<string, HTMLInputElement>())
|
|
239
82
|
|
|
240
|
-
// Update the ref handling functions
|
|
241
83
|
function setInputRef(el: any, key: string) {
|
|
242
84
|
if (el?.$el instanceof HTMLInputElement) {
|
|
243
85
|
editInputRef.value.set(key, el.$el)
|
|
@@ -248,265 +90,9 @@ function setInputRef(el: any, key: string) {
|
|
|
248
90
|
}
|
|
249
91
|
}
|
|
250
92
|
|
|
251
|
-
// // Determines if the given cell is within the currently selected range
|
|
252
|
-
// function isCellSelected(row: number, col: number): boolean {
|
|
253
|
-
// if (!selectionStart.value || !selectionEnd.value) return false
|
|
254
|
-
// const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
|
|
255
|
-
// const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
|
|
256
|
-
// const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
|
|
257
|
-
// const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
|
|
258
|
-
// return row >= startRow && row <= endRow && col >= startCol && col <= endCol
|
|
259
|
-
// }
|
|
260
|
-
|
|
261
|
-
// Mouse event handlers to manage the selection range
|
|
262
|
-
function handleMouseDown(row: number, col: number) {
|
|
263
|
-
selectionStart.value = { row, col }
|
|
264
|
-
selectionEnd.value = { row, col }
|
|
265
|
-
isSelecting.value = true
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function handleMouseOver(row: number, col: number) {
|
|
269
|
-
if (isSelecting.value && selectionStart.value) {
|
|
270
|
-
selectionEnd.value = { row, col }
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function handleMouseUp() {
|
|
275
|
-
isSelecting.value = false
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Add types and state for undo/redo functionality
|
|
279
|
-
interface SpreadsheetChange {
|
|
280
|
-
type: 'cell' | 'row' | 'paste'
|
|
281
|
-
data: {
|
|
282
|
-
rows: Array<{ [key: string]: any }>
|
|
283
|
-
selection?: { start: CellPosition, end: CellPosition }
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const undoStack = ref<SpreadsheetChange[]>([])
|
|
288
|
-
const redoStack = ref<SpreadsheetChange[]>([])
|
|
289
|
-
|
|
290
|
-
// Function to save state before making changes
|
|
291
|
-
function saveState(type: SpreadsheetChange['type']) {
|
|
292
|
-
undoStack.value.push({
|
|
293
|
-
type,
|
|
294
|
-
data: {
|
|
295
|
-
rows: JSON.parse(JSON.stringify(localRows.value)),
|
|
296
|
-
selection: selectionStart.value && selectionEnd.value
|
|
297
|
-
? {
|
|
298
|
-
start: { ...selectionStart.value },
|
|
299
|
-
end: { ...selectionEnd.value }
|
|
300
|
-
}
|
|
301
|
-
: undefined
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
// Clear redo stack when new changes are made
|
|
305
|
-
redoStack.value = []
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Undo/Redo functions
|
|
309
|
-
function undo() {
|
|
310
|
-
const change = undoStack.value.pop()
|
|
311
|
-
if (change) {
|
|
312
|
-
// Save current state to redo stack
|
|
313
|
-
redoStack.value.push({
|
|
314
|
-
type: change.type,
|
|
315
|
-
data: {
|
|
316
|
-
rows: JSON.parse(JSON.stringify(localRows.value)),
|
|
317
|
-
selection: selectionStart.value && selectionEnd.value
|
|
318
|
-
? {
|
|
319
|
-
start: { ...selectionStart.value },
|
|
320
|
-
end: { ...selectionEnd.value }
|
|
321
|
-
}
|
|
322
|
-
: undefined
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
// Restore previous state
|
|
327
|
-
localRows.value = JSON.parse(JSON.stringify(change.data.rows))
|
|
328
|
-
if (change.data.selection) {
|
|
329
|
-
selectionStart.value = { ...change.data.selection.start }
|
|
330
|
-
selectionEnd.value = { ...change.data.selection.end }
|
|
331
|
-
}
|
|
332
|
-
emitUpdate()
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function redo() {
|
|
337
|
-
const change = redoStack.value.pop()
|
|
338
|
-
if (change) {
|
|
339
|
-
// Save current state to undo stack
|
|
340
|
-
undoStack.value.push({
|
|
341
|
-
type: change.type,
|
|
342
|
-
data: {
|
|
343
|
-
rows: JSON.parse(JSON.stringify(localRows.value)),
|
|
344
|
-
selection: selectionStart.value && selectionEnd.value
|
|
345
|
-
? {
|
|
346
|
-
start: { ...selectionStart.value },
|
|
347
|
-
end: { ...selectionEnd.value }
|
|
348
|
-
}
|
|
349
|
-
: undefined
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
// Restore next state
|
|
354
|
-
localRows.value = JSON.parse(JSON.stringify(change.data.rows))
|
|
355
|
-
if (change.data.selection) {
|
|
356
|
-
selectionStart.value = { ...change.data.selection.start }
|
|
357
|
-
selectionEnd.value = { ...change.data.selection.end }
|
|
358
|
-
}
|
|
359
|
-
emitUpdate()
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Update updateCell to use undo stack
|
|
364
|
-
function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
|
|
365
|
-
const column = columns.value.find(col => col.key === key)
|
|
366
|
-
if (column?.locked) { return }
|
|
367
|
-
|
|
368
|
-
saveState('cell')
|
|
369
|
-
const parsedValue = parseValueForFormat(newValue, column?.format)
|
|
370
|
-
// If the parsed value is null/undefined, use the default value
|
|
371
|
-
localRows.value[rowIndex][key] = parsedValue ?? column?.defaultValue ?? (column?.format === 'boolean' ? false : '')
|
|
372
|
-
emitUpdate()
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// After other ref declarations but before computed properties
|
|
376
|
-
// const visibleColumns = ref<string[]>([])
|
|
377
|
-
|
|
378
|
-
// After the columns computed property
|
|
379
|
-
// Initialize visibleColumns with all columns when columns change
|
|
380
|
-
watch(() => columns.value, (newColumns) => {
|
|
381
|
-
if (newColumns.length > 0 && visibleColumns.value.length === 0) {
|
|
382
|
-
visibleColumns.value = newColumns.filter(col => !col.hidden).map(col => col.key)
|
|
383
|
-
}
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
// Update the fixedColumns computed property
|
|
387
|
-
const fixedColumns = computed(() => {
|
|
388
|
-
return columns.value.filter(col => col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
// Update the scrollableColumns computed property
|
|
392
|
-
const scrollableColumns = computed(() => {
|
|
393
|
-
return columns.value.filter(col => !col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
// Update createEmptyRow to use defaultValue from column config
|
|
397
|
-
function createEmptyRow(): { [key: string]: any } {
|
|
398
|
-
const newRow: { [key: string]: any } = {}
|
|
399
|
-
columns.value.forEach((col) => {
|
|
400
|
-
// Use defaultValue if provided, otherwise use format-specific defaults
|
|
401
|
-
if (col.defaultValue !== undefined) {
|
|
402
|
-
newRow[col.key] = col.defaultValue
|
|
403
|
-
} else {
|
|
404
|
-
newRow[col.key] = col.format === 'boolean' ? false : ''
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
return newRow
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Update addRow to use the createEmptyRow function
|
|
411
|
-
function addRow() {
|
|
412
|
-
if (columns.value.length) {
|
|
413
|
-
saveState('row')
|
|
414
|
-
localRows.value.push(createEmptyRow())
|
|
415
|
-
emitUpdate()
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Copy function using Navigator Clipboard API
|
|
420
|
-
async function copySelection() {
|
|
421
|
-
if (!selectionStart.value || !selectionEnd.value) { return }
|
|
422
|
-
|
|
423
|
-
const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
|
|
424
|
-
const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
|
|
425
|
-
const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
|
|
426
|
-
const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
|
|
427
|
-
|
|
428
|
-
const selectedData = []
|
|
429
|
-
for (let i = startRow; i <= endRow; i++) {
|
|
430
|
-
const rowData = []
|
|
431
|
-
for (let j = startCol; j <= endCol; j++) {
|
|
432
|
-
const columnKey = columns.value[j].key
|
|
433
|
-
const value = localRows.value[i][columnKey]
|
|
434
|
-
rowData.push(formatCellValue(value, columns.value[j].format))
|
|
435
|
-
}
|
|
436
|
-
selectedData.push(rowData)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Convert to TSV format for clipboard
|
|
440
|
-
const tsvContent = selectedData.map(row => row.join('\t')).join('\n')
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
await navigator.clipboard.writeText(tsvContent)
|
|
444
|
-
} catch (err) {
|
|
445
|
-
console.error('Failed to copy to clipboard:', err)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Add a ref for the search input
|
|
450
|
-
const searchInputRef = ref<any>(null)
|
|
451
|
-
|
|
452
|
-
// Add a method to check if the search input is focused
|
|
453
|
-
function isSearchFocused(): boolean {
|
|
454
|
-
const inputElement = searchInputRef.value?.$el?.querySelector('input')
|
|
455
|
-
return document.activeElement === inputElement
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Update the pasteSelection function to handle search focus
|
|
459
|
-
async function pasteSelection() {
|
|
460
|
-
if (!selectionStart.value) { return }
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
const clipboardText = await navigator.clipboard.readText()
|
|
464
|
-
const rows = clipboardText.split('\n').map(row => row.split('\t'))
|
|
465
|
-
|
|
466
|
-
saveState('paste')
|
|
467
|
-
|
|
468
|
-
const startRow = selectionStart.value.row
|
|
469
|
-
const startCol = selectionStart.value.col
|
|
470
|
-
|
|
471
|
-
// Calculate how many new rows we need to add
|
|
472
|
-
const neededRows = startRow + rows.length - localRows.value.length
|
|
473
|
-
if (neededRows > 0) {
|
|
474
|
-
// Add the required number of new rows
|
|
475
|
-
for (let i = 0; i < neededRows; i++) {
|
|
476
|
-
localRows.value.push(createEmptyRow())
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Update the data
|
|
481
|
-
rows.forEach((rowData, rowIndex) => {
|
|
482
|
-
const targetRow = startRow + rowIndex
|
|
483
|
-
rowData.forEach((cellValue, colIndex) => {
|
|
484
|
-
const targetCol = startCol + colIndex
|
|
485
|
-
if (targetCol >= columns.value.length) { return }
|
|
486
|
-
|
|
487
|
-
const columnKey = columns.value[targetCol].key
|
|
488
|
-
if (!isCellEditable(columnKey)) { return }
|
|
489
|
-
|
|
490
|
-
const { format } = columns.value[targetCol]
|
|
491
|
-
localRows.value[targetRow][columnKey] = parseValueForFormat(cellValue, format)
|
|
492
|
-
})
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
emitUpdate()
|
|
496
|
-
} catch (err) {
|
|
497
|
-
console.error('Failed to paste from clipboard:', err)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Add a variable to track the original value before editing
|
|
502
|
-
const editingOriginalValue = ref<string | null>(null)
|
|
503
|
-
|
|
504
|
-
// Update the startEditing function to handle focus properly
|
|
505
93
|
function startEditing(row: number, col: number, initialKey?: string) {
|
|
506
94
|
const columnKey = columns.value[col]?.key
|
|
507
95
|
if (!columnKey) { return }
|
|
508
|
-
|
|
509
|
-
// Only check editability when we're actually going to edit
|
|
510
96
|
if (initialKey !== undefined && !isCellEditable(columnKey)) { return }
|
|
511
97
|
|
|
512
98
|
editingCell.value = { row, col }
|
|
@@ -516,183 +102,99 @@ function startEditing(row: number, col: number, initialKey?: string) {
|
|
|
516
102
|
updateCell(row, columnKey, initialKey)
|
|
517
103
|
}
|
|
518
104
|
|
|
519
|
-
// Focus the input on the next tick after Vue has updated the DOM
|
|
520
105
|
nextTick(() => {
|
|
521
|
-
const
|
|
522
|
-
const input = editInputRef.value.get(inputKey)
|
|
106
|
+
const input = editInputRef.value.get(`cell-${row}-${col}`)
|
|
523
107
|
if (input) {
|
|
524
108
|
input.focus()
|
|
525
|
-
|
|
526
|
-
if (initialKey !== undefined) {
|
|
527
|
-
input.value = initialKey
|
|
528
|
-
}
|
|
109
|
+
if (initialKey !== undefined) { input.value = initialKey }
|
|
529
110
|
}
|
|
530
111
|
})
|
|
531
112
|
}
|
|
532
113
|
|
|
533
|
-
// Update the stopEditing function to handle cancellation
|
|
534
114
|
function stopEditing(cancelled = false) {
|
|
535
115
|
if (cancelled && editingCell.value && editingOriginalValue.value !== null) {
|
|
536
116
|
const { row, col } = editingCell.value
|
|
537
117
|
const columnKey = columns.value[col]?.key
|
|
538
|
-
if (columnKey) {
|
|
539
|
-
localRows.value[row][columnKey] = editingOriginalValue.value
|
|
540
|
-
}
|
|
118
|
+
if (columnKey) { localRows.value[row][columnKey] = editingOriginalValue.value }
|
|
541
119
|
}
|
|
542
120
|
editingCell.value = null
|
|
543
121
|
editingOriginalValue.value = null
|
|
544
122
|
}
|
|
545
123
|
|
|
546
|
-
// Update the handleStopEditingAndBlur function
|
|
547
124
|
function handleStopEditingAndBlur(cancelled = false) {
|
|
548
125
|
nextTick(() => {
|
|
549
126
|
stopEditing(cancelled)
|
|
550
127
|
nextTick(() => {
|
|
551
|
-
|
|
552
|
-
const spreadsheet = document.querySelector('.spreadsheet') as HTMLElement
|
|
553
|
-
if (spreadsheet) {
|
|
554
|
-
spreadsheet.focus()
|
|
555
|
-
}
|
|
128
|
+
(document.querySelector('.spreadsheet') as HTMLElement)?.focus()
|
|
556
129
|
})
|
|
557
130
|
})
|
|
558
131
|
}
|
|
559
132
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
133
|
+
function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
|
|
134
|
+
const column = columns.value.find(col => col.key === key)
|
|
135
|
+
if (column?.locked) { return }
|
|
136
|
+
saveState('cell')
|
|
137
|
+
const parsed = parseValueForFormat(newValue, column?.format)
|
|
138
|
+
localRows.value[rowIndex][key] = parsed ?? column?.defaultValue ?? (column?.format === 'boolean' ? false : '')
|
|
139
|
+
emitUpdate()
|
|
566
140
|
}
|
|
567
141
|
|
|
568
|
-
//
|
|
569
|
-
function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
|
|
570
|
-
// If this cell is not already in edit mode
|
|
571
|
-
if (!(editingCell.value && editingCell.value.row === row && editingCell.value.col === col)) {
|
|
572
|
-
// Start editing if a printable character or Enter is pressed
|
|
573
|
-
if ((event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'Enter') {
|
|
574
|
-
event.preventDefault()
|
|
575
|
-
startEditing(row, col, event.key.length === 1 ? event.key : undefined)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
142
|
+
// ─── Search ───────────────────────────────────────────────────────────────────
|
|
579
143
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
// Don't intercept keyboard shortcuts when editing a cell
|
|
583
|
-
if (editingCell.value || isSearchFocused()) { return }
|
|
584
|
-
console.log('handleSpreadsheetKeyDown', event)
|
|
585
|
-
const isCtrlOrCmd = event.ctrlKey || event.metaKey
|
|
144
|
+
const search = ref('')
|
|
145
|
+
const searchInputRef = ref<any>(null)
|
|
586
146
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
event.preventDefault()
|
|
591
|
-
copySelection()
|
|
592
|
-
break
|
|
593
|
-
case 'v':
|
|
594
|
-
event.preventDefault()
|
|
595
|
-
pasteSelection()
|
|
596
|
-
break
|
|
597
|
-
case 'z':
|
|
598
|
-
event.preventDefault()
|
|
599
|
-
if (event.shiftKey) {
|
|
600
|
-
redo()
|
|
601
|
-
} else {
|
|
602
|
-
undo()
|
|
603
|
-
}
|
|
604
|
-
break
|
|
605
|
-
case 'y':
|
|
606
|
-
event.preventDefault()
|
|
607
|
-
redo()
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
}
|
|
147
|
+
function isSearchFocused(): boolean {
|
|
148
|
+
const input = searchInputRef.value?.$el?.querySelector('input')
|
|
149
|
+
return document.activeElement === input
|
|
611
150
|
}
|
|
612
151
|
|
|
613
|
-
// Add computed properties for undo/redo stack state
|
|
614
|
-
const canUndo = computed(() => undoStack.value.length > 0)
|
|
615
|
-
const canRedo = computed(() => redoStack.value.length > 0)
|
|
616
|
-
|
|
617
|
-
// Add after other ref declarations
|
|
618
|
-
const search = ref('')
|
|
619
|
-
|
|
620
|
-
// Add the filteredRows computed property after the columns computed
|
|
621
152
|
const filteredRows = computed(() => {
|
|
622
153
|
if (!search.value) { return localRows.value }
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// Check all values in the row, including hidden columns
|
|
627
|
-
return Object.values(row).some((value) => {
|
|
628
|
-
if (value === null || value === undefined) { return false }
|
|
629
|
-
return String(value).toLowerCase().includes(searchTerm)
|
|
630
|
-
})
|
|
631
|
-
})
|
|
154
|
+
const term = search.value.toLowerCase()
|
|
155
|
+
return localRows.value.filter(row => Object.values(row).some(v => v != null && String(v).toLowerCase().includes(term))
|
|
156
|
+
)
|
|
632
157
|
})
|
|
633
158
|
|
|
634
|
-
//
|
|
635
|
-
const columnWidths = ref<Map<string, number>>(new Map())
|
|
636
|
-
const isResizing = ref(false)
|
|
637
|
-
const resizingColumn = ref<string | null>(null)
|
|
638
|
-
const startX = ref<number>(0)
|
|
639
|
-
const startWidth = ref<number>(0)
|
|
640
|
-
|
|
641
|
-
// Add after other function declarations
|
|
642
|
-
function handleResizeStart(e: MouseEvent, columnKey: string) {
|
|
643
|
-
isResizing.value = true
|
|
644
|
-
resizingColumn.value = columnKey
|
|
645
|
-
startX.value = e.pageX
|
|
646
|
-
|
|
647
|
-
// Find the column header element
|
|
648
|
-
const columnHeader = (e.target as HTMLElement).closest('th')
|
|
649
|
-
if (!columnHeader) { return }
|
|
650
|
-
|
|
651
|
-
// Get the actual computed width of the column
|
|
652
|
-
const computedWidth = columnHeader.getBoundingClientRect().width
|
|
653
|
-
const currentWidth = columnWidths.value.get(columnKey) || computedWidth
|
|
654
|
-
startWidth.value = currentWidth
|
|
655
|
-
|
|
656
|
-
// Add event listeners for mousemove and mouseup
|
|
657
|
-
document.addEventListener('mousemove', handleResizeMove)
|
|
658
|
-
document.addEventListener('mouseup', handleResizeEnd)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function handleResizeMove(e: MouseEvent) {
|
|
662
|
-
if (!isResizing.value || !resizingColumn.value) { return }
|
|
663
|
-
|
|
664
|
-
e.preventDefault()
|
|
159
|
+
// ─── Row management ───────────────────────────────────────────────────────────
|
|
665
160
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
161
|
+
function addRow() {
|
|
162
|
+
if (!columns.value.length) { return }
|
|
163
|
+
saveState('row')
|
|
164
|
+
localRows.value.push(createEmptyRow())
|
|
165
|
+
emitUpdate()
|
|
670
166
|
}
|
|
671
167
|
|
|
672
|
-
|
|
673
|
-
isResizing.value = false
|
|
674
|
-
resizingColumn.value = null
|
|
168
|
+
// ─── Keyboard ────────────────────────────────────────────────────────────────
|
|
675
169
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
170
|
+
function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
|
|
171
|
+
const isThisCell = editingCell.value?.row === row && editingCell.value?.col === col
|
|
172
|
+
if (!isThisCell && ((event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'Enter')) {
|
|
173
|
+
event.preventDefault()
|
|
174
|
+
startEditing(row, col, event.key.length === 1 ? event.key : undefined)
|
|
175
|
+
}
|
|
679
176
|
}
|
|
680
177
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
})
|
|
178
|
+
function handleSpreadsheetKeyDown(event: KeyboardEvent) {
|
|
179
|
+
if (editingCell.value || isSearchFocused()) { return }
|
|
180
|
+
const isCtrlOrCmd = event.ctrlKey || event.metaKey
|
|
181
|
+
if (!isCtrlOrCmd) { return }
|
|
686
182
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
183
|
+
switch (event.key.toLowerCase()) {
|
|
184
|
+
case 'c': event.preventDefault(); copySelection(); break
|
|
185
|
+
case 'v': event.preventDefault(); pasteSelection(); break
|
|
186
|
+
case 'z':
|
|
187
|
+
event.preventDefault()
|
|
188
|
+
event.shiftKey ? redo() : undo()
|
|
189
|
+
break
|
|
190
|
+
case 'y': event.preventDefault(); redo(); break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
691
193
|
</script>
|
|
692
194
|
|
|
693
195
|
<template>
|
|
694
196
|
<div class="w-100p" tabindex="-1" @keydown="handleSpreadsheetKeyDown">
|
|
695
|
-
<div class="flex gap-05 py-05 justify-content-end m_flex-wrap">
|
|
197
|
+
<div class="flex gap-05 py-05 justify-content-end m_flex-wrap spreadsheet-toolbar">
|
|
696
198
|
<label v-if="label" class="label me-auto">{{ label }}</label>
|
|
697
199
|
<div class="flex gap-075">
|
|
698
200
|
<TextInput
|
|
@@ -701,7 +203,7 @@ const columnOptions = computed(() => {
|
|
|
701
203
|
/>
|
|
702
204
|
<Dropdown flat thin icon="more_vert">
|
|
703
205
|
<ListItem title="Paste" icon="paste" @click="pasteSelection" />
|
|
704
|
-
<ListItem title="
|
|
206
|
+
<ListItem title="Copy" icon="copy" @click="copySelection" />
|
|
705
207
|
<ListItem title="Undo" icon="undo" :disabled="!canUndo" @click="undo" />
|
|
706
208
|
<ListItem title="Redo" icon="redo" :disabled="!canRedo" @click="redo" />
|
|
707
209
|
<ToggleInput v-model="wrapText" :label="$t('spreadsheet.wrapText')" />
|
|
@@ -719,7 +221,7 @@ const columnOptions = computed(() => {
|
|
|
719
221
|
</div>
|
|
720
222
|
<CheckInput
|
|
721
223
|
v-for="col in columnOptions" :key="col.key" v-model="visibleColumns"
|
|
722
|
-
:value="col.key" :label="col.label || col.key"
|
|
224
|
+
:value="col.key" :label="col.label || col.key"
|
|
723
225
|
/>
|
|
724
226
|
</div>
|
|
725
227
|
</Dropdown>
|
|
@@ -728,28 +230,29 @@ const columnOptions = computed(() => {
|
|
|
728
230
|
</div>
|
|
729
231
|
<div class="spreadsheet" @mouseup="handleMouseUp">
|
|
730
232
|
<div class="flex w-100p relative">
|
|
731
|
-
<!-- Fixed Columns -->
|
|
732
233
|
<SpreadsheetTable
|
|
733
234
|
v-if="fixedColumns.length" :columns="fixedColumns" :rows="filteredRows"
|
|
734
|
-
:is-fixed="true" :show-row-numbers="true" :column-widths="columnWidths"
|
|
735
|
-
:sort-
|
|
235
|
+
:is-fixed="true" :show-row-numbers="true" :column-widths="columnWidths"
|
|
236
|
+
:sort-column="sortColumn" :sort-direction="sortDirection"
|
|
237
|
+
:selection-start="selectionStart" :selection-end="selectionEnd"
|
|
736
238
|
:editing-cell="editingCell" :base-column-index="0" :wrap-text="wrapText"
|
|
737
|
-
class="sticky z-2 start-0 bg-white"
|
|
239
|
+
class="sticky z-2 start-0 bg-white"
|
|
240
|
+
@sortColumn="sortByColumn" @resizeStart="handleResizeStart"
|
|
738
241
|
@selectRow="selectEntireRow" @cellMouseDown="handleMouseDown" @cellMouseOver="handleMouseOver"
|
|
739
242
|
@cellEditStart="startEditing" @cellKeyDown="handleCellKeyDown" @updateCell="updateCell"
|
|
740
243
|
@stopEditing="handleStopEditingAndBlur" @setInputRef="setInputRef"
|
|
741
244
|
/>
|
|
742
|
-
|
|
743
|
-
<!-- Scrollable Columns -->
|
|
744
245
|
<div class="flex-shrink flex-grow">
|
|
745
246
|
<SpreadsheetTable
|
|
746
|
-
:columns="scrollableColumns" :rows="filteredRows"
|
|
747
|
-
:show-row-numbers="!fixedColumns.length" :column-widths="columnWidths"
|
|
748
|
-
:
|
|
247
|
+
:columns="scrollableColumns" :rows="filteredRows"
|
|
248
|
+
:is-fixed="false" :show-row-numbers="!fixedColumns.length" :column-widths="columnWidths"
|
|
249
|
+
:empty-message="search ? `No results for '${search}'` : undefined"
|
|
250
|
+
:sort-column="sortColumn" :sort-direction="sortDirection"
|
|
251
|
+
:selection-start="selectionStart" :selection-end="selectionEnd"
|
|
749
252
|
:editing-cell="editingCell" :base-column-index="fixedColumns.length" :wrap-text="wrapText"
|
|
750
|
-
@sortColumn="sortByColumn" @resizeStart="handleResizeStart"
|
|
751
|
-
@
|
|
752
|
-
@cellKeyDown="handleCellKeyDown" @updateCell="updateCell"
|
|
253
|
+
@sortColumn="sortByColumn" @resizeStart="handleResizeStart"
|
|
254
|
+
@selectRow="selectEntireRow" @cellMouseDown="handleMouseDown" @cellMouseOver="handleMouseOver"
|
|
255
|
+
@cellEditStart="startEditing" @cellKeyDown="handleCellKeyDown" @updateCell="updateCell"
|
|
753
256
|
@stopEditing="handleStopEditingAndBlur" @setInputRef="setInputRef"
|
|
754
257
|
/>
|
|
755
258
|
</div>
|
|
@@ -760,12 +263,17 @@ const columnOptions = computed(() => {
|
|
|
760
263
|
</template>
|
|
761
264
|
|
|
762
265
|
<style>
|
|
763
|
-
/* Spreadsheet container styles */
|
|
764
266
|
.spreadsheet {
|
|
765
267
|
user-select: none;
|
|
766
268
|
outline: 1px solid var(--border-color);
|
|
767
269
|
}
|
|
768
270
|
|
|
271
|
+
.spreadsheet-toolbar {
|
|
272
|
+
border-bottom: 1px solid var(--border-color);
|
|
273
|
+
padding-bottom: 0.375rem;
|
|
274
|
+
margin-bottom: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
769
277
|
body>div:has(.spreadsheet) ::-webkit-scrollbar-track {
|
|
770
278
|
background: var(--input-bg) !important;
|
|
771
279
|
}
|