@mikestools/usetable 0.0.1 → 0.0.2
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/README.md +586 -519
- package/dist/index.d.ts +722 -121
- package/dist/usetable.js +1263 -1078
- package/dist/usetable.umd.cjs +7 -7
- package/package.json +20 -20
- package/showcase/examples/BasicExample.vue +474 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section id="basicTable">
|
|
3
|
+
<h2 class="h4 mb-3">
|
|
4
|
+
<IconTable class="me-2" />
|
|
5
|
+
Product Inventory Dashboard
|
|
6
|
+
</h2>
|
|
7
|
+
<p class="text-muted mb-4">
|
|
8
|
+
A comprehensive example demonstrating useTable's core features: sorting, filtering, selection,
|
|
9
|
+
inline editing, aggregation, and undo/redo — all reactive and type-safe.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<!-- Toolbar -->
|
|
13
|
+
<div class="card shadow-sm mb-3">
|
|
14
|
+
<div class="card-body py-2">
|
|
15
|
+
<div class="row g-2 align-items-center">
|
|
16
|
+
<!-- Search -->
|
|
17
|
+
<div class="col-lg-3">
|
|
18
|
+
<div class="input-group input-group-sm">
|
|
19
|
+
<span class="input-group-text">
|
|
20
|
+
<IconSearch />
|
|
21
|
+
</span>
|
|
22
|
+
<input
|
|
23
|
+
v-model="searchQuery"
|
|
24
|
+
type="text"
|
|
25
|
+
class="form-control"
|
|
26
|
+
placeholder="Search products..."
|
|
27
|
+
@input="handleSearch"
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
v-if="searchQuery"
|
|
31
|
+
class="btn btn-outline-secondary"
|
|
32
|
+
type="button"
|
|
33
|
+
@click="clearSearch"
|
|
34
|
+
>
|
|
35
|
+
<IconX />
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Category Filter -->
|
|
41
|
+
<div class="col-lg-2">
|
|
42
|
+
<select
|
|
43
|
+
v-model="categoryFilter"
|
|
44
|
+
class="form-select form-select-sm"
|
|
45
|
+
@change="handleCategoryFilter"
|
|
46
|
+
>
|
|
47
|
+
<option value="">
|
|
48
|
+
All Categories
|
|
49
|
+
</option>
|
|
50
|
+
<option
|
|
51
|
+
v-for="cat in categories"
|
|
52
|
+
:key="cat"
|
|
53
|
+
:value="cat"
|
|
54
|
+
>
|
|
55
|
+
{{ cat }}
|
|
56
|
+
</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Actions -->
|
|
61
|
+
<div class="col-lg-7">
|
|
62
|
+
<div class="d-flex gap-2 justify-content-end flex-wrap">
|
|
63
|
+
<button
|
|
64
|
+
class="btn btn-primary btn-sm"
|
|
65
|
+
@click="addProduct"
|
|
66
|
+
>
|
|
67
|
+
<IconPlus class="me-1" />
|
|
68
|
+
Add Product
|
|
69
|
+
</button>
|
|
70
|
+
<button
|
|
71
|
+
class="btn btn-outline-danger btn-sm"
|
|
72
|
+
:disabled="selectedCount === 0"
|
|
73
|
+
@click="deleteSelected"
|
|
74
|
+
>
|
|
75
|
+
<IconTrash class="me-1" />
|
|
76
|
+
Delete ({{ selectedCount }})
|
|
77
|
+
</button>
|
|
78
|
+
<div class="btn-group">
|
|
79
|
+
<button
|
|
80
|
+
class="btn btn-outline-secondary btn-sm"
|
|
81
|
+
:disabled="!canUndo"
|
|
82
|
+
title="Undo (Ctrl+Z)"
|
|
83
|
+
@click="handleUndo"
|
|
84
|
+
>
|
|
85
|
+
<IconArrowCounterclockwise />
|
|
86
|
+
</button>
|
|
87
|
+
<button
|
|
88
|
+
class="btn btn-outline-secondary btn-sm"
|
|
89
|
+
:disabled="!canRedo"
|
|
90
|
+
title="Redo (Ctrl+Y)"
|
|
91
|
+
@click="handleRedo"
|
|
92
|
+
>
|
|
93
|
+
<IconArrowClockwise />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
<button
|
|
97
|
+
class="btn btn-outline-secondary btn-sm"
|
|
98
|
+
@click="resetTable"
|
|
99
|
+
>
|
|
100
|
+
<IconArrowRepeat class="me-1" />
|
|
101
|
+
Reset
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Table -->
|
|
110
|
+
<div class="card shadow-sm mb-3">
|
|
111
|
+
<div class="table-responsive">
|
|
112
|
+
<table
|
|
113
|
+
ref="tableRef"
|
|
114
|
+
class="table table-hover table-striped mb-0 align-middle"
|
|
115
|
+
>
|
|
116
|
+
<!-- Table structure built via useTable -->
|
|
117
|
+
</table>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Footer Stats -->
|
|
122
|
+
<div class="card shadow-sm mb-4">
|
|
123
|
+
<div class="card-body py-2">
|
|
124
|
+
<div class="row text-center">
|
|
125
|
+
<div class="col">
|
|
126
|
+
<div class="small text-muted">
|
|
127
|
+
Products
|
|
128
|
+
</div>
|
|
129
|
+
<div class="fw-bold">
|
|
130
|
+
{{ rowCount }}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="col">
|
|
134
|
+
<div class="small text-muted">
|
|
135
|
+
Total Stock
|
|
136
|
+
</div>
|
|
137
|
+
<div class="fw-bold">
|
|
138
|
+
{{ totalStock.toLocaleString() }}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="col">
|
|
142
|
+
<div class="small text-muted">
|
|
143
|
+
Total Value
|
|
144
|
+
</div>
|
|
145
|
+
<div class="fw-bold text-success">
|
|
146
|
+
${{ totalValue.toLocaleString() }}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="col">
|
|
150
|
+
<div class="small text-muted">
|
|
151
|
+
Avg Price
|
|
152
|
+
</div>
|
|
153
|
+
<div class="fw-bold">
|
|
154
|
+
${{ avgPrice.toFixed(2) }}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="col">
|
|
158
|
+
<div class="small text-muted">
|
|
159
|
+
Low Stock
|
|
160
|
+
</div>
|
|
161
|
+
<div
|
|
162
|
+
class="fw-bold"
|
|
163
|
+
:class="lowStockCount > 0 ? 'text-warning' : 'text-success'"
|
|
164
|
+
>
|
|
165
|
+
{{ lowStockCount }}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Instructions -->
|
|
173
|
+
<div class="alert alert-info mb-4">
|
|
174
|
+
<strong>Try it:</strong>
|
|
175
|
+
<ul class="mb-0 small">
|
|
176
|
+
<li><strong>Sort:</strong> Click column headers</li>
|
|
177
|
+
<li><strong>Select:</strong> Click rows (Shift+click for range, Ctrl+click for multi)</li>
|
|
178
|
+
<li><strong>Edit:</strong> Double-click any cell to edit inline</li>
|
|
179
|
+
<li><strong>Search:</strong> Filter products in real-time</li>
|
|
180
|
+
<li><strong>Undo/Redo:</strong> Ctrl+Z / Ctrl+Y or toolbar buttons</li>
|
|
181
|
+
</ul>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Code Example -->
|
|
185
|
+
<CodeBlock :code="codeExample" />
|
|
186
|
+
</section>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<script setup lang="ts">
|
|
190
|
+
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
191
|
+
import { useTable } from '@mikestools/usetable'
|
|
192
|
+
|
|
193
|
+
import CodeBlock from '@mikestools/usetools/components/CodeBlock.vue'
|
|
194
|
+
import IconTable from 'bootstrap-icons/icons/table.svg?component'
|
|
195
|
+
import IconSearch from 'bootstrap-icons/icons/search.svg?component'
|
|
196
|
+
import IconX from 'bootstrap-icons/icons/x-lg.svg?component'
|
|
197
|
+
import IconPlus from 'bootstrap-icons/icons/plus-lg.svg?component'
|
|
198
|
+
import IconTrash from 'bootstrap-icons/icons/trash.svg?component'
|
|
199
|
+
import IconArrowCounterclockwise from 'bootstrap-icons/icons/arrow-counterclockwise.svg?component'
|
|
200
|
+
import IconArrowClockwise from 'bootstrap-icons/icons/arrow-clockwise.svg?component'
|
|
201
|
+
import IconArrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg?component'
|
|
202
|
+
|
|
203
|
+
const tableRef = ref<HTMLTableElement>()
|
|
204
|
+
|
|
205
|
+
// Reactive state
|
|
206
|
+
const rowCount = ref(0)
|
|
207
|
+
const selectedCount = ref(0)
|
|
208
|
+
const canUndo = ref(false)
|
|
209
|
+
const canRedo = ref(false)
|
|
210
|
+
const searchQuery = ref('')
|
|
211
|
+
const categoryFilter = ref('')
|
|
212
|
+
|
|
213
|
+
// Aggregation computed values
|
|
214
|
+
const totalStock = ref(0)
|
|
215
|
+
const totalValue = ref(0)
|
|
216
|
+
const avgPrice = ref(0)
|
|
217
|
+
const lowStockCount = ref(0)
|
|
218
|
+
|
|
219
|
+
// Sample data
|
|
220
|
+
const categories = ['Electronics', 'Accessories', 'Audio', 'Storage', 'Display']
|
|
221
|
+
|
|
222
|
+
const sampleProducts = [
|
|
223
|
+
['PRD-001', 'MacBook Pro 14"', 'Electronics', 1999.99, 25, 'Active'],
|
|
224
|
+
['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
|
|
225
|
+
['PRD-003', 'AirPods Pro', 'Audio', 249.99, 75, 'Active'],
|
|
226
|
+
['PRD-004', 'USB-C Hub', 'Accessories', 79.99, 200, 'Active'],
|
|
227
|
+
['PRD-005', 'Studio Display', 'Display', 1599.99, 12, 'Low Stock'],
|
|
228
|
+
['PRD-006', 'Magic Mouse', 'Accessories', 79.99, 180, 'Active'],
|
|
229
|
+
['PRD-007', 'HomePod Mini', 'Audio', 99.99, 90, 'Active'],
|
|
230
|
+
['PRD-008', 'iPad Pro 12.9"', 'Electronics', 1099.99, 35, 'Active'],
|
|
231
|
+
['PRD-009', 'External SSD 1TB', 'Storage', 149.99, 8, 'Low Stock'],
|
|
232
|
+
['PRD-010', 'Thunderbolt Cable', 'Accessories', 49.99, 300, 'Active'],
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
let productCounter = 10
|
|
236
|
+
let table: ReturnType<typeof useTable> | undefined
|
|
237
|
+
let cleanupEditing: (() => void) | undefined
|
|
238
|
+
let cleanupKeyboard: (() => void) | undefined
|
|
239
|
+
|
|
240
|
+
onMounted(() => {
|
|
241
|
+
const element = tableRef.value
|
|
242
|
+
if (!element) return
|
|
243
|
+
|
|
244
|
+
table = useTable(ref(element))
|
|
245
|
+
|
|
246
|
+
// Set up table structure
|
|
247
|
+
table.setCaption('Product Inventory')
|
|
248
|
+
table.setHeaders(['SKU', 'Product Name', 'Category', 'Price ($)', 'Stock', 'Status'])
|
|
249
|
+
table.setData(sampleProducts)
|
|
250
|
+
|
|
251
|
+
// Add footer with totals
|
|
252
|
+
updateFooter()
|
|
253
|
+
|
|
254
|
+
// Enable features
|
|
255
|
+
cleanupEditing = table.enableEditing()
|
|
256
|
+
cleanupKeyboard = table.enableKeyboardNavigation()
|
|
257
|
+
|
|
258
|
+
// Set row key function for identification (using SKU)
|
|
259
|
+
table.setRowKeyFunction((row) => String(row[0]))
|
|
260
|
+
|
|
261
|
+
// Watch for changes
|
|
262
|
+
table.onDataChange(() => {
|
|
263
|
+
updateStats()
|
|
264
|
+
updateFooter()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
table.onRowSelectionChange((selected) => {
|
|
268
|
+
selectedCount.value = selected.size
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
table.onSortChange(() => {
|
|
272
|
+
updateStats()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Initial stats
|
|
276
|
+
updateStats()
|
|
277
|
+
|
|
278
|
+
// Add keyboard shortcuts
|
|
279
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
onUnmounted(() => {
|
|
283
|
+
cleanupEditing?.()
|
|
284
|
+
cleanupKeyboard?.()
|
|
285
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
289
|
+
if (event.ctrlKey || event.metaKey) {
|
|
290
|
+
if (event.key === 'z') {
|
|
291
|
+
event.preventDefault()
|
|
292
|
+
handleUndo()
|
|
293
|
+
} else if (event.key === 'y') {
|
|
294
|
+
event.preventDefault()
|
|
295
|
+
handleRedo()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function updateStats() {
|
|
301
|
+
if (!table) return
|
|
302
|
+
|
|
303
|
+
rowCount.value = table.rowCount.value
|
|
304
|
+
canUndo.value = table.canUndo()
|
|
305
|
+
canRedo.value = table.canRedo()
|
|
306
|
+
|
|
307
|
+
// Aggregations
|
|
308
|
+
totalStock.value = table.sum(4)
|
|
309
|
+
const priceSum = table.sum(3)
|
|
310
|
+
avgPrice.value = rowCount.value > 0 ? priceSum / rowCount.value : 0
|
|
311
|
+
totalValue.value = calculateTotalValue()
|
|
312
|
+
lowStockCount.value = table.count(5, (val) => val === 'Low Stock')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function calculateTotalValue(): number {
|
|
316
|
+
if (!table) return 0
|
|
317
|
+
const data = table.data.value
|
|
318
|
+
return data.reduce((sum, row) => {
|
|
319
|
+
const price = Number(row[3]) || 0
|
|
320
|
+
const stock = Number(row[4]) || 0
|
|
321
|
+
return sum + price * stock
|
|
322
|
+
}, 0)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function updateFooter() {
|
|
326
|
+
if (!table) return
|
|
327
|
+
const priceSum = table.sum(3)
|
|
328
|
+
const stockSum = table.sum(4)
|
|
329
|
+
table.setFooter(['', 'TOTALS', '', `$${priceSum.toFixed(2)}`, stockSum.toString(), ''])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function addProduct() {
|
|
333
|
+
if (!table) return
|
|
334
|
+
productCounter++
|
|
335
|
+
const newProduct = [
|
|
336
|
+
`PRD-${String(productCounter).padStart(3, '0')}`,
|
|
337
|
+
`New Product ${productCounter}`,
|
|
338
|
+
categories[Math.floor(Math.random() * categories.length)],
|
|
339
|
+
(Math.random() * 500 + 50).toFixed(2),
|
|
340
|
+
Math.floor(Math.random() * 100 + 10),
|
|
341
|
+
'Active',
|
|
342
|
+
]
|
|
343
|
+
table.addRow(newProduct)
|
|
344
|
+
updateStats()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function deleteSelected() {
|
|
348
|
+
if (!table) return
|
|
349
|
+
const selected = Array.from(table.selectedRows.value).sort((a, b) => b - a)
|
|
350
|
+
table.transaction(() => {
|
|
351
|
+
for (const index of selected) {
|
|
352
|
+
table!.removeRow(index)
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
table.clearSelection()
|
|
356
|
+
updateStats()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function handleUndo() {
|
|
360
|
+
if (!table) return
|
|
361
|
+
table.undo()
|
|
362
|
+
updateStats()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function handleRedo() {
|
|
366
|
+
if (!table) return
|
|
367
|
+
table.redo()
|
|
368
|
+
updateStats()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function handleSearch() {
|
|
372
|
+
if (!table) return
|
|
373
|
+
if (searchQuery.value) {
|
|
374
|
+
table.filterRows((row) => {
|
|
375
|
+
const query = searchQuery.value.toLowerCase()
|
|
376
|
+
return row.some((cell) => String(cell).toLowerCase().includes(query))
|
|
377
|
+
})
|
|
378
|
+
} else {
|
|
379
|
+
table.clearFilters()
|
|
380
|
+
}
|
|
381
|
+
updateStats()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function clearSearch() {
|
|
385
|
+
searchQuery.value = ''
|
|
386
|
+
handleSearch()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleCategoryFilter() {
|
|
390
|
+
if (!table) return
|
|
391
|
+
if (categoryFilter.value) {
|
|
392
|
+
table.filterColumnByValue(2, categoryFilter.value)
|
|
393
|
+
} else {
|
|
394
|
+
table.clearFilters()
|
|
395
|
+
}
|
|
396
|
+
updateStats()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function resetTable() {
|
|
400
|
+
if (!table) return
|
|
401
|
+
table.setData(sampleProducts)
|
|
402
|
+
table.clearSelection()
|
|
403
|
+
table.clearFilters()
|
|
404
|
+
searchQuery.value = ''
|
|
405
|
+
categoryFilter.value = ''
|
|
406
|
+
productCounter = 10
|
|
407
|
+
updateStats()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Watch for filter changes to clear search when category changes
|
|
411
|
+
watch(categoryFilter, () => {
|
|
412
|
+
if (categoryFilter.value && searchQuery.value) {
|
|
413
|
+
searchQuery.value = ''
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const codeExample = `import { ref, onMounted, onUnmounted } from 'vue'
|
|
418
|
+
import { useTable } from '@mikestools/usetable'
|
|
419
|
+
|
|
420
|
+
const tableRef = ref<HTMLTableElement>()
|
|
421
|
+
let table: ReturnType<typeof useTable>
|
|
422
|
+
|
|
423
|
+
onMounted(() => {
|
|
424
|
+
table = useTable(ref(tableRef.value!))
|
|
425
|
+
|
|
426
|
+
// Set up structure
|
|
427
|
+
table.setHeaders(['SKU', 'Product', 'Category', 'Price', 'Stock', 'Status'])
|
|
428
|
+
table.setData([
|
|
429
|
+
['PRD-001', 'MacBook Pro', 'Electronics', 1999.99, 25, 'Active'],
|
|
430
|
+
['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
|
|
431
|
+
])
|
|
432
|
+
|
|
433
|
+
// Enable inline editing (double-click to edit)
|
|
434
|
+
const cleanupEdit = table.enableEditing()
|
|
435
|
+
|
|
436
|
+
// Enable keyboard navigation (arrow keys)
|
|
437
|
+
const cleanupNav = table.enableKeyboardNavigation()
|
|
438
|
+
|
|
439
|
+
// Set row key for identification
|
|
440
|
+
table.setRowKeyFunction((row) => String(row[0]))
|
|
441
|
+
|
|
442
|
+
// React to changes
|
|
443
|
+
table.onDataChange((data) => {
|
|
444
|
+
console.log('Data changed:', data)
|
|
445
|
+
// Save to database, localStorage, etc.
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// Aggregations
|
|
449
|
+
const totalStock = table.sum(4) // Sum of Stock column
|
|
450
|
+
const avgPrice = table.average(3) // Average of Price column
|
|
451
|
+
const lowStock = table.count(5, (v) => v === 'Low Stock')
|
|
452
|
+
|
|
453
|
+
// Sorting
|
|
454
|
+
table.sortColumnAscending(1) // Sort by Product name
|
|
455
|
+
|
|
456
|
+
// Filtering
|
|
457
|
+
table.filterColumnByValue(2, 'Electronics') // Show only Electronics
|
|
458
|
+
|
|
459
|
+
// Selection
|
|
460
|
+
table.selectRow(0)
|
|
461
|
+
table.selectRowRange(0, 2) // Select rows 0-2
|
|
462
|
+
|
|
463
|
+
// Undo/Redo
|
|
464
|
+
table.setCell(0, 4, 30) // Change stock
|
|
465
|
+
table.undo() // Revert change
|
|
466
|
+
|
|
467
|
+
// Transactions (atomic operations)
|
|
468
|
+
table.transaction(() => {
|
|
469
|
+
table.addRow(['PRD-003', 'New', 'Audio', 199.99, 50, 'Active'])
|
|
470
|
+
table.setCell(0, 4, 20)
|
|
471
|
+
// If any fails, all changes roll back
|
|
472
|
+
})
|
|
473
|
+
})`
|
|
474
|
+
</script>
|