ketekny-ui-kit 1.0.31 → 1.0.32

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 +372 -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.32",
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
+ currentPageRowsCache: [],
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
+ const keys = [];
127
+ const seen = new Set();
128
+ this.currentPageRowsCache.forEach((rowItem) => {
129
+ const key = this.resolveIncomingItemKey(rowItem);
130
+ if (!key || seen.has(key)) return;
131
+ const sourceItem = this.currentItemsByKey.get(key);
132
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
133
+ seen.add(key);
134
+ keys.push(key);
135
+ });
136
+ return keys;
137
+ },
138
+ allSelectableSelected() {
139
+ if (!this.pageSelectableKeys.length) return false;
140
+ return this.pageSelectableKeys.every((key) => this.selectedKeySet.has(key));
141
+ },
142
+ partiallySelectableSelected() {
143
+ if (!this.pageSelectableKeys.length || this.allSelectableSelected) return false;
144
+ return this.pageSelectableKeys.some((key) => this.selectedKeySet.has(key));
145
+ },
146
+ },
147
+
148
+ created() {
149
+ this.rebuildItemCaches();
150
+ this.applyExternalModelValue(this.modelValue);
94
151
  },
95
152
 
96
153
  watch: {
154
+ items: {
155
+ handler() {
156
+ this.rebuildItemCaches();
157
+ this.reconcileSelectionWithItems();
158
+ },
159
+ deep: false,
160
+ },
161
+ itemSelectable: {
162
+ handler() {
163
+ this.rebuildItemCaches();
164
+ this.reconcileSelectionWithItems();
165
+ },
166
+ deep: false,
167
+ },
168
+ itemKey: {
169
+ handler() {
170
+ this.rebuildItemCaches();
171
+ this.reconcileSelectionWithItems();
172
+ },
173
+ deep: false,
174
+ },
97
175
  modelValue(val) {
98
- this.internalSelection = this.normalizeModelValue(val);
176
+ this.applyExternalModelValue(val);
177
+ },
178
+ selectionMode() {
179
+ this.applyExternalModelValue(this.modelValue);
99
180
  },
100
181
  },
101
182
 
102
183
  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
- }
184
+ handleSelectAllChange(event) {
185
+ if (this.effectiveSelectionMode !== "multiple" || this.disabled) return;
186
+ const shouldSelect = Boolean(event?.target?.checked);
187
+ const nextKeySet = new Set(this.selectedKeys);
110
188
 
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);
189
+ this.pageSelectableKeys.forEach((key) => {
190
+ if (shouldSelect) nextKeySet.add(key);
191
+ else nextKeySet.delete(key);
192
+ });
193
+
194
+ const nextKeys = this.keyedItems
195
+ .map((item) => item.__kdt_key)
196
+ .filter((itemKey) => nextKeySet.has(itemKey));
197
+
198
+ this.commitSelectionByKeys(nextKeys, { emit: true, anchorKey: null });
124
199
  },
