@serhiitupilow/nuxt-table 0.1.3 → 0.1.5

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 CHANGED
@@ -1,37 +1,41 @@
1
1
  # @serhiitupilow/nuxt-table
2
2
 
3
- Nuxt module that provides a single `NuxtTable` component with:
3
+ A Nuxt module that registers a global `NuxtTable` component for data tables with:
4
4
 
5
- - sorting
6
- - filtering
7
- - column visibility manager
8
- - column resize
5
+ - client-side sorting
6
+ - client-side filtering
9
7
  - optional drag-and-drop column reordering
10
- - class-based styling (no Tailwind classes in component templates)
8
+ - optional column resize
9
+ - persisted column order/visibility/widths in `localStorage`
10
+ - configurable cell/header/filter rendering
11
11
 
12
- ## Install
12
+ ## Requirements
13
13
 
14
- Use any package manager:
14
+ - `nuxt >= 3.11.0`
15
+ - `vue >= 3.4.0`
16
+
17
+ ## Installation
15
18
 
16
19
  ```bash
17
20
  npm i @serhiitupilow/nuxt-table
18
21
  # or
22
+ pnpm add @serhiitupilow/nuxt-table
23
+ # or
19
24
  yarn add @serhiitupilow/nuxt-table
20
25
  # or
21
26
  bun add @serhiitupilow/nuxt-table
22
- # or
23
- pnpm add @serhiitupilow/nuxt-table
24
27
  ```
25
28
 
26
- ## Nuxt config
29
+ ## Nuxt setup
27
30
 
28
31
  ```ts
32
+ // nuxt.config.ts
29
33
  export default defineNuxtConfig({
30
34
  modules: ["@serhiitupilow/nuxt-table"],
31
35
  });
32
36
  ```
33
37
 
34
- Optional module config:
38
+ ### Module options
35
39
 
36
40
  ```ts
37
41
  export default defineNuxtConfig({
@@ -42,21 +46,38 @@ export default defineNuxtConfig({
42
46
  });
43
47
  ```
44
48
 
45
- ## Usage
49
+ | Option | Type | Default | Description |
50
+ | --------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------ |
51
+ | `injectDefaultStyles` | `boolean` | `true` | Injects bundled CSS from the module runtime. Set to `false` if you fully style classes yourself. |
52
+
53
+ ## Quick start
46
54
 
47
55
  ```vue
48
56
  <script setup lang="ts">
49
- import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/dist/runtime/types/table";
57
+ import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/runtime";
58
+
59
+ type UserRow = {
60
+ id: number;
61
+ name: string;
62
+ status: "active" | "paused";
63
+ createdAt: string;
64
+ };
50
65
 
51
66
  const columns: NuxtTableColumn[] = [
52
67
  { key: "id", label: "ID", sortable: true, filterable: true },
53
68
  { key: "name", label: "Name", sortable: true, filterable: true },
54
69
  { key: "status", label: "Status", sortable: true, filterable: true },
70
+ {
71
+ key: "createdAt",
72
+ label: "Created",
73
+ sortable: true,
74
+ formatter: (value) => new Date(String(value)).toLocaleDateString(),
75
+ },
55
76
  ];
56
77
 
57
- const rows = [
58
- { id: 1, name: "Alice", status: "active" },
59
- { id: 2, name: "Bob", status: "paused" },
78
+ const rows: UserRow[] = [
79
+ { id: 1, name: "Alice", status: "active", createdAt: "2026-02-01" },
80
+ { id: 2, name: "Bob", status: "paused", createdAt: "2026-02-14" },
60
81
  ];
61
82
 
62
83
  function onColumnOrderChange(payload: {
@@ -80,24 +101,557 @@ function onColumnOrderChange(payload: {
80
101
  </template>
81
102
  ```
82
103
 
104
+ ## Public runtime exports
105
+
106
+ ```ts
107
+ import {
108
+ useNuxtTable,
109
+ type NuxtTableClassNames,
110
+ type NuxtTableColumn,
111
+ type NuxtTableColumnOrderChange,
112
+ type NuxtTableManualFilterChange,
113
+ type NuxtTableManualSortChange,
114
+ type TableRow,
115
+ type UseNuxtTableOptions,
116
+ type ValueResolver,
117
+ } from "@serhiitupilow/nuxt-table/runtime";
118
+ ```
119
+
120
+ ## `NuxtTable` component API
121
+
122
+ ### Props
123
+
124
+ | Prop | Type | Default | Description |
125
+ | -------------------- | -------------------------------------------- | -------------- | ----------------------------------------------------------------------- |
126
+ | `columns` | `NuxtTableColumn[]` | required | Column definitions. |
127
+ | `rows` | `TableRow[]` | required | Data rows. |
128
+ | `enabledColumns` | `string[]` | `undefined` | Explicitly controls visible columns (in the current ordered sequence). |
129
+ | `storageKey` | `string` | `"nuxt-table"` | Prefix for persisted table UI state in `localStorage`. |
130
+ | `rowKey` | `string \| (row, index) => string \| number` | `"id"` | Unique key resolver for row rendering. |
131
+ | `title` | `string` | `"Table"` | Legacy prop kept for compatibility (not currently rendered in UI). |
132
+ | `showToolbar` | `boolean` | `true` | Legacy prop kept for compatibility (toolbar is not currently rendered). |
133
+ | `enableColumnDnd` | `boolean` | `false` | Enables drag-and-drop header reordering. |
134
+ | `enableColumnResize` | `boolean` | `true` | Enables resize handle on header cells. |
135
+ | `classNames` | `Partial<NuxtTableClassNames>` | `{}` | Class overrides for semantic class hooks. |
136
+
137
+ ### Events
138
+
139
+ | Event | Payload | Description |
140
+ | ---------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
141
+ | `column-order-change` | `NuxtTableColumnOrderChange` | Emitted after successful drag-and-drop reorder. |
142
+ | `manual-sort-change` | `NuxtTableManualSortChange` | Emitted when sorting is changed for a column with `sortFunction`. |
143
+ | `manual-filter-change` | `NuxtTableManualFilterChange` | Emitted when filter value changes for a column with `filterFunction` / `filterFn`. |
144
+
145
+ ### Behavior notes
146
+
147
+ - Filtering is applied before sorting.
148
+ - Sorting cycles by click: `asc -> desc -> off`.
149
+ - If a column has `filterFunction` / `filterFn`, built-in filtering is disabled for that column and table emits `manual-filter-change`.
150
+ - If a column has `sortFunction`, built-in sorting is disabled for that column and table emits `manual-sort-change`.
151
+ - Column width has a minimum of `140px`.
152
+ - Empty state text: `No rows match the current filters.`
153
+ - Rendering is table-only (no built-in toolbar/summary controls).
154
+ - DnD headers use cursor states: `grab` and `grabbing`.
155
+
156
+ ## Column definition (`NuxtTableColumn`)
157
+
158
+ ```ts
159
+ type ValueResolver = string | ((row: TableRow) => unknown);
160
+
161
+ interface NuxtTableColumn {
162
+ key: string;
163
+ label: string;
164
+ sortable?: boolean;
165
+ filterable?: boolean;
166
+ sortAscComponent?: Component;
167
+ sortDescComponent?: Component;
168
+ sortDefaultComponent?: Component;
169
+ sortKey?: ValueResolver;
170
+ sortFunction?: (
171
+ leftRow: TableRow,
172
+ rightRow: TableRow,
173
+ column: NuxtTableColumn,
174
+ tableRows: TableRow[],
175
+ direction: "asc" | "desc",
176
+ ) => number;
177
+ filterKey?: ValueResolver;
178
+ formatter?: (value: unknown, row: TableRow) => string;
179
+ filterFunction?: (
180
+ row: TableRow,
181
+ filterValue: unknown,
182
+ column: NuxtTableColumn,
183
+ tableRows: TableRow[],
184
+ ) => boolean;
185
+ filterFn?: (
186
+ row: TableRow,
187
+ filterValue: unknown,
188
+ column: NuxtTableColumn,
189
+ tableRows: TableRow[],
190
+ ) => boolean;
191
+ cellComponent?: Component;
192
+ filterComponent?: Component;
193
+ headerClassName?: string;
194
+ cellClassName?: string;
195
+ }
196
+ ```
197
+
198
+ ### Field details
199
+
200
+ - `key`: primary accessor path for display value. Supports dot notation through resolvers (for example: `user.profile.name`) when used by `sortKey`/`filterKey`.
201
+ - `sortKey`: alternate accessor/function used for sorting.
202
+ - `sortFunction`: enables manual sort mode for that column. Table emits `manual-sort-change` and does not sort rows automatically.
203
+ - `sortAscComponent` / `sortDescComponent` / `sortDefaultComponent`: optional sort button content per state. If not provided, defaults are `Asc`, `Desc`, and `Sort`.
204
+ - `filterKey`: alternate accessor/function used for default text filtering.
205
+ - `formatter`: transforms display value for default body rendering (`<span>{{ value }}</span>`).
206
+ - `filterFunction`: enables manual filter mode for that column. Table emits `manual-filter-change` and does not filter rows automatically.
207
+ - `filterFn`: legacy alias for `filterFunction` (same manual behavior).
208
+ - `cellComponent`: custom body renderer receives `row`, `column`, and `value`.
209
+ - `filterComponent`: custom header filter renderer receives `modelValue` and `column`, and should emit `update:model-value`.
210
+
211
+ ## Persistence model
212
+
213
+ State is persisted per `storageKey` in `localStorage` with keys:
214
+
215
+ - `${storageKey}:order`
216
+ - `${storageKey}:enabledColumns`
217
+ - `${storageKey}:widths`
218
+
219
+ Persisted values are validated against current `columns`; unknown keys are ignored.
220
+
83
221
  ## Styling
