ketekny-ui-kit 1.0.27 → 1.0.29
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/package.json +1 -1
- package/src/ui/kDatatable.vue +195 -12
package/package.json
CHANGED
package/src/ui/kDatatable.vue
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="['k-datatable-wrapper rounded-lg border border-primary/15', disabled ? 'pointer-events-none opacity-60' : '']">
|
|
2
|
+
<div ref="tableWrapper" :class="['k-datatable-wrapper rounded-lg border border-primary/15', disabled ? 'pointer-events-none opacity-60' : '']">
|
|
3
3
|
<EasyDataTable
|
|
4
4
|
v-bind="$attrs"
|
|
5
5
|
:headers="headers"
|
|
6
6
|
:items="items"
|
|
7
7
|
:search="search"
|
|
8
|
+
:theme-color="themeColor"
|
|
8
9
|
:show-select="effectiveSelectionMode !== 'none'"
|
|
9
10
|
:single-select="effectiveSelectionMode === 'single'"
|
|
10
11
|
v-if="effectiveSelectionMode !== 'none'"
|
|
11
|
-
|
|
12
|
+
:items-selected="internalSelection"
|
|
13
|
+
@update:items-selected="handleSelectionUpdate"
|
|
12
14
|
:itemSelectable="itemSelectable"
|
|
15
|
+
:body-row-class-name="composeBodyRowClassName"
|
|
13
16
|
alternating
|
|
14
17
|
buttons-pagination
|
|
15
18
|
table-class-name="customize-table"
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
</EasyDataTable>
|
|
22
25
|
|
|
23
26
|
<!-- Render separate table without v-model when no selection -->
|
|
24
|
-
<EasyDataTable v-else v-bind="$attrs" :headers="headers" :items="items" :search="search" :show-select="false" alternating buttons-pagination table-class-name="customize-table">
|
|
27
|
+
<EasyDataTable v-else v-bind="$attrs" :headers="headers" :items="items" :search="search" :theme-color="themeColor" :show-select="false" :body-row-class-name="composeBodyRowClassName" alternating buttons-pagination table-class-name="customize-table">
|
|
25
28
|
<template v-for="(_, name) in $slots" :key="name" v-slot:[name]="slotProps">
|
|
26
29
|
<slot :name="name" v-bind="slotProps" />
|
|
27
30
|
</template>
|
|
@@ -42,6 +45,7 @@ export default {
|
|
|
42
45
|
headers: { type: Array, required: true },
|
|
43
46
|
items: { type: Array, required: true },
|
|
44
47
|
search: { type: String, default: "" },
|
|
48
|
+
themeColor: { type: String, default: "#2563eb" },
|
|
45
49
|
|
|
46
50
|
selectionMode: {
|
|
47
51
|
type: String,
|
|
@@ -69,8 +73,20 @@ export default {
|
|
|
69
73
|
data() {
|
|
70
74
|
return {
|
|
71
75
|
internalSelection: this.normalizeModelValue(this.modelValue),
|
|
76
|
+
pendingSelectionInteraction: null,
|
|
77
|
+
lastSelectionAnchor: null,
|
|
78
|
+
lastSelectionAnchorRowNumber: null,
|
|
72
79
|
};
|
|
73
80
|
},
|
|
81
|
+
created() {
|
|
82
|
+
this.currentPageRowsCache = [];
|
|
83
|
+
},
|
|
84
|
+
mounted() {
|
|
85
|
+
this.bindSelectionInteractionListener();
|
|
86
|
+
},
|
|
87
|
+
beforeUnmount() {
|
|
88
|
+
this.unbindSelectionInteractionListener();
|
|
89
|
+
},
|
|
74
90
|
computed: {
|
|
75
91
|
effectiveSelectionMode() {
|
|
76
92
|
return this.disabled ? "none" : this.selectionMode;
|
|
@@ -81,16 +97,183 @@ export default {
|
|
|
81
97
|
modelValue(val) {
|
|
82
98
|
this.internalSelection = this.normalizeModelValue(val);
|
|
83
99
|
},
|
|
84
|
-
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
methods: {
|
|
103
|
+
handleSelectionUpdate(newSelection) {
|
|
85
104
|
if (this.selectionMode === "single") {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.$emit("update:modelValue",
|
|
105
|
+
const nextSelection = Array.isArray(newSelection) ? newSelection : [];
|
|
106
|
+
this.internalSelection = nextSelection;
|
|
107
|
+
this.$emit("update:modelValue", nextSelection[0] || null);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.selectionMode === "multiple") {
|
|
112
|
+
const oldSelection = Array.isArray(this.internalSelection) ? this.internalSelection : [];
|
|
113
|
+
const candidateSelection = Array.isArray(newSelection) ? newSelection : [];
|
|
114
|
+
const nextSelection = this.resolveShiftRangeSelection(candidateSelection, oldSelection);
|
|
115
|
+
this.internalSelection = nextSelection;
|
|
116
|
+
this.$emit("update:modelValue", nextSelection);
|
|
89
117
|
}
|
|
90
118
|
},
|
|
91
|
-
|
|
119
|
+
bindSelectionInteractionListener() {
|
|
120
|
+
this.$refs.tableWrapper?.addEventListener("click", this.captureSelectionInteraction, true);
|
|
121
|
+
},
|
|
122
|
+
unbindSelectionInteractionListener() {
|
|
123
|
+
this.$refs.tableWrapper?.removeEventListener("click", this.captureSelectionInteraction, true);
|
|
124
|
+
},
|
|
125
|
+
captureSelectionInteraction(event) {
|
|
126
|
+
if (this.selectionMode !== "multiple" || this.disabled || !(event.target instanceof Element)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
92
129
|
|
|
93
|
-
|
|
130
|
+
const checkbox = event.target.closest(".easy-checkbox");
|
|
131
|
+
if (!checkbox) return;
|
|
132
|
+
|
|
133
|
+
// Ignore the header "select all" checkbox.
|
|
134
|
+
const row = checkbox.closest("tbody tr");
|
|
135
|
+
if (!row) return;
|
|
136
|
+
|
|
137
|
+
const rowNumber = this.resolveClickedRowNumber(row);
|
|
138
|
+
if (!rowNumber) return;
|
|
139
|
+
const clickedItem = this.currentPageRowsCache[rowNumber - 1];
|
|
140
|
+
|
|
141
|
+
if (!event.shiftKey && clickedItem) {
|
|
142
|
+
this.lastSelectionAnchor = clickedItem;
|
|
143
|
+
this.lastSelectionAnchorRowNumber = rowNumber;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event.shiftKey && this.lastSelectionAnchor && clickedItem) {
|
|
147
|
+
const anchorIndex = this.findItemIndex(this.lastSelectionAnchor);
|
|
148
|
+
const targetIndex = this.findItemIndex(clickedItem);
|
|
149
|
+
if (anchorIndex !== -1 && targetIndex !== -1) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
const shouldSelectRange = !this.containsItem(this.internalSelection, clickedItem);
|
|
153
|
+
const nextSelection = this.applyRangeSelection(this.internalSelection, anchorIndex, targetIndex, rowNumber, shouldSelectRange);
|
|
154
|
+
this.pendingSelectionInteraction = null;
|
|
155
|
+
this.internalSelection = nextSelection;
|
|
156
|
+
this.$emit("update:modelValue", nextSelection);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.pendingSelectionInteraction = {
|
|
162
|
+
shiftKey: event.shiftKey,
|
|
163
|
+
rowNumber,
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
resolveClickedRowNumber(rowElement) {
|
|
167
|
+
const tbody = rowElement.parentElement;
|
|
168
|
+
if (!tbody) return null;
|
|
169
|
+
|
|
170
|
+
const selectableRows = Array.from(tbody.querySelectorAll("tr")).filter((row) => row.querySelector(".easy-checkbox"));
|
|
171
|
+
const rowIndex = selectableRows.indexOf(rowElement);
|
|
172
|
+
return rowIndex === -1 ? null : rowIndex + 1;
|
|
173
|
+
},
|
|
174
|
+
composeBodyRowClassName(item, rowNumber) {
|
|
175
|
+
if (rowNumber === 1) {
|
|
176
|
+
this.currentPageRowsCache = [];
|
|
177
|
+
}
|
|
178
|
+
this.currentPageRowsCache[rowNumber - 1] = this.normalizeItem(item);
|
|
179
|
+
|
|
180
|
+
const externalBodyRowClass = this.$attrs["body-row-class-name"] ?? this.$attrs.bodyRowClassName;
|
|
181
|
+
if (typeof externalBodyRowClass === "function") {
|
|
182
|
+
return externalBodyRowClass(item, rowNumber);
|
|
183
|
+
}
|
|
184
|
+
if (typeof externalBodyRowClass === "string") {
|
|
185
|
+
return externalBodyRowClass;
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
188
|
+
},
|
|
189
|
+
resolveShiftRangeSelection(newSelection, oldSelection = []) {
|
|
190
|
+
if (this.selectionMode !== "multiple") return newSelection;
|
|
191
|
+
|
|
192
|
+
const interaction = this.pendingSelectionInteraction;
|
|
193
|
+
this.pendingSelectionInteraction = null;
|
|
194
|
+
|
|
195
|
+
const toggledItemFromInteraction = interaction?.rowNumber ? this.currentPageRowsCache[interaction.rowNumber - 1] : null;
|
|
196
|
+
const toggledItem = toggledItemFromInteraction ?? this.getToggledItem(oldSelection, newSelection);
|
|
197
|
+
if (!toggledItem) return newSelection;
|
|
198
|
+
|
|
199
|
+
if (!interaction?.shiftKey || !this.lastSelectionAnchor) {
|
|
200
|
+
this.lastSelectionAnchor = toggledItem;
|
|
201
|
+
this.lastSelectionAnchorRowNumber = interaction?.rowNumber ?? null;
|
|
202
|
+
return newSelection;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const anchorIndex = this.findItemIndex(this.lastSelectionAnchor);
|
|
206
|
+
const targetIndex = this.findItemIndex(toggledItem);
|
|
207
|
+
if (anchorIndex === -1 || targetIndex === -1) {
|
|
208
|
+
this.lastSelectionAnchor = toggledItem;
|
|
209
|
+
this.lastSelectionAnchorRowNumber = interaction?.rowNumber ?? null;
|
|
210
|
+
return newSelection;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rangeItems = this.getRangeItems(anchorIndex, targetIndex, interaction.rowNumber);
|
|
214
|
+
if (!rangeItems.length) return newSelection;
|
|
215
|
+
const shouldSelectRange = this.containsItem(newSelection, toggledItem);
|
|
216
|
+
return this.applyRangeSelection(newSelection, anchorIndex, targetIndex, interaction.rowNumber, shouldSelectRange);
|
|
217
|
+
},
|
|
218
|
+
applyRangeSelection(baseSelection, anchorIndex, targetIndex, targetRowNumber, shouldSelectRange) {
|
|
219
|
+
const rangeItems = this.getRangeItems(anchorIndex, targetIndex, targetRowNumber);
|
|
220
|
+
if (!rangeItems.length) return baseSelection;
|
|
221
|
+
|
|
222
|
+
let nextSelection = [...baseSelection];
|
|
223
|
+
if (shouldSelectRange) {
|
|
224
|
+
rangeItems.forEach((item) => {
|
|
225
|
+
if (!this.containsItem(nextSelection, item)) {
|
|
226
|
+
nextSelection.push(this.normalizeItem(item));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
nextSelection = nextSelection.filter((selectedItem) => !rangeItems.some((item) => this.areItemsEqual(selectedItem, item)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return this.areSelectionsEqual(nextSelection, baseSelection) ? baseSelection : nextSelection;
|
|
234
|
+
},
|
|
235
|
+
getRangeItems(anchorIndex, targetIndex, targetRowNumber) {
|
|
236
|
+
const anchorRowNumber = this.lastSelectionAnchorRowNumber;
|
|
237
|
+
if (anchorRowNumber && targetRowNumber) {
|
|
238
|
+
const [start, end] = anchorRowNumber <= targetRowNumber ? [anchorRowNumber, targetRowNumber] : [targetRowNumber, anchorRowNumber];
|
|
239
|
+
const visibleRange = this.currentPageRowsCache.slice(start - 1, end).filter((item) => item);
|
|
240
|
+
if (visibleRange.length === end - start + 1) {
|
|
241
|
+
return visibleRange;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const [start, end] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
|
|
246
|
+
return this.items.slice(start, end + 1);
|
|
247
|
+
},
|
|
248
|
+
normalizeItem(item) {
|
|
249
|
+
if (!item || typeof item !== "object") return item;
|
|
250
|
+
const normalized = { ...item };
|
|
251
|
+
delete normalized.checkbox;
|
|
252
|
+
delete normalized.index;
|
|
253
|
+
return normalized;
|
|
254
|
+
},
|
|
255
|
+
areItemsEqual(left, right) {
|
|
256
|
+
return JSON.stringify(this.normalizeItem(left)) === JSON.stringify(this.normalizeItem(right));
|
|
257
|
+
},
|
|
258
|
+
containsItem(collection, item) {
|
|
259
|
+
return collection.some((current) => this.areItemsEqual(current, item));
|
|
260
|
+
},
|
|
261
|
+
getToggledItem(previousSelection, nextSelection) {
|
|
262
|
+
const added = nextSelection.filter((item) => !this.containsItem(previousSelection, item));
|
|
263
|
+
if (added.length === 1) return this.normalizeItem(added[0]);
|
|
264
|
+
|
|
265
|
+
const removed = previousSelection.filter((item) => !this.containsItem(nextSelection, item));
|
|
266
|
+
if (removed.length === 1) return this.normalizeItem(removed[0]);
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
},
|
|
270
|
+
findItemIndex(itemToFind) {
|
|
271
|
+
return this.items.findIndex((item) => this.areItemsEqual(item, itemToFind));
|
|
272
|
+
},
|
|
273
|
+
areSelectionsEqual(first, second) {
|
|
274
|
+
if (first.length !== second.length) return false;
|
|
275
|
+
return first.every((item) => this.containsItem(second, item));
|
|
276
|
+
},
|
|
94
277
|
normalizeModelValue(val) {
|
|
95
278
|
if (this.selectionMode === "none") return [];
|
|
96
279
|
if (this.selectionMode === "single") return val ? [val] : [];
|
|
@@ -129,7 +312,7 @@ export default {
|
|
|
129
312
|
--easy-table-body-row-height: 3.2rem;
|
|
130
313
|
--easy-table-body-row-font-size: 12px;
|
|
131
314
|
--easy-table-body-row-hover-font-color: #1f2937;
|
|
132
|
-
--easy-table-body-row-hover-background-color: #
|
|
315
|
+
--easy-table-body-row-hover-background-color: #eff6ff;
|
|
133
316
|
--easy-table-body-item-padding: 0.5rem 0.75rem;
|
|
134
317
|
|
|
135
318
|
--easy-table-footer-background-color: transparent;
|
|
@@ -146,7 +329,7 @@ export default {
|
|
|
146
329
|
|
|
147
330
|
--easy-table-scrollbar-track-color: transparent;
|
|
148
331
|
--easy-table-scrollbar-color: transparent;
|
|
149
|
-
--easy-table-scrollbar-thumb-color: #
|
|
332
|
+
--easy-table-scrollbar-thumb-color: #93c5fd;
|
|
150
333
|
--easy-table-scrollbar-corner-color: transparent;
|
|
151
334
|
|
|
152
335
|
--easy-table-loading-mask-background-color: rgba(255, 255, 255, 0.6);
|
|
@@ -157,7 +340,7 @@ export default {
|
|
|
157
340
|
background-color: #fff; /* white dropdown background */
|
|
158
341
|
color: #1f2937; /* gray-800 text */
|
|
159
342
|
padding: 0.25rem 0.5rem; /* small padding */
|
|
160
|
-
border: 1px solid #
|
|
343
|
+
border: 1px solid #bfdbfe;
|
|
161
344
|
border-radius: 0.25rem;
|
|
162
345
|
}
|
|
163
346
|
|