@bagelink/vue 0.0.1220 → 0.0.1227

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.
Files changed (80) hide show
  1. package/dist/components/BglComponent.vue.d.ts.map +1 -1
  2. package/dist/components/Btn.vue.d.ts.map +1 -1
  3. package/dist/components/Calendar/Index.vue.d.ts +8 -4
  4. package/dist/components/Calendar/Index.vue.d.ts.map +1 -1
  5. package/dist/components/Calendar/components/header/Header.vue.d.ts +2 -0
  6. package/dist/components/Calendar/components/header/Header.vue.d.ts.map +1 -1
  7. package/dist/components/Calendar/components/month/AgendaEventTile.vue.d.ts.map +1 -1
  8. package/dist/components/Calendar/components/month/AgendaEvents.vue.d.ts.map +1 -1
  9. package/dist/components/Calendar/components/month/Day.vue.d.ts.map +1 -1
  10. package/dist/components/Calendar/components/month/Event.vue.d.ts.map +1 -1
  11. package/dist/components/Calendar/components/month/Month.vue.d.ts +2 -2
  12. package/dist/components/Calendar/components/month/Month.vue.d.ts.map +1 -1
  13. package/dist/components/Calendar/components/month/WeekDay.vue.d.ts.map +1 -1
  14. package/dist/components/Calendar/components/partials/EventFlyout.vue.d.ts.map +1 -1
  15. package/dist/components/Calendar/components/week/Day.vue.d.ts.map +1 -1
  16. package/dist/components/Calendar/components/week/Week.vue.d.ts +2 -2
  17. package/dist/components/Calendar/components/week/WeekTimeline.vue.d.ts.map +1 -1
  18. package/dist/components/Calendar/constants.d.ts.map +1 -1
  19. package/dist/components/Calendar/language/index.d.ts +2 -1
  20. package/dist/components/Calendar/language/index.d.ts.map +1 -1
  21. package/dist/components/Calendar/language/keys.d.ts +66 -63
  22. package/dist/components/Calendar/language/keys.d.ts.map +1 -1
  23. package/dist/components/Spreadsheet/Index.vue.d.ts +25 -0
  24. package/dist/components/Spreadsheet/Index.vue.d.ts.map +1 -0
  25. package/dist/components/form/BagelForm.vue.d.ts +2 -1
  26. package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
  27. package/dist/components/form/inputs/DatePick.vue.d.ts +1 -0
  28. package/dist/components/form/inputs/DatePick.vue.d.ts.map +1 -1
  29. package/dist/components/form/inputs/DatePicker.vue.d.ts.map +1 -1
  30. package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
  31. package/dist/components/index.d.ts +1 -0
  32. package/dist/components/index.d.ts.map +1 -1
  33. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  34. package/dist/components/lightbox/lightbox.types.d.ts +2 -1
  35. package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
  36. package/dist/composables/useSchemaField.d.ts +9 -5
  37. package/dist/composables/useSchemaField.d.ts.map +1 -1
  38. package/dist/index.cjs +1879 -1161
  39. package/dist/index.mjs +1880 -1162
  40. package/dist/style.css +733 -763
  41. package/dist/types/BagelForm.d.ts +2 -1
  42. package/dist/types/BagelForm.d.ts.map +1 -1
  43. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/Btn.vue +3 -0
  46. package/src/components/Calendar/Index.vue +13 -16
  47. package/src/components/Calendar/components/header/Header.vue +17 -139
  48. package/src/components/Calendar/components/month/AgendaEventTile.vue +1 -10
  49. package/src/components/Calendar/components/month/AgendaEvents.vue +7 -53
  50. package/src/components/Calendar/components/month/Day.vue +12 -30
  51. package/src/components/Calendar/components/month/Event.vue +10 -67
  52. package/src/components/Calendar/components/month/Month.vue +10 -56
  53. package/src/components/Calendar/components/month/WeekDay.vue +1 -11
  54. package/src/components/Calendar/components/partials/EventFlyout.vue +2 -1
  55. package/src/components/Calendar/components/week/Day.vue +4 -18
  56. package/src/components/Calendar/components/week/DayEvent.vue +1 -1
  57. package/src/components/Calendar/components/week/FullDayEvent.vue +2 -2
  58. package/src/components/Calendar/components/week/Week.vue +1 -1
  59. package/src/components/Calendar/components/week/WeekTimeline.vue +13 -38
  60. package/src/components/Calendar/constants.ts +11 -11
  61. package/src/components/Calendar/language/index.ts +6 -3
  62. package/src/components/Calendar/language/keys.ts +91 -88
  63. package/src/components/Calendar/styles/_variables.css +38 -42
  64. package/src/components/Spreadsheet/Index.vue +867 -0
  65. package/src/components/form/BagelForm.vue +7 -3
  66. package/src/components/form/inputs/DatePick.vue +6 -2
  67. package/src/components/form/inputs/DatePicker.vue +2 -2
  68. package/src/components/form/inputs/NumberInput.vue +2 -2
  69. package/src/components/index.ts +1 -0
  70. package/src/components/lightbox/Lightbox.vue +5 -5
  71. package/src/components/lightbox/lightbox.types.ts +2 -1
  72. package/src/composables/useSchemaField.ts +36 -12
  73. package/src/styles/buttons.css +81 -73
  74. package/src/styles/layout.css +25 -0
  75. package/src/styles/mobilLayout.css +25 -0
  76. package/src/styles/text.css +82 -1
  77. package/src/styles/theme.css +269 -258
  78. package/src/types/BagelForm.ts +2 -1
  79. package/src/utils/BagelFormUtils.ts +2 -1
  80. package/src/components/Calendar/index.ts +0 -4
