@bagelink/vue 1.0.16 → 1.0.22

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