84
222
 
85
- `NuxtTable` uses semantic class names (like `nuxt-table__header-cell`) and receives a `classNames` prop for overrides.
223
+ The component uses semantic class hooks. You can:
224
+
225
+ 1. use injected default styles, and/or
226
+ 2. override classes via `classNames`, and/or
227
+ 3. provide your own global CSS.
86
228
 
87
- You can style globally in your project CSS:
229
+ ### Default class keys (`NuxtTableClassNames`)
88
230
 
89
- ```css
90
- .nuxt-table__header-cell {
91
- background: #f8fafc;
231
+ ```ts
232
+ interface NuxtTableClassNames {
233
+ root: string;
234
+ toolbar: string;
235
+ toolbarTitle: string;
236
+ toolbarActions: string;
237
+ toolbarButton: string;
238
+ columnManager: string;
239
+ columnManagerTitle: string;
240
+ columnManagerItem: string;
241
+ tableWrapper: string;
242
+ table: string;
243
+ tableHead: string;
244
+ tableBody: string;
245
+ bodyRow: string;
246
+ emptyCell: string;
247
+ headerCell: string;
248
+ headerCellDragSource: string;
249
+ headerCellDragOver: string;
250
+ headerTop: string;
251
+ headerLabel: string;
252
+ sortButton: string;
253
+ filterInput: string;
254
+ resizeHandle: string;
255
+ bodyCell: string;
92
256
  }
93
257
  ```
94
258
 
95
- Or override class names from props:
259
+ > Some toolbar-related class keys remain in the public type for compatibility, even though the current component template renders only the table.
260
+
261
+ ### `classNames` example
96
262
 
97
263
  ```vue
98
264
  <NuxtTable
99
265
  :columns="columns"
100
266
  :rows="rows"
101
- :class-names="{ table: 'my-table', headerCell: 'my-header-cell' }"
267
+ :class-names="{
268
+ table: 'my-table',
269
+ headerCell: 'my-header-cell',
270
+ bodyCell: 'my-body-cell',
271
+ filterInput: 'my-filter-input',
272
+ }"
102
273
  />
