@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.
- package/dist/components/Spreadsheet/Index.vue.d.ts +89 -1
- package/dist/components/Spreadsheet/Index.vue.d.ts.map +1 -1
- package/dist/components/Spreadsheet/SpreadsheetTable.vue.d.ts +37 -0
- package/dist/components/Spreadsheet/SpreadsheetTable.vue.d.ts.map +1 -0
- package/dist/composables/useSchemaField.d.ts.map +1 -1
- package/dist/index.cjs +1070 -940
- package/dist/index.mjs +1070 -940
- package/dist/style.css +45 -19
- package/dist/utils/BagelFormUtils.d.ts +11 -0
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Spreadsheet/Index.vue +176 -257
- package/src/components/Spreadsheet/SpreadsheetTable.vue +316 -0
- package/src/composables/useSchemaField.ts +3 -1
- package/src/utils/BagelFormUtils.ts +30 -0
|
@@ -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 {
|