ketekny-ui-kit 1.0.31 → 1.0.33

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 +385 -188
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.31",
4
+ "version": "1.0.33",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -1,31 +1,51 @@
1
1
  <template>
2
- <div ref="tableWrapper" :class="['k-datatable-wrapper rounded-lg border border-primary/15', disabled ? 'pointer-events-none opacity-60' : '']">
2
+ <div
3
+ ref="tableWrapper"
4
+ :class="[
5
+ 'k-datatable-wrapper rounded-lg border border-primary/15',
6
+ disabled ? 'pointer-events-none opacity-60' : '',
7
+ ]"
8
+ >
3
9
  <EasyDataTable
4
10
  v-bind="$attrs"
5
- :headers="headers"
6
- :items="items"
11
+ :headers="computedHeaders"
12
+ :items="keyedItems"
7
13
  :search="search"
8
14
  :theme-color="themeColor"
9
- :show-select="effectiveSelectionMode !== 'none'"
10
- :single-select="effectiveSelectionMode === 'single'"
11
- v-if="effectiveSelectionMode !== 'none'"
12
- :items-selected="internalSelection"
13
- @update:items-selected="handleSelectionUpdate"
14
- :itemSelectable="itemSelectable"
15
+ :show-select="false"
15
16
  :body-row-class-name="composeBodyRowClassName"
16
17
  alternating
17
18
  buttons-pagination
18
19
  table-class-name="customize-table"
19
20
  >
20
- <!-- Forward all named slots -->
21
- <template v-for="(_, name) in $slots" :key="name" v-slot:[name]="slotProps">
22
- <slot :name="name" v-bind="slotProps" />
21
+ <template v-if="effectiveSelectionMode === 'multiple'" #header-__kdt_select>
22
+ <div class="k-datatable-select-cell">
23
+ <input
24
+ type="checkbox"
25
+ class="easy-checkbox"
26
+ :checked="allSelectableSelected"
27
+ :indeterminate.prop="partiallySelectableSelected"
28
+ :disabled="disabled || pageSelectableKeys.length === 0"
29
+ @click.stop
30
+ @change="handleSelectAllChange"
31
+ />
32
+ </div>
33
+ </template>
34
+
35
+ <template v-if="effectiveSelectionMode !== 'none'" #item-__kdt_select="slotItem">
36
+ <div class="k-datatable-select-cell">
37
+ <input
38
+ type="checkbox"
39
+ class="easy-checkbox"
40
+ :checked="isItemSelected(slotItem)"
41
+ :disabled="disabled || !isItemSelectable(slotItem)"
42
+ @click.stop
43
+ @change="($event) => handleItemSelectionChange($event, slotItem)"
44
+ />
45
+ </div>
23
46
  </template>
24
- </EasyDataTable>
25
47
 
26
- <!-- Render separate table without v-model when no selection -->
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">
28
- <template v-for="(_, name) in $slots" :key="name" v-slot:[name]="slotProps">
48
+ <template v-for="name in forwardedSlotNames" :key="name" v-slot:[name]="slotProps">
29
49
  <slot :name="name" v-bind="slotProps" />
30
50
  </template>
31
51
  </EasyDataTable>
