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