103
274
  ```
275
+
276
+ ## Advanced examples
277
+
278
+ ### Enable / disable visible columns
279
+
280
+ Use `enabledColumns` to control what is rendered.
281
+
282
+ ```vue
283
+ <script setup lang="ts">
284
+ const enabledColumns = ref<string[]>(["id", "name", "status"]);
285
+
286
+ function toggleStatusColumn() {
287
+ if (enabledColumns.value.includes("status")) {
288
+ enabledColumns.value = enabledColumns.value.filter(
289
+ (key) => key !== "status",
290
+ );
291
+ return;
292
+ }
293
+
294
+ enabledColumns.value = [...enabledColumns.value, "status"];
295
+ }
296
+ </script>
297
+
298
+ <template>
299
+ <button type="button" @click="toggleStatusColumn">
300
+ Toggle status column
301
+ </button>
302
+
303
+ <NuxtTable
304
+ :columns="columns"
305
+ :rows="rows"
306
+ :enabled-columns="enabledColumns"
307
+ />
308
+ </template>
309
+ ```
310
+
311
+ ### Custom sort state components (ASC / DESC / default)
312
+
313
+ ```vue
314
+ <!-- SortAsc.vue -->
315
+ <template><span>↑ ASC</span></template>
316
+
317
+ <!-- SortDesc.vue -->
318
+ <template><span>↓ DESC</span></template>
319
+
320
+ <!-- SortIdle.vue -->
321
+ <template><span>↕ SORT</span></template>
322
+ ```
323
+
324
+ ```ts
325
+ import SortAsc from "~/components/SortAsc.vue";
326
+ import SortDesc from "~/components/SortDesc.vue";
327
+ import SortIdle from "~/components/SortIdle.vue";
328
+
329
+ const columns: NuxtTableColumn[] = [
330
+ {
331
+ key: "name",
332
+ label: "Name",
333
+ sortable: true,
334
+ sortAscComponent: SortAsc,
335
+ sortDescComponent: SortDesc,
336
+ sortDefaultComponent: SortIdle,
337
+ },
338
+ ];
339
+ ```
340
+
341
+ If these components are not provided, the table automatically uses the default labels.
342
+
343
+ ### Detailed `manual-filter-change` and `manual-sort-change` flow
344
+
345
+ When `filterFunction` / `sortFunction` is set, table switches that column to manual mode and emits `manual-filter-change` / `manual-sort-change`. You control final dataset in parent component.
346
+
347
+ ```vue
348
+ <script setup lang="ts">
349
+ import { computed, ref } from "vue";
350
+ import type {
351
+ NuxtTableColumn,
352
+ NuxtTableManualFilterChange,
353
+ NuxtTableManualSortChange,
354
+ } from "@serhiitupilow/nuxt-table/runtime";
355
+
356
+ type TicketRow = {
357
+ id: number;
358
+ title: string;
359
+ priority: "low" | "medium" | "high" | "critical";
360
+ status: "todo" | "in_progress" | "done";
361
+ createdAt: string;
362
+ };
363
+
364
+ const allRows = ref<TicketRow[]>([
365
+ {
366
+ id: 1,
367
+ title: "Fix auth flow",
368
+ priority: "high",
369
+ status: "in_progress",
370
+ createdAt: "2026-02-18",
371
+ },
372
+ {
373
+ id: 2,
374
+ title: "Write docs",
375
+ priority: "low",
376
+ status: "done",
377
+ createdAt: "2026-02-10",
378
+ },
379
+ {
380
+ id: 3,
381
+ title: "Release v1",
382
+ priority: "critical",
383
+ status: "todo",
384
+ createdAt: "2026-02-20",
385
+ },
386
+ ]);
387
+
388
+ const columns: NuxtTableColumn[] = [
389
+ { key: "id", label: "ID", sortable: true },
390
+ { key: "title", label: "Title", sortable: true, filterable: true },
391
+ {
392
+ key: "status",
393
+ label: "Status",
394
+ filterable: true,
395
+ filterFunction: () => true,
396
+ },
397
+ {
398
+ key: "priority",
399
+ label: "Priority",
400
+ sortable: true,
401
+ sortFunction: () => 0,
402
+ },
403
+ ];
404
+
405
+ const manualStatusFilter = ref<string>("");
406
+ const manualSort = ref<{ key: string; direction: "asc" | "desc" | null }>({
407
+ key: "",
408
+ direction: null,
409
+ });
410
+
411
+ const rows = computed(() => {
412
+ const statusFiltered = allRows.value.filter((row) => {
413
+ if (!manualStatusFilter.value) {
414
+ return true;
415
+ }
416
+
417
+ return row.status === manualStatusFilter.value;
418
+ });
419
+
420
+ if (manualSort.value.key !== "priority" || !manualSort.value.direction) {
421
+ return statusFiltered;
422
+ }
423
+
424
+ const rank: Record<TicketRow["priority"], number> = {
425
+ low: 0,
426
+ medium: 1,
427
+ high: 2,
428
+ critical: 3,
429
+ };
430
+
431
+ const directionMultiplier = manualSort.value.direction === "asc" ? 1 : -1;
432
+
433
+ return [...statusFiltered].sort((left, right) => {
434
+ return (rank[left.priority] - rank[right.priority]) * directionMultiplier;
435
+ });
436
+ });
437
+
438
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
439
+ if (payload.columnKey !== "status") {
440
+ return;
441
+ }
442
+
443
+ manualStatusFilter.value = String(payload.value ?? "").trim();
444
+ }
445
+
446
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
447
+ manualSort.value = {
448
+ key: payload.columnKey,
449
+ direction: payload.direction,
450
+ };
451
+ }
452
+ </script>
453
+
454
+ <template>
455
+ <NuxtTable
456
+ :columns="columns"
457
+ :rows="rows"
458
+ @manual-filter-change="onManualFilterChange"
459
+ @manual-sort-change="onManualSortChange"
460
+ />
461
+ </template>
462
+ ```
463
+
464
+ Notes:
465
+
466
+ - In manual mode, the table does not transform rows for that column; you pass already transformed `rows` from outside.
467
+ - Use payload `columnKey`, `value`, `direction`, `rows`, and `filters` from events to build server/client-side query logic.
468
+ - `filterFn` remains supported as a legacy alias, but `filterFunction` is preferred.
469
+
470
+ ### Server-side `manual-filter-change` / `manual-sort-change` example
471
+
472
+ Use manual events to request data from backend and pass ready rows back to table.
473
+
474
+ ```vue
475
+ <script setup lang="ts">
476
+ import { ref } from "vue";
477
+ import type {
478
+ NuxtTableColumn,
479
+ NuxtTableManualFilterChange,
480
+ NuxtTableManualSortChange,
481
+ } from "@serhiitupilow/nuxt-table/runtime";
482
+
483
+ type UserRow = {
484
+ id: number;
485
+ name: string;
486
+ status: "active" | "paused";
487
+ createdAt: string;
488
+ };
489
+
490
+ const rows = ref<UserRow[]>([]);
491
+ const loading = ref(false);
492
+
493
+ const query = ref<{
494
+ page: number;
495
+ pageSize: number;
496
+ status: string;
497
+ sortKey: string;
498
+ sortDirection: "asc" | "desc" | "";
499
+ }>({
500
+ page: 1,
501
+ pageSize: 20,
502
+ status: "",
503
+ sortKey: "",
504
+ sortDirection: "",
505
+ });
506
+
507
+ const columns: NuxtTableColumn[] = [
508
+ { key: "id", label: "ID", sortable: true },
509
+ { key: "name", label: "Name", sortable: true, filterable: true },
510
+ {
511
+ key: "status",
512
+ label: "Status",
513
+ filterable: true,
514
+ filterFunction: () => true,
515
+ },
516
+ {
517
+ key: "createdAt",
518
+ label: "Created",
519
+ sortable: true,
520
+ sortFunction: () => 0,
521
+ },
522
+ ];
523
+
524
+ async function fetchRows() {
525
+ loading.value = true;
526
+
527
+ try {
528
+ const data = await $fetch<UserRow[]>("/api/users", {
529
+ query: {
530
+ page: query.value.page,
531
+ pageSize: query.value.pageSize,
532
+ status: query.value.status,
533
+ sortKey: query.value.sortKey,
534
+ sortDirection: query.value.sortDirection,
535
+ },
536
+ });
537
+
538
+ rows.value = data;
539
+ } finally {
540
+ loading.value = false;
541
+ }
542
+ }
543
+
544
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
545
+ if (payload.columnKey === "status") {
546
+ query.value.status = String(payload.value ?? "").trim();
547
+ query.value.page = 1;
548
+ }
549
+
550
+ fetchRows();
551
+ }
552
+
553
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
554
+ query.value.sortKey = payload.columnKey;
555
+ query.value.sortDirection = payload.direction ?? "";
556
+ query.value.page = 1;
557
+
558
+ fetchRows();
559
+ }
560
+
561
+ await fetchRows();
562
+ </script>
563
+
564
+ <template>
565
+ <NuxtTable
566
+ :columns="columns"
567
+ :rows="rows"
568
+ @manual-filter-change="onManualFilterChange"
569
+ @manual-sort-change="onManualSortChange"
570
+ />
571
+
572
+ <p v-if="loading">Loading...</p>
573
+ </template>
574
+ ```
575
+
576
+ What happens here:
577
+
578
+ - `filterFunction` and `sortFunction` switch corresponding columns to manual mode.
579
+ - Table emits `manual-filter-change` / `manual-sort-change` instead of transforming `rows` internally.
580
+ - Parent updates query params, calls API, and passes backend result as new `rows`.
581
+
582
+ ### Custom filter component
583
+
584
+ ```vue
585
+ <!-- StatusFilter.vue -->
586
+ <script setup lang="ts">
587
+ const props = defineProps<{
588
+ modelValue: unknown;
589
+ column: { key: string; label: string };
590
+ }>();
591
+
592
+ const emit = defineEmits<{
593
+ "update:model-value": [value: string];
594
+ }>();
595
+ </script>
596
+
597
+ <template>
598
+ <select
599
+ :value="String(props.modelValue ?? '')"
600
+ @change="
601
+ emit('update:model-value', ($event.target as HTMLSelectElement).value)
602
+ "
603
+ >
604
+ <option value="">All</option>
605
+ <option value="active">Active</option>
606
+ <option value="paused">Paused</option>
607
+ </select>
608
+ </template>
609
+ ```
610
+
611
+ ```ts
612
+ const columns: NuxtTableColumn[] = [
613
+ {
614
+ key: "status",
615
+ label: "Status",
616
+ filterable: true,
617
+ filterComponent: StatusFilter,
618
+ },
619
+ ];
620
+ ```
621
+
622
+ ### Custom cell component
623
+
624
+ ```vue
625
+ <!-- NameCell.vue -->
626
+ <script setup lang="ts">
627
+ const props = defineProps<{
628
+ row: Record<string, unknown>;
629
+ value: unknown;
630
+ }>();
631
+ </script>
632
+
633
+ <template>
634
+ <strong>{{ props.value }}</strong>
635
+ </template>
636
+ ```
637
+
638
+ ```ts
639
+ const columns: NuxtTableColumn[] = [
640
+ {
641
+ key: "name",
642
+ label: "Name",
643
+ cellComponent: NameCell,
644
+ },
645
+ ];
646
+ ```
647
+
648
+ ## Troubleshooting
649
+
650
+ - DnD does nothing: ensure `enableColumnDnd` is `true`.
651
+ - Filters do nothing: ensure column has `filterable: true` or a `filterComponent` that emits `update:model-value`.
652
+ - Unexpected row keys: set a stable `rowKey` function for datasets without `id`.
653
+ - Style conflicts: disable `injectDefaultStyles` and provide full custom CSS.
654
+
655
+ ## License
656
+
657
+ MIT
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serhiitupilow/nuxt-table",
3
3
  "configKey": "nuxtTable",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1 +1 @@
1
- .nuxt-table{display:grid;gap:1rem}.nuxt-table__toolbar{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.nuxt-table__toolbar-title{font-size:1.125rem;font-weight:600;margin:0}.nuxt-table__toolbar-actions{align-items:center;display:flex;gap:.5rem;position:relative}.nuxt-table__toolbar-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.875rem;line-height:1;padding:.5rem .75rem}.nuxt-table__column-manager{background:#fff;border:1px solid #d1d5db;border-radius:.5rem;box-shadow:0 10px 20px rgba(15,23,42,.14);min-width:14rem;padding:.75rem;position:absolute;right:0;top:2.75rem;z-index:30}.nuxt-table__column-manager-title{font-size:.875rem;font-weight:600;margin:0 0 .5rem}.nuxt-table__column-manager-item{align-items:center;cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;margin-bottom:.375rem}.nuxt-table__column-manager-item:last-child{margin-bottom:0}.nuxt-table__wrapper{border:1px solid #d1d5db;border-radius:.5rem;overflow-x:auto}.nuxt-table__table{border-collapse:collapse;min-width:100%;table-layout:fixed;width:100%}.nuxt-table__head{background:#f8fafc}.nuxt-table__header-cell{border-bottom:1px solid #e5e7eb;border-right:1px solid #e5e7eb;padding:.75rem;position:relative;text-align:left;transition:background-color .2s ease,box-shadow .2s ease,transform .2s ease,opacity .2s ease;vertical-align:top}.nuxt-table__header-cell:last-child{border-right:0}.nuxt-table__header-cell--drag-source{box-shadow:inset 0 0 0 1px #9ca3af;opacity:.82}.nuxt-table__header-cell--drag-over{background:#f1f5f9;box-shadow:inset 0 0 0 2px #cbd5e1}.nuxt-table__header-top{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.5rem}.nuxt-table__header-label{font-weight:600}.nuxt-table__sort-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.75rem;line-height:1;padding:.25rem .5rem}.nuxt-table__filter-input{border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;padding:.375rem .5rem;width:100%}.nuxt-table__resize-handle{cursor:col-resize;height:100%;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:.5rem}.nuxt-table__body-row{border-bottom:1px solid #e5e7eb}.nuxt-table__body-row:last-child{border-bottom:0}.nuxt-table__body-cell{border-right:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.nuxt-table__body-cell:last-child{border-right:0}.nuxt-table__empty-cell{color:#6b7280;padding:1rem;text-align:center}
1
+ .nuxt-table{display:grid;gap:1rem}.nuxt-table__toolbar{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.nuxt-table__toolbar-title{font-size:1.125rem;font-weight:600;margin:0}.nuxt-table__toolbar-actions{align-items:center;display:flex;gap:.5rem;position:relative}.nuxt-table__toolbar-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.875rem;line-height:1;padding:.5rem .75rem}.nuxt-table__column-manager{position:relative}.nuxt-table__column-manager>summary{list-style:none}.nuxt-table__column-manager>summary::-webkit-details-marker{display:none}.nuxt-table__column-manager[open]{background:#fff;border:1px solid #d1d5db;border-radius:.5rem;box-shadow:0 10px 20px rgba(15,23,42,.14);min-width:14rem;padding:.75rem;position:absolute;right:0;top:0;z-index:30}.nuxt-table__column-manager[open]>summary{margin-bottom:.5rem}.nuxt-table__column-manager-title{font-size:.875rem;font-weight:600;margin:0 0 .5rem}.nuxt-table__column-manager-item{align-items:center;cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;margin-bottom:.375rem}.nuxt-table__column-manager-item:last-child{margin-bottom:0}.nuxt-table__wrapper{border:1px solid #d1d5db;border-radius:.5rem;overflow-x:auto}.nuxt-table__table{border-collapse:collapse;min-width:100%;table-layout:fixed;width:100%}.nuxt-table__head{background:#f8fafc}.nuxt-table__header-cell{border-bottom:1px solid #e5e7eb;border-right:1px solid #e5e7eb;padding:.75rem;position:relative;text-align:left;transition:background-color .2s ease,box-shadow .2s ease,transform .2s ease,opacity .2s ease;vertical-align:top}.nuxt-table__header-cell:last-child{border-right:0}.nuxt-table__header-cell--drag-source{box-shadow:inset 0 0 0 1px #9ca3af;opacity:.82}.nuxt-table__header-cell--drag-over{background:#f1f5f9;box-shadow:inset 0 0 0 2px #cbd5e1}.nuxt-table__header-cell--dnd{cursor:grab}.nuxt-table__header-cell--dragging{cursor:grabbing}.nuxt-table__header-top{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.5rem}.nuxt-table__header-label{font-weight:600}.nuxt-table__sort-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.75rem;line-height:1;padding:.25rem .5rem}.nuxt-table__filter-input{border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;padding:.375rem .5rem;width:100%}.nuxt-table__resize-handle{cursor:col-resize;height:100%;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:.5rem}.nuxt-table__body-row{border-bottom:1px solid #e5e7eb}.nuxt-table__body-row:last-child{border-bottom:0}.nuxt-table__body-cell{border-right:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.nuxt-table__body-cell:last-child{border-right:0}.nuxt-table__empty-cell{color:#6b7280;padding:1rem;text-align:center}
@@ -1,7 +1,8 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
5
+ enabledColumns?: string[];
5
6
  storageKey?: string;
6
7
  rowKey?: string | ((row: TableRow, index: number) => string | number);
7
8
  title?: string;
@@ -12,8 +13,12 @@ type __VLS_Props = {
12
13
  };
13
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
15
  columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
16
+ manualSortChange: (payload: NuxtTableManualSortChange) => any;
17
+ manualFilterChange: (payload: NuxtTableManualFilterChange) => any;
15
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
19
  onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
20
+ onManualSortChange?: ((payload: NuxtTableManualSortChange) => any) | undefined;
21
+ onManualFilterChange?: ((payload: NuxtTableManualFilterChange) => any) | undefined;
17
22
  }>, {
18
23
  storageKey: string;
19
24
  rowKey: string | ((row: TableRow, index: number) => string | number);
@@ -4,6 +4,7 @@ import { useNuxtTable } from "../composables/useNuxtTable";
4
4
  const props = defineProps({
5
5
  columns: { type: Array, required: true },
6
6
  rows: { type: Array, required: true },
7
+ enabledColumns: { type: Array, required: false },
7
8
  storageKey: { type: String, required: false, default: "nuxt-table" },
8
9
  rowKey: { type: [String, Function], required: false, default: "id" },
9
10
  title: { type: String, required: false, default: "Table" },
@@ -12,7 +13,7 @@ const props = defineProps({
12
13
  enableColumnResize: { type: Boolean, required: false, default: true },
13
14
  classNames: { type: Object, required: false }
14
15
  });
15
- const emit = defineEmits(["columnOrderChange"]);
16
+ const emit = defineEmits(["columnOrderChange", "manualSortChange", "manualFilterChange"]);
16
17
  const defaultClassNames = {
17
18
  root: "nuxt-table",
18
19
  toolbar: "nuxt-table__toolbar",
@@ -49,16 +50,11 @@ const {
49
50
  visibleColumns,
50
51
  sortedRows,
51
52
  filters,
52
- isColumnManagerOpen,
53
- enabledColumnKeys,
54
53
  dragSourceColumnKey,
55
54
  dragOverColumnKey,
56
55
  getSortDirection,
57
56
  toggleSort,
58
57
  setFilter,
59
- clearAllFilters,
60
- toggleColumn,
61
- resetColumns,
62
58
  onHeaderDragStart,
63
59
  onHeaderDragOver,
64
60
  onHeaderDragLeave,
@@ -77,65 +73,31 @@ const {
77
73
  enableColumnDnd: toRef(props, "enableColumnDnd"),
78
74
  onColumnOrderChange: (payload) => {
79
75
  emit("columnOrderChange", payload);
76
+ },
77
+ onManualSortChange: (payload) => {
78
+ emit("manualSortChange", payload);
79
+ },
80
+ onManualFilterChange: (payload) => {
81
+ emit("manualFilterChange", payload);
80
82
  }
81
83
  });
84
+ const displayedColumns = computed(() => {
85
+ if (!props.enabledColumns) {
86
+ return visibleColumns.value;
87
+ }
88
+ const enabledKeySet = new Set(props.enabledColumns);
89
+ return orderedColumns.value.filter((column) => enabledKeySet.has(column.key));
90
+ });
82
91
  </script>
83
92
 
84
93
  <template>
85
94
  <div :class="mergedClassNames.root">
86
- <div v-if="props.showToolbar" :class="mergedClassNames.toolbar">
87
- <h2 :class="mergedClassNames.toolbarTitle">{{ props.title }}</h2>
88
-
89
- <div :class="mergedClassNames.toolbarActions">
90
- <button
91
- type="button"
92
- :class="mergedClassNames.toolbarButton"
93
- @click="isColumnManagerOpen = !isColumnManagerOpen"
94
- >
95
- Columns
96
- </button>
97
- <button
98
- type="button"
99
- :class="mergedClassNames.toolbarButton"
100
- @click="clearAllFilters"
101
- >
102
- Clear Filters
103
- </button>
104
- <button
105
- type="button"
106
- :class="mergedClassNames.toolbarButton"
107
- @click="resetColumns"
108
- >
109
- Reset Columns
110
- </button>
111
-
112
- <div
113
- v-if="isColumnManagerOpen"
114
- :class="mergedClassNames.columnManager"
115
- >
116
- <p :class="mergedClassNames.columnManagerTitle">Enable Columns</p>
117
- <label
118
- v-for="column in orderedColumns"
119
- :key="`manager-${column.key}`"
120
- :class="mergedClassNames.columnManagerItem"
121
- >
122
- <input
123
- type="checkbox"
124
- :checked="enabledColumnKeys.includes(column.key)"
125
- @change="toggleColumn(column.key)"
126
- >
127
- <span>{{ column.label }}</span>
128
- </label>
129
- </div>
130
- </div>
131
- </div>
132
-
133
95
  <div :class="mergedClassNames.tableWrapper">
134
96
  <table :class="mergedClassNames.table">
135
97
  <thead :class="mergedClassNames.tableHead">
136
98
  <tr>
137
99
  <NuxtTableHeaderCell
138
- v-for="column in visibleColumns"
100
+ v-for="column in displayedColumns"
139
101
  :key="column.key"
140
102
  :column="column"
141
103
  :filter-value="filters[column.key]"
@@ -165,7 +127,7 @@ const {
165
127
  :class="mergedClassNames.bodyRow"
166
128
  >
167
129
  <NuxtTableBodyCell
168
- v-for="column in visibleColumns"
130
+ v-for="column in displayedColumns"
169
131
  :key="`${resolveRowKey(row, rowIndex)}-${column.key}`"
170
132
  :row="row"
171
133
  :row-key="resolveRowKey(row, rowIndex)"
@@ -177,7 +139,7 @@ const {
177
139
  </tr>
178
140
  <tr v-if="sortedRows.length === 0">
179
141
  <td
180
- :colspan="Math.max(visibleColumns.length, 1)"
142
+ :colspan="Math.max(displayedColumns.length, 1)"
181
143
  :class="mergedClassNames.emptyCell"
182
144
  >
183
145
  No rows match the current filters.
@@ -1,7 +1,8 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
5
+ enabledColumns?: string[];
5
6
  storageKey?: string;
6
7
  rowKey?: string | ((row: TableRow, index: number) => string | number);
7
8
  title?: string;
@@ -12,8 +13,12 @@ type __VLS_Props = {
12
13
  };
13
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
15
  columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
16
+ manualSortChange: (payload: NuxtTableManualSortChange) => any;
17
+ manualFilterChange: (payload: NuxtTableManualFilterChange) => any;
15
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
19
  onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
20
+ onManualSortChange?: ((payload: NuxtTableManualSortChange) => any) | undefined;
21
+ onManualFilterChange?: ((payload: NuxtTableManualFilterChange) => any) | undefined;
17
22
  }>, {
18
23
  storageKey: string;
19
24
  rowKey: string | ((row: TableRow, index: number) => string | number);
@@ -1,10 +1,10 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from "../types/table.js";
3
3
  type __VLS_Props = {
4
4
  column: NuxtTableColumn;
5
5
  filterValue: unknown;
6
6
  columnStyle: Record<string, string | undefined>;
7
- sortDirection: 'asc' | 'desc' | null;
7
+ sortDirection: "asc" | "desc" | null;
8
8
  isDragSource: boolean;
9
9
  isDragOver: boolean;
10
10
  isDndEnabled: boolean;
@@ -21,26 +21,58 @@ const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd
21
21
  :class="[
22
22
  props.classNames.headerCell,
23
23
  props.column.headerClassName,
24
+ props.isDndEnabled ? 'nuxt-table__header-cell--dnd' : '',
25
+ props.isDragSource ? 'nuxt-table__header-cell--dragging' : '',
24
26
  props.isDragSource ? props.classNames.headerCellDragSource : '',
25
27
  props.isDragOver && !props.isDragSource ? props.classNames.headerCellDragOver : ''
26
28
  ]"
27
29
  :draggable="props.isDndEnabled"
28
- @dragstart="props.isDndEnabled ? emit('dragStart', props.column.key) : void 0"
29
- @dragover.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
30
- @dragenter.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
31
- @dragleave="props.isDndEnabled ? emit('dragLeave', props.column.key) : void 0"
30
+ @dragstart="
31
+ props.isDndEnabled ? emit('dragStart', props.column.key) : void 0
32
+ "
33
+ @dragover.prevent="
34
+ props.isDndEnabled ? emit('dragOver', props.column.key) : void 0
35
+ "
36
+ @dragenter.prevent="
37
+ props.isDndEnabled ? emit('dragOver', props.column.key) : void 0
38
+ "
39
+ @dragleave="
40
+ props.isDndEnabled ? emit('dragLeave', props.column.key) : void 0
41
+ "
32
42
  @drop="props.isDndEnabled ? emit('drop', props.column.key) : void 0"
33
43
  @dragend="props.isDndEnabled ? emit('dragEnd') : void 0"
34
44
  >
35
45
  <div :class="props.classNames.headerTop">
36
- <span :class="props.classNames.headerLabel">{{ props.column.label }}</span>
46
+ <span :class="props.classNames.headerLabel">{{
47
+ props.column.label
48
+ }}</span>
37
49
  <button
38
50
  v-if="props.column.sortable"
39
51
  type="button"
40
52
  :class="props.classNames.sortButton"
41
53
  @click="emit('toggleSort', props.column)"
42
54
  >
43
- <span v-if="props.sortDirection === 'asc'">Asc</span>
55
+ <component
56
+ :is="props.column.sortAscComponent"
57
+ v-if="props.sortDirection === 'asc' && props.column.sortAscComponent"
58
+ :column="props.column"
59
+ :sort-direction="props.sortDirection"
60
+ />
61
+ <component
62
+ :is="props.column.sortDescComponent"
63
+ v-else-if="
64
+ props.sortDirection === 'desc' && props.column.sortDescComponent
65
+ "
66
+ :column="props.column"
67
+ :sort-direction="props.sortDirection"
68
+ />
69
+ <component
70
+ :is="props.column.sortDefaultComponent"
71
+ v-else-if="!props.sortDirection && props.column.sortDefaultComponent"
72
+ :column="props.column"
73
+ :sort-direction="props.sortDirection"
74
+ />
75
+ <span v-else-if="props.sortDirection === 'asc'">Asc</span>
44
76
  <span v-else-if="props.sortDirection === 'desc'">Desc</span>
45
77
  <span v-else>Sort</span>
46
78
  </button>
@@ -59,8 +91,14 @@ const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd
59
91
  type="text"
60
92
  :class="props.classNames.filterInput"
61
93
  :placeholder="`Filter ${props.column.label}`"
62
- @input="emit('setFilter', props.column.key, $event.target.value)"
63
- >
94
+ @input="
95
+ emit(
96
+ 'setFilter',
97
+ props.column.key,
98
+ $event.target.value
99
+ )
100
+ "
101
+ />
64
102
 
65
103
  <div
66
104
  v-if="props.isResizeEnabled"
@@ -1,10 +1,10 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from "../types/table.js";
3
3
  type __VLS_Props = {
4
4
  column: NuxtTableColumn;
5
5
  filterValue: unknown;
6
6
  columnStyle: Record<string, string | undefined>;
7
- sortDirection: 'asc' | 'desc' | null;
7
+ sortDirection: "asc" | "desc" | null;
8
8
  isDragSource: boolean;
9
9
  isDragOver: boolean;
10
10
  isDndEnabled: boolean;
@@ -1,20 +1,17 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from "../types/table.js";
3
3
  export declare function useNuxtTable(options: UseNuxtTableOptions): {
4
4
  orderedColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
5
5
  visibleColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
6
6
  sortedRows: import("vue").ComputedRef<TableRow[]>;
7
7
  filters: import("vue").Ref<Record<string, unknown>, Record<string, unknown>>;
8
- isColumnManagerOpen: import("vue").Ref<boolean, boolean>;
9
8
  enabledColumnKeys: import("vue").Ref<string[], string[]>;
10
9
  dragSourceColumnKey: import("vue").Ref<string | null, string | null>;
11
10
  dragOverColumnKey: import("vue").Ref<string | null, string | null>;
12
11
  getSortDirection: (columnKey: string) => "asc" | "desc" | null;
13
12
  toggleSort: (column: NuxtTableColumn) => void;
14
13
  setFilter: (columnKey: string, value: unknown) => void;
15
- clearAllFilters: () => void;
16
14
  toggleColumn: (columnKey: string) => void;
17
- resetColumns: () => void;
18
15
  onHeaderDragStart: (columnKey: string) => void;
19
16
  onHeaderDragOver: (columnKey: string) => void;
20
17
  onHeaderDragLeave: (columnKey: string) => void;
@@ -1,18 +1,28 @@
1
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ watch
8
+ } from "vue";
2
9
  const MIN_COLUMN_WIDTH = 140;
3
10
  export function useNuxtTable(options) {
4
11
  const columnOrder = ref([]);
5
12
  const enabledColumnKeys = ref([]);
6
- const sortState = ref(null);
13
+ const sortState = ref(
14
+ null
15
+ );
7
16
  const filters = ref({});
8
- const isColumnManagerOpen = ref(false);
9
17
  const dragSourceColumnKey = ref(null);
10
18
  const dragOverColumnKey = ref(null);
11
19
  const hasLoadedPersistence = ref(false);
12
20
  const headerElements = ref({});
13
21
  const columnWidths = ref({});
14
22
  const activeResize = ref(null);
15
- const availableColumnKeys = computed(() => options.columns.value.map((column) => column.key));
23
+ const availableColumnKeys = computed(
24
+ () => options.columns.value.map((column) => column.key)
25
+ );
16
26
  const columnsByKey = computed(() => {
17
27
  return new Map(options.columns.value.map((column) => [column.key, column]));
18
28
  });
@@ -20,7 +30,9 @@ export function useNuxtTable(options) {
20
30
  return columnOrder.value.map((columnKey) => columnsByKey.value.get(columnKey)).filter((column) => Boolean(column));
21
31
  });
22
32
  const visibleColumns = computed(() => {
23
- return orderedColumns.value.filter((column) => enabledColumnKeys.value.includes(column.key));
33
+ return orderedColumns.value.filter(
34
+ (column) => enabledColumnKeys.value.includes(column.key)
35
+ );
24
36
  });
25
37
  const filteredRows = computed(() => {
26
38
  return options.rows.value.filter((row) => {
@@ -29,10 +41,14 @@ export function useNuxtTable(options) {
29
41
  if (!isFilterActive(filterValue)) {
30
42
  return true;
31
43
  }
32
- if (column.filterFn) {
33
- return column.filterFn(row, filterValue, column);
44
+ const customFilterFunction = column.filterFunction ?? column.filterFn;
45
+ if (customFilterFunction) {
46
+ return true;
34
47
  }
35
- const candidate = resolveColumnValue(row, column.filterKey ?? column.key);
48
+ const candidate = resolveColumnValue(
49
+ row,
50
+ column.filterKey ?? column.key
51
+ );
36
52
  const candidateText = String(candidate ?? "").toLowerCase();
37
53
  const filterText = String(filterValue ?? "").toLowerCase();
38
54
  return candidateText.includes(filterText);
@@ -47,8 +63,13 @@ export function useNuxtTable(options) {
47
63
  if (!activeColumn) {
48
64
  return filteredRows.value;
49
65
  }
50
- const directionMultiplier = sortState.value.direction === "asc" ? 1 : -1;
66
+ const sortDirection = sortState.value.direction;
67
+ const directionMultiplier = sortDirection === "asc" ? 1 : -1;
51
68
  const accessor = activeColumn.sortKey ?? activeColumn.key;
69
+ const customSortFunction = activeColumn.sortFunction;
70
+ if (customSortFunction) {
71
+ return filteredRows.value;
72
+ }
52
73
  return [...filteredRows.value].sort((leftRow, rightRow) => {
53
74
  const leftValue = resolveColumnValue(leftRow, accessor);
54
75
  const rightValue = resolveColumnValue(rightRow, accessor);
@@ -73,9 +94,18 @@ export function useNuxtTable(options) {
73
94
  if (!hasLoadedPersistence.value || !import.meta.client) {
74
95
  return;
75
96
  }
76
- localStorage.setItem(buildStorageKey("order"), JSON.stringify(columnOrder.value));
77
- localStorage.setItem(buildStorageKey("enabledColumns"), JSON.stringify(enabledColumnKeys.value));
78
- localStorage.setItem(buildStorageKey("widths"), JSON.stringify(columnWidths.value));
97
+ localStorage.setItem(
98
+ buildStorageKey("order"),
99
+ JSON.stringify(columnOrder.value)
100
+ );
101
+ localStorage.setItem(
102
+ buildStorageKey("enabledColumns"),
103
+ JSON.stringify(enabledColumnKeys.value)
104
+ );
105
+ localStorage.setItem(
106
+ buildStorageKey("widths"),
107
+ JSON.stringify(columnWidths.value)
108
+ );
79
109
  },
80
110
  { deep: true }
81
111
  );
@@ -88,7 +118,9 @@ export function useNuxtTable(options) {
88
118
  columnOrder.value = [...currentKeys];
89
119
  } else {
90
120
  const currentKeySet = new Set(currentKeys);
91
- const keptKeys = columnOrder.value.filter((key) => currentKeySet.has(key));
121
+ const keptKeys = columnOrder.value.filter(
122
+ (key) => currentKeySet.has(key)
123
+ );
92
124
  const newKeys = currentKeys.filter((key) => !keptKeys.includes(key));
93
125
  columnOrder.value = [...keptKeys, ...newKeys];
94
126
  }
@@ -96,8 +128,12 @@ export function useNuxtTable(options) {
96
128
  enabledColumnKeys.value = [...currentKeys];
97
129
  } else {
98
130
  const currentKeySet = new Set(currentKeys);
99
- const keptEnabledKeys = enabledColumnKeys.value.filter((key) => currentKeySet.has(key));
100
- const missingEnabledKeys = currentKeys.filter((key) => !keptEnabledKeys.includes(key));
131
+ const keptEnabledKeys = enabledColumnKeys.value.filter(
132
+ (key) => currentKeySet.has(key)
133
+ );
134
+ const missingEnabledKeys = currentKeys.filter(
135
+ (key) => !keptEnabledKeys.includes(key)
136
+ );
101
137
  enabledColumnKeys.value = [...keptEnabledKeys, ...missingEnabledKeys];
102
138
  }
103
139
  const nextFilters = {};
@@ -123,20 +159,28 @@ export function useNuxtTable(options) {
123
159
  if (persistedOrder) {
124
160
  const parsedOrder = JSON.parse(persistedOrder);
125
161
  if (Array.isArray(parsedOrder)) {
126
- const validPersistedOrder = parsedOrder.filter((key) => {
127
- return typeof key === "string" && availableColumnKeys.value.includes(key);
128
- });
129
- const missingKeys = availableColumnKeys.value.filter((key) => !validPersistedOrder.includes(key));
162
+ const validPersistedOrder = parsedOrder.filter(
163
+ (key) => {
164
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
165
+ }
166
+ );
167
+ const missingKeys = availableColumnKeys.value.filter(
168
+ (key) => !validPersistedOrder.includes(key)
169
+ );
130
170
  columnOrder.value = [...validPersistedOrder, ...missingKeys];
131
171
  }
132
172
  }
133
- const persistedEnabledColumns = localStorage.getItem(buildStorageKey("enabledColumns"));
173
+ const persistedEnabledColumns = localStorage.getItem(
174
+ buildStorageKey("enabledColumns")
175
+ );
134
176
  if (persistedEnabledColumns) {
135
177
  const parsedEnabledColumns = JSON.parse(persistedEnabledColumns);
136
178
  if (Array.isArray(parsedEnabledColumns)) {
137
- enabledColumnKeys.value = parsedEnabledColumns.filter((key) => {
138
- return typeof key === "string" && availableColumnKeys.value.includes(key);
139
- });
179
+ enabledColumnKeys.value = parsedEnabledColumns.filter(
180
+ (key) => {
181
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
182
+ }
183
+ );
140
184
  }
141
185
  }
142
186
  const persistedWidths = localStorage.getItem(buildStorageKey("widths"));
@@ -174,38 +218,71 @@ export function useNuxtTable(options) {
174
218
  }
175
219
  if (!sortState.value || sortState.value.key !== column.key) {
176
220
  sortState.value = { key: column.key, direction: "asc" };
221
+ if (column.sortFunction) {
222
+ options.onManualSortChange?.({
223
+ columnKey: column.key,
224
+ direction: "asc",
225
+ column,
226
+ rows: [...options.rows.value],
227
+ filters: { ...filters.value }
228
+ });
229
+ }
177
230
  return;
178
231
  }
179
232
  if (sortState.value.direction === "asc") {
180
233
  sortState.value = { key: column.key, direction: "desc" };
234
+ if (column.sortFunction) {
235
+ options.onManualSortChange?.({
236
+ columnKey: column.key,
237
+ direction: "desc",
238
+ column,
239
+ rows: [...options.rows.value],
240
+ filters: { ...filters.value }
241
+ });
242
+ }
181
243
  return;
182
244
  }
183
245
  sortState.value = null;
246
+ if (column.sortFunction) {
247
+ options.onManualSortChange?.({
248
+ columnKey: column.key,
249
+ direction: null,
250
+ column,
251
+ rows: [...options.rows.value],
252
+ filters: { ...filters.value }
253
+ });
254
+ }
184
255
  }
185
256
  function setFilter(columnKey, value) {
186
257
  filters.value[columnKey] = value;
187
- }
188
- function clearAllFilters() {
189
- const nextFilters = {};
190
- for (const key of availableColumnKeys.value) {
191
- nextFilters[key] = "";
258
+ const column = columnsByKey.value.get(columnKey);
259
+ if (!column) {
260
+ return;
192
261
  }
193
- filters.value = nextFilters;
262
+ const customFilterFunction = column.filterFunction ?? column.filterFn;
263
+ if (!customFilterFunction) {
264
+ return;
265
+ }
266
+ options.onManualFilterChange?.({
267
+ columnKey,
268
+ value,
269
+ column,
270
+ rows: [...options.rows.value],
271
+ filters: { ...filters.value }
272
+ });
194
273
  }
195
274
  function toggleColumn(columnKey) {
196
275
  if (enabledColumnKeys.value.includes(columnKey)) {
197
276
  if (enabledColumnKeys.value.length === 1) {
198
277
  return;
199
278
  }
200
- enabledColumnKeys.value = enabledColumnKeys.value.filter((key) => key !== columnKey);
279
+ enabledColumnKeys.value = enabledColumnKeys.value.filter(
280
+ (key) => key !== columnKey
281
+ );
201
282
  return;
202
283
  }
203
284
  enabledColumnKeys.value = [...enabledColumnKeys.value, columnKey];
204
285
  }
205
- function resetColumns() {
206
- columnOrder.value = [...availableColumnKeys.value];
207
- enabledColumnKeys.value = [...availableColumnKeys.value];
208
- }
209
286
  function onHeaderDragStart(columnKey) {
210
287
  if (!options.enableColumnDnd.value || activeResize.value) {
211
288
  return;
@@ -299,7 +376,10 @@ export function useNuxtTable(options) {
299
376
  return;
300
377
  }
301
378
  const delta = event.clientX - activeResize.value.startX;
302
- const nextWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(activeResize.value.startWidth + delta));
379
+ const nextWidth = Math.max(
380
+ MIN_COLUMN_WIDTH,
381
+ Math.round(activeResize.value.startWidth + delta)
382
+ );
303
383
  columnWidths.value = {
304
384
  ...columnWidths.value,
305
385
  [activeResize.value.columnKey]: nextWidth
@@ -393,7 +473,9 @@ export function useNuxtTable(options) {
393
473
  return value.length > 0;
394
474
  }
395
475
  if (typeof value === "object") {
396
- return Object.values(value).some((nestedValue) => isFilterActive(nestedValue));
476
+ return Object.values(value).some(
477
+ (nestedValue) => isFilterActive(nestedValue)
478
+ );
397
479
  }
398
480
  return true;
399
481
  }
@@ -430,16 +512,13 @@ export function useNuxtTable(options) {
430
512
  visibleColumns,
431
513
  sortedRows,
432
514
  filters,
433
- isColumnManagerOpen,
434
515
  enabledColumnKeys,
435
516
  dragSourceColumnKey,
436
517
  dragOverColumnKey,
437
518
  getSortDirection,
438
519
  toggleSort,
439
520
  setFilter,
440
- clearAllFilters,
441
521
  toggleColumn,
442
- resetColumns,
443
522
  onHeaderDragStart,
444
523
  onHeaderDragOver,
445
524
  onHeaderDragLeave,
@@ -1,2 +1,2 @@
1
- export { useNuxtTable } from './composables/useNuxtTable.js';
2
- export type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow, UseNuxtTableOptions, ValueResolver, } from './types/table.js';
1
+ export { useNuxtTable } from "./composables/useNuxtTable.js";
2
+ export type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow, UseNuxtTableOptions, ValueResolver, } from "./types/table.js";
@@ -1,4 +1,4 @@
1
- import type { Component, Ref } from 'vue';
1
+ import type { Component, Ref } from "vue";
2
2
  export type TableRow = Record<string, any>;
3
3
  export type ValueResolver = string | ((row: TableRow) => unknown);
4
4
  export interface NuxtTableColumn {
@@ -6,10 +6,15 @@ export interface NuxtTableColumn {
6
6
  label: string;
7
7
  sortable?: boolean;
8
8
  filterable?: boolean;
9
+ sortAscComponent?: Component;
10
+ sortDescComponent?: Component;
11
+ sortDefaultComponent?: Component;
9
12
  sortKey?: ValueResolver;
13
+ sortFunction?: (leftRow: TableRow, rightRow: TableRow, column: NuxtTableColumn, tableRows: TableRow[], direction: "asc" | "desc") => number;
10
14
  filterKey?: ValueResolver;
11
15
  formatter?: (value: unknown, row: TableRow) => string;
12
- filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn) => boolean;
16
+ filterFunction?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn, tableRows: TableRow[]) => boolean;
17
+ filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn, tableRows: TableRow[]) => boolean;
13
18
  cellComponent?: Component;
14
19
  filterComponent?: Component;
15
20
  headerClassName?: string;
@@ -21,6 +26,20 @@ export interface NuxtTableColumnOrderChange {
21
26
  fromIndex: number;
22
27
  toIndex: number;
23
28
  }
29
+ export interface NuxtTableManualSortChange {
30
+ columnKey: string;
31
+ direction: "asc" | "desc" | null;
32
+ column: NuxtTableColumn;
33
+ rows: TableRow[];
34
+ filters: Record<string, unknown>;
35
+ }
36
+ export interface NuxtTableManualFilterChange {
37
+ columnKey: string;
38
+ value: unknown;
39
+ column: NuxtTableColumn;
40
+ rows: TableRow[];
41
+ filters: Record<string, unknown>;
42
+ }
24
43
  export interface NuxtTableClassNames {
25
44
  root: string;
26
45
  toolbar: string;
@@ -53,4 +72,6 @@ export interface UseNuxtTableOptions {
53
72
  rowKey: Ref<string | ((row: TableRow, index: number) => string | number)>;
54
73
  enableColumnDnd: Ref<boolean>;
55
74
  onColumnOrderChange?: (payload: NuxtTableColumnOrderChange) => void;
75
+ onManualSortChange?: (payload: NuxtTableManualSortChange) => void;
76
+ onManualFilterChange?: (payload: NuxtTableManualFilterChange) => void;
56
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serhiitupilow/nuxt-table",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Nuxt module with a functional table component (sorting, filtering, column visibility, resize, optional DnD)",
5
5
  "type": "module",
6
6
  "license": "MIT",