@@ -0,0 +1,867 @@
1
+ <script lang="ts" setup>
2
+ import { Btn, Icon, CheckInput, TextInput } from '@bagelink/vue'
3
+ import { computed, ref, watch, nextTick } from 'vue'
4
+
5
+ // Define column configuration types
6
+ type ColumnFormat = 'text' | 'number' | 'currency' | 'date' | 'percentage' | 'image' | 'boolean'
7
+
8
+ interface ColumnConfig {
9
+ key: string
10
+ label?: string
11
+ locked?: boolean
12
+ format?: ColumnFormat
13
+ sortable?: boolean
14
+ width?: string
15
+ fixed?: boolean
16
+ hidden?: boolean
17
+ defaultValue?: any
18
+ }
19
+
20
+ // Define props interface with column configuration
21
+ interface Props {
22
+ modelValue: Array<Record<string, any>>
23
+ columnConfig?: ColumnConfig[]
24
+ label?: string
25
+ allowAddRow?: boolean
26
+ }
27
+
28
+ const { modelValue, columnConfig, label, allowAddRow = true } = defineProps<Props>()
29
+ const emit = defineEmits(['update:modelValue'])
30
+
31
+ // Helper function to flatten an object with dot notation
32
+ function flattenObject(obj: Record<string, any>, prefix = ''): Record<string, any> {
33
+ const flattened: Record<string, any> = {}
34
+
35
+ for (const key in obj) {
36
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
37
+ const value = obj[key]
38
+ const newKey = prefix ? `${prefix}.${key}` : key
39
+
40
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
41
+ Object.assign(flattened, flattenObject(value, newKey))
42
+ } else {
43
+ flattened[newKey] = value
44
+ }
45
+ }
46
+ }
47
+
48
+ return flattened
49
+ }
50
+
51
+ // Helper function to unflatten an object from dot notation
52
+ function unflattenObject(obj: Record<string, any>): Record<string, any> {
53
+ const result: Record<string, any> = {}
54
+
55
+ for (const key in obj) {
56
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
57
+ const keys = key.split('.')
58
+ let current = result
59
+
60
+ for (let i = 0; i < keys.length - 1; i++) {
61
+ const k = keys[i]
62
+ if (!current[k]) {
63
+ current[k] = {}
64
+ }
65
+ current = current[k]
66
+ }
67
+
68
+ current[keys[keys.length - 1]] = obj[key]
69
+ }
70
+ }
71
+
72
+ return result
73
+ }
74
+
75
+ // Create local copy of the modelValue with flattened objects
76
+ const localRows = ref<Array<Record<string, any>>>(
77
+ modelValue
78
+ ? modelValue.map(row => flattenObject({ ...row }))
79
+ : []
80
+ )
81
+
82
+ // Update the watch handler to handle flattening
83
+ watch(() => modelValue, (newVal) => {
84
+ localRows.value = newVal
85
+ ? newVal.map(row => flattenObject({ ...row }))
86
+ : []
87
+ })
88
+
89
+ // Update the emit to unflatten the data
90
+ function emitUpdate() {
91
+ emit('update:modelValue', localRows.value.map(row => unflattenObject({ ...row })))
92
+ }
93
+
94
+ // Sort state
95
+ const sortColumn = ref<string | null>(null)
96
+ const sortDirection = ref<'asc' | 'desc'>('asc')
97
+
98
+ // Update the columns computed property
99
+ const columns = computed(() => {
100
+ // Get all unique keys from the data
101
+ const dataKeys = localRows.value.length
102
+ ? new Set(localRows.value.flatMap(row => Object.keys(row)))
103
+ : new Set<string>()
104
+
105
+ // Create a map of configured columns
106
+ const configMap = new Map(
107
+ (columnConfig || []).map(config => [config.key, config])
108
+ )
109
+
110
+ // Merge configured columns with data keys
111
+ const allKeys = new Set([...dataKeys, ...(columnConfig?.map(c => c.key) || [])])
112
+
113
+ return Array.from(allKeys).map((key) => {
114
+ const configuredColumn = configMap.get(key)
115
+ return {
116
+ key,
117
+ label: configuredColumn?.label ?? key,
118
+ locked: configuredColumn?.locked ?? false,
119
+ sortable: configuredColumn?.sortable ?? true,
120
+ format: configuredColumn?.format ?? 'text',
121
+ width: configuredColumn?.width,
122
+ fixed: configuredColumn?.fixed ?? false,
123
+ hidden: configuredColumn?.hidden ?? false,
124
+ defaultValue: configuredColumn?.defaultValue
125
+ } as ColumnConfig
126
+ })
127
+ })
128
+
129
+ // Format cell value based on column configuration
130
+ function formatCellValue(value: any, format?: ColumnFormat): string {
131
+ if (value === null || value === undefined) return ''
132
+
133
+ switch (format) {
134
+ case 'image':
135
+ return String(value) // Return raw value for images
136
+ case 'boolean':
137
+ return '' // Return empty string since we're using CheckInput component
138
+ case 'number':
139
+ return Number(value).toLocaleString()
140
+ case 'currency':
141
+ return Number(value).toLocaleString(undefined, {
142
+ style: 'currency',
143
+ currency: 'USD'
144
+ })
145
+ case 'date':
146
+ return value ? new Date(value).toLocaleDateString() : ''
147
+ case 'percentage':
148
+ return Number(value).toLocaleString(undefined, {
149
+ style: 'percent',
150
+ minimumFractionDigits: 2
151
+ })
152
+ default:
153
+ return String(value)
154
+ }
155
+ }
156
+
157
+ // Update the parseValueForFormat function to handle type conversions properly
158
+ function parseValueForFormat(value: string | boolean, format?: ColumnFormat): any {
159
+ switch (format) {
160
+ case 'boolean':
161
+ return typeof value === 'boolean' ? value : value === 'true'
162
+ case 'number':
163
+ case 'percentage':
164
+ case 'currency':
165
+ if (typeof value === 'boolean') return null
166
+ return value === '' ? null : Number(String(value).replace(/[^0-9.-]/g, ''))
167
+ case 'date':
168
+ if (typeof value === 'boolean') return null
169
+ return value === '' ? null : new Date(String(value)).toISOString()
170
+ default:
171
+ return String(value)
172
+ }
173
+ }
174
+
175
+ // Check if a cell is editable
176
+ function isCellEditable(columnKey: string): boolean {
177
+ const column = columns.value.find(col => col.key === columnKey)
178
+ return !(column?.locked ?? false)
179
+ }
180
+
181
+ // Sort rows by column
182
+ function sortByColumn(columnKey: string) {
183
+ const column = columns.value.find(col => col.key === columnKey)
184
+ if (!column?.sortable) return
185
+
186
+ if (sortColumn.value === columnKey) {
187
+ sortDirection.value = sortDirection.value === 'desc' ? 'asc' : 'desc'
188
+ } else {
189
+ sortColumn.value = columnKey
190
+ sortDirection.value = 'desc'
191
+ }
192
+
193
+ const sorted = [...localRows.value].sort((a, b) => {
194
+ const aVal = a[columnKey]
195
+ const bVal = b[columnKey]
196
+
197
+ if (aVal === bVal) return 0
198
+ if (aVal === null || aVal === undefined) return 1
199
+ if (bVal === null || bVal === undefined) return -1
200
+
201
+ const modifier = sortDirection.value === 'desc' ? 1 : -1
202
+ return aVal < bVal ? -modifier : modifier
203
+ })
204
+
205
+ localRows.value = sorted
206
+ emitUpdate()
207
+ }
208
+
209
+ // Variables to handle cell selection
210
+ interface CellPosition {
211
+ row: number
212
+ col: number
213
+ }
214
+ const isSelecting = ref(false)
215
+ const selectionStart = ref<CellPosition | null>(null)
216
+ const selectionEnd = ref<CellPosition | null>(null)
217
+
218
+ // Variable to hold copied cell data
219
+ const copiedData = ref<string[][]>([])
220
+
221
+ // Reactive variable to track the currently editing cell
222
+ const editingCell = ref<CellPosition | null>(null)
223
+
224
+ // Update the editInputRef to use a Map
225
+ const editInputRef = ref(new Map<string, HTMLInputElement>())
226
+
227
+ // Update the ref handling functions
228
+ function setInputRef(el: any, key: string) {
229
+ if (el?.$el instanceof HTMLInputElement) {
230
+ editInputRef.value.set(key, el.$el)
231
+ } else if (el instanceof HTMLInputElement) {
232
+ editInputRef.value.set(key, el)
233
+ } else {
234
+ editInputRef.value.delete(key)
235
+ }
236
+ }
237
+
238
+ // Determines if the given cell is within the currently selected range
239
+ function isCellSelected(row: number, col: number): boolean {
240
+ if (!selectionStart.value || !selectionEnd.value) return false
241
+ const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
242
+ const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
243
+ const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
244
+ const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
245
+ return row >= startRow && row <= endRow && col >= startCol && col <= endCol
246
+ }
247
+
248
+ // Mouse event handlers to manage the selection range
249
+ function handleMouseDown(row: number, col: number) {
250
+ selectionStart.value = { row, col }
251
+ selectionEnd.value = { row, col }
252
+ isSelecting.value = true
253
+ }
254
+
255
+ function handleMouseOver(row: number, col: number) {
256
+ if (isSelecting.value && selectionStart.value) {
257
+ selectionEnd.value = { row, col }
258
+ }
259
+ }
260
+
261
+ function handleMouseUp() {
262
+ isSelecting.value = false
263
+ }
264
+
265
+ // Add types and state for undo/redo functionality
266
+ interface SpreadsheetChange {
267
+ type: 'cell' | 'row' | 'paste'
268
+ data: {
269
+ rows: Array<Record<string, any>>
270
+ selection?: { start: CellPosition, end: CellPosition }
271
+ }
272
+ }
273
+
274
+ const undoStack = ref<SpreadsheetChange[]>([])
275
+ const redoStack = ref<SpreadsheetChange[]>([])
276
+
277
+ // Function to save state before making changes
278
+ function saveState(type: SpreadsheetChange['type']) {
279
+ undoStack.value.push({
280
+ type,
281
+ data: {
282
+ rows: JSON.parse(JSON.stringify(localRows.value)),
283
+ selection: selectionStart.value && selectionEnd.value
284
+ ? {
285
+ start: { ...selectionStart.value },
286
+ end: { ...selectionEnd.value }
287
+ }
288
+ : undefined
289
+ }
290
+ })
291
+ // Clear redo stack when new changes are made
292
+ redoStack.value = []
293
+ }
294
+
295
+ // Undo/Redo functions
296
+ function undo() {
297
+ const change = undoStack.value.pop()
298
+ if (change) {
299
+ // Save current state to redo stack
300
+ redoStack.value.push({
301
+ type: change.type,
302
+ data: {
303
+ rows: JSON.parse(JSON.stringify(localRows.value)),
304
+ selection: selectionStart.value && selectionEnd.value
305
+ ? {
306
+ start: { ...selectionStart.value },
307
+ end: { ...selectionEnd.value }
308
+ }
309
+ : undefined
310
+ }
311
+ })
312
+
313
+ // Restore previous state
314
+ localRows.value = JSON.parse(JSON.stringify(change.data.rows))
315
+ if (change.data.selection) {
316
+ selectionStart.value = { ...change.data.selection.start }
317
+ selectionEnd.value = { ...change.data.selection.end }
318
+ }
319
+ emitUpdate()
320
+ }
321
+ }
322
+
323
+ function redo() {
324
+ const change = redoStack.value.pop()
325
+ if (change) {
326
+ // Save current state to undo stack
327
+ undoStack.value.push({
328
+ type: change.type,
329
+ data: {
330
+ rows: JSON.parse(JSON.stringify(localRows.value)),
331
+ selection: selectionStart.value && selectionEnd.value
332
+ ? {
333
+ start: { ...selectionStart.value },
334
+ end: { ...selectionEnd.value }
335
+ }
336
+ : undefined
337
+ }
338
+ })
339
+
340
+ // Restore next state
341
+ localRows.value = JSON.parse(JSON.stringify(change.data.rows))
342
+ if (change.data.selection) {
343
+ selectionStart.value = { ...change.data.selection.start }
344
+ selectionEnd.value = { ...change.data.selection.end }
345
+ }
346
+ emitUpdate()
347
+ }
348
+ }
349
+
350
+ // Update updateCell to use undo stack
351
+ function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
352
+ const column = columns.value.find(col => col.key === key)
353
+ if (column?.locked) return
354
+
355
+ saveState('cell')
356
+ const parsedValue = parseValueForFormat(newValue, column?.format)
357
+ // If the parsed value is null/undefined, use the default value
358
+ localRows.value[rowIndex][key] = parsedValue ?? column?.defaultValue ?? (column?.format === 'boolean' ? false : '')
359
+ emitUpdate()
360
+ }
361
+
362
+ // Update the fixed and scrollable columns computed properties to filter out hidden columns
363
+ const fixedColumns = computed(() => {
364
+ return columns.value.filter(col => col.fixed && !col.hidden)
365
+ })
366
+
367
+ const scrollableColumns = computed(() => {
368
+ return columns.value.filter(col => !col.fixed && !col.hidden)
369
+ })
370
+
371
+ // Update createEmptyRow to use defaultValue from column config
372
+ function createEmptyRow(): Record<string, any> {
373
+ const newRow: Record<string, any> = {}
374
+ columns.value.forEach((col) => {
375
+ // Use defaultValue if provided, otherwise use format-specific defaults
376
+ if (col.defaultValue !== undefined) {
377
+ newRow[col.key] = col.defaultValue
378
+ } else {
379
+ newRow[col.key] = col.format === 'boolean' ? false : ''
380
+ }
381
+ })
382
+ return newRow
383
+ }
384
+
385
+ // Update addRow to use the createEmptyRow function
386
+ function addRow() {
387
+ if (columns.value.length) {
388
+ saveState('row')
389
+ localRows.value.push(createEmptyRow())
390
+ emitUpdate()
391
+ }
392
+ }
393
+
394
+ // Copy function using Navigator Clipboard API
395
+ async function copySelection() {
396
+ if (!selectionStart.value || !selectionEnd.value) return
397
+
398
+ const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
399
+ const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
400
+ const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
401
+ const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
402
+
403
+ const selectedData = []
404
+ for (let i = startRow; i <= endRow; i++) {
405
+ const rowData = []
406
+ for (let j = startCol; j <= endCol; j++) {
407
+ const columnKey = columns.value[j].key
408
+ const value = localRows.value[i][columnKey]
409
+ rowData.push(formatCellValue(value, columns.value[j].format))
410
+ }
411
+ selectedData.push(rowData)
412
+ }
413
+
414
+ // Convert to TSV format for clipboard
415
+ const tsvContent = selectedData.map(row => row.join('\t')).join('\n')
416
+
417
+ try {
418
+ await navigator.clipboard.writeText(tsvContent)
419
+ } catch (err) {
420
+ console.error('Failed to copy to clipboard:', err)
421
+ }
422
+ }
423
+
424
+ // Paste function using Navigator Clipboard API
425
+ async function pasteSelection() {
426
+ if (!selectionStart.value) return
427
+
428
+ try {
429
+ const clipboardText = await navigator.clipboard.readText()
430
+ const rows = clipboardText.split('\n').map(row => row.split('\t'))
431
+
432
+ saveState('paste')
433
+
434
+ const startRow = selectionStart.value.row
435
+ const startCol = selectionStart.value.col
436
+
437
+ // Calculate how many new rows we need to add
438
+ const neededRows = startRow + rows.length - localRows.value.length
439
+ if (neededRows > 0) {
440
+ // Add the required number of new rows
441
+ for (let i = 0; i < neededRows; i++) {
442
+ localRows.value.push(createEmptyRow())
443
+ }
444
+ }
445
+
446
+ // Update the data
447
+ rows.forEach((rowData, rowIndex) => {
448
+ const targetRow = startRow + rowIndex
449
+ rowData.forEach((cellValue, colIndex) => {
450
+ const targetCol = startCol + colIndex
451
+ if (targetCol >= columns.value.length) return
452
+
453
+ const columnKey = columns.value[targetCol].key
454
+ if (!isCellEditable(columnKey)) return
455
+
456
+ const { format } = columns.value[targetCol]
457
+ localRows.value[targetRow][columnKey] = parseValueForFormat(cellValue, format)
458
+ })
459
+ })
460
+
461
+ emitUpdate()
462
+ } catch (err) {
463
+ console.error('Failed to paste from clipboard:', err)
464
+ }
465
+ }
466
+
467
+ // Add a variable to track the original value before editing
468
+ const editingOriginalValue = ref<string | null>(null)
469
+
470
+ // Update the startEditing function to handle focus properly
471
+ function startEditing(row: number, col: number, initialKey?: string) {
472
+ const columnKey = columns.value[col]?.key
473
+ if (!columnKey) return
474
+
475
+ // Only check editability when we're actually going to edit
476
+ if (initialKey !== undefined && !isCellEditable(columnKey)) return
477
+
478
+ editingCell.value = { row, col }
479
+ editingOriginalValue.value = localRows.value[row][columnKey]?.toString() ?? ''
480
+
481
+ if (initialKey !== undefined) {
482
+ updateCell(row, columnKey, initialKey)
483
+ }
484
+
485
+ // Focus the input on the next tick after Vue has updated the DOM
486
+ nextTick(() => {
487
+ const inputKey = `cell-${row}-${col}`
488
+ const input = editInputRef.value.get(inputKey)
489
+ if (input) {
490
+ input.focus()
491
+ // If we have an initial key, set the selection to the end
492
+ if (initialKey !== undefined) {
493
+ input.value = initialKey
494
+ }
495
+ }
496
+ })
497
+ }
498
+
499
+ // Update the stopEditing function to handle cancellation
500
+ function stopEditing(cancelled = false) {
501
+ if (cancelled && editingCell.value && editingOriginalValue.value !== null) {
502
+ const { row, col } = editingCell.value
503
+ const columnKey = columns.value[col]?.key
504
+ if (columnKey) {
505
+ localRows.value[row][columnKey] = editingOriginalValue.value
506
+ }
507
+ }
508
+ editingCell.value = null
509
+ editingOriginalValue.value = null
510
+ }
511
+
512
+ // Update the handleStopEditingAndBlur function
513
+ function handleStopEditingAndBlur(cancelled = false) {
514
+ nextTick(() => {
515
+ stopEditing(cancelled)
516
+ nextTick(() => {
517
+ // Force focus back to the main spreadsheet container
518
+ const spreadsheet = document.querySelector('.spreadsheet') as HTMLElement
519
+ if (spreadsheet) {
520
+ spreadsheet.focus()
521
+ }
522
+ })
523
+ })
524
+ }
525
+
526
+ window.addEventListener('mouseup', handleMouseUp)
527
+
528
+ // First add a function to handle row selection
529
+ function selectEntireRow(rowIndex: number) {
530
+ selectionStart.value = { row: rowIndex, col: 0 }
531
+ selectionEnd.value = { row: rowIndex, col: columns.value.length - 1 }
532
+ }
533
+
534
+ // Add back the handleCellKeyDown function
535
+ function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
536
+ // If this cell is not already in edit mode
537
+ if (!(editingCell.value && editingCell.value.row === row && editingCell.value.col === col)) {
538
+ // Start editing if a printable character or Enter is pressed
539
+ if ((event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'Enter') {
540
+ event.preventDefault()
541
+ startEditing(row, col, event.key.length === 1 ? event.key : undefined)
542
+ }
543
+ }
544
+ }
545
+
546
+ // Update keyboard shortcuts to include undo/redo
547
+ function handleSpreadsheetKeyDown(event: KeyboardEvent) {
548
+ // Don't intercept keyboard shortcuts when editing a cell
549
+ if (editingCell.value) return
550
+
551
+ const isCtrlOrCmd = event.ctrlKey || event.metaKey
552
+
553
+ if (isCtrlOrCmd) {
554
+ switch (event.key.toLowerCase()) {
555
+ case 'c':
556
+ event.preventDefault()
557
+ copySelection()
558
+ break
559
+ case 'v':
560
+ event.preventDefault()
561
+ pasteSelection()
562
+ break
563
+ case 'z':
564
+ event.preventDefault()
565
+ if (event.shiftKey) {
566
+ redo()
567
+ } else {
568
+ undo()
569
+ }
570
+ break
571
+ case 'y':
572
+ event.preventDefault()
573
+ redo()
574
+ break
575
+ }
576
+ }
577
+ }
578
+
579
+ // Add computed properties for undo/redo stack state
580
+ const canUndo = computed(() => undoStack.value.length > 0)
581
+ const canRedo = computed(() => redoStack.value.length > 0)
582
+
583
+ // Add after other ref declarations
584
+ const search = ref('')
585
+
586
+ // Add the filteredRows computed property after the columns computed
587
+ const filteredRows = computed(() => {
588
+ if (!search.value) return localRows.value
589
+
590
+ const searchTerm = search.value.toLowerCase()
591
+ return localRows.value.filter((row) => {
592
+ // Check all values in the row, including hidden columns
593
+ return Object.values(row).some((value) => {
594
+ if (value === null || value === undefined) return false
595
+ return String(value).toLowerCase().includes(searchTerm)
596
+ })
597
+ })
598
+ })
599
+ </script>
600
+
601
+ <template>
602
+ <div class="w-100p overflow-hidden" tabindex="-1" @keydown="handleSpreadsheetKeyDown">
603
+ <div class="flex gap-05 py-05 justify-content-end m_flex-wrap">
604
+ <label v-if="label" class="label me-auto">{{ label }}</label>
605
+ <div class="flex gap-075">
606
+ <TextInput v-model="search" icon="search" placeholder="Search" class="m-0 max-w200px" />
607
+ <Btn v-tooltip="'Paste'" flat thin round icon="paste" @click="pasteSelection" />
608
+ <Btn v-tooltip="'copy'" flat thin round icon="copy" @click="copySelection" />
609
+ <Btn v-tooltip="'Undo'" flat thin round icon="undo" :disabled="!canUndo" @click="undo" />
610
+ <Btn v-tooltip="'Redo'" flat thin round icon="redo" :disabled="!canRedo" @click="redo" />
611
+ </div>
612
+ </div>
613
+ <div class="spreadsheet" @mouseup="handleMouseUp">
614
+ <div class="flex w-100p relative">
615
+ <!-- Fixed Columns -->
616
+ <table v-if="fixedColumns.length" class="fixed-columns sticky z-2 start-0 bg-white">
617
+ <thead>
618
+ <tr>
619
+ <th class="row-number-header bg-white" />
620
+ <th
621
+ v-for="col in fixedColumns"
622
+ :key="col.key"
623
+ >
624
+ <span @click="col.sortable && sortByColumn(col.key)">
625
+ {{ col.label || col.key }}
626
+ </span>
627
+ <Icon
628
+ v-if="sortColumn === col.key"
629
+ class="line-height-0 transition-400"
630
+ name="keyboard_arrow_down" :class="{ 'rotate-180': sortDirection === 'desc' }"
631
+ />
632
+ </th>
633
+ </tr>
634
+ </thead>
635
+ <tbody>
636
+ <tr v-for="(row, rowIndex) in filteredRows" :key="rowIndex">
637
+ <td class="row-number txt-center hover user-select-none pointer txt12 regular" @click="selectEntireRow(rowIndex)">
638
+ {{ rowIndex + 1 }}
639
+ </td>
640
+ <td
641
+ v-for="col in fixedColumns"
642
+ :key="col.key"
643
+ :class="{
644
+ selected: isCellSelected(rowIndex, fixedColumns.indexOf(col)),
645
+ locked: !isCellEditable(col.key),
646
+ }"
647
+ :style="{ width: col.width }"
648
+ :tabindex="col.hidden ? undefined : 0"
649
+ @mousedown="handleMouseDown(rowIndex, fixedColumns.indexOf(col))"
650
+ @mouseover="handleMouseOver(rowIndex, fixedColumns.indexOf(col))"
651
+ @focusin="handleMouseOver(rowIndex, fixedColumns.indexOf(col))"
652
+ @dblclick="startEditing(rowIndex, fixedColumns.indexOf(col))"
653
+ @keydown="handleCellKeyDown($event, rowIndex, fixedColumns.indexOf(col))"
654
+ >
655
+ <template v-if="editingCell && editingCell.row === rowIndex && editingCell.col === fixedColumns.indexOf(col)">
656
+ <input
657
+ :ref="el => setInputRef(el, `cell-${rowIndex}-${fixedColumns.indexOf(col)}`)"
658
+ :value="row[col.key]"
659
+ type="text"
660
+ class="spreadsheet-input"
661
+ @input="(e: Event) => updateCell(rowIndex, col.key, (e.target as HTMLInputElement).value)"
662
+ @blur="handleStopEditingAndBlur(false)"
663
+ @keydown.enter.prevent="handleStopEditingAndBlur(false)"
664
+ @keydown.esc.prevent="handleStopEditingAndBlur(true)"
665
+ @mousedown.stop
666
+ >
667
+ <span class="spreadsheet-cell spreadsheetCellPlaceHolder">{{ formatCellValue(row[col.key], col.format) }}</span>
668
+ </template>
669
+ <template v-else>
670
+ <template v-if="col.format === 'image'">
671
+ <div class="h40px w-100p flex align-items-center justify-content-center overflow-hidden">
672
+ <img class=" w-100p h-100p contain radius-05" :src="row[col.key]" :alt="col.label || col.key">
673
+ </div>
674
+ </template>
675
+ <template v-else-if="col.format === 'boolean'">
676
+ <CheckInput
677
+ :modelValue="!!row[col.key]"
678
+ :disabled="!isCellEditable(col.key)"
679
+ @update:modelValue="(value: boolean | any[] | undefined) => updateCell(rowIndex, col.key, !!value)"
680
+ @mousedown.stop
681
+ />
682
+ </template>
683
+ <template v-else>
684
+ <span class="spreadsheet-cell">{{ formatCellValue(row[col.key], col.format) }}</span>
685
+ </template>
686
+ </template>
687
+ </td>
688
+ </tr>
689
+ </tbody>
690
+ </table>
691
+
692
+ <!-- Scrollable Columns -->
693
+ <div class="flex-shrink flex-grow overflow-x">
694
+ <table class="w-100p">
695
+ <thead>
696
+ <tr>
697
+ <th v-if="!fixedColumns.length" class="row-number-header bg-white" />
698
+ <th
699
+ v-for="col in scrollableColumns"
700
+ :key="col.key"
701
+ >
702
+ <span @click="col.sortable && sortByColumn(col.key)">
703
+ {{ col.label || col.key }}
704
+ </span>
705
+
706
+ <Icon v-if="sortColumn === col.key" name="keyboard_arrow_down" class="line-height-0 inline-block" :class="{ 'rotate-180': sortColumn === col.key && sortDirection === 'desc' }" />
707
+ </th>
708
+ </tr>
709
+ </thead>
710
+ <tbody>
711
+ <tr v-for="(row, rowIndex) in filteredRows" :key="rowIndex">
712
+ <td v-if="!fixedColumns.length" class="row-number txt-center hover user-select-none pointer txt12 regular" @click="selectEntireRow(rowIndex)">
713
+ {{ rowIndex + 1 }}
714
+ </td>
715
+ <td
716
+ v-for="(col, colIndex) in scrollableColumns"
717
+ :key="col.key"
718
+ :class="{
719
+ selected: isCellSelected(rowIndex, fixedColumns.length + colIndex),
720
+ locked: !isCellEditable(col.key),
721
+ }"
722
+ :style="{ width: col.width }"
723
+ :tabindex="col.hidden ? undefined : 0"
724
+ @mousedown="handleMouseDown(rowIndex, fixedColumns.length + colIndex)"
725
+ @mouseover="handleMouseOver(rowIndex, fixedColumns.length + colIndex)"
726
+ @focusin="handleMouseOver(rowIndex, fixedColumns.length + colIndex)"
727
+ @dblclick="startEditing(rowIndex, fixedColumns.length + colIndex)"
728
+ @keydown="handleCellKeyDown($event, rowIndex, fixedColumns.length + colIndex)"
729
+ >
730
+ <template v-if="editingCell && editingCell.row === rowIndex && editingCell.col === (fixedColumns.length + colIndex)">
731
+ <input
732
+ :ref="el => setInputRef(el, `cell-${rowIndex}-${fixedColumns.length + colIndex}`)"
733
+ :value="row[col.key]"
734
+ type="text"
735
+ class="spreadsheet-input"
736
+ @input="(e: Event) => updateCell(rowIndex, col.key, (e.target as HTMLInputElement).value)"
737
+ @blur="handleStopEditingAndBlur(false)"
738
+ @keydown.enter.prevent="handleStopEditingAndBlur(false)"
739
+ @keydown.esc.prevent="handleStopEditingAndBlur(true)"
740
+ @mousedown.stop
741
+ >
742
+ <span class="spreadsheet-cell spreadsheetCellPlaceHolder">{{ formatCellValue(row[col.key], col.format) }}</span>
743
+ </template>
744
+ <template v-else>
745
+ <template v-if="col.format === 'image'">
746
+ <div v-if="row[col.key]" class="h40px w-100p flex align-items-center justify-content-center overflow-hidden">
747
+ <img class=" w-100p h-100p contain radius-05" :src="row[col.key]" :alt="col.label || col.key">
748
+ </div>
749
+ </template>
750
+ <template v-else-if="col.format === 'boolean'">
751
+ <CheckInput
752
+ :modelValue="!!row[col.key]"
753
+ :disabled="!isCellEditable(col.key)"
754
+ @update:modelValue="(value: boolean | any[] | undefined) => updateCell(rowIndex, col.key, !!value)"
755
+ @mousedown.stop
756
+ />
757
+ </template>
758
+ <template v-else>
759
+ <span class="spreadsheet-cell">{{ formatCellValue(row[col.key], col.format) }}</span>
760
+ </template>
761
+ </template>
762
+ </td>
763
+ </tr>
764
+ </tbody>
765
+ </table>
766
+ </div>
767
+ </div>
768
+ <Btn v-if="allowAddRow" outline thin round icon="add" value="Add Row" class="mt-05" @click="addRow" />
769
+ </div>
770
+ </div>
771
+ </template>
772
+
773
+ <style scoped>
774
+ .fixed-columns {
775
+ border-right: 2px solid var(--border-color);
776
+ }
777
+ .spreadsheet table {
778
+ border-collapse: collapse;
779
+ }
780
+ .spreadsheet th, .spreadsheet td {
781
+ border: 1px solid var(--border-color);
782
+ padding: 0.1rem 0.5rem;
783
+ min-width: 80px;
784
+ background: var(--bgl-white);
785
+ user-select: none;
786
+ }
787
+ .spreadsheet th {
788
+ background: var(--input-bg);
789
+ white-space: nowrap;
790
+ position: relative;
791
+ padding: 0.25rem 0.5rem;
792
+ font-weight: 500;
793
+ text-align: start;
794
+ }
795
+ .spreadsheet th .bgl_icon-font{
796
+ vertical-align: middle;
797
+ }
798
+ .spreadsheet td.selected {
799
+ background: var(--bgl-primary-light);
800
+ }
801
+ .spreadsheet td.locked {
802
+ background: var(--bgl-gray-light);
803
+ cursor: default;
804
+ }
805
+ .spreadsheet td.locked.selected {
806
+ background: var(--bgl-primary-light);
807
+ }
808
+ .spreadsheet td {
809
+ height: 40px;
810
+ vertical-align: middle;
811
+ }
812
+ .spreadsheet td:has(img){
813
+ padding: 0;
814
+ }
815
+ .spreadsheet td span{
816
+ display: block;
817
+ display: -webkit-box;
818
+ max-width: 100%;
819
+ -webkit-box-orient: vertical;
820
+ overflow: hidden;
821
+ text-overflow: ellipsis;
822
+ -webkit-line-clamp: 1;
823
+ word-break: break-all;
824
+ }
825
+ .spreadsheet input {
826
+ width: 100%;
827
+ border: none;
828
+ background: transparent;
829
+ padding: 0;
830
+ margin: 0;
831
+ min-height: 0;
832
+ min-width: 0;
833
+ }
834
+ .spreadsheet input:focus {
835
+ outline: 2px solid var(--bgl-primary);
836
+ outline-offset: 6px;
837
+ }
838
+ .spreadsheet th.sortable {
839
+ cursor: pointer;
840
+ }
841
+ .row-number-header, .row-number {
842
+ width: fit-content;
843
+ min-width: fit-content !important;
844
+ padding: 0.1rem 0.7rem !important;
845
+ }
846
+ .spreadsheet td .bgl-checkbox{
847
+ margin: 0;
848
+ text-align: center;
849
+ justify-content: center;
850
+
851
+ }
852
+ .spreadsheet td:has(.bgl-checkbox){
853
+ text-align: center;
854
+ background: var(--input-bg);
855
+ }
856
+ .spreadsheet td:has(:checked){
857
+ background: var(--bgl-primary-light);
858
+ }
859
+
860
+ .spreadsheetCellPlaceHolder{
861
+ height: 0px;
862
+ overflow: hidden;
863
+ opactiy: 0;
864
+ poiner-events: none;
865
+ user-select: none;
866
+ }
867
+ </style>