@bagelink/vue 1.0.16 → 1.0.18

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,317 @@
1
+ <script lang="ts" setup>
2
+ import { Icon, CheckInput } from '@bagelink/vue'
3
+ import { computed } from 'vue'
4
+
5
+ // Define column configuration types from parent
6
+ interface CellPosition {
7
+ row: number
8
+ col: number
9
+ }
10
+
11
+ interface ColumnConfig {
12
+ key: string
13
+ label?: string
14
+ locked?: boolean
15
+ format?: 'text' | 'number' | 'currency' | 'date' | 'percentage' | 'image' | 'boolean'
16
+ sortable?: boolean
17
+ width?: string
18
+ fixed?: boolean
19
+ hidden?: boolean
20
+ defaultValue?: any
21
+ }
22
+
23
+ interface Props {
24
+ columns: ColumnConfig[]
25
+ rows: Array<Record<string, any>>
26
+ isFixed: boolean
27
+ showRowNumbers: boolean
28
+ columnWidths: Map<string, number>
29
+ sortColumn: string | null
30
+ sortDirection: 'asc' | 'desc'
31
+ selectionStart: CellPosition | null
32
+ selectionEnd: CellPosition | null
33
+ editingCell: CellPosition | null
34
+ baseColumnIndex: number
35
+ }
36
+
37
+ const props = defineProps<Props>()
38
+
39
+ const emit = defineEmits<{
40
+ (e: 'sortColumn', key: string): void
41
+ (e: 'resizeStart', event: MouseEvent, key: string): void
42
+ (e: 'selectRow', rowIndex: number): void
43
+ (e: 'cellMouseDown', rowIndex: number, colIndex: number): void
44
+ (e: 'cellMouseOver', rowIndex: number, colIndex: number): void
45
+ (e: 'cellEditStart', rowIndex: number, colIndex: number): void
46
+ (e: 'cellKeyDown', event: KeyboardEvent, rowIndex: number, colIndex: number): void
47
+ (e: 'updateCell', rowIndex: number, key: string, value: string | boolean): void
48
+ (e: 'stopEditing', cancelled: boolean): void
49
+ (e: 'setInputRef', el: any, key: string): void
50
+ }>()
51
+
52
+ // Calculate the actual column index including the base index
53
+ const getActualColumnIndex = (localIndex: number) => props.baseColumnIndex + localIndex
54
+
55
+ // Determines if a cell is selected
56
+ function isCellSelected(row: number, localColIndex: number): boolean {
57
+ if (!props.selectionStart || !props.selectionEnd) return false
58
+
59
+ const colIndex = getActualColumnIndex(localColIndex)
60
+ const startRow = Math.min(props.selectionStart.row, props.selectionEnd.row)
61
+ const endRow = Math.max(props.selectionStart.row, props.selectionEnd.row)
62
+ const startCol = Math.min(props.selectionStart.col, props.selectionEnd.col)
63
+ const endCol = Math.max(props.selectionStart.col, props.selectionEnd.col)
64
+
65
+ return row >= startRow && row <= endRow && colIndex >= startCol && colIndex <= endCol
66
+ }
67
+
68
+ // Check if a cell is editable
69
+ function isCellEditable(columnKey: string): boolean {
70
+ const column = props.columns.find(col => col.key === columnKey)
71
+ return !(column?.locked ?? false)
72
+ }
73
+
74
+ // Check if a cell is currently being edited
75
+ function isEditing(row: number, localColIndex: number): boolean {
76
+ if (!props.editingCell) return false
77
+ const colIndex = getActualColumnIndex(localColIndex)
78
+ return props.editingCell.row === row && props.editingCell.col === colIndex
79
+ }
80
+
81
+ // Format cell value based on column configuration
82
+ function formatCellValue(value: any, format?: ColumnConfig['format']): string {
83
+ if (value === null || value === undefined) return ''
84
+
85
+ switch (format) {
86
+ case 'image':
87
+ return String(value) // Return raw value for images
88
+ case 'boolean':
89
+ return '' // Return empty string since we're using CheckInput component
90
+ case 'number':
91
+ return Number(value).toLocaleString()
92
+ case 'currency':
93
+ return Number(value).toLocaleString(undefined, {
94
+ style: 'currency',
95
+ currency: 'USD'
96
+ })
97
+ case 'date':
98
+ return value ? new Date(value).toLocaleDateString() : ''
99
+ case 'percentage':
100
+ return Number(value).toLocaleString(undefined, {
101
+ style: 'percent',
102
+ minimumFractionDigits: 2
103
+ })
104
+ default:
105
+ return String(value)
106
+ }
107
+ }
108
+ </script>
109
+
110
+ <template>
111
+ <table :class="{ 'fixed-columns': isFixed, 'w-100p': !isFixed }">
112
+ <thead>
113
+ <tr>
114
+ <th v-if="showRowNumbers" class="row-number-header bg-white" />
115
+ <th
116
+ v-for="col in columns"
117
+ :key="col.key"
118
+ :style="{ width: columnWidths.get(col.key) ? `${columnWidths.get(col.key)}px` : col.width }"
119
+ >
120
+ <div class="th-content">
121
+ <span @click="col.sortable && $emit('sortColumn', col.key)">
122
+ {{ col.label || col.key }}
123
+ </span>
124
+ <Icon
125
+ v-if="sortColumn === col.key"
126
+ class="line-height-0 transition-400"
127
+ name="keyboard_arrow_down"
128
+ :class="{ 'rotate-180': sortDirection === 'desc' }"
129
+ />
130
+ <div
131
+ class="resize-handle"
132
+ @mousedown.stop="$emit('resizeStart', $event, col.key)"
133
+ />
134
+ </div>
135
+ </th>
136
+ </tr>
137
+ </thead>
138
+ <tbody>
139
+ <tr v-for="(row, rowIndex) in rows" :key="rowIndex">
140
+ <td
141
+ v-if="showRowNumbers"
142
+ class="row-number txt-center hover user-select-none pointer txt12 regular"
143
+ @click="$emit('selectRow', rowIndex)"
144
+ >
145
+ {{ rowIndex + 1 }}
146
+ </td>
147
+ <td
148
+ v-for="(col, colIndex) in columns"
149
+ :key="col.key"
150
+ :class="{
151
+ selected: isCellSelected(rowIndex, colIndex),
152
+ locked: !isCellEditable(col.key),
153
+ }"
154
+ :style="{ width: columnWidths.get(col.key) ? `${columnWidths.get(col.key)}px` : col.width }"
155
+ :tabindex="col.hidden ? undefined : 0"
156
+ @mousedown="$emit('cellMouseDown', rowIndex, getActualColumnIndex(colIndex))"
157
+ @mouseover="$emit('cellMouseOver', rowIndex, getActualColumnIndex(colIndex))"
158
+ @focusin="$emit('cellMouseOver', rowIndex, getActualColumnIndex(colIndex))"
159
+ @dblclick="$emit('cellEditStart', rowIndex, getActualColumnIndex(colIndex))"
160
+ @keydown="$emit('cellKeyDown', $event, rowIndex, getActualColumnIndex(colIndex))"
161
+ >
162
+ <template v-if="isEditing(rowIndex, colIndex)">
163
+ <input
164
+ :ref="el => $emit('setInputRef', el, `cell-${rowIndex}-${getActualColumnIndex(colIndex)}`)"
165
+ :value="row[col.key]"
166
+ type="text"
167
+ class="spreadsheet-input"
168
+ @input="(e) => $emit('updateCell', rowIndex, col.key, (e.target as HTMLInputElement).value)"
169
+ @blur="$emit('stopEditing', false)"
170
+ @keydown.enter.prevent="$emit('stopEditing', false)"
171
+ @keydown.esc.prevent="$emit('stopEditing', true)"
172
+ @mousedown.stop
173
+ >
174
+ <span class="spreadsheet-cell spreadsheetCellPlaceHolder">{{ formatCellValue(row[col.key], col.format) }}</span>
175
+ </template>
176
+ <template v-else>
177
+ <template v-if="col.format === 'image'">
178
+ <div class="h40px w-100p flex align-items-center justify-content-center overflow-hidden">
179
+ <img class="w-100p h-100p contain radius-05" :src="row[col.key]" :alt="col.label || col.key">
180
+ </div>
181
+ </template>
182
+ <template v-else-if="col.format === 'boolean'">
183
+ <CheckInput
184
+ :modelValue="!!row[col.key]"
185
+ :disabled="!isCellEditable(col.key)"
186
+ @update:modelValue="(value) => $emit('updateCell', rowIndex, col.key, !!value)"
187
+ @mousedown.stop
188
+ />
189
+ </template>
190
+ <template v-else>
191
+ <span class="spreadsheet-cell">{{ formatCellValue(row[col.key], col.format) }}</span>
192
+ </template>
193
+ </template>
194
+ </td>
195
+ </tr>
196
+ </tbody>
197
+ </table>
198
+ </template>
199
+
200
+ <style scoped>
201
+ .fixed-columns {
202
+ border-right: 2px solid var(--border-color);
203
+ }
204
+ table {
205
+ border-collapse: collapse;
206
+ }
207
+ th, td {
208
+ border: 1px solid var(--border-color);
209
+ padding: 0.1rem 0.5rem;
210
+ min-width: 80px;
211
+ background: var(--bgl-white);
212
+ user-select: none;
213
+ }
214
+ th {
215
+ background: var(--input-bg);
216
+ white-space: nowrap;
217
+ position: relative;
218
+ padding: 0.25rem 0.5rem;
219
+ font-weight: 500;
220
+ text-align: start;
221
+ }
222
+ th .bgl_icon-font{
223
+ vertical-align: middle;
224
+ }
225
+ td.selected {
226
+ background: var(--bgl-primary-light);
227
+ }
228
+ td.locked {
229
+ background: var(--bgl-gray-light);
230
+ cursor: default;
231
+ }
232
+ td.locked.selected {
233
+ background: var(--bgl-primary-light);
234
+ }
235
+ td {
236
+ height: 40px;
237
+ vertical-align: middle;
238
+ }
239
+ td:has(img){
240
+ padding: 0;
241
+ }
242
+ td span{
243
+ display: block;
244
+ display: -webkit-box;
245
+ max-width: 100%;
246
+ -webkit-box-orient: vertical;
247
+ overflow: hidden;
248
+ text-overflow: ellipsis;
249
+ -webkit-line-clamp: 1;
250
+ word-break: break-all;
251
+ }
252
+ input {
253
+ width: 100%;
254
+ border: none;
255
+ background: transparent;
256
+ padding: 0;
257
+ margin: 0;
258
+ min-height: 0;
259
+ min-width: 0;
260
+ }
261
+ input:focus {
262
+ outline: 2px solid var(--bgl-primary);
263
+ outline-offset: 6px;
264
+ }
265
+ th.sortable {
266
+ cursor: pointer;
267
+ }
268
+ .row-number-header, .row-number {
269
+ width: fit-content;
270
+ min-width: fit-content !important;
271
+ padding: 0.1rem 0.7rem !important;
272
+ }
273
+ td .bgl-checkbox{
274
+ margin: 0;
275
+ text-align: center;
276
+ justify-content: center;
277
+ }
278
+ td:has(.bgl-checkbox){
279
+ text-align: center;
280
+ background: var(--input-bg);
281
+ }
282
+ td:has(:checked){
283
+ background: var(--bgl-primary-light);
284
+ }
285
+
286
+ .spreadsheetCellPlaceHolder{
287
+ height: 0px;
288
+ overflow: hidden;
289
+ opacity: 0;
290
+ pointer-events: none;
291
+ user-select: none;
292
+ }
293
+
294
+ .th-content {
295
+ position: relative;
296
+ display: flex;
297
+ align-items: center;
298
+ width: 100%;
299
+ height: 100%;
300
+ }
301
+
302
+ .resize-handle {
303
+ position: absolute;
304
+ right: -8px;
305
+ top: 0;
306
+ bottom: 0;
307
+ width: 4px;
308
+ z-index: 100;
309
+ cursor: col-resize;
310
+ background: transparent;
311
+ transition: background 0.2s;
312
+ }
313
+
314
+ .resize-handle:hover {
315
+ background: var(--bgl-primary);
316
+ }
317
+ </style>