@@ -39,7 +59,7 @@ import "vue3-easy-data-table/dist/style.css";
39
59
  export default {
40
60
  name: "kDatatable",
41
61
  components: { EasyDataTable },
42
- inheritAttrs: false, // Optional: for even cleaner control (optional)
62
+ inheritAttrs: false,
43
63
 
44
64
  props: {
45
65
  headers: { type: Array, required: true },
@@ -59,7 +79,12 @@ export default {
59
79
 
60
80
  itemSelectable: {
61
81
  type: Function,
62
- default: () => () => true, // all selectable by default
82
+ default: () => true,
83
+ },
84
+
85
+ itemKey: {
86
+ type: [String, Function, null],
87
+ default: null,
63
88
  },
64
89
 
65
90
  modelValue: {
@@ -72,115 +97,195 @@ export default {
72
97
 
73
98
  data() {
74
99
  return {
75
- internalSelection: this.normalizeModelValue(this.modelValue),
76
- pendingSelectionInteraction: null,
77
- lastSelectionAnchor: null,
78
- lastSelectionAnchorRowNumber: null,
100
+ internalSelection: [],
101
+ selectedKeys: [],
102
+ selectedKeySet: new Set(),
103
+ lastSelectionAnchorKey: null,
104
+ _pageSelectableKeys: [],
105
+ keyedItems: [],
106
+ currentItemsByKey: new Map(),
107
+ baseKeyToKeys: new Map(),
108
+ sourceItemToKey: new WeakMap(),
109
+ selectableKeys: [],
79
110
  };
80
111
  },
81
- created() {
82
- this.currentPageRowsCache = [];
83
- },
84
- mounted() {
85
- this.bindSelectionInteractionListener();
86
- },
87
- beforeUnmount() {
88
- this.unbindSelectionInteractionListener();
89
- },
112
+
90
113
  computed: {
91
114
  effectiveSelectionMode() {
92
115
  return this.disabled ? "none" : this.selectionMode;
93
116
  },
117
+ computedHeaders() {
118
+ if (this.effectiveSelectionMode === "none") return this.headers;
119
+ return [{ text: "", value: "__kdt_select", width: 36 }, ...this.headers];
120
+ },
121
+ forwardedSlotNames() {
122
+ const reserved = new Set(["header-__kdt_select", "item-__kdt_select"]);
123
+ return Object.keys(this.$slots).filter((name) => !reserved.has(name));
124
+ },
125
+ pageSelectableKeys() {
126
+ return this._pageSelectableKeys;
127
+ },
128
+ allSelectableSelected() {
129
+ if (!this.pageSelectableKeys.length) return false;
130
+ return this.pageSelectableKeys.every((key) => this.selectedKeySet.has(key));
131
+ },
132
+ partiallySelectableSelected() {
133
+ if (!this.pageSelectableKeys.length || this.allSelectableSelected) return false;
134
+ return this.pageSelectableKeys.some((key) => this.selectedKeySet.has(key));
135
+ },
136
+ },
137
+
138
+ created() {
139
+ this.currentPageRowsCache = []; // non-reactive — mutated during EasyDataTable render
140
+ this._pageRenderScheduled = false; // non-reactive flag
141
+ this.rebuildItemCaches();
142
+ this.applyExternalModelValue(this.modelValue);
94
143
  },
95
144
 
96
145
  watch: {
146
+ items: {
147
+ handler() {
148
+ this.rebuildItemCaches();
149
+ this.reconcileSelectionWithItems();
150
+ },
151
+ deep: false,
152
+ },
153
+ itemSelectable: {
154
+ handler() {
155
+ this.rebuildItemCaches();
156
+ this.reconcileSelectionWithItems();
157
+ },
158
+ deep: false,
159
+ },
160
+ itemKey: {
161
+ handler() {
162
+ this.rebuildItemCaches();
163
+ this.reconcileSelectionWithItems();
164
+ },
165
+ deep: false,
166
+ },
97
167
  modelValue(val) {
98
- this.internalSelection = this.normalizeModelValue(val);
168
+ this.applyExternalModelValue(val);
169
+ },
170
+ selectionMode() {
171
+ this.applyExternalModelValue(this.modelValue);
99
172
  },
100
173
  },
101
174
 
102
175
  methods: {
103
- handleSelectionUpdate(newSelection) {
104
- if (this.selectionMode === "single") {
105
- const nextSelection = Array.isArray(newSelection) ? newSelection : [];
106
- this.internalSelection = nextSelection;
107
- this.$emit("update:modelValue", nextSelection[0] || null);
108
- return;
109
- }
176
+ handleSelectAllChange(event) {
177
+ if (this.effectiveSelectionMode !== "multiple" || this.disabled) return;
178
+ const shouldSelect = Boolean(event?.target?.checked);
179
+ const nextKeySet = new Set(this.selectedKeys);
110
180
 
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);
117
- }
118
- },
119
- bindSelectionInteractionListener() {
120
- this.$refs.tableWrapper?.addEventListener("click", this.captureSelectionInteraction, true);
121
- },
122
- unbindSelectionInteractionListener() {
123
- this.$refs.tableWrapper?.removeEventListener("click", this.captureSelectionInteraction, true);
181
+ this.pageSelectableKeys.forEach((key) => {
182
+ if (shouldSelect) nextKeySet.add(key);
183
+ else nextKeySet.delete(key);
184
+ });
185
+
186
+ const nextKeys = this.keyedItems
187
+ .map((item) => item.__kdt_key)
188
+ .filter((itemKey) => nextKeySet.has(itemKey));
189
+
190
+ this.commitSelectionByKeys(nextKeys, { emit: true, anchorKey: null });
124
191
  },
125
- captureSelectionInteraction(event) {
126
- if (this.selectionMode !== "multiple" || this.disabled || !(event.target instanceof Element)) {
192
+ handleItemSelectionChange(event, slotItem) {
193
+ if (this.effectiveSelectionMode === "none" || this.disabled) return;
194
+
195
+ const key = this.resolveIncomingItemKey(slotItem);
196
+ if (!key) return;
197
+
198
+ const sourceItem = this.currentItemsByKey.get(key);
199
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
200
+
201
+ const shouldSelect = Boolean(event?.target?.checked);
202
+
203
+ if (this.selectionMode === "single") {
204
+ this.commitSelectionByKeys(shouldSelect ? [key] : [], {
205
+ emit: true,
206
+ anchorKey: shouldSelect ? key : null,
207
+ });
127
208
  return;
128
209
  }
129
210
 
130
- const checkbox = event.target.closest(".easy-checkbox");
131
- if (!checkbox) return;
211
+ if (event?.shiftKey && this.lastSelectionAnchorKey && key !== this.lastSelectionAnchorKey) {
212
+ const selectableKeySet = new Set(this.selectableKeys);
213
+ const rangeKeys = this.resolveRangeKeys(this.lastSelectionAnchorKey, key).filter((rangeKey) =>
214
+ selectableKeySet.has(rangeKey),
215
+ );
216
+ const nextKeySet = new Set(this.selectedKeys);
132
217
 
133
- const row = checkbox.closest("tbody tr");
134
- if (!row) {
135
- // Clear pending row interaction so bulk select doesn't reuse stale row state.
136
- this.pendingSelectionInteraction = null;
218
+ rangeKeys.forEach((rangeKey) => {
219
+ if (shouldSelect) nextKeySet.add(rangeKey);
220
+ else nextKeySet.delete(rangeKey);
221
+ });
222
+
223
+ const nextKeys = this.keyedItems
224
+ .map((item) => item.__kdt_key)
225
+ .filter((itemKey) => nextKeySet.has(itemKey));
226
+
227
+ this.commitSelectionByKeys(nextKeys, { emit: true });
137
228
  return;
138
229
  }
139
230
 
140
- const rowNumber = this.resolveClickedRowNumber(row);
141
- if (!rowNumber) return;
142
- const clickedItem = this.currentPageRowsCache[rowNumber - 1];
231
+ const nextKeySet = new Set(this.selectedKeys);
232
+ if (shouldSelect) nextKeySet.add(key);
233
+ else nextKeySet.delete(key);
143
234
 
144
- if (!event.shiftKey && clickedItem) {
145
- this.lastSelectionAnchor = clickedItem;
146
- this.lastSelectionAnchorRowNumber = rowNumber;
147
- }
235
+ const nextKeys = this.keyedItems
236
+ .map((item) => item.__kdt_key)
237
+ .filter((itemKey) => nextKeySet.has(itemKey));
148
238
 
149
- if (event.shiftKey && this.lastSelectionAnchor && clickedItem) {
150
- const anchorIndex = this.findItemIndex(this.lastSelectionAnchor);
151
- const targetIndex = this.findItemIndex(clickedItem);
152
- if (anchorIndex !== -1 && targetIndex !== -1) {
153
- event.preventDefault();
154
- event.stopPropagation();
155
- const shouldSelectRange = !this.containsItem(this.internalSelection, clickedItem);
156
- const nextSelection = this.applyRangeSelection(this.internalSelection, anchorIndex, targetIndex, rowNumber, shouldSelectRange);
157
- this.pendingSelectionInteraction = null;
158
- this.internalSelection = nextSelection;
159
- this.$emit("update:modelValue", nextSelection);
160
- return;
161
- }
239
+ this.commitSelectionByKeys(nextKeys, { emit: true, anchorKey: key });
240
+ },
241
+ isItemSelected(item) {
242
+ const key = this.resolveIncomingItemKey(item);
243
+ return key != null && this.selectedKeySet.has(key);
244
+ },
245
+ isItemSelectable(item) {
246
+ if (typeof this.itemSelectable !== "function") return true;
247
+ try {
248
+ return this.itemSelectable(this.normalizeItem(item)) !== false;
249
+ } catch {
250
+ return false;
162
251
  }
163
-
164
- this.pendingSelectionInteraction = {
165
- shiftKey: event.shiftKey,
166
- rowNumber,
167
- };
168
252
  },
169
- resolveClickedRowNumber(rowElement) {
170
- const tbody = rowElement.parentElement;
171
- if (!tbody) return null;
253
+ resolveRangeKeys(anchorKey, targetKey) {
254
+ const visibleKeys = this.currentPageRowsCache
255
+ .map((item) => this.resolveIncomingItemKey(item))
256
+ .filter((key) => key != null);
257
+
258
+ const fallbackKeys = this.keyedItems.map((item) => item.__kdt_key);
259
+ const keyOrders = visibleKeys.length ? [visibleKeys, fallbackKeys] : [fallbackKeys];
260
+
261
+ for (const orderedKeys of keyOrders) {
262
+ const anchorIndex = orderedKeys.indexOf(anchorKey);
263
+ const targetIndex = orderedKeys.indexOf(targetKey);
264
+ if (anchorIndex === -1 || targetIndex === -1) continue;
265
+
266
+ const [start, end] =
267
+ anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
268
+ return orderedKeys.slice(start, end + 1);
269
+ }
172
270
 
173
- const selectableRows = Array.from(tbody.querySelectorAll("tr")).filter((row) => row.querySelector(".easy-checkbox"));
174
- const rowIndex = selectableRows.indexOf(rowElement);
175
- return rowIndex === -1 ? null : rowIndex + 1;
271
+ return [targetKey];
176
272
  },
177
273
  composeBodyRowClassName(item, rowNumber) {
178
274
  if (rowNumber === 1) {
179
275
  this.currentPageRowsCache = [];
180
276
  }
181
- this.currentPageRowsCache[rowNumber - 1] = this.normalizeItem(item);
277
+ this.currentPageRowsCache[rowNumber - 1] = item;
278
+
279
+ if (!this._pageRenderScheduled) {
280
+ this._pageRenderScheduled = true;
281
+ this.$nextTick(() => {
282
+ this._pageRenderScheduled = false;
283
+ this._pageSelectableKeys = this._computePageSelectableKeys();
284
+ });
285
+ }
182
286
 
183
- const externalBodyRowClass = this.$attrs["body-row-class-name"] ?? this.$attrs.bodyRowClassName;
287
+ const externalBodyRowClass =
288
+ this.$attrs["body-row-class-name"] ?? this.$attrs.bodyRowClassName;
184
289
  if (typeof externalBodyRowClass === "function") {
185
290
  return externalBodyRowClass(item, rowNumber);
186
291
  }
@@ -189,126 +294,213 @@ export default {
189
294
  }
190
295
  return "";
191
296
  },
192
- resolveShiftRangeSelection(newSelection, oldSelection = []) {
193
- if (this.selectionMode !== "multiple") return newSelection;
297
+ _computePageSelectableKeys() {
298
+ const keys = [];
299
+ const seen = new Set();
300
+ this.currentPageRowsCache.forEach((rowItem) => {
301
+ const key = this.resolveIncomingItemKey(rowItem);
302
+ if (!key || seen.has(key)) return;
303
+ const sourceItem = this.currentItemsByKey.get(key);
304
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
305
+ seen.add(key);
306
+ keys.push(key);
307
+ });
308
+ return keys;
309
+ },
310
+ rebuildItemCaches() {
311
+ const keyedItems = [];
312
+ const currentItemsByKey = new Map();
313
+ const baseKeyToKeys = new Map();
314
+ const sourceItemToKey = new WeakMap();
315
+ const selectableKeys = [];
316
+
317
+ this.items.forEach((sourceItem, index) => {
318
+ const baseKey = this.deriveBaseKey(sourceItem, index);
319
+ const uniqueKey = this.ensureUniqueKey(baseKey, currentItemsByKey);
320
+ const normalizedItem = this.normalizeItem(sourceItem);
321
+ const keyedItem =
322
+ normalizedItem && typeof normalizedItem === "object"
323
+ ? { ...normalizedItem, __kdt_key: uniqueKey }
324
+ : { value: normalizedItem, __kdt_key: uniqueKey };
325
+
326
+ keyedItems.push(keyedItem);
327
+ currentItemsByKey.set(uniqueKey, sourceItem);
328
+
329
+ if (sourceItem && typeof sourceItem === "object") {
330
+ sourceItemToKey.set(sourceItem, uniqueKey);
331
+ }
194
332
 
195
- const interaction = this.pendingSelectionInteraction;
196
- this.pendingSelectionInteraction = null;
333
+ const keyList = baseKeyToKeys.get(baseKey) ?? [];
334
+ keyList.push(uniqueKey);
335
+ baseKeyToKeys.set(baseKey, keyList);
197
336
 
198
- const toggledItemFromInteraction = interaction?.rowNumber ? this.currentPageRowsCache[interaction.rowNumber - 1] : null;
199
- const hasInteractionToggle = toggledItemFromInteraction
200
- ? this.didItemSelectionChange(toggledItemFromInteraction, oldSelection, newSelection)
201
- : false;
202
- const toggledItem = hasInteractionToggle ? toggledItemFromInteraction : this.getToggledItem(oldSelection, newSelection);
203
- if (!toggledItem) return newSelection;
337
+ if (this.isItemSelectable(sourceItem)) {
338
+ selectableKeys.push(uniqueKey);
339
+ }
340
+ });
341
+
342
+ this.keyedItems = keyedItems;
343
+ this.currentItemsByKey = currentItemsByKey;
344
+ this.baseKeyToKeys = baseKeyToKeys;
345
+ this.sourceItemToKey = sourceItemToKey;
346
+ this.selectableKeys = selectableKeys;
347
+ this.currentPageRowsCache = [];
348
+ },
349
+ ensureUniqueKey(baseKey, usedKeysMap) {
350
+ if (!usedKeysMap.has(baseKey)) return baseKey;
351
+ let suffix = 1;
352
+ let candidate = `${baseKey}#${suffix}`;
353
+ while (usedKeysMap.has(candidate)) {
354
+ suffix += 1;
355
+ candidate = `${baseKey}#${suffix}`;
356
+ }
357
+ return candidate;
358
+ },
359
+ deriveBaseKey(item, index = null) {
360
+ const normalized = this.normalizeItem(item);
361
+ const configuredKey = this.resolveConfiguredKey(normalized, index);
362
+ if (configuredKey != null && configuredKey !== "") {
363
+ return `k:${String(configuredKey)}`;
364
+ }
204
365
 
205
- if (!interaction?.shiftKey || !hasInteractionToggle || !this.lastSelectionAnchor) {
206
- this.lastSelectionAnchor = toggledItem;
207
- this.lastSelectionAnchorRowNumber = hasInteractionToggle ? interaction?.rowNumber ?? null : null;
208
- return newSelection;
366
+ if (normalized && typeof normalized === "object") {
367
+ if (normalized.id != null) return `id:${String(normalized.id)}`;
368
+ if (normalized.uuid != null) return `uuid:${String(normalized.uuid)}`;
369
+ if (normalized.hospIncNumber != null) {
370
+ return `hospIncNumber:${String(normalized.hospIncNumber)}`;
371
+ }
209
372
  }
210
373
 
211
- const anchorIndex = this.findItemIndex(this.lastSelectionAnchor);
212
- const targetIndex = this.findItemIndex(toggledItem);
213
- if (anchorIndex === -1 || targetIndex === -1) {
214
- this.lastSelectionAnchor = toggledItem;
215
- this.lastSelectionAnchorRowNumber = interaction?.rowNumber ?? null;
216
- return newSelection;
374
+ if (index != null) return `index:${index}`;
375
+
376
+ try {
377
+ return `json:${JSON.stringify(normalized)}`;
378
+ } catch {
379
+ return "fallback:0";
380
+ }
381
+ },
382
+ resolveConfiguredKey(item, index = null) {
383
+ if (!this.itemKey) return null;
384
+
385
+ if (typeof this.itemKey === "function") {
386
+ try {
387
+ return this.itemKey(item, index);
388
+ } catch {
389
+ return null;
390
+ }
217
391
  }
218
392
 
219
- const rangeItems = this.getRangeItems(anchorIndex, targetIndex, interaction.rowNumber);
220
- if (!rangeItems.length) return newSelection;
221
- const shouldSelectRange = this.containsItem(newSelection, toggledItem);
222
- return this.applyRangeSelection(newSelection, anchorIndex, targetIndex, interaction.rowNumber, shouldSelectRange);
223
- },
224
- applyRangeSelection(baseSelection, anchorIndex, targetIndex, targetRowNumber, shouldSelectRange) {
225
- const rangeItems = this.getRangeItems(anchorIndex, targetIndex, targetRowNumber);
226
- if (!rangeItems.length) return baseSelection;
227
-
228
- let nextSelection = [...baseSelection];
229
- if (shouldSelectRange) {
230
- rangeItems.forEach((item) => {
231
- if (!this.containsItem(nextSelection, item)) {
232
- nextSelection.push(this.normalizeItem(item));
233
- }
234
- });
235
- } else {
236
- nextSelection = nextSelection.filter((selectedItem) => !rangeItems.some((item) => this.areItemsEqual(selectedItem, item)));
393
+ if (typeof this.itemKey === "string" && item && typeof item === "object") {
394
+ return item[this.itemKey];
237
395
  }
238
396
 
239
- return this.areSelectionsEqual(nextSelection, baseSelection) ? baseSelection : nextSelection;
397
+ return null;
240
398
  },
241
- getRangeItems(anchorIndex, targetIndex, targetRowNumber) {
242
- const anchorRowNumber = this.lastSelectionAnchorRowNumber;
243
- if (anchorRowNumber && targetRowNumber) {
244
- const [start, end] = anchorRowNumber <= targetRowNumber ? [anchorRowNumber, targetRowNumber] : [targetRowNumber, anchorRowNumber];
245
- const visibleRange = this.currentPageRowsCache.slice(start - 1, end).filter((item) => item);
246
- if (visibleRange.length === end - start + 1) {
247
- return visibleRange;
399
+ resolveIncomingItemKey(item, index = null) {
400
+ const normalized = this.normalizeItem(item);
401
+
402
+ if (normalized && typeof normalized === "object") {
403
+ const slotKey = normalized.__kdt_key;
404
+ if (slotKey != null && this.currentItemsByKey.has(String(slotKey))) {
405
+ return String(slotKey);
406
+ }
407
+
408
+ const byReference = this.sourceItemToKey.get(normalized);
409
+ if (byReference && this.currentItemsByKey.has(byReference)) {
410
+ return byReference;
248
411
  }
249
412
  }
250
413
 
251
- const [start, end] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
252
- return this.items.slice(start, end + 1);
414
+ const baseKey = this.deriveBaseKey(normalized, index);
415
+ const candidates = this.baseKeyToKeys.get(baseKey) ?? [];
416
+
417
+ if (candidates.length === 1) return candidates[0];
418
+ if (candidates.length > 1) return candidates[0];
419
+ return null;
253
420
  },
254
421
  normalizeItem(item) {
255
422
  if (!item || typeof item !== "object") return item;
256
423
  const normalized = { ...item };
257
424
  delete normalized.checkbox;
258
425
  delete normalized.index;
426
+ delete normalized.__kdt_select;
259
427
  return normalized;
260
428
  },
261
- extractStableKey(item) {
262
- const normalized = this.normalizeItem(item);
263
- if (!normalized || typeof normalized !== "object") return null;
264
- if (normalized.id != null) return `id:${normalized.id}`;
265
- if (normalized.uuid != null) return `uuid:${normalized.uuid}`;
266
- if (normalized.hospIncNumber != null) return `hospIncNumber:${normalized.hospIncNumber}`;
267
- return null;
268
- },
269
- areItemsEqual(left, right) {
270
- const leftKey = this.extractStableKey(left);
271
- const rightKey = this.extractStableKey(right);
272
- if (leftKey != null || rightKey != null) {
273
- return leftKey != null && leftKey === rightKey;
429
+ applyExternalModelValue(value) {
430
+ if (this.selectionMode === "none") {
431
+ this.commitSelectionByKeys([], { emit: false, anchorKey: null });
432
+ return;
274
433
  }
275
434
 
276
- const normalizedLeft = this.normalizeItem(left);
277
- const normalizedRight = this.normalizeItem(right);
278
- try {
279
- return JSON.stringify(normalizedLeft) === JSON.stringify(normalizedRight);
280
- } catch {
281
- return normalizedLeft === normalizedRight;
435
+ if (this.selectionMode === "single") {
436
+ const key = value ? this.resolveIncomingItemKey(value) : null;
437
+ this.commitSelectionByKeys(key ? [key] : [], { emit: false, anchorKey: key ?? null });
438
+ return;
282
439
  }
440
+
441
+ const incomingArray = Array.isArray(value) ? value : [];
442
+ const nextKeys = incomingArray
443
+ .map((item, index) => this.resolveIncomingItemKey(item, index))
444
+ .filter((key) => key != null);
445
+ this.commitSelectionByKeys(nextKeys, { emit: false });
283
446
  },
284
- containsItem(collection, item) {
285
- return collection.some((current) => this.areItemsEqual(current, item));
286
- },
287
- didItemSelectionChange(item, previousSelection, nextSelection) {
288
- return this.containsItem(previousSelection, item) !== this.containsItem(nextSelection, item);
447
+ reconcileSelectionWithItems() {
448
+ const changed = this.commitSelectionByKeys(this.selectedKeys, { emit: false });
449
+ if (changed) {
450
+ this.emitSelection();
451
+ }
289
452
  },
290
- getToggledItem(previousSelection, nextSelection) {
291
- const added = nextSelection.filter((item) => !this.containsItem(previousSelection, item));
292
- if (added.length === 1) return this.normalizeItem(added[0]);
293
- if (added.length > 1) return null;
453
+ commitSelectionByKeys(keys, { emit = true, anchorKey } = {}) {
454
+ const requestedKeys = Array.isArray(keys) ? keys : [];
455
+ const nextKeys = [];
456
+ const seen = new Set();
457
+
458
+ requestedKeys.forEach((key) => {
459
+ if (key == null || seen.has(key)) return;
460
+ const sourceItem = this.currentItemsByKey.get(key);
461
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
462
+ seen.add(key);
463
+ nextKeys.push(key);
464
+ });
465
+
466
+ if (this.selectionMode === "single" && nextKeys.length > 1) {
467
+ nextKeys.splice(1);
468
+ }
294
469
 
295
- const removed = previousSelection.filter((item) => !this.containsItem(nextSelection, item));
296
- if (removed.length === 1) return this.normalizeItem(removed[0]);
470
+ const hasChanged = !this.areKeyArraysEqual(nextKeys, this.selectedKeys);
297
471
 
298
- return null;
299
- },
300
- findItemIndex(itemToFind) {
301
- return this.items.findIndex((item) => this.areItemsEqual(item, itemToFind));
472
+ this.selectedKeys = nextKeys;
473
+ this.selectedKeySet = new Set(nextKeys);
474
+ this.internalSelection = nextKeys
475
+ .map((key) => this.currentItemsByKey.get(key))
476
+ .filter((item) => item != null);
477
+
478
+ if (anchorKey !== undefined) {
479
+ this.lastSelectionAnchorKey = anchorKey;
480
+ }
481
+
482
+ if (emit && hasChanged) {
483
+ this.emitSelection();
484
+ }
485
+
486
+ return hasChanged;
302
487
  },
303
- areSelectionsEqual(first, second) {
304
- if (first.length !== second.length) return false;
305
- return first.every((item) => this.containsItem(second, item));
488
+ areKeyArraysEqual(left, right) {
489
+ if (left.length !== right.length) return false;
490
+ return left.every((key, index) => key === right[index]);
306
491
  },
307
- normalizeModelValue(val) {
308
- if (this.selectionMode === "none") return [];
309
- if (this.selectionMode === "single") return val ? [val] : [];
310
- if (this.selectionMode === "multiple") return Array.isArray(val) ? val : [];
311
- return [];
492
+ emitSelection() {
493
+ if (this.selectionMode === "single") {
494
+ this.$emit("update:modelValue", this.internalSelection[0] ?? null);
495
+ return;
496
+ }
497
+
498
+ if (this.selectionMode === "multiple") {
499
+ this.$emit("update:modelValue", [...this.internalSelection]);
500
+ return;
501
+ }
502
+
503
+ this.$emit("update:modelValue", null);
312
504
  },
313
505
  },
314
506
  };
@@ -348,12 +540,11 @@ export default {
348
540
  --easy-table-footer-background-color: transparent;
349
541
  --easy-table-footer-font-color: #1f2937;
350
542
  --easy-table-footer-font-size: 14px;
351
- --easy-table-footer-padding: 1rem; /* Added padding */
543
+ --easy-table-footer-padding: 1rem;
352
544
  --easy-table-footer-height: auto;
353
545
 
354
546
  --easy-table-rows-per-page-selector-width: 70px;
355
547
  --easy-table-rows-per-page-selector-option-padding: 10px;
356
- --easy-table-rows-per-page-selector-z-index: 1;
357
548
  --easy-table-rows-per-page-selector-z-index: 200;
358
549
  --easy-table-rows-per-page-selector-font-color: #1f2937;
359
550
 
@@ -365,11 +556,17 @@ export default {
365
556
  --easy-table-loading-mask-background-color: rgba(255, 255, 255, 0.6);
366
557
  }
367
558
 
559
+ .k-datatable-select-cell {
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: center;
563
+ }
564
+
368
565
  /* Target the rows-per-page dropdown */
369
566
  .customize-table .easy-data-table__footer select {
370
- background-color: #fff; /* white dropdown background */
371
- color: #1f2937; /* gray-800 text */
372
- padding: 0.25rem 0.5rem; /* small padding */
567
+ background-color: #fff;
568
+ color: #1f2937;
569
+ padding: 0.25rem 0.5rem;
373
570
  border: 1px solid #bfdbfe;
374
571
  border-radius: 0.25rem;
375
572
  }