125
- captureSelectionInteraction(event) {
126
- if (this.selectionMode !== "multiple" || this.disabled || !(event.target instanceof Element)) {
200
+ handleItemSelectionChange(event, slotItem) {
201
+ if (this.effectiveSelectionMode === "none" || this.disabled) return;
202
+
203
+ const key = this.resolveIncomingItemKey(slotItem);
204
+ if (!key) return;
205
+
206
+ const sourceItem = this.currentItemsByKey.get(key);
207
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
208
+
209
+ const shouldSelect = Boolean(event?.target?.checked);
210
+
211
+ if (this.selectionMode === "single") {
212
+ this.commitSelectionByKeys(shouldSelect ? [key] : [], {
213
+ emit: true,
214
+ anchorKey: shouldSelect ? key : null,
215
+ });
127
216
  return;
128
217
  }
129
218
 
130
- const checkbox = event.target.closest(".easy-checkbox");
131
- if (!checkbox) return;
219
+ if (event?.shiftKey && this.lastSelectionAnchorKey && key !== this.lastSelectionAnchorKey) {
220
+ const selectableKeySet = new Set(this.selectableKeys);
221
+ const rangeKeys = this.resolveRangeKeys(this.lastSelectionAnchorKey, key).filter((rangeKey) =>
222
+ selectableKeySet.has(rangeKey),
223
+ );
224
+ const nextKeySet = new Set(this.selectedKeys);
225
+
226
+ rangeKeys.forEach((rangeKey) => {
227
+ if (shouldSelect) nextKeySet.add(rangeKey);
228
+ else nextKeySet.delete(rangeKey);
229
+ });
230
+
231
+ const nextKeys = this.keyedItems
232
+ .map((item) => item.__kdt_key)
233
+ .filter((itemKey) => nextKeySet.has(itemKey));
132
234
 
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;
235
+ this.commitSelectionByKeys(nextKeys, { emit: true });
137
236
  return;
138
237
  }
139
238
 
140
- const rowNumber = this.resolveClickedRowNumber(row);
141
- if (!rowNumber) return;
142
- const clickedItem = this.currentPageRowsCache[rowNumber - 1];
239
+ const nextKeySet = new Set(this.selectedKeys);
240
+ if (shouldSelect) nextKeySet.add(key);
241
+ else nextKeySet.delete(key);
143
242
 
144
- if (!event.shiftKey && clickedItem) {
145
- this.lastSelectionAnchor = clickedItem;
146
- this.lastSelectionAnchorRowNumber = rowNumber;
147
- }
243
+ const nextKeys = this.keyedItems
244
+ .map((item) => item.__kdt_key)
245
+ .filter((itemKey) => nextKeySet.has(itemKey));
148
246
 
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
- }
247
+ this.commitSelectionByKeys(nextKeys, { emit: true, anchorKey: key });
248
+ },
249
+ isItemSelected(item) {
250
+ const key = this.resolveIncomingItemKey(item);
251
+ return key != null && this.selectedKeySet.has(key);
252
+ },
253
+ isItemSelectable(item) {
254
+ if (typeof this.itemSelectable !== "function") return true;
255
+ try {
256
+ return this.itemSelectable(this.normalizeItem(item)) !== false;
257
+ } catch {
258
+ return false;
162
259
  }
163
-
164
- this.pendingSelectionInteraction = {
165
- shiftKey: event.shiftKey,
166
- rowNumber,
167
- };
168
260
  },
169
- resolveClickedRowNumber(rowElement) {
170
- const tbody = rowElement.parentElement;
171
- if (!tbody) return null;
261
+ resolveRangeKeys(anchorKey, targetKey) {
262
+ const visibleKeys = this.currentPageRowsCache
263
+ .map((item) => this.resolveIncomingItemKey(item))
264
+ .filter((key) => key != null);
265
+
266
+ const fallbackKeys = this.keyedItems.map((item) => item.__kdt_key);
267
+ const keyOrders = visibleKeys.length ? [visibleKeys, fallbackKeys] : [fallbackKeys];
268
+
269
+ for (const orderedKeys of keyOrders) {
270
+ const anchorIndex = orderedKeys.indexOf(anchorKey);
271
+ const targetIndex = orderedKeys.indexOf(targetKey);
272
+ if (anchorIndex === -1 || targetIndex === -1) continue;
273
+
274
+ const [start, end] =
275
+ anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
276
+ return orderedKeys.slice(start, end + 1);
277
+ }
172
278
 
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;
279
+ return [targetKey];
176
280
  },
177
281
  composeBodyRowClassName(item, rowNumber) {
178
282
  if (rowNumber === 1) {
179
283
  this.currentPageRowsCache = [];
180
284
  }
181
- this.currentPageRowsCache[rowNumber - 1] = this.normalizeItem(item);
285
+ this.currentPageRowsCache[rowNumber - 1] = item;
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,200 @@ export default {
189
294
  }
190
295
  return "";
191
296
  },
192
- resolveShiftRangeSelection(newSelection, oldSelection = []) {
193
- if (this.selectionMode !== "multiple") return newSelection;
297
+ rebuildItemCaches() {
298
+ const keyedItems = [];
299
+ const currentItemsByKey = new Map();
300
+ const baseKeyToKeys = new Map();
301
+ const sourceItemToKey = new WeakMap();
302
+ const selectableKeys = [];
303
+
304
+ this.items.forEach((sourceItem, index) => {
305
+ const baseKey = this.deriveBaseKey(sourceItem, index);
306
+ const uniqueKey = this.ensureUniqueKey(baseKey, currentItemsByKey);
307
+ const normalizedItem = this.normalizeItem(sourceItem);
308
+ const keyedItem =
309
+ normalizedItem && typeof normalizedItem === "object"
310
+ ? { ...normalizedItem, __kdt_key: uniqueKey }
311
+ : { value: normalizedItem, __kdt_key: uniqueKey };
312
+
313
+ keyedItems.push(keyedItem);
314
+ currentItemsByKey.set(uniqueKey, sourceItem);
315
+
316
+ if (sourceItem && typeof sourceItem === "object") {
317
+ sourceItemToKey.set(sourceItem, uniqueKey);
318
+ }
194
319
 
195
- const interaction = this.pendingSelectionInteraction;
196
- this.pendingSelectionInteraction = null;
320
+ const keyList = baseKeyToKeys.get(baseKey) ?? [];
321
+ keyList.push(uniqueKey);
322
+ baseKeyToKeys.set(baseKey, keyList);
197
323
 
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;
324
+ if (this.isItemSelectable(sourceItem)) {
325
+ selectableKeys.push(uniqueKey);
326
+ }
327
+ });
328
+
329
+ this.keyedItems = keyedItems;
330
+ this.currentItemsByKey = currentItemsByKey;
331
+ this.baseKeyToKeys = baseKeyToKeys;
332
+ this.sourceItemToKey = sourceItemToKey;
333
+ this.selectableKeys = selectableKeys;
334
+ this.currentPageRowsCache = [];
335
+ },
336
+ ensureUniqueKey(baseKey, usedKeysMap) {
337
+ if (!usedKeysMap.has(baseKey)) return baseKey;
338
+ let suffix = 1;
339
+ let candidate = `${baseKey}#${suffix}`;
340
+ while (usedKeysMap.has(candidate)) {
341
+ suffix += 1;
342
+ candidate = `${baseKey}#${suffix}`;
343
+ }
344
+ return candidate;
345
+ },
346
+ deriveBaseKey(item, index = null) {
347
+ const normalized = this.normalizeItem(item);
348
+ const configuredKey = this.resolveConfiguredKey(normalized, index);
349
+ if (configuredKey != null && configuredKey !== "") {
350
+ return `k:${String(configuredKey)}`;
351
+ }
204
352
 
205
- if (!interaction?.shiftKey || !hasInteractionToggle || !this.lastSelectionAnchor) {
206
- this.lastSelectionAnchor = toggledItem;
207
- this.lastSelectionAnchorRowNumber = hasInteractionToggle ? interaction?.rowNumber ?? null : null;
208
- return newSelection;
353
+ if (normalized && typeof normalized === "object") {
354
+ if (normalized.id != null) return `id:${String(normalized.id)}`;
355
+ if (normalized.uuid != null) return `uuid:${String(normalized.uuid)}`;
356
+ if (normalized.hospIncNumber != null) {
357
+ return `hospIncNumber:${String(normalized.hospIncNumber)}`;
358
+ }
209
359
  }
210
360
 
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;
361
+ if (index != null) return `index:${index}`;
362
+
363
+ try {
364
+ return `json:${JSON.stringify(normalized)}`;
365
+ } catch {
366
+ return "fallback:0";
367
+ }
368
+ },
369
+ resolveConfiguredKey(item, index = null) {
370
+ if (!this.itemKey) return null;
371
+
372
+ if (typeof this.itemKey === "function") {
373
+ try {
374
+ return this.itemKey(item, index);
375
+ } catch {
376
+ return null;
377
+ }
217
378
  }
218
379
 
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)));
380
+ if (typeof this.itemKey === "string" && item && typeof item === "object") {
381
+ return item[this.itemKey];
237
382
  }
238
383
 
239
- return this.areSelectionsEqual(nextSelection, baseSelection) ? baseSelection : nextSelection;
384
+ return null;
240
385
  },
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;
386
+ resolveIncomingItemKey(item, index = null) {
387
+ const normalized = this.normalizeItem(item);
388
+
389
+ if (normalized && typeof normalized === "object") {
390
+ const slotKey = normalized.__kdt_key;
391
+ if (slotKey != null && this.currentItemsByKey.has(String(slotKey))) {
392
+ return String(slotKey);
393
+ }
394
+
395
+ const byReference = this.sourceItemToKey.get(normalized);
396
+ if (byReference && this.currentItemsByKey.has(byReference)) {
397
+ return byReference;
248
398
  }
249
399
  }
250
400
 
251
- const [start, end] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
252
- return this.items.slice(start, end + 1);
401
+ const baseKey = this.deriveBaseKey(normalized, index);
402
+ const candidates = this.baseKeyToKeys.get(baseKey) ?? [];
403
+
404
+ if (candidates.length === 1) return candidates[0];
405
+ if (candidates.length > 1) return candidates[0];
406
+ return null;
253
407
  },
254
408
  normalizeItem(item) {
255
409
  if (!item || typeof item !== "object") return item;
256
410
  const normalized = { ...item };
257
411
  delete normalized.checkbox;
258
412
  delete normalized.index;
413
+ delete normalized.__kdt_select;
259
414
  return normalized;
260
415
  },
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;
416
+ applyExternalModelValue(value) {
417
+ if (this.selectionMode === "none") {
418
+ this.commitSelectionByKeys([], { emit: false, anchorKey: null });
419
+ return;
274
420
  }
275
421
 
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;
422
+ if (this.selectionMode === "single") {
423
+ const key = value ? this.resolveIncomingItemKey(value) : null;
424
+ this.commitSelectionByKeys(key ? [key] : [], { emit: false, anchorKey: key ?? null });
425
+ return;
282
426
  }
427
+
428
+ const incomingArray = Array.isArray(value) ? value : [];
429
+ const nextKeys = incomingArray
430
+ .map((item, index) => this.resolveIncomingItemKey(item, index))
431
+ .filter((key) => key != null);
432
+ this.commitSelectionByKeys(nextKeys, { emit: false });
283
433
  },
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);
434
+ reconcileSelectionWithItems() {
435
+ const changed = this.commitSelectionByKeys(this.selectedKeys, { emit: false });
436
+ if (changed) {
437
+ this.emitSelection();
438
+ }
289
439
  },
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;
440
+ commitSelectionByKeys(keys, { emit = true, anchorKey } = {}) {
441
+ const requestedKeys = Array.isArray(keys) ? keys : [];
442
+ const nextKeys = [];
443
+ const seen = new Set();
444
+
445
+ requestedKeys.forEach((key) => {
446
+ if (key == null || seen.has(key)) return;
447
+ const sourceItem = this.currentItemsByKey.get(key);
448
+ if (!sourceItem || !this.isItemSelectable(sourceItem)) return;
449
+ seen.add(key);
450
+ nextKeys.push(key);
451
+ });
452
+
453
+ if (this.selectionMode === "single" && nextKeys.length > 1) {
454
+ nextKeys.splice(1);
455
+ }
294
456
 
295
- const removed = previousSelection.filter((item) => !this.containsItem(nextSelection, item));
296
- if (removed.length === 1) return this.normalizeItem(removed[0]);
457
+ const hasChanged = !this.areKeyArraysEqual(nextKeys, this.selectedKeys);
297
458
 
298
- return null;
299
- },
300
- findItemIndex(itemToFind) {
301
- return this.items.findIndex((item) => this.areItemsEqual(item, itemToFind));
459
+ this.selectedKeys = nextKeys;
460
+ this.selectedKeySet = new Set(nextKeys);
461
+ this.internalSelection = nextKeys
462
+ .map((key) => this.currentItemsByKey.get(key))
463
+ .filter((item) => item != null);
464
+
465
+ if (anchorKey !== undefined) {
466
+ this.lastSelectionAnchorKey = anchorKey;
467
+ }
468
+
469
+ if (emit && hasChanged) {
470
+ this.emitSelection();
471
+ }
472
+
473
+ return hasChanged;
302
474
  },
