@serhiitupilow/nuxt-table 0.1.3
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 +103 -0
- package/dist/module.d.mts +9 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +31 -0
- package/dist/runtime/assets/styles.css +1 -0
- package/dist/runtime/components/NuxtTable.d.vue.ts +26 -0
- package/dist/runtime/components/NuxtTable.vue +190 -0
- package/dist/runtime/components/NuxtTable.vue.d.ts +26 -0
- package/dist/runtime/components/NuxtTableBodyCell.d.vue.ts +12 -0
- package/dist/runtime/components/NuxtTableBodyCell.vue +27 -0
- package/dist/runtime/components/NuxtTableBodyCell.vue.d.ts +12 -0
- package/dist/runtime/components/NuxtTableHeaderCell.d.vue.ts +35 -0
- package/dist/runtime/components/NuxtTableHeaderCell.vue +71 -0
- package/dist/runtime/components/NuxtTableHeaderCell.vue.d.ts +35 -0
- package/dist/runtime/composables/useNuxtTable.d.ts +34 -0
- package/dist/runtime/composables/useNuxtTable.js +454 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/types/table.d.ts +56 -0
- package/dist/runtime/types/table.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @serhiitupilow/nuxt-table
|
|
2
|
+
|
|
3
|
+
Nuxt module that provides a single `NuxtTable` component with:
|
|
4
|
+
|
|
5
|
+
- sorting
|
|
6
|
+
- filtering
|
|
7
|
+
- column visibility manager
|
|
8
|
+
- column resize
|
|
9
|
+
- optional drag-and-drop column reordering
|
|
10
|
+
- class-based styling (no Tailwind classes in component templates)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Use any package manager:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i @serhiitupilow/nuxt-table
|
|
18
|
+
# or
|
|
19
|
+
yarn add @serhiitupilow/nuxt-table
|
|
20
|
+
# or
|
|
21
|
+
bun add @serhiitupilow/nuxt-table
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @serhiitupilow/nuxt-table
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Nuxt config
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default defineNuxtConfig({
|
|
30
|
+
modules: ["@serhiitupilow/nuxt-table"],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optional module config:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export default defineNuxtConfig({
|
|
38
|
+
modules: ["@serhiitupilow/nuxt-table"],
|
|
39
|
+
nuxtTable: {
|
|
40
|
+
injectDefaultStyles: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/dist/runtime/types/table";
|
|
50
|
+
|
|
51
|
+
const columns: NuxtTableColumn[] = [
|
|
52
|
+
{ key: "id", label: "ID", sortable: true, filterable: true },
|
|
53
|
+
{ key: "name", label: "Name", sortable: true, filterable: true },
|
|
54
|
+
{ key: "status", label: "Status", sortable: true, filterable: true },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const rows = [
|
|
58
|
+
{ id: 1, name: "Alice", status: "active" },
|
|
59
|
+
{ id: 2, name: "Bob", status: "paused" },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
function onColumnOrderChange(payload: {
|
|
63
|
+
order: string[];
|
|
64
|
+
movedKey: string;
|
|
65
|
+
fromIndex: number;
|
|
66
|
+
toIndex: number;
|
|
67
|
+
}) {
|
|
68
|
+
console.log("new order", payload.order);
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<NuxtTable
|
|
74
|
+
:columns="columns"
|
|
75
|
+
:rows="rows"
|
|
76
|
+
storage-key="users-table"
|
|
77
|
+
:enable-column-dnd="true"
|
|
78
|
+
@column-order-change="onColumnOrderChange"
|
|
79
|
+
/>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Styling
|
|
84
|
+
|
|
85
|
+
`NuxtTable` uses semantic class names (like `nuxt-table__header-cell`) and receives a `classNames` prop for overrides.
|
|
86
|
+
|
|
87
|
+
You can style globally in your project CSS:
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
.nuxt-table__header-cell {
|
|
91
|
+
background: #f8fafc;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or override class names from props:
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<NuxtTable
|
|
99
|
+
:columns="columns"
|
|
100
|
+
:rows="rows"
|
|
101
|
+
:class-names="{ table: 'my-table', headerCell: 'my-header-cell' }"
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
injectDefaultStyles: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
7
|
+
|
|
8
|
+
export { _default as default };
|
|
9
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module$1 = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "@serhiitupilow/nuxt-table",
|
|
6
|
+
configKey: "nuxtTable"
|
|
7
|
+
},
|
|
8
|
+
defaults: {
|
|
9
|
+
injectDefaultStyles: true
|
|
10
|
+
},
|
|
11
|
+
setup(options, nuxt) {
|
|
12
|
+
const resolver = createResolver(import.meta.url);
|
|
13
|
+
addComponentsDir({
|
|
14
|
+
path: resolver.resolve("./runtime/components"),
|
|
15
|
+
prefix: "",
|
|
16
|
+
pathPrefix: false,
|
|
17
|
+
global: true,
|
|
18
|
+
extensions: ["vue"],
|
|
19
|
+
transpile: true
|
|
20
|
+
});
|
|
21
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
22
|
+
if (options.injectDefaultStyles) {
|
|
23
|
+
const stylesPath = resolver.resolve("./runtime/assets/styles.css");
|
|
24
|
+
if (!nuxt.options.css.includes(stylesPath)) {
|
|
25
|
+
nuxt.options.css.push(stylesPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { module$1 as default };
|
|
@@ -0,0 +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}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
columns: NuxtTableColumn[];
|
|
4
|
+
rows: TableRow[];
|
|
5
|
+
storageKey?: string;
|
|
6
|
+
rowKey?: string | ((row: TableRow, index: number) => string | number);
|
|
7
|
+
title?: string;
|
|
8
|
+
showToolbar?: boolean;
|
|
9
|
+
enableColumnDnd?: boolean;
|
|
10
|
+
enableColumnResize?: boolean;
|
|
11
|
+
classNames?: Partial<NuxtTableClassNames>;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
+
columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
|
+
onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
storageKey: string;
|
|
19
|
+
rowKey: string | ((row: TableRow, index: number) => string | number);
|
|
20
|
+
title: string;
|
|
21
|
+
showToolbar: boolean;
|
|
22
|
+
enableColumnDnd: boolean;
|
|
23
|
+
enableColumnResize: boolean;
|
|
24
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const _default: typeof __VLS_export;
|
|
26
|
+
export default _default;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, toRef } from "vue";
|
|
3
|
+
import { useNuxtTable } from "../composables/useNuxtTable";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
columns: { type: Array, required: true },
|
|
6
|
+
rows: { type: Array, required: true },
|
|
7
|
+
storageKey: { type: String, required: false, default: "nuxt-table" },
|
|
8
|
+
rowKey: { type: [String, Function], required: false, default: "id" },
|
|
9
|
+
title: { type: String, required: false, default: "Table" },
|
|
10
|
+
showToolbar: { type: Boolean, required: false, default: true },
|
|
11
|
+
enableColumnDnd: { type: Boolean, required: false, default: false },
|
|
12
|
+
enableColumnResize: { type: Boolean, required: false, default: true },
|
|
13
|
+
classNames: { type: Object, required: false }
|
|
14
|
+
});
|
|
15
|
+
const emit = defineEmits(["columnOrderChange"]);
|
|
16
|
+
const defaultClassNames = {
|
|
17
|
+
root: "nuxt-table",
|
|
18
|
+
toolbar: "nuxt-table__toolbar",
|
|
19
|
+
toolbarTitle: "nuxt-table__toolbar-title",
|
|
20
|
+
toolbarActions: "nuxt-table__toolbar-actions",
|
|
21
|
+
toolbarButton: "nuxt-table__toolbar-button",
|
|
22
|
+
columnManager: "nuxt-table__column-manager",
|
|
23
|
+
columnManagerTitle: "nuxt-table__column-manager-title",
|
|
24
|
+
columnManagerItem: "nuxt-table__column-manager-item",
|
|
25
|
+
tableWrapper: "nuxt-table__wrapper",
|
|
26
|
+
table: "nuxt-table__table",
|
|
27
|
+
tableHead: "nuxt-table__head",
|
|
28
|
+
tableBody: "nuxt-table__body",
|
|
29
|
+
bodyRow: "nuxt-table__body-row",
|
|
30
|
+
emptyCell: "nuxt-table__empty-cell",
|
|
31
|
+
headerCell: "nuxt-table__header-cell",
|
|
32
|
+
headerCellDragSource: "nuxt-table__header-cell--drag-source",
|
|
33
|
+
headerCellDragOver: "nuxt-table__header-cell--drag-over",
|
|
34
|
+
headerTop: "nuxt-table__header-top",
|
|
35
|
+
headerLabel: "nuxt-table__header-label",
|
|
36
|
+
sortButton: "nuxt-table__sort-button",
|
|
37
|
+
filterInput: "nuxt-table__filter-input",
|
|
38
|
+
resizeHandle: "nuxt-table__resize-handle",
|
|
39
|
+
bodyCell: "nuxt-table__body-cell"
|
|
40
|
+
};
|
|
41
|
+
const mergedClassNames = computed(() => {
|
|
42
|
+
return {
|
|
43
|
+
...defaultClassNames,
|
|
44
|
+
...props.classNames ?? {}
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const {
|
|
48
|
+
orderedColumns,
|
|
49
|
+
visibleColumns,
|
|
50
|
+
sortedRows,
|
|
51
|
+
filters,
|
|
52
|
+
isColumnManagerOpen,
|
|
53
|
+
enabledColumnKeys,
|
|
54
|
+
dragSourceColumnKey,
|
|
55
|
+
dragOverColumnKey,
|
|
56
|
+
getSortDirection,
|
|
57
|
+
toggleSort,
|
|
58
|
+
setFilter,
|
|
59
|
+
clearAllFilters,
|
|
60
|
+
toggleColumn,
|
|
61
|
+
resetColumns,
|
|
62
|
+
onHeaderDragStart,
|
|
63
|
+
onHeaderDragOver,
|
|
64
|
+
onHeaderDragLeave,
|
|
65
|
+
onHeaderDrop,
|
|
66
|
+
onHeaderDragEnd,
|
|
67
|
+
getColumnStyle,
|
|
68
|
+
startColumnResize,
|
|
69
|
+
setHeaderElement,
|
|
70
|
+
resolveDisplayValue,
|
|
71
|
+
resolveRowKey
|
|
72
|
+
} = useNuxtTable({
|
|
73
|
+
columns: toRef(props, "columns"),
|
|
74
|
+
rows: toRef(props, "rows"),
|
|
75
|
+
storageKey: toRef(props, "storageKey"),
|
|
76
|
+
rowKey: toRef(props, "rowKey"),
|
|
77
|
+
enableColumnDnd: toRef(props, "enableColumnDnd"),
|
|
78
|
+
onColumnOrderChange: (payload) => {
|
|
79
|
+
emit("columnOrderChange", payload);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<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
|
+
<div :class="mergedClassNames.tableWrapper">
|
|
134
|
+
<table :class="mergedClassNames.table">
|
|
135
|
+
<thead :class="mergedClassNames.tableHead">
|
|
136
|
+
<tr>
|
|
137
|
+
<NuxtTableHeaderCell
|
|
138
|
+
v-for="column in visibleColumns"
|
|
139
|
+
:key="column.key"
|
|
140
|
+
:column="column"
|
|
141
|
+
:filter-value="filters[column.key]"
|
|
142
|
+
:column-style="getColumnStyle(column.key)"
|
|
143
|
+
:sort-direction="getSortDirection(column.key)"
|
|
144
|
+
:is-drag-source="dragSourceColumnKey === column.key"
|
|
145
|
+
:is-drag-over="dragOverColumnKey === column.key"
|
|
146
|
+
:is-dnd-enabled="props.enableColumnDnd"
|
|
147
|
+
:is-resize-enabled="props.enableColumnResize"
|
|
148
|
+
:class-names="mergedClassNames"
|
|
149
|
+
:set-header-element="setHeaderElement"
|
|
150
|
+
@drag-start="onHeaderDragStart"
|
|
151
|
+
@drag-over="onHeaderDragOver"
|
|
152
|
+
@drag-leave="onHeaderDragLeave"
|
|
153
|
+
@drop="onHeaderDrop"
|
|
154
|
+
@drag-end="onHeaderDragEnd"
|
|
155
|
+
@toggle-sort="toggleSort"
|
|
156
|
+
@set-filter="setFilter"
|
|
157
|
+
@resize-start="startColumnResize"
|
|
158
|
+
/>
|
|
159
|
+
</tr>
|
|
160
|
+
</thead>
|
|
161
|
+
<tbody :class="mergedClassNames.tableBody">
|
|
162
|
+
<tr
|
|
163
|
+
v-for="(row, rowIndex) in sortedRows"
|
|
164
|
+
:key="resolveRowKey(row, rowIndex)"
|
|
165
|
+
:class="mergedClassNames.bodyRow"
|
|
166
|
+
>
|
|
167
|
+
<NuxtTableBodyCell
|
|
168
|
+
v-for="column in visibleColumns"
|
|
169
|
+
:key="`${resolveRowKey(row, rowIndex)}-${column.key}`"
|
|
170
|
+
:row="row"
|
|
171
|
+
:row-key="resolveRowKey(row, rowIndex)"
|
|
172
|
+
:column="column"
|
|
173
|
+
:value="resolveDisplayValue(row, column)"
|
|
174
|
+
:column-style="getColumnStyle(column.key)"
|
|
175
|
+
:class-names="mergedClassNames"
|
|
176
|
+
/>
|
|
177
|
+
</tr>
|
|
178
|
+
<tr v-if="sortedRows.length === 0">
|
|
179
|
+
<td
|
|
180
|
+
:colspan="Math.max(visibleColumns.length, 1)"
|
|
181
|
+
:class="mergedClassNames.emptyCell"
|
|
182
|
+
>
|
|
183
|
+
No rows match the current filters.
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
</tbody>
|
|
187
|
+
</table>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
columns: NuxtTableColumn[];
|
|
4
|
+
rows: TableRow[];
|
|
5
|
+
storageKey?: string;
|
|
6
|
+
rowKey?: string | ((row: TableRow, index: number) => string | number);
|
|
7
|
+
title?: string;
|
|
8
|
+
showToolbar?: boolean;
|
|
9
|
+
enableColumnDnd?: boolean;
|
|
10
|
+
enableColumnResize?: boolean;
|
|
11
|
+
classNames?: Partial<NuxtTableClassNames>;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
+
columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
|
+
onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
storageKey: string;
|
|
19
|
+
rowKey: string | ((row: TableRow, index: number) => string | number);
|
|
20
|
+
title: string;
|
|
21
|
+
showToolbar: boolean;
|
|
22
|
+
enableColumnDnd: boolean;
|
|
23
|
+
enableColumnResize: boolean;
|
|
24
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const _default: typeof __VLS_export;
|
|
26
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NuxtTableClassNames, NuxtTableColumn, TableRow } from '../types/table.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
row: TableRow;
|
|
4
|
+
column: NuxtTableColumn;
|
|
5
|
+
rowKey: string | number;
|
|
6
|
+
value: unknown;
|
|
7
|
+
columnStyle: Record<string, string | undefined>;
|
|
8
|
+
classNames: NuxtTableClassNames;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
row: { type: Object, required: true },
|
|
4
|
+
column: { type: Object, required: true },
|
|
5
|
+
rowKey: { type: [String, Number], required: true },
|
|
6
|
+
value: { type: null, required: true },
|
|
7
|
+
columnStyle: { type: Object, required: true },
|
|
8
|
+
classNames: { type: Object, required: true }
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<td
|
|
14
|
+
:key="`${props.rowKey}-${props.column.key}`"
|
|
15
|
+
:style="props.columnStyle"
|
|
16
|
+
:class="[props.classNames.bodyCell, props.column.cellClassName]"
|
|
17
|
+
>
|
|
18
|
+
<component
|
|
19
|
+
:is="props.column.cellComponent"
|
|
20
|
+
v-if="props.column.cellComponent"
|
|
21
|
+
:row="props.row"
|
|
22
|
+
:column="props.column"
|
|
23
|
+
:value="props.value"
|
|
24
|
+
/>
|
|
25
|
+
<span v-else>{{ props.value }}</span>
|
|
26
|
+
</td>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NuxtTableClassNames, NuxtTableColumn, TableRow } from '../types/table.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
row: TableRow;
|
|
4
|
+
column: NuxtTableColumn;
|
|
5
|
+
rowKey: string | number;
|
|
6
|
+
value: unknown;
|
|
7
|
+
columnStyle: Record<string, string | undefined>;
|
|
8
|
+
classNames: NuxtTableClassNames;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentPublicInstance } from 'vue';
|
|
2
|
+
import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
column: NuxtTableColumn;
|
|
5
|
+
filterValue: unknown;
|
|
6
|
+
columnStyle: Record<string, string | undefined>;
|
|
7
|
+
sortDirection: 'asc' | 'desc' | null;
|
|
8
|
+
isDragSource: boolean;
|
|
9
|
+
isDragOver: boolean;
|
|
10
|
+
isDndEnabled: boolean;
|
|
11
|
+
isResizeEnabled: boolean;
|
|
12
|
+
classNames: NuxtTableClassNames;
|
|
13
|
+
setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
drop: (columnKey: string) => any;
|
|
17
|
+
toggleSort: (column: NuxtTableColumn) => any;
|
|
18
|
+
setFilter: (columnKey: string, value: unknown) => any;
|
|
19
|
+
dragStart: (columnKey: string) => any;
|
|
20
|
+
dragOver: (columnKey: string) => any;
|
|
21
|
+
dragLeave: (columnKey: string) => any;
|
|
22
|
+
dragEnd: () => any;
|
|
23
|
+
resizeStart: (event: MouseEvent, columnKey: string) => any;
|
|
24
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
25
|
+
onDrop?: ((columnKey: string) => any) | undefined;
|
|
26
|
+
onToggleSort?: ((column: NuxtTableColumn) => any) | undefined;
|
|
27
|
+
onSetFilter?: ((columnKey: string, value: unknown) => any) | undefined;
|
|
28
|
+
onDragStart?: ((columnKey: string) => any) | undefined;
|
|
29
|
+
onDragOver?: ((columnKey: string) => any) | undefined;
|
|
30
|
+
onDragLeave?: ((columnKey: string) => any) | undefined;
|
|
31
|
+
onDragEnd?: (() => any) | undefined;
|
|
32
|
+
onResizeStart?: ((event: MouseEvent, columnKey: string) => any) | undefined;
|
|
33
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
34
|
+
declare const _default: typeof __VLS_export;
|
|
35
|
+
export default _default;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
column: { type: Object, required: true },
|
|
4
|
+
filterValue: { type: null, required: true },
|
|
5
|
+
columnStyle: { type: Object, required: true },
|
|
6
|
+
sortDirection: { type: [String, null], required: true },
|
|
7
|
+
isDragSource: { type: Boolean, required: true },
|
|
8
|
+
isDragOver: { type: Boolean, required: true },
|
|
9
|
+
isDndEnabled: { type: Boolean, required: true },
|
|
10
|
+
isResizeEnabled: { type: Boolean, required: true },
|
|
11
|
+
classNames: { type: Object, required: true },
|
|
12
|
+
setHeaderElement: { type: Function, required: true }
|
|
13
|
+
});
|
|
14
|
+
const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd", "toggleSort", "setFilter", "resizeStart"]);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<th
|
|
19
|
+
:ref="(element) => props.setHeaderElement(props.column.key, element)"
|
|
20
|
+
:style="props.columnStyle"
|
|
21
|
+
:class="[
|
|
22
|
+
props.classNames.headerCell,
|
|
23
|
+
props.column.headerClassName,
|
|
24
|
+
props.isDragSource ? props.classNames.headerCellDragSource : '',
|
|
25
|
+
props.isDragOver && !props.isDragSource ? props.classNames.headerCellDragOver : ''
|
|
26
|
+
]"
|
|
27
|
+
: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"
|
|
32
|
+
@drop="props.isDndEnabled ? emit('drop', props.column.key) : void 0"
|
|
33
|
+
@dragend="props.isDndEnabled ? emit('dragEnd') : void 0"
|
|
34
|
+
>
|
|
35
|
+
<div :class="props.classNames.headerTop">
|
|
36
|
+
<span :class="props.classNames.headerLabel">{{ props.column.label }}</span>
|
|
37
|
+
<button
|
|
38
|
+
v-if="props.column.sortable"
|
|
39
|
+
type="button"
|
|
40
|
+
:class="props.classNames.sortButton"
|
|
41
|
+
@click="emit('toggleSort', props.column)"
|
|
42
|
+
>
|
|
43
|
+
<span v-if="props.sortDirection === 'asc'">Asc</span>
|
|
44
|
+
<span v-else-if="props.sortDirection === 'desc'">Desc</span>
|
|
45
|
+
<span v-else>Sort</span>
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<component
|
|
50
|
+
:is="props.column.filterComponent"
|
|
51
|
+
v-if="props.column.filterComponent"
|
|
52
|
+
:model-value="props.filterValue"
|
|
53
|
+
:column="props.column"
|
|
54
|
+
@update:model-value="emit('setFilter', props.column.key, $event)"
|
|
55
|
+
/>
|
|
56
|
+
<input
|
|
57
|
+
v-else-if="props.column.filterable"
|
|
58
|
+
:value="String(props.filterValue ?? '')"
|
|
59
|
+
type="text"
|
|
60
|
+
:class="props.classNames.filterInput"
|
|
61
|
+
:placeholder="`Filter ${props.column.label}`"
|
|
62
|
+
@input="emit('setFilter', props.column.key, $event.target.value)"
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
v-if="props.isResizeEnabled"
|
|
67
|
+
:class="props.classNames.resizeHandle"
|
|
68
|
+
@mousedown.stop.prevent="emit('resizeStart', $event, props.column.key)"
|
|
69
|
+
/>
|
|
70
|
+
</th>
|
|
71
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentPublicInstance } from 'vue';
|
|
2
|
+
import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
column: NuxtTableColumn;
|
|
5
|
+
filterValue: unknown;
|
|
6
|
+
columnStyle: Record<string, string | undefined>;
|
|
7
|
+
sortDirection: 'asc' | 'desc' | null;
|
|
8
|
+
isDragSource: boolean;
|
|
9
|
+
isDragOver: boolean;
|
|
10
|
+
isDndEnabled: boolean;
|
|
11
|
+
isResizeEnabled: boolean;
|
|
12
|
+
classNames: NuxtTableClassNames;
|
|
13
|
+
setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
drop: (columnKey: string) => any;
|
|
17
|
+
toggleSort: (column: NuxtTableColumn) => any;
|
|
18
|
+
setFilter: (columnKey: string, value: unknown) => any;
|
|
19
|
+
dragStart: (columnKey: string) => any;
|
|
20
|
+
dragOver: (columnKey: string) => any;
|
|
21
|
+
dragLeave: (columnKey: string) => any;
|
|
22
|
+
dragEnd: () => any;
|
|
23
|
+
resizeStart: (event: MouseEvent, columnKey: string) => any;
|
|
24
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
25
|
+
onDrop?: ((columnKey: string) => any) | undefined;
|
|
26
|
+
onToggleSort?: ((column: NuxtTableColumn) => any) | undefined;
|
|
27
|
+
onSetFilter?: ((columnKey: string, value: unknown) => any) | undefined;
|
|
28
|
+
onDragStart?: ((columnKey: string) => any) | undefined;
|
|
29
|
+
onDragOver?: ((columnKey: string) => any) | undefined;
|
|
30
|
+
onDragLeave?: ((columnKey: string) => any) | undefined;
|
|
31
|
+
onDragEnd?: (() => any) | undefined;
|
|
32
|
+
onResizeStart?: ((event: MouseEvent, columnKey: string) => any) | undefined;
|
|
33
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
34
|
+
declare const _default: typeof __VLS_export;
|
|
35
|
+
export default _default;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ComponentPublicInstance } from 'vue';
|
|
2
|
+
import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from '../types/table.js';
|
|
3
|
+
export declare function useNuxtTable(options: UseNuxtTableOptions): {
|
|
4
|
+
orderedColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
|
|
5
|
+
visibleColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
|
|
6
|
+
sortedRows: import("vue").ComputedRef<TableRow[]>;
|
|
7
|
+
filters: import("vue").Ref<Record<string, unknown>, Record<string, unknown>>;
|
|
8
|
+
isColumnManagerOpen: import("vue").Ref<boolean, boolean>;
|
|
9
|
+
enabledColumnKeys: import("vue").Ref<string[], string[]>;
|
|
10
|
+
dragSourceColumnKey: import("vue").Ref<string | null, string | null>;
|
|
11
|
+
dragOverColumnKey: import("vue").Ref<string | null, string | null>;
|
|
12
|
+
getSortDirection: (columnKey: string) => "asc" | "desc" | null;
|
|
13
|
+
toggleSort: (column: NuxtTableColumn) => void;
|
|
14
|
+
setFilter: (columnKey: string, value: unknown) => void;
|
|
15
|
+
clearAllFilters: () => void;
|
|
16
|
+
toggleColumn: (columnKey: string) => void;
|
|
17
|
+
resetColumns: () => void;
|
|
18
|
+
onHeaderDragStart: (columnKey: string) => void;
|
|
19
|
+
onHeaderDragOver: (columnKey: string) => void;
|
|
20
|
+
onHeaderDragLeave: (columnKey: string) => void;
|
|
21
|
+
onHeaderDrop: (targetColumnKey: string) => Promise<void>;
|
|
22
|
+
onHeaderDragEnd: () => void;
|
|
23
|
+
getColumnStyle: (columnKey: string) => {
|
|
24
|
+
width?: undefined;
|
|
25
|
+
minWidth?: undefined;
|
|
26
|
+
} | {
|
|
27
|
+
width: string;
|
|
28
|
+
minWidth: string;
|
|
29
|
+
};
|
|
30
|
+
startColumnResize: (event: MouseEvent, columnKey: string) => void;
|
|
31
|
+
setHeaderElement: (columnKey: string, element: Element | ComponentPublicInstance | null) => void;
|
|
32
|
+
resolveDisplayValue: (row: TableRow, column: NuxtTableColumn) => any;
|
|
33
|
+
resolveRowKey: (row: TableRow, index: number) => any;
|
|
34
|
+
};
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
2
|
+
const MIN_COLUMN_WIDTH = 140;
|
|
3
|
+
export function useNuxtTable(options) {
|
|
4
|
+
const columnOrder = ref([]);
|
|
5
|
+
const enabledColumnKeys = ref([]);
|
|
6
|
+
const sortState = ref(null);
|
|
7
|
+
const filters = ref({});
|
|
8
|
+
const isColumnManagerOpen = ref(false);
|
|
9
|
+
const dragSourceColumnKey = ref(null);
|
|
10
|
+
const dragOverColumnKey = ref(null);
|
|
11
|
+
const hasLoadedPersistence = ref(false);
|
|
12
|
+
const headerElements = ref({});
|
|
13
|
+
const columnWidths = ref({});
|
|
14
|
+
const activeResize = ref(null);
|
|
15
|
+
const availableColumnKeys = computed(() => options.columns.value.map((column) => column.key));
|
|
16
|
+
const columnsByKey = computed(() => {
|
|
17
|
+
return new Map(options.columns.value.map((column) => [column.key, column]));
|
|
18
|
+
});
|
|
19
|
+
const orderedColumns = computed(() => {
|
|
20
|
+
return columnOrder.value.map((columnKey) => columnsByKey.value.get(columnKey)).filter((column) => Boolean(column));
|
|
21
|
+
});
|
|
22
|
+
const visibleColumns = computed(() => {
|
|
23
|
+
return orderedColumns.value.filter((column) => enabledColumnKeys.value.includes(column.key));
|
|
24
|
+
});
|
|
25
|
+
const filteredRows = computed(() => {
|
|
26
|
+
return options.rows.value.filter((row) => {
|
|
27
|
+
return orderedColumns.value.every((column) => {
|
|
28
|
+
const filterValue = filters.value[column.key];
|
|
29
|
+
if (!isFilterActive(filterValue)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (column.filterFn) {
|
|
33
|
+
return column.filterFn(row, filterValue, column);
|
|
34
|
+
}
|
|
35
|
+
const candidate = resolveColumnValue(row, column.filterKey ?? column.key);
|
|
36
|
+
const candidateText = String(candidate ?? "").toLowerCase();
|
|
37
|
+
const filterText = String(filterValue ?? "").toLowerCase();
|
|
38
|
+
return candidateText.includes(filterText);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
const sortedRows = computed(() => {
|
|
43
|
+
if (!sortState.value) {
|
|
44
|
+
return filteredRows.value;
|
|
45
|
+
}
|
|
46
|
+
const activeColumn = columnsByKey.value.get(sortState.value.key);
|
|
47
|
+
if (!activeColumn) {
|
|
48
|
+
return filteredRows.value;
|
|
49
|
+
}
|
|
50
|
+
const directionMultiplier = sortState.value.direction === "asc" ? 1 : -1;
|
|
51
|
+
const accessor = activeColumn.sortKey ?? activeColumn.key;
|
|
52
|
+
return [...filteredRows.value].sort((leftRow, rightRow) => {
|
|
53
|
+
const leftValue = resolveColumnValue(leftRow, accessor);
|
|
54
|
+
const rightValue = resolveColumnValue(rightRow, accessor);
|
|
55
|
+
return compareValues(leftValue, rightValue) * directionMultiplier;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
initializeColumnState();
|
|
60
|
+
loadPersistedState();
|
|
61
|
+
hasLoadedPersistence.value = true;
|
|
62
|
+
});
|
|
63
|
+
watch(
|
|
64
|
+
() => options.columns.value,
|
|
65
|
+
() => {
|
|
66
|
+
initializeColumnState();
|
|
67
|
+
},
|
|
68
|
+
{ deep: true }
|
|
69
|
+
);
|
|
70
|
+
watch(
|
|
71
|
+
[columnOrder, enabledColumnKeys, columnWidths],
|
|
72
|
+
() => {
|
|
73
|
+
if (!hasLoadedPersistence.value || !import.meta.client) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
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));
|
|
79
|
+
},
|
|
80
|
+
{ deep: true }
|
|
81
|
+
);
|
|
82
|
+
onBeforeUnmount(() => {
|
|
83
|
+
stopResizing();
|
|
84
|
+
});
|
|
85
|
+
function initializeColumnState() {
|
|
86
|
+
const currentKeys = availableColumnKeys.value;
|
|
87
|
+
if (!columnOrder.value.length) {
|
|
88
|
+
columnOrder.value = [...currentKeys];
|
|
89
|
+
} else {
|
|
90
|
+
const currentKeySet = new Set(currentKeys);
|
|
91
|
+
const keptKeys = columnOrder.value.filter((key) => currentKeySet.has(key));
|
|
92
|
+
const newKeys = currentKeys.filter((key) => !keptKeys.includes(key));
|
|
93
|
+
columnOrder.value = [...keptKeys, ...newKeys];
|
|
94
|
+
}
|
|
95
|
+
if (!enabledColumnKeys.value.length) {
|
|
96
|
+
enabledColumnKeys.value = [...currentKeys];
|
|
97
|
+
} else {
|
|
98
|
+
const currentKeySet = new Set(currentKeys);
|
|
99
|
+
const keptEnabledKeys = enabledColumnKeys.value.filter((key) => currentKeySet.has(key));
|
|
100
|
+
const missingEnabledKeys = currentKeys.filter((key) => !keptEnabledKeys.includes(key));
|
|
101
|
+
enabledColumnKeys.value = [...keptEnabledKeys, ...missingEnabledKeys];
|
|
102
|
+
}
|
|
103
|
+
const nextFilters = {};
|
|
104
|
+
for (const key of currentKeys) {
|
|
105
|
+
nextFilters[key] = filters.value[key] ?? "";
|
|
106
|
+
}
|
|
107
|
+
filters.value = nextFilters;
|
|
108
|
+
const nextWidths = {};
|
|
109
|
+
for (const key of currentKeys) {
|
|
110
|
+
const width = columnWidths.value[key];
|
|
111
|
+
if (typeof width === "number" && Number.isFinite(width)) {
|
|
112
|
+
nextWidths[key] = width;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
columnWidths.value = nextWidths;
|
|
116
|
+
}
|
|
117
|
+
function loadPersistedState() {
|
|
118
|
+
if (!import.meta.client) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const persistedOrder = localStorage.getItem(buildStorageKey("order"));
|
|
123
|
+
if (persistedOrder) {
|
|
124
|
+
const parsedOrder = JSON.parse(persistedOrder);
|
|
125
|
+
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));
|
|
130
|
+
columnOrder.value = [...validPersistedOrder, ...missingKeys];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const persistedEnabledColumns = localStorage.getItem(buildStorageKey("enabledColumns"));
|
|
134
|
+
if (persistedEnabledColumns) {
|
|
135
|
+
const parsedEnabledColumns = JSON.parse(persistedEnabledColumns);
|
|
136
|
+
if (Array.isArray(parsedEnabledColumns)) {
|
|
137
|
+
enabledColumnKeys.value = parsedEnabledColumns.filter((key) => {
|
|
138
|
+
return typeof key === "string" && availableColumnKeys.value.includes(key);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const persistedWidths = localStorage.getItem(buildStorageKey("widths"));
|
|
143
|
+
if (persistedWidths) {
|
|
144
|
+
const parsedWidths = JSON.parse(persistedWidths);
|
|
145
|
+
if (parsedWidths && typeof parsedWidths === "object") {
|
|
146
|
+
const nextWidths = {};
|
|
147
|
+
for (const key of availableColumnKeys.value) {
|
|
148
|
+
const width = parsedWidths[key];
|
|
149
|
+
if (typeof width === "number" && Number.isFinite(width) && width >= MIN_COLUMN_WIDTH) {
|
|
150
|
+
nextWidths[key] = width;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
columnWidths.value = nextWidths;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
columnOrder.value = [...availableColumnKeys.value];
|
|
158
|
+
enabledColumnKeys.value = [...availableColumnKeys.value];
|
|
159
|
+
columnWidths.value = {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function buildStorageKey(segment) {
|
|
163
|
+
return `${options.storageKey.value}:${segment}`;
|
|
164
|
+
}
|
|
165
|
+
function getSortDirection(columnKey) {
|
|
166
|
+
if (sortState.value?.key !== columnKey) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return sortState.value.direction;
|
|
170
|
+
}
|
|
171
|
+
function toggleSort(column) {
|
|
172
|
+
if (!column.sortable) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!sortState.value || sortState.value.key !== column.key) {
|
|
176
|
+
sortState.value = { key: column.key, direction: "asc" };
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (sortState.value.direction === "asc") {
|
|
180
|
+
sortState.value = { key: column.key, direction: "desc" };
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
sortState.value = null;
|
|
184
|
+
}
|
|
185
|
+
function setFilter(columnKey, value) {
|
|
186
|
+
filters.value[columnKey] = value;
|
|
187
|
+
}
|
|
188
|
+
function clearAllFilters() {
|
|
189
|
+
const nextFilters = {};
|
|
190
|
+
for (const key of availableColumnKeys.value) {
|
|
191
|
+
nextFilters[key] = "";
|
|
192
|
+
}
|
|
193
|
+
filters.value = nextFilters;
|
|
194
|
+
}
|
|
195
|
+
function toggleColumn(columnKey) {
|
|
196
|
+
if (enabledColumnKeys.value.includes(columnKey)) {
|
|
197
|
+
if (enabledColumnKeys.value.length === 1) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
enabledColumnKeys.value = enabledColumnKeys.value.filter((key) => key !== columnKey);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
enabledColumnKeys.value = [...enabledColumnKeys.value, columnKey];
|
|
204
|
+
}
|
|
205
|
+
function resetColumns() {
|
|
206
|
+
columnOrder.value = [...availableColumnKeys.value];
|
|
207
|
+
enabledColumnKeys.value = [...availableColumnKeys.value];
|
|
208
|
+
}
|
|
209
|
+
function onHeaderDragStart(columnKey) {
|
|
210
|
+
if (!options.enableColumnDnd.value || activeResize.value) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
dragSourceColumnKey.value = columnKey;
|
|
214
|
+
}
|
|
215
|
+
function onHeaderDragOver(columnKey) {
|
|
216
|
+
if (!options.enableColumnDnd.value) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!dragSourceColumnKey.value || dragSourceColumnKey.value === columnKey) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
dragOverColumnKey.value = columnKey;
|
|
223
|
+
}
|
|
224
|
+
function onHeaderDragLeave(columnKey) {
|
|
225
|
+
if (dragOverColumnKey.value === columnKey) {
|
|
226
|
+
dragOverColumnKey.value = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function onHeaderDrop(targetColumnKey) {
|
|
230
|
+
if (!options.enableColumnDnd.value) {
|
|
231
|
+
dragSourceColumnKey.value = null;
|
|
232
|
+
dragOverColumnKey.value = null;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!dragSourceColumnKey.value || dragSourceColumnKey.value === targetColumnKey) {
|
|
236
|
+
dragSourceColumnKey.value = null;
|
|
237
|
+
dragOverColumnKey.value = null;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const beforeRects = getHeaderRects();
|
|
241
|
+
const sourceIndex = columnOrder.value.indexOf(dragSourceColumnKey.value);
|
|
242
|
+
const targetIndex = columnOrder.value.indexOf(targetColumnKey);
|
|
243
|
+
if (sourceIndex < 0 || targetIndex < 0) {
|
|
244
|
+
dragSourceColumnKey.value = null;
|
|
245
|
+
dragOverColumnKey.value = null;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const nextOrder = [...columnOrder.value];
|
|
249
|
+
const [moved] = nextOrder.splice(sourceIndex, 1);
|
|
250
|
+
if (!moved) {
|
|
251
|
+
dragSourceColumnKey.value = null;
|
|
252
|
+
dragOverColumnKey.value = null;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
nextOrder.splice(targetIndex, 0, moved);
|
|
256
|
+
columnOrder.value = nextOrder;
|
|
257
|
+
await nextTick();
|
|
258
|
+
animateHeaderReorder(beforeRects);
|
|
259
|
+
const payload = {
|
|
260
|
+
order: [...nextOrder],
|
|
261
|
+
movedKey: moved,
|
|
262
|
+
fromIndex: sourceIndex,
|
|
263
|
+
toIndex: targetIndex
|
|
264
|
+
};
|
|
265
|
+
options.onColumnOrderChange?.(payload);
|
|
266
|
+
dragSourceColumnKey.value = null;
|
|
267
|
+
dragOverColumnKey.value = null;
|
|
268
|
+
}
|
|
269
|
+
function onHeaderDragEnd() {
|
|
270
|
+
dragSourceColumnKey.value = null;
|
|
271
|
+
dragOverColumnKey.value = null;
|
|
272
|
+
}
|
|
273
|
+
function getColumnStyle(columnKey) {
|
|
274
|
+
const width = columnWidths.value[columnKey];
|
|
275
|
+
if (!width) {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
const safeWidth = Math.max(MIN_COLUMN_WIDTH, width);
|
|
279
|
+
return {
|
|
280
|
+
width: `${safeWidth}px`,
|
|
281
|
+
minWidth: `${safeWidth}px`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function startColumnResize(event, columnKey) {
|
|
285
|
+
if (!import.meta.client) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const currentWidth = columnWidths.value[columnKey] ?? headerElements.value[columnKey]?.getBoundingClientRect().width ?? MIN_COLUMN_WIDTH;
|
|
289
|
+
activeResize.value = {
|
|
290
|
+
columnKey,
|
|
291
|
+
startX: event.clientX,
|
|
292
|
+
startWidth: Math.max(MIN_COLUMN_WIDTH, currentWidth)
|
|
293
|
+
};
|
|
294
|
+
window.addEventListener("mousemove", onColumnResizeMove);
|
|
295
|
+
window.addEventListener("mouseup", onColumnResizeEnd);
|
|
296
|
+
}
|
|
297
|
+
function onColumnResizeMove(event) {
|
|
298
|
+
if (!activeResize.value) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const delta = event.clientX - activeResize.value.startX;
|
|
302
|
+
const nextWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(activeResize.value.startWidth + delta));
|
|
303
|
+
columnWidths.value = {
|
|
304
|
+
...columnWidths.value,
|
|
305
|
+
[activeResize.value.columnKey]: nextWidth
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function onColumnResizeEnd() {
|
|
309
|
+
stopResizing();
|
|
310
|
+
}
|
|
311
|
+
function stopResizing() {
|
|
312
|
+
if (!import.meta.client) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
activeResize.value = null;
|
|
316
|
+
window.removeEventListener("mousemove", onColumnResizeMove);
|
|
317
|
+
window.removeEventListener("mouseup", onColumnResizeEnd);
|
|
318
|
+
}
|
|
319
|
+
function setHeaderElement(columnKey, element) {
|
|
320
|
+
headerElements.value[columnKey] = element instanceof HTMLTableCellElement ? element : null;
|
|
321
|
+
}
|
|
322
|
+
function getHeaderRects() {
|
|
323
|
+
const rectMap = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const column of visibleColumns.value) {
|
|
325
|
+
const element = headerElements.value[column.key];
|
|
326
|
+
if (element) {
|
|
327
|
+
rectMap.set(column.key, element.getBoundingClientRect());
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return rectMap;
|
|
331
|
+
}
|
|
332
|
+
function animateHeaderReorder(beforeRects) {
|
|
333
|
+
if (!import.meta.client || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
for (const [columnKey, beforeRect] of beforeRects.entries()) {
|
|
337
|
+
const element = headerElements.value[columnKey];
|
|
338
|
+
if (!element) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const afterRect = element.getBoundingClientRect();
|
|
342
|
+
const deltaX = beforeRect.left - afterRect.left;
|
|
343
|
+
if (Math.abs(deltaX) < 1) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
element.animate(
|
|
347
|
+
[
|
|
348
|
+
{ transform: `translateX(${deltaX}px)` },
|
|
349
|
+
{ transform: "translateX(0)" }
|
|
350
|
+
],
|
|
351
|
+
{
|
|
352
|
+
duration: 220,
|
|
353
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)"
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function resolveDisplayValue(row, column) {
|
|
359
|
+
const value = resolveColumnValue(row, column.key);
|
|
360
|
+
if (column.formatter) {
|
|
361
|
+
return column.formatter(value, row);
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
function resolveRowKey(row, index) {
|
|
366
|
+
if (typeof options.rowKey.value === "function") {
|
|
367
|
+
return options.rowKey.value(row, index);
|
|
368
|
+
}
|
|
369
|
+
return row[options.rowKey.value] ?? index;
|
|
370
|
+
}
|
|
371
|
+
function resolveColumnValue(row, resolver) {
|
|
372
|
+
if (typeof resolver === "function") {
|
|
373
|
+
return resolver(row);
|
|
374
|
+
}
|
|
375
|
+
const pathParts = resolver.split(".");
|
|
376
|
+
let currentValue = row;
|
|
377
|
+
for (const pathPart of pathParts) {
|
|
378
|
+
if (currentValue == null) {
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
currentValue = currentValue[pathPart];
|
|
382
|
+
}
|
|
383
|
+
return currentValue;
|
|
384
|
+
}
|
|
385
|
+
function isFilterActive(value) {
|
|
386
|
+
if (value == null) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (typeof value === "string") {
|
|
390
|
+
return value.trim().length > 0;
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
return value.length > 0;
|
|
394
|
+
}
|
|
395
|
+
if (typeof value === "object") {
|
|
396
|
+
return Object.values(value).some((nestedValue) => isFilterActive(nestedValue));
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
function compareValues(leftValue, rightValue) {
|
|
401
|
+
if (leftValue == null && rightValue == null) {
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
if (leftValue == null) {
|
|
405
|
+
return -1;
|
|
406
|
+
}
|
|
407
|
+
if (rightValue == null) {
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
411
|
+
return leftValue - rightValue;
|
|
412
|
+
}
|
|
413
|
+
const leftDate = leftValue instanceof Date ? leftValue.getTime() : null;
|
|
414
|
+
const rightDate = rightValue instanceof Date ? rightValue.getTime() : null;
|
|
415
|
+
if (leftDate !== null && rightDate !== null) {
|
|
416
|
+
return leftDate - rightDate;
|
|
417
|
+
}
|
|
418
|
+
const leftText = String(leftValue).toLowerCase();
|
|
419
|
+
const rightText = String(rightValue).toLowerCase();
|
|
420
|
+
if (leftText < rightText) {
|
|
421
|
+
return -1;
|
|
422
|
+
}
|
|
423
|
+
if (leftText > rightText) {
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
orderedColumns,
|
|
430
|
+
visibleColumns,
|
|
431
|
+
sortedRows,
|
|
432
|
+
filters,
|
|
433
|
+
isColumnManagerOpen,
|
|
434
|
+
enabledColumnKeys,
|
|
435
|
+
dragSourceColumnKey,
|
|
436
|
+
dragOverColumnKey,
|
|
437
|
+
getSortDirection,
|
|
438
|
+
toggleSort,
|
|
439
|
+
setFilter,
|
|
440
|
+
clearAllFilters,
|
|
441
|
+
toggleColumn,
|
|
442
|
+
resetColumns,
|
|
443
|
+
onHeaderDragStart,
|
|
444
|
+
onHeaderDragOver,
|
|
445
|
+
onHeaderDragLeave,
|
|
446
|
+
onHeaderDrop,
|
|
447
|
+
onHeaderDragEnd,
|
|
448
|
+
getColumnStyle,
|
|
449
|
+
startColumnResize,
|
|
450
|
+
setHeaderElement,
|
|
451
|
+
resolveDisplayValue,
|
|
452
|
+
resolveRowKey
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useNuxtTable } from "./composables/useNuxtTable.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Component, Ref } from 'vue';
|
|
2
|
+
export type TableRow = Record<string, any>;
|
|
3
|
+
export type ValueResolver = string | ((row: TableRow) => unknown);
|
|
4
|
+
export interface NuxtTableColumn {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
filterable?: boolean;
|
|
9
|
+
sortKey?: ValueResolver;
|
|
10
|
+
filterKey?: ValueResolver;
|
|
11
|
+
formatter?: (value: unknown, row: TableRow) => string;
|
|
12
|
+
filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn) => boolean;
|
|
13
|
+
cellComponent?: Component;
|
|
14
|
+
filterComponent?: Component;
|
|
15
|
+
headerClassName?: string;
|
|
16
|
+
cellClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface NuxtTableColumnOrderChange {
|
|
19
|
+
order: string[];
|
|
20
|
+
movedKey: string;
|
|
21
|
+
fromIndex: number;
|
|
22
|
+
toIndex: number;
|
|
23
|
+
}
|
|
24
|
+
export interface NuxtTableClassNames {
|
|
25
|
+
root: string;
|
|
26
|
+
toolbar: string;
|
|
27
|
+
toolbarTitle: string;
|
|
28
|
+
toolbarActions: string;
|
|
29
|
+
toolbarButton: string;
|
|
30
|
+
columnManager: string;
|
|
31
|
+
columnManagerTitle: string;
|
|
32
|
+
columnManagerItem: string;
|
|
33
|
+
tableWrapper: string;
|
|
34
|
+
table: string;
|
|
35
|
+
tableHead: string;
|
|
36
|
+
tableBody: string;
|
|
37
|
+
bodyRow: string;
|
|
38
|
+
emptyCell: string;
|
|
39
|
+
headerCell: string;
|
|
40
|
+
headerCellDragSource: string;
|
|
41
|
+
headerCellDragOver: string;
|
|
42
|
+
headerTop: string;
|
|
43
|
+
headerLabel: string;
|
|
44
|
+
sortButton: string;
|
|
45
|
+
filterInput: string;
|
|
46
|
+
resizeHandle: string;
|
|
47
|
+
bodyCell: string;
|
|
48
|
+
}
|
|
49
|
+
export interface UseNuxtTableOptions {
|
|
50
|
+
columns: Ref<NuxtTableColumn[]>;
|
|
51
|
+
rows: Ref<TableRow[]>;
|
|
52
|
+
storageKey: Ref<string>;
|
|
53
|
+
rowKey: Ref<string | ((row: TableRow, index: number) => string | number)>;
|
|
54
|
+
enableColumnDnd: Ref<boolean>;
|
|
55
|
+
onColumnOrderChange?: (payload: NuxtTableColumnOrderChange) => void;
|
|
56
|
+
}
|
|
File without changes
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@serhiitupilow/nuxt-table",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Nuxt module with a functional table component (sorting, filtering, column visibility, resize, optional DnD)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/module.mjs",
|
|
8
|
+
"types": "./dist/types.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types.d.mts",
|
|
12
|
+
"import": "./dist/module.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./runtime": {
|
|
15
|
+
"types": "./dist/runtime/index.d.ts",
|
|
16
|
+
"import": "./dist/runtime/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "nuxt-module-build build",
|
|
27
|
+
"dev": "nuxt-module-build dev",
|
|
28
|
+
"prepack": "nuxt-module-build build"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"nuxt": ">=3.11.0",
|
|
32
|
+
"vue": ">=3.4.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@nuxt/kit": "^4.3.1",
|
|
36
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
37
|
+
"nuxt": "^4.3.1",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vue": "^3.5.28",
|
|
40
|
+
"vue-tsc": "^3.2.5"
|
|
41
|
+
}
|
|
42
|
+
}
|