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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ui/kDatatable.vue +195 -12
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.27",
4
+ "version": "1.0.29",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -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
- v-model:items-selected="internalSelection"
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
- internalSelection(newVal) {
100
+ },
101
+
102
+ methods: {
103
+ handleSelectionUpdate(newSelection) {
85
104
  if (this.selectionMode === "single") {
86
- this.$emit("update:modelValue", newVal[0] || null);
87
- } else if (this.selectionMode === "multiple") {
88
- this.$emit("update:modelValue", newVal);
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
- methods: {
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: #ecfdf3;
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: #86efac;
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 #bbf7d0;
343
+ border: 1px solid #bfdbfe;
161
344
  border-radius: 0.25rem;
162
345
  }
163
346