303
- areSelectionsEqual(first, second) {
304
- if (first.length !== second.length) return false;
305
- return first.every((item) => this.containsItem(second, item));
475
+ areKeyArraysEqual(left, right) {
476
+ if (left.length !== right.length) return false;
477
+ return left.every((key, index) => key === right[index]);
306
478
  },
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 [];
479
+ emitSelection() {
480
+ if (this.selectionMode === "single") {
481
+ this.$emit("update:modelValue", this.internalSelection[0] ?? null);
482
+ return;
483
+ }
484
+
485
+ if (this.selectionMode === "multiple") {
486
+ this.$emit("update:modelValue", [...this.internalSelection]);
487
+ return;
488
+ }
489
+
490
+ this.$emit("update:modelValue", null);
312
491
  },
313
492
  },
314
493
  };
@@ -348,12 +527,11 @@ export default {
348
527
  --easy-table-footer-background-color: transparent;
349
528
  --easy-table-footer-font-color: #1f2937;
350
529
  --easy-table-footer-font-size: 14px;
351
- --easy-table-footer-padding: 1rem; /* Added padding */
530
+ --easy-table-footer-padding: 1rem;
352
531
  --easy-table-footer-height: auto;
353
532
 
354
533
  --easy-table-rows-per-page-selector-width: 70px;
355
534
  --easy-table-rows-per-page-selector-option-padding: 10px;
356
- --easy-table-rows-per-page-selector-z-index: 1;
357
535
  --easy-table-rows-per-page-selector-z-index: 200;
358
536
  --easy-table-rows-per-page-selector-font-color: #1f2937;
359
537
 
@@ -365,11 +543,17 @@ export default {
365
543
  --easy-table-loading-mask-background-color: rgba(255, 255, 255, 0.6);
366
544
  }
367
545
 
546
+ .k-datatable-select-cell {
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ }
551
+
368
552
  /* Target the rows-per-page dropdown */
369
553
  .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 */
554
+ background-color: #fff;
555
+ color: #1f2937;
556
+ padding: 0.25rem 0.5rem;
373
557
  border: 1px solid #bfdbfe;
374
558
  border-radius: 0.25rem;
375
559
  }