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 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.28",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -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
- v-model:items-selected="internalSelection"
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
- internalSelection(newVal) {
98
+ },
99
+
100
+ methods: {
101
+ handleSelectionUpdate(newSelection) {
85
102
  if (this.selectionMode === "single") {
86
- this.$emit("update:modelValue", newVal[0] || null);
87
- } else if (this.selectionMode === "multiple") {
88
- this.$emit("update:modelValue", newVal);
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
- methods: {
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] : [];