nexa-ui-kit 0.7.11 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/NButton.js +224 -224
- package/dist/components/NButton.nexa +274 -274
- package/dist/components/NCard.js +2 -2
- package/dist/components/NDataTable.js +58 -60
- package/dist/components/NDataTable.nexa +203 -204
- package/dist/components/NInputNumber.js +16 -16
- package/dist/components/NInputNumber.nexa +232 -232
- package/dist/components/NModal.js +130 -130
- package/dist/components/NModal.nexa +226 -226
- package/package.json +4 -4
- package/src/components/NButton.nexa +274 -274
- package/src/components/NDataTable.nexa +203 -204
- package/src/components/NInputNumber.nexa +232 -232
- package/src/components/NModal.nexa +226 -226
|
@@ -1,204 +1,203 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { signal, computed, effect } from 'nexa-framework'
|
|
3
|
-
import NPaginator from './NPaginator.nexa'
|
|
4
|
-
const props = defineProps({ value: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] }, stripedRows: { type: Boolean, default: false }, hoverRows: { type: Boolean, default: true }, size: { type: String, default: 'md' }, scrollable: { type: Boolean, default: true }, paginator: { type: Boolean, default: true }, rows: { type: Number, default: 10 }, first: { type: Number, default: 0 }, rowsPerPageOptions: { type: Array, default: () => [10, 20, 50] }, selectionMode: { type: String, default: '' }, selection: { type: null, default: null }, dataKey: { type: String, default: '' }, sortField: { type: String, default: '' }, sortOrder: { type: Number, default: 1 }, resizableColumns: { type: Boolean, default: true }, columnResizeMode: { type: String, default: 'fit' }, filterDisplay: { type: String, default: 'row' }, globalFilter: { type: String, default: '' }, globalFilterFields: { type: Array, default: null }, filters: { type: null, default: null }, lazy: { type: Boolean, default: false }, totalRecords: { type: Number, default: 0 }, emptyMessage: { type: String, default: 'No records found' }, showGridlines: { type: Boolean, default: false } })
|
|
5
|
-
const emit = defineEmits(['update:first', 'update:rows', 'update:selection', 'update:sortField', 'update:sortOrder', 'update:globalFilter', 'update:filters', 'updateFirst', 'updateRows', 'updateSelection', 'updateSortField', 'updateSortOrder', 'updateGlobalFilter', 'updateFilters', 'page', 'rowSelect', 'rowUnselect', 'sort', 'filter'])
|
|
6
|
-
const internalFirst = signal(props.first || 0)
|
|
7
|
-
const internalRows = signal(props.rows || 10)
|
|
8
|
-
const internalSortField = signal(props.sortField || '')
|
|
9
|
-
const internalSortOrder = signal(props.sortOrder || 1)
|
|
10
|
-
const internalFilters = signal((props.filters && typeof props.filters === 'object') ? props.filters : { __global: props.globalFilter || '' })
|
|
11
|
-
const effectiveFilters = computed(() => (props.filters && typeof props.filters === 'object') ? props.filters : internalFilters.value)
|
|
12
|
-
const columnWidths = signal({})
|
|
13
|
-
const resizing = signal(null)
|
|
14
|
-
const internalSelection = signal(props.selection)
|
|
15
|
-
|
|
16
|
-
effect(() => { internalFirst.value = props.first || 0 })
|
|
17
|
-
effect(() => { internalRows.value = props.rows || 10 })
|
|
18
|
-
effect(() => { internalSortField.value = props.sortField || '' })
|
|
19
|
-
effect(() => { internalSortOrder.value = props.sortOrder || 1 })
|
|
20
|
-
let lastPropGlobal = props.globalFilter || ''
|
|
21
|
-
effect(() => {
|
|
22
|
-
const next = props.globalFilter || ''
|
|
23
|
-
if (next === lastPropGlobal) return
|
|
24
|
-
lastPropGlobal = next
|
|
25
|
-
if (props.filters && typeof props.filters === 'object') return
|
|
26
|
-
internalFilters.value = { ...internalFilters.value, __global: next }
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
let lastPropSelection = props.selection
|
|
30
|
-
effect(() => {
|
|
31
|
-
const next = props.selection
|
|
32
|
-
if (next === lastPropSelection) return
|
|
33
|
-
lastPropSelection = next
|
|
34
|
-
internalSelection.value = next
|
|
35
|
-
})
|
|
36
|
-
const normalizeColumns = computed(() => (props.columns || []).map((c, idx) => { const field = c.field || `col_${idx}`; const header = c.header ?? field; const sortable = !!c.sortable; const filterable = c.filterable !== false; const type = c.type || 'text'; const width = c.width || ''; const minWidth = c.minWidth || '120px'; const align = c.align || (type === 'number' ? 'right' : 'left'); const filterMatchMode = c.filterMatchMode || 'contains'; return { ...c, field, header, sortable, filterable, type, width, minWidth, align, filterMatchMode } }))
|
|
37
|
-
const getRowKey = (row, index) => (props.dataKey && row && typeof row === 'object' && props.dataKey in row) ? String(row[props.dataKey]) : String(index)
|
|
38
|
-
const effectiveSelection = computed(() => internalSelection.value)
|
|
39
|
-
const isRowSelected = (row, index) => {
|
|
40
|
-
if (!props.selectionMode) return false
|
|
41
|
-
const selection = effectiveSelection.value
|
|
42
|
-
const key = getRowKey(row, index)
|
|
43
|
-
if (props.selectionMode === 'single') {
|
|
44
|
-
if (!selection) return false
|
|
45
|
-
if (props.dataKey && typeof selection === 'object' && selection && props.dataKey in selection) return String(selection[props.dataKey]) === key
|
|
46
|
-
return selection === row
|
|
47
|
-
}
|
|
48
|
-
const current = Array.isArray(selection) ? selection : []
|
|
49
|
-
if (props.dataKey) return current.some(s => s && typeof s === 'object' && props.dataKey in s && String(s[props.dataKey]) === key)
|
|
50
|
-
return current.includes(row)
|
|
51
|
-
}
|
|
52
|
-
const toggleRowSelection = (row, index) => {
|
|
53
|
-
if (!props.selectionMode) return
|
|
54
|
-
const selected = isRowSelected(row, index)
|
|
55
|
-
if (props.selectionMode === 'single') {
|
|
56
|
-
const next = selected ? null : row
|
|
57
|
-
internalSelection.value = next
|
|
58
|
-
emit('update:selection', next)
|
|
59
|
-
emit('updateSelection', next)
|
|
60
|
-
emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
|
|
64
|
-
const key = getRowKey(row, index)
|
|
65
|
-
const next = selected
|
|
66
|
-
? (props.dataKey ? current.filter(r => String(r?.[props.dataKey]) !== key) : current.filter(r => r !== row))
|
|
67
|
-
: [...current, row]
|
|
68
|
-
internalSelection.value = next
|
|
69
|
-
emit('update:selection', next)
|
|
70
|
-
emit('updateSelection', next)
|
|
71
|
-
emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const allVisibleSelected = computed(() => {
|
|
75
|
-
if (props.selectionMode !== 'multiple') return false
|
|
76
|
-
const rows = visibleRows.value
|
|
77
|
-
if (rows.length === 0) return false
|
|
78
|
-
for (let i = 0; i < rows.length; i++) {
|
|
79
|
-
if (!isRowSelected(rows[i], i + (internalFirst.value || 0))) return false
|
|
80
|
-
}
|
|
81
|
-
return true
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const toggleAllVisible = () => {
|
|
85
|
-
if (props.selectionMode !== 'multiple') return
|
|
86
|
-
const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
|
|
87
|
-
const rows = visibleRows.value
|
|
88
|
-
const startIndex = internalFirst.value || 0
|
|
89
|
-
if (rows.length === 0) return
|
|
90
|
-
if (allVisibleSelected.value) {
|
|
91
|
-
if (!props.dataKey) {
|
|
92
|
-
const next = current.filter(r => !rows.includes(r))
|
|
93
|
-
internalSelection.value = next
|
|
94
|
-
emit('update:selection', next)
|
|
95
|
-
emit('updateSelection', next)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
const keys = rows.map((r, i) => getRowKey(r, startIndex + i))
|
|
99
|
-
const next = current.filter(r => !keys.includes(String(r?.[props.dataKey])))
|
|
100
|
-
internalSelection.value = next
|
|
101
|
-
emit('update:selection', next)
|
|
102
|
-
emit('updateSelection', next)
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
if (!props.dataKey) {
|
|
106
|
-
const next = [...current, ...rows.filter(r => !current.includes(r))]
|
|
107
|
-
internalSelection.value = next
|
|
108
|
-
emit('update:selection', next)
|
|
109
|
-
emit('updateSelection', next)
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
const existing = new Set(current.map(r => String(r?.[props.dataKey])))
|
|
113
|
-
const next = [...current, ...rows.filter((r) => !existing.has(String(r?.[props.dataKey])))]
|
|
114
|
-
internalSelection.value = next
|
|
115
|
-
emit('update:selection', next)
|
|
116
|
-
emit('updateSelection', next)
|
|
117
|
-
}
|
|
118
|
-
const getHeaderContent = (col) => (col.headerTemplate && typeof col.headerTemplate === 'function') ? col.headerTemplate(col) : col.header
|
|
119
|
-
const getRawValue = (row, col) => { if (!row) return ''; if (col.field && typeof row === 'object' && col.field in row) return row[col.field]; return '' }
|
|
120
|
-
const getCellContent = (row, col, index) => { const raw = getRawValue(row, col); if (col.body && typeof col.body === 'function') return col.body(row, { index, column: col, field: col.field, value: raw }); return raw }
|
|
121
|
-
const matchFilter = (value, query, mode) => { const q = String(query ?? '').toLowerCase().trim(); if (!q) return true; const v = String(value ?? '').toLowerCase(); if (mode === 'equals') return v === q; if (mode === 'startsWith') return v.startsWith(q); if (mode === 'endsWith') return v.endsWith(q); return v.includes(q) }
|
|
122
|
-
const filteredRows = computed(() => { const rows = props.value || []; if (props.lazy) return rows; const cols = normalizeColumns.value; const filters = effectiveFilters.value || {}; const global = String(filters.__global || '').trim(); const keys = Object.keys(filters).filter(k => k !== '__global'); if (!global && keys.length === 0) return rows; const globalFields = Array.isArray(props.globalFilterFields) && props.globalFilterFields.length ? props.globalFilterFields : cols.map(c => c.field); return rows.filter((row) => { if (global) { const ok = globalFields.some((f) => matchFilter(getRawValue(row, { field: f }), global, 'contains')); if (!ok) return false } for (const c of cols) { const f = filters[c.field]; if (!f) continue; if (!matchFilter(getRawValue(row, c), f.value, f.matchMode || c.filterMatchMode || 'contains')) return false } return true }) })
|
|
123
|
-
const sortedRows = computed(() => { if (props.lazy) return filteredRows.value; const rows = [...filteredRows.value]; const field = internalSortField.value; if (!field) return rows; const col = normalizeColumns.value.find(c => c.field === field); const order = internalSortOrder.value === -1 ? -1 : 1; const type = col?.type || 'text'; rows.sort((a, b) => { const av = getCellContent(a, { field }, -1); const bv = getCellContent(b, { field }, -1); if (type === 'number') return (Number(av) - Number(bv)) * order; return String(av ?? '').localeCompare(String(bv ?? ''), undefined, { numeric: true, sensitivity: 'base' }) * order }); return rows })
|
|
124
|
-
const totalRecords = computed(() => props.lazy ? (props.totalRecords || (props.value || []).length) : sortedRows.value.length)
|
|
125
|
-
const visibleRows = computed(() => props.lazy ? (props.value || []) : (!props.paginator ? sortedRows.value : sortedRows.value.slice(internalFirst.value || 0, (internalFirst.value || 0) + (internalRows.value || 10))))
|
|
126
|
-
const setSort = (col) => {
|
|
127
|
-
if (!col.sortable) return
|
|
128
|
-
const nextField = col.field
|
|
129
|
-
let nextOrder = 1
|
|
130
|
-
if (internalSortField.value === nextField) nextOrder = internalSortOrder.value === 1 ? -1 : 1
|
|
131
|
-
internalSortField.value = nextField
|
|
132
|
-
internalSortOrder.value = nextOrder
|
|
133
|
-
internalFirst.value = 0
|
|
134
|
-
emit('update:sortField', nextField)
|
|
135
|
-
emit('update:sortOrder', nextOrder)
|
|
136
|
-
emit('updateSortField', nextField)
|
|
137
|
-
emit('updateSortOrder', nextOrder)
|
|
138
|
-
emit('sort', { sortField: nextField, sortOrder: nextOrder })
|
|
139
|
-
}
|
|
140
|
-
const onSortClick = (e) => { const field = e.currentTarget?.dataset?.field; if (!field) return; const col = normalizeColumns.value.find(c => c.field === field); if (!col) return; setSort(col) }
|
|
141
|
-
const setGlobal = (e) => {
|
|
142
|
-
const next = e.target.value
|
|
143
|
-
const nextFilters = { ...(effectiveFilters.value || {}), __global: next }
|
|
144
|
-
if (props.filters && typeof props.filters === 'object') { emit('update:filters', nextFilters); emit('updateFilters', nextFilters) } else { internalFilters.value = nextFilters }
|
|
145
|
-
internalFirst.value = 0
|
|
146
|
-
emit('update:globalFilter', next)
|
|
147
|
-
emit('updateGlobalFilter', next)
|
|
148
|
-
emit('filter', { global: next, filters: nextFilters })
|
|
149
|
-
}
|
|
150
|
-
const setColumnFilter = (field, e) => { const next = e.target.value; const current = { ...(effectiveFilters.value || {}) }; const col = normalizeColumns.value.find(c => c.field === field); if (!next) delete current[field]; else current[field] = { value: next, matchMode: col?.filterMatchMode || current[field]?.matchMode || 'contains' }; if (props.filters && typeof props.filters === 'object') { emit('update:filters', current); emit('updateFilters', current) } else { internalFilters.value = current } internalFirst.value = 0; emit('filter', { field, value: next, filters: current }) }
|
|
151
|
-
const onColumnFilterInput = (e) => { const field = e.target?.dataset?.field; if (!field) return; setColumnFilter(field, e) }
|
|
152
|
-
const onPage = ({ first, rows }) => {
|
|
153
|
-
internalFirst.value = first
|
|
154
|
-
internalRows.value = rows
|
|
155
|
-
emit('update:first', first)
|
|
156
|
-
emit('update:rows', rows)
|
|
157
|
-
emit('updateFirst', first)
|
|
158
|
-
emit('updateRows', rows)
|
|
159
|
-
emit('page', { first, rows })
|
|
160
|
-
}
|
|
161
|
-
const getWidth = (col) => columnWidths.value[col.field] || col.width || ''
|
|
162
|
-
const startResize = (e) => { if (!props.resizableColumns) return; const field = e.currentTarget?.dataset?.field; if (!field) return; e.preventDefault(); e.stopPropagation(); const th = e.currentTarget?.parentElement; const base = th?.getBoundingClientRect?.().width || parseFloat(th?.style?.width || '') || 160; resizing.value = { field, startX: e.clientX, startW: base }; const onMove = (ev) => { if (!resizing.value) return; const delta = ev.clientX - resizing.value.startX; const next = Math.max(80, resizing.value.startW + delta); columnWidths.value = { ...columnWidths.value, [resizing.value.field]: `${Math.round(next)}px` } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); resizing.value = null }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp) }
|
|
163
|
-
</script>
|
|
164
|
-
|
|
165
|
-
<template>
|
|
166
|
-
<div class="n-dt" :class="[`n-dt-${size}`, showGridlines ? 'is-grid' : '', scrollable ? 'is-scroll' : '']">
|
|
167
|
-
<div class="n-dt-toolbar"><div class="n-dt-global"><input class="n-dt-global-input" :value="effectiveFilters.value.__global || ''" placeholder="Search..." @input="setGlobal" /></div></div>
|
|
168
|
-
<div class="n-dt-wrapper">
|
|
169
|
-
<table class="n-dt-table" :class="{ 'is-striped': stripedRows, 'is-hover': hoverRows }">
|
|
170
|
-
<thead class="n-dt-thead">
|
|
171
|
-
<tr class="n-dt-head-row">
|
|
172
|
-
<th v-if="selectionMode" class="n-dt-th is-selection"><input v-if="selectionMode === 'multiple'" class="n-dt-selectbox" type="checkbox" :checked="allVisibleSelected.value" @click.stop="toggleAllVisible" /></th>
|
|
173
|
-
<th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="[`is-${col.align}`, col.sortable ? 'is-sortable' : '']" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }" :data-field="col.field" @click="onSortClick">
|
|
174
|
-
<div class="n-dt-th-content">
|
|
175
|
-
<span class="n-dt-th-text">{{ $slots.value && $slots.value[`header-${col.field}`] ? $slots[`header-${col.field}`]({ column: col }) : getHeaderContent(col) }}</span>
|
|
176
|
-
<span v-if="col.sortable" class="n-dt-sort" :class="{ 'is-active': internalSortField.value === col.field }">
|
|
177
|
-
<span v-if="internalSortField.value !== col.field" class="n-dt-sort-icon">↕</span>
|
|
178
|
-
<span v-else class="n-dt-sort-icon">{{ internalSortOrder.value === 1 ? '↑' : '↓' }}</span>
|
|
179
|
-
</span>
|
|
180
|
-
</div>
|
|
181
|
-
<span v-if="resizableColumns" class="n-dt-resizer" :data-field="col.field" @mousedown="startResize"></span>
|
|
182
|
-
</th>
|
|
183
|
-
</tr>
|
|
184
|
-
<tr v-if="filterDisplay === 'row'" class="n-dt-filter-row"><th v-if="selectionMode" class="n-dt-th is-selection"></th><th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }"><input v-if="col.filterable" class="n-dt-filter" :data-field="col.field" :value="(effectiveFilters.value[col.field]?.value) || ''" placeholder="Filter" @input="onColumnFilterInput" /></th></tr>
|
|
185
|
-
</thead>
|
|
186
|
-
<tbody class="n-dt-tbody">
|
|
187
|
-
<tr v-for="(row, i) in visibleRows.value" :key="getRowKey(row, i + internalFirst.value)" class="n-dt-row" :class="{ 'is-selected': isRowSelected(row, i + internalFirst.value) }" @click="toggleRowSelection(row, i + internalFirst.value)">
|
|
188
|
-
<td v-if="selectionMode" class="n-dt-td is-selection"><input class="n-dt-selectbox" type="checkbox" :checked="isRowSelected(row, i + internalFirst.value)" @click.stop="toggleRowSelection(row, i + internalFirst.value)" /></td>
|
|
189
|
-
<td v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-td" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }">
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
</tr>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
</style>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed, effect } from 'nexa-framework'
|
|
3
|
+
import NPaginator from './NPaginator.nexa'
|
|
4
|
+
const props = defineProps({ value: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] }, stripedRows: { type: Boolean, default: false }, hoverRows: { type: Boolean, default: true }, size: { type: String, default: 'md' }, scrollable: { type: Boolean, default: true }, paginator: { type: Boolean, default: true }, rows: { type: Number, default: 10 }, first: { type: Number, default: 0 }, rowsPerPageOptions: { type: Array, default: () => [10, 20, 50] }, selectionMode: { type: String, default: '' }, selection: { type: null, default: null }, dataKey: { type: String, default: '' }, sortField: { type: String, default: '' }, sortOrder: { type: Number, default: 1 }, resizableColumns: { type: Boolean, default: true }, columnResizeMode: { type: String, default: 'fit' }, filterDisplay: { type: String, default: 'row' }, globalFilter: { type: String, default: '' }, globalFilterFields: { type: Array, default: null }, filters: { type: null, default: null }, lazy: { type: Boolean, default: false }, totalRecords: { type: Number, default: 0 }, emptyMessage: { type: String, default: 'No records found' }, showGridlines: { type: Boolean, default: false } })
|
|
5
|
+
const emit = defineEmits(['update:first', 'update:rows', 'update:selection', 'update:sortField', 'update:sortOrder', 'update:globalFilter', 'update:filters', 'updateFirst', 'updateRows', 'updateSelection', 'updateSortField', 'updateSortOrder', 'updateGlobalFilter', 'updateFilters', 'page', 'rowSelect', 'rowUnselect', 'sort', 'filter'])
|
|
6
|
+
const internalFirst = signal(props.first || 0)
|
|
7
|
+
const internalRows = signal(props.rows || 10)
|
|
8
|
+
const internalSortField = signal(props.sortField || '')
|
|
9
|
+
const internalSortOrder = signal(props.sortOrder || 1)
|
|
10
|
+
const internalFilters = signal((props.filters && typeof props.filters === 'object') ? props.filters : { __global: props.globalFilter || '' })
|
|
11
|
+
const effectiveFilters = computed(() => (props.filters && typeof props.filters === 'object') ? props.filters : internalFilters.value)
|
|
12
|
+
const columnWidths = signal({})
|
|
13
|
+
const resizing = signal(null)
|
|
14
|
+
const internalSelection = signal(props.selection)
|
|
15
|
+
|
|
16
|
+
effect(() => { internalFirst.value = props.first || 0 })
|
|
17
|
+
effect(() => { internalRows.value = props.rows || 10 })
|
|
18
|
+
effect(() => { internalSortField.value = props.sortField || '' })
|
|
19
|
+
effect(() => { internalSortOrder.value = props.sortOrder || 1 })
|
|
20
|
+
let lastPropGlobal = props.globalFilter || ''
|
|
21
|
+
effect(() => {
|
|
22
|
+
const next = props.globalFilter || ''
|
|
23
|
+
if (next === lastPropGlobal) return
|
|
24
|
+
lastPropGlobal = next
|
|
25
|
+
if (props.filters && typeof props.filters === 'object') return
|
|
26
|
+
internalFilters.value = { ...internalFilters.value, __global: next }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
let lastPropSelection = props.selection
|
|
30
|
+
effect(() => {
|
|
31
|
+
const next = props.selection
|
|
32
|
+
if (next === lastPropSelection) return
|
|
33
|
+
lastPropSelection = next
|
|
34
|
+
internalSelection.value = next
|
|
35
|
+
})
|
|
36
|
+
const normalizeColumns = computed(() => (props.columns || []).map((c, idx) => { const field = c.field || `col_${idx}`; const header = c.header ?? field; const sortable = !!c.sortable; const filterable = c.filterable !== false; const type = c.type || 'text'; const width = c.width || ''; const minWidth = c.minWidth || '120px'; const align = c.align || (type === 'number' ? 'right' : 'left'); const filterMatchMode = c.filterMatchMode || 'contains'; return { ...c, field, header, sortable, filterable, type, width, minWidth, align, filterMatchMode } }))
|
|
37
|
+
const getRowKey = (row, index) => (props.dataKey && row && typeof row === 'object' && props.dataKey in row) ? String(row[props.dataKey]) : String(index)
|
|
38
|
+
const effectiveSelection = computed(() => internalSelection.value)
|
|
39
|
+
const isRowSelected = (row, index) => {
|
|
40
|
+
if (!props.selectionMode) return false
|
|
41
|
+
const selection = effectiveSelection.value
|
|
42
|
+
const key = getRowKey(row, index)
|
|
43
|
+
if (props.selectionMode === 'single') {
|
|
44
|
+
if (!selection) return false
|
|
45
|
+
if (props.dataKey && typeof selection === 'object' && selection && props.dataKey in selection) return String(selection[props.dataKey]) === key
|
|
46
|
+
return selection === row
|
|
47
|
+
}
|
|
48
|
+
const current = Array.isArray(selection) ? selection : []
|
|
49
|
+
if (props.dataKey) return current.some(s => s && typeof s === 'object' && props.dataKey in s && String(s[props.dataKey]) === key)
|
|
50
|
+
return current.includes(row)
|
|
51
|
+
}
|
|
52
|
+
const toggleRowSelection = (row, index) => {
|
|
53
|
+
if (!props.selectionMode) return
|
|
54
|
+
const selected = isRowSelected(row, index)
|
|
55
|
+
if (props.selectionMode === 'single') {
|
|
56
|
+
const next = selected ? null : row
|
|
57
|
+
internalSelection.value = next
|
|
58
|
+
emit('update:selection', next)
|
|
59
|
+
emit('updateSelection', next)
|
|
60
|
+
emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
|
|
64
|
+
const key = getRowKey(row, index)
|
|
65
|
+
const next = selected
|
|
66
|
+
? (props.dataKey ? current.filter(r => String(r?.[props.dataKey]) !== key) : current.filter(r => r !== row))
|
|
67
|
+
: [...current, row]
|
|
68
|
+
internalSelection.value = next
|
|
69
|
+
emit('update:selection', next)
|
|
70
|
+
emit('updateSelection', next)
|
|
71
|
+
emit(selected ? 'rowUnselect' : 'rowSelect', { data: row })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allVisibleSelected = computed(() => {
|
|
75
|
+
if (props.selectionMode !== 'multiple') return false
|
|
76
|
+
const rows = visibleRows.value
|
|
77
|
+
if (rows.length === 0) return false
|
|
78
|
+
for (let i = 0; i < rows.length; i++) {
|
|
79
|
+
if (!isRowSelected(rows[i], i + (internalFirst.value || 0))) return false
|
|
80
|
+
}
|
|
81
|
+
return true
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const toggleAllVisible = () => {
|
|
85
|
+
if (props.selectionMode !== 'multiple') return
|
|
86
|
+
const current = Array.isArray(effectiveSelection.value) ? effectiveSelection.value : []
|
|
87
|
+
const rows = visibleRows.value
|
|
88
|
+
const startIndex = internalFirst.value || 0
|
|
89
|
+
if (rows.length === 0) return
|
|
90
|
+
if (allVisibleSelected.value) {
|
|
91
|
+
if (!props.dataKey) {
|
|
92
|
+
const next = current.filter(r => !rows.includes(r))
|
|
93
|
+
internalSelection.value = next
|
|
94
|
+
emit('update:selection', next)
|
|
95
|
+
emit('updateSelection', next)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const keys = rows.map((r, i) => getRowKey(r, startIndex + i))
|
|
99
|
+
const next = current.filter(r => !keys.includes(String(r?.[props.dataKey])))
|
|
100
|
+
internalSelection.value = next
|
|
101
|
+
emit('update:selection', next)
|
|
102
|
+
emit('updateSelection', next)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (!props.dataKey) {
|
|
106
|
+
const next = [...current, ...rows.filter(r => !current.includes(r))]
|
|
107
|
+
internalSelection.value = next
|
|
108
|
+
emit('update:selection', next)
|
|
109
|
+
emit('updateSelection', next)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
const existing = new Set(current.map(r => String(r?.[props.dataKey])))
|
|
113
|
+
const next = [...current, ...rows.filter((r) => !existing.has(String(r?.[props.dataKey])))]
|
|
114
|
+
internalSelection.value = next
|
|
115
|
+
emit('update:selection', next)
|
|
116
|
+
emit('updateSelection', next)
|
|
117
|
+
}
|
|
118
|
+
const getHeaderContent = (col) => (col.headerTemplate && typeof col.headerTemplate === 'function') ? col.headerTemplate(col) : col.header
|
|
119
|
+
const getRawValue = (row, col) => { if (!row) return ''; if (col.field && typeof row === 'object' && col.field in row) return row[col.field]; return '' }
|
|
120
|
+
const getCellContent = (row, col, index) => { const raw = getRawValue(row, col); if (col.body && typeof col.body === 'function') return col.body(row, { index, column: col, field: col.field, value: raw }); return raw }
|
|
121
|
+
const matchFilter = (value, query, mode) => { const q = String(query ?? '').toLowerCase().trim(); if (!q) return true; const v = String(value ?? '').toLowerCase(); if (mode === 'equals') return v === q; if (mode === 'startsWith') return v.startsWith(q); if (mode === 'endsWith') return v.endsWith(q); return v.includes(q) }
|
|
122
|
+
const filteredRows = computed(() => { const rows = props.value || []; if (props.lazy) return rows; const cols = normalizeColumns.value; const filters = effectiveFilters.value || {}; const global = String(filters.__global || '').trim(); const keys = Object.keys(filters).filter(k => k !== '__global'); if (!global && keys.length === 0) return rows; const globalFields = Array.isArray(props.globalFilterFields) && props.globalFilterFields.length ? props.globalFilterFields : cols.map(c => c.field); return rows.filter((row) => { if (global) { const ok = globalFields.some((f) => matchFilter(getRawValue(row, { field: f }), global, 'contains')); if (!ok) return false } for (const c of cols) { const f = filters[c.field]; if (!f) continue; if (!matchFilter(getRawValue(row, c), f.value, f.matchMode || c.filterMatchMode || 'contains')) return false } return true }) })
|
|
123
|
+
const sortedRows = computed(() => { if (props.lazy) return filteredRows.value; const rows = [...filteredRows.value]; const field = internalSortField.value; if (!field) return rows; const col = normalizeColumns.value.find(c => c.field === field); const order = internalSortOrder.value === -1 ? -1 : 1; const type = col?.type || 'text'; rows.sort((a, b) => { const av = getCellContent(a, { field }, -1); const bv = getCellContent(b, { field }, -1); if (type === 'number') return (Number(av) - Number(bv)) * order; return String(av ?? '').localeCompare(String(bv ?? ''), undefined, { numeric: true, sensitivity: 'base' }) * order }); return rows })
|
|
124
|
+
const totalRecords = computed(() => props.lazy ? (props.totalRecords || (props.value || []).length) : sortedRows.value.length)
|
|
125
|
+
const visibleRows = computed(() => props.lazy ? (props.value || []) : (!props.paginator ? sortedRows.value : sortedRows.value.slice(internalFirst.value || 0, (internalFirst.value || 0) + (internalRows.value || 10))))
|
|
126
|
+
const setSort = (col) => {
|
|
127
|
+
if (!col.sortable) return
|
|
128
|
+
const nextField = col.field
|
|
129
|
+
let nextOrder = 1
|
|
130
|
+
if (internalSortField.value === nextField) nextOrder = internalSortOrder.value === 1 ? -1 : 1
|
|
131
|
+
internalSortField.value = nextField
|
|
132
|
+
internalSortOrder.value = nextOrder
|
|
133
|
+
internalFirst.value = 0
|
|
134
|
+
emit('update:sortField', nextField)
|
|
135
|
+
emit('update:sortOrder', nextOrder)
|
|
136
|
+
emit('updateSortField', nextField)
|
|
137
|
+
emit('updateSortOrder', nextOrder)
|
|
138
|
+
emit('sort', { sortField: nextField, sortOrder: nextOrder })
|
|
139
|
+
}
|
|
140
|
+
const onSortClick = (e) => { const field = e.currentTarget?.dataset?.field; if (!field) return; const col = normalizeColumns.value.find(c => c.field === field); if (!col) return; setSort(col) }
|
|
141
|
+
const setGlobal = (e) => {
|
|
142
|
+
const next = e.target.value
|
|
143
|
+
const nextFilters = { ...(effectiveFilters.value || {}), __global: next }
|
|
144
|
+
if (props.filters && typeof props.filters === 'object') { emit('update:filters', nextFilters); emit('updateFilters', nextFilters) } else { internalFilters.value = nextFilters }
|
|
145
|
+
internalFirst.value = 0
|
|
146
|
+
emit('update:globalFilter', next)
|
|
147
|
+
emit('updateGlobalFilter', next)
|
|
148
|
+
emit('filter', { global: next, filters: nextFilters })
|
|
149
|
+
}
|
|
150
|
+
const setColumnFilter = (field, e) => { const next = e.target.value; const current = { ...(effectiveFilters.value || {}) }; const col = normalizeColumns.value.find(c => c.field === field); if (!next) delete current[field]; else current[field] = { value: next, matchMode: col?.filterMatchMode || current[field]?.matchMode || 'contains' }; if (props.filters && typeof props.filters === 'object') { emit('update:filters', current); emit('updateFilters', current) } else { internalFilters.value = current } internalFirst.value = 0; emit('filter', { field, value: next, filters: current }) }
|
|
151
|
+
const onColumnFilterInput = (e) => { const field = e.target?.dataset?.field; if (!field) return; setColumnFilter(field, e) }
|
|
152
|
+
const onPage = ({ first, rows }) => {
|
|
153
|
+
internalFirst.value = first
|
|
154
|
+
internalRows.value = rows
|
|
155
|
+
emit('update:first', first)
|
|
156
|
+
emit('update:rows', rows)
|
|
157
|
+
emit('updateFirst', first)
|
|
158
|
+
emit('updateRows', rows)
|
|
159
|
+
emit('page', { first, rows })
|
|
160
|
+
}
|
|
161
|
+
const getWidth = (col) => columnWidths.value[col.field] || col.width || ''
|
|
162
|
+
const startResize = (e) => { if (!props.resizableColumns) return; const field = e.currentTarget?.dataset?.field; if (!field) return; e.preventDefault(); e.stopPropagation(); const th = e.currentTarget?.parentElement; const base = th?.getBoundingClientRect?.().width || parseFloat(th?.style?.width || '') || 160; resizing.value = { field, startX: e.clientX, startW: base }; const onMove = (ev) => { if (!resizing.value) return; const delta = ev.clientX - resizing.value.startX; const next = Math.max(80, resizing.value.startW + delta); columnWidths.value = { ...columnWidths.value, [resizing.value.field]: `${Math.round(next)}px` } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); resizing.value = null }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp) }
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<template>
|
|
166
|
+
<div class="n-dt" :class="[`n-dt-${size}`, showGridlines ? 'is-grid' : '', scrollable ? 'is-scroll' : '']">
|
|
167
|
+
<div class="n-dt-toolbar"><div class="n-dt-global"><input class="n-dt-global-input" :value="effectiveFilters.value.__global || ''" placeholder="Search..." @input="setGlobal" /></div></div>
|
|
168
|
+
<div class="n-dt-wrapper">
|
|
169
|
+
<table class="n-dt-table" :class="{ 'is-striped': stripedRows, 'is-hover': hoverRows }">
|
|
170
|
+
<thead class="n-dt-thead">
|
|
171
|
+
<tr class="n-dt-head-row">
|
|
172
|
+
<th v-if="selectionMode" class="n-dt-th is-selection"><input v-if="selectionMode === 'multiple'" class="n-dt-selectbox" type="checkbox" :checked="allVisibleSelected.value" @click.stop="toggleAllVisible" /></th>
|
|
173
|
+
<th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="[`is-${col.align}`, col.sortable ? 'is-sortable' : '']" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }" :data-field="col.field" @click="onSortClick">
|
|
174
|
+
<div class="n-dt-th-content">
|
|
175
|
+
<span class="n-dt-th-text">{{ $slots.value && $slots.value[`header-${col.field}`] ? $slots[`header-${col.field}`]({ column: col }) : getHeaderContent(col) }}</span>
|
|
176
|
+
<span v-if="col.sortable" class="n-dt-sort" :class="{ 'is-active': internalSortField.value === col.field }">
|
|
177
|
+
<span v-if="internalSortField.value !== col.field" class="n-dt-sort-icon">↕</span>
|
|
178
|
+
<span v-else class="n-dt-sort-icon">{{ internalSortOrder.value === 1 ? '↑' : '↓' }}</span>
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
<span v-if="resizableColumns" class="n-dt-resizer" :data-field="col.field" @mousedown="startResize"></span>
|
|
182
|
+
</th>
|
|
183
|
+
</tr>
|
|
184
|
+
<tr v-if="filterDisplay === 'row'" class="n-dt-filter-row"><th v-if="selectionMode" class="n-dt-th is-selection"></th><th v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-th" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }"><input v-if="col.filterable" class="n-dt-filter" :data-field="col.field" :value="(effectiveFilters.value[col.field]?.value) || ''" placeholder="Filter" @input="onColumnFilterInput" /></th></tr>
|
|
185
|
+
</thead>
|
|
186
|
+
<tbody class="n-dt-tbody">
|
|
187
|
+
<tr v-for="(row, i) in visibleRows.value" :key="getRowKey(row, i + internalFirst.value)" class="n-dt-row" :class="{ 'is-selected': isRowSelected(row, i + internalFirst.value) }" @click="toggleRowSelection(row, i + internalFirst.value)">
|
|
188
|
+
<td v-if="selectionMode" class="n-dt-td is-selection"><input class="n-dt-selectbox" type="checkbox" :checked="isRowSelected(row, i + internalFirst.value)" @click.stop="toggleRowSelection(row, i + internalFirst.value)" /></td>
|
|
189
|
+
<td v-for="col in normalizeColumns.value" :key="col.field" class="n-dt-td" :class="`is-${col.align}`" :style="{ width: getWidth(col) || undefined, minWidth: col.minWidth }">
|
|
190
|
+
{{ $slots.value && $slots.value[`body-${col.field}`] ? $slots[`body-${col.field}`]({ data: row, column: col, index: i + internalFirst.value }) : getCellContent(row, col, i + internalFirst.value) }}
|
|
191
|
+
</td>
|
|
192
|
+
</tr>
|
|
193
|
+
<tr v-if="visibleRows.value.length === 0" class="n-dt-empty-row"><td :colspan="normalizeColumns.value.length + (selectionMode ? 1 : 0)" class="n-dt-empty">{{ emptyMessage }}</td></tr>
|
|
194
|
+
</tbody>
|
|
195
|
+
</table>
|
|
196
|
+
</div>
|
|
197
|
+
<NPaginator v-if="paginator" :first="internalFirst.value" :rows="internalRows.value" :totalRecords="totalRecords.value" :rowsPerPageOptions="rowsPerPageOptions" @page="onPage" />
|
|
198
|
+
</div>
|
|
199
|
+
</template>
|
|
200
|
+
|
|
201
|
+
<style scoped>
|
|
202
|
+
.n-dt{border:1px solid var(--n-color-border);border-radius:var(--n-radius-lg);background:var(--n-color-surface);overflow:hidden;font-family:var(--n-font-sans)}.n-dt-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--n-space-3) var(--n-space-4);border-bottom:1px solid var(--n-color-border);background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(0,0,0,.08))}.n-dt-global{display:flex;align-items:center;gap:var(--n-space-2)}.n-dt-global-input{width:280px;max-width:100%;background:var(--n-color-bg);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);padding:0.55rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-sm);outline:none;box-sizing:border-box}.n-dt-global-input:focus{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-dt-wrapper{width:100%;overflow:auto}.n-dt-table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.n-dt-thead{background:var(--n-color-surface)}.n-dt-th,.n-dt-td{padding:0.75rem 0.9rem;border-bottom:1px solid var(--n-color-border);color:var(--n-color-text);font-size:var(--n-text-sm);vertical-align:middle}.n-dt-th{position:relative;overflow:hidden;background:rgba(0,0,0,.10);color:var(--n-color-text-secondary);font-weight:var(--n-weight-semibold);user-select:none}.n-dt-th.is-sortable{cursor:pointer}.n-dt-th-content{display:flex;align-items:center;justify-content:space-between;gap:0.5rem}.n-dt-sort{display:inline-flex;align-items:center;gap:0.25rem;color:var(--n-color-text-muted)}.n-dt-sort.is-active{color:var(--n-color-primary)}.n-dt-resizer{position:absolute;right:0;top:0;bottom:0;width:8px;cursor:col-resize}.n-dt-filter-row .n-dt-th{background:rgba(0,0,0,.06)}.n-dt-filter{width:100%;max-width:100%;display:block;background:var(--n-color-bg);border:1px solid var(--n-color-border);border-radius:var(--n-radius-sm);padding:0.35rem 0.5rem;color:var(--n-color-text);font-size:var(--n-text-xs);outline:none;box-sizing:border-box}.n-dt-filter:focus{border-color:var(--n-color-primary)}.n-dt-td{background:transparent;color:var(--n-color-text)}.n-dt-row.is-selected .n-dt-td{background:rgba(59,130,246,.12)}.n-dt-table.is-striped .n-dt-row:nth-child(even) .n-dt-td{background:rgba(0,0,0,.06)}.n-dt-table.is-hover .n-dt-row:hover .n-dt-td{background:rgba(255,255,255,.06)}.n-dt-empty{text-align:center;color:var(--n-color-text-muted);padding:1.25rem}.is-left{text-align:left}.is-right{text-align:right}.is-center{text-align:center}.is-selection{width:44px;min-width:44px;max-width:44px;text-align:center}.n-dt-selectbox{width:16px;height:16px;accent-color:var(--n-color-primary)}.n-dt-check{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;border:1px solid var(--n-color-border);color:var(--n-color-primary)}.is-grid .n-dt-td,.is-grid .n-dt-th{border-right:1px solid var(--n-color-border)}.is-grid .n-dt-td:last-child,.is-grid .n-dt-th:last-child{border-right:none}.n-dt-sm .n-dt-th,.n-dt-sm .n-dt-td{padding:0.55rem 0.7rem}.n-dt-lg .n-dt-th,.n-dt-lg .n-dt-td{padding:0.9rem 1rem}
|
|
203
|
+
</style>
|