@keenthemes/ktui 1.2.0 → 1.2.1
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/dist/ktui.js +3349 -1550
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +1 -1
- package/lib/cjs/components/clipboard/clipboard.d.ts +37 -0
- package/lib/cjs/components/clipboard/clipboard.d.ts.map +1 -0
- package/lib/cjs/components/clipboard/clipboard.js +402 -0
- package/lib/cjs/components/clipboard/clipboard.js.map +1 -0
- package/lib/cjs/components/clipboard/index.d.ts +3 -0
- package/lib/cjs/components/clipboard/index.d.ts.map +1 -0
- package/lib/cjs/components/clipboard/index.js +6 -0
- package/lib/cjs/components/clipboard/index.js.map +1 -0
- package/lib/cjs/components/clipboard/types.d.ts +44 -0
- package/lib/cjs/components/clipboard/types.d.ts.map +1 -0
- package/lib/cjs/components/clipboard/types.js +7 -0
- package/lib/cjs/components/clipboard/types.js.map +1 -0
- package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/range-slider/index.d.ts +7 -0
- package/lib/cjs/components/range-slider/index.d.ts.map +1 -0
- package/lib/cjs/components/range-slider/index.js +10 -0
- package/lib/cjs/components/range-slider/index.js.map +1 -0
- package/lib/cjs/components/range-slider/range-slider.d.ts +42 -0
- package/lib/cjs/components/range-slider/range-slider.d.ts.map +1 -0
- package/lib/cjs/components/range-slider/range-slider.js +254 -0
- package/lib/cjs/components/range-slider/range-slider.js.map +1 -0
- package/lib/cjs/components/range-slider/types.d.ts +33 -0
- package/lib/cjs/components/range-slider/types.d.ts.map +1 -0
- package/lib/cjs/components/range-slider/types.js +7 -0
- package/lib/cjs/components/range-slider/types.js.map +1 -0
- package/lib/cjs/components/rating/rating.d.ts.map +1 -1
- package/lib/cjs/components/rating/rating.js +8 -3
- package/lib/cjs/components/rating/rating.js.map +1 -1
- package/lib/cjs/components/repeater/repeater.d.ts.map +1 -1
- package/lib/cjs/components/repeater/repeater.js +3 -2
- package/lib/cjs/components/repeater/repeater.js.map +1 -1
- package/lib/cjs/components/select/utils.d.ts.map +1 -1
- package/lib/cjs/components/select/utils.js +3 -1
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.d.ts.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +3 -1
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/cjs/index.d.ts +8 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +9 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/clipboard/clipboard.d.ts +37 -0
- package/lib/esm/components/clipboard/clipboard.d.ts.map +1 -0
- package/lib/esm/components/clipboard/clipboard.js +399 -0
- package/lib/esm/components/clipboard/clipboard.js.map +1 -0
- package/lib/esm/components/clipboard/index.d.ts +3 -0
- package/lib/esm/components/clipboard/index.d.ts.map +1 -0
- package/lib/esm/components/clipboard/index.js +2 -0
- package/lib/esm/components/clipboard/index.js.map +1 -0
- package/lib/esm/components/clipboard/types.d.ts +44 -0
- package/lib/esm/components/clipboard/types.d.ts.map +1 -0
- package/lib/esm/components/clipboard/types.js +6 -0
- package/lib/esm/components/clipboard/types.js.map +1 -0
- package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/range-slider/index.d.ts +7 -0
- package/lib/esm/components/range-slider/index.d.ts.map +1 -0
- package/lib/esm/components/range-slider/index.js +6 -0
- package/lib/esm/components/range-slider/index.js.map +1 -0
- package/lib/esm/components/range-slider/range-slider.d.ts +42 -0
- package/lib/esm/components/range-slider/range-slider.d.ts.map +1 -0
- package/lib/esm/components/range-slider/range-slider.js +251 -0
- package/lib/esm/components/range-slider/range-slider.js.map +1 -0
- package/lib/esm/components/range-slider/types.d.ts +33 -0
- package/lib/esm/components/range-slider/types.d.ts.map +1 -0
- package/lib/esm/components/range-slider/types.js +6 -0
- package/lib/esm/components/range-slider/types.js.map +1 -0
- package/lib/esm/components/rating/rating.d.ts.map +1 -1
- package/lib/esm/components/rating/rating.js +8 -3
- package/lib/esm/components/rating/rating.js.map +1 -1
- package/lib/esm/components/repeater/repeater.d.ts.map +1 -1
- package/lib/esm/components/repeater/repeater.js +3 -2
- package/lib/esm/components/repeater/repeater.js.map +1 -1
- package/lib/esm/components/select/utils.d.ts.map +1 -1
- package/lib/esm/components/select/utils.js +3 -1
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/components/sticky/sticky.d.ts.map +1 -1
- package/lib/esm/components/sticky/sticky.js +3 -1
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/lib/esm/index.d.ts +8 -0
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +6 -0
- package/lib/esm/index.js.map +1 -1
- package/package.json +1 -2
- package/src/components/clipboard/__tests__/clipboard.test.ts +438 -0
- package/src/components/clipboard/clipboard.ts +416 -0
- package/src/components/clipboard/index.ts +2 -0
- package/src/components/clipboard/types.ts +51 -0
- package/src/components/datatable/__tests__/currency-sort.test.ts +2 -10
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +2 -2
- package/src/components/datatable/__tests__/race-conditions.test.ts +11 -14
- package/src/components/datatable/datatable.ts +3 -5
- package/src/components/range-slider/__tests__/range-slider.test.ts +659 -0
- package/src/components/range-slider/index.ts +11 -0
- package/src/components/range-slider/range-slider.ts +276 -0
- package/src/components/range-slider/types.ts +36 -0
- package/src/components/rating/__tests__/rating.test.ts +11 -4
- package/src/components/rating/rating.ts +22 -11
- package/src/components/repeater/__tests__/repeater.test.ts +19 -6
- package/src/components/repeater/repeater.ts +5 -3
- package/src/components/select/__tests__/ux-behaviors.test.ts +21 -3
- package/src/components/select/utils.ts +5 -1
- package/src/components/sticky/__tests__/sticky.test.ts +10 -3
- package/src/components/sticky/sticky.ts +14 -24
- package/src/components/sticky/types.ts +3 -3
- package/src/index.ts +17 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
|
|
3
|
+
* Copyright 2025 by Keenthemes Inc
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import KTComponent from '../component';
|
|
7
|
+
import KTData from '../../helpers/data';
|
|
8
|
+
import KTDom from '../../helpers/dom';
|
|
9
|
+
import {
|
|
10
|
+
KTClipboardActionType,
|
|
11
|
+
KTClipboardConfigInterface,
|
|
12
|
+
KTClipboardInterface,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
type KTClipboardEventPayload = {
|
|
16
|
+
action: KTClipboardActionType;
|
|
17
|
+
text: string | null;
|
|
18
|
+
target: string | null;
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
KTClipboard: typeof KTClipboard;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class KTClipboard extends KTComponent implements KTClipboardInterface {
|
|
29
|
+
protected override _name: string = 'clipboard';
|
|
30
|
+
|
|
31
|
+
protected override _defaultConfig: KTClipboardConfigInterface = {
|
|
32
|
+
target: '',
|
|
33
|
+
text: '',
|
|
34
|
+
action: 'copy',
|
|
35
|
+
copiedClass: '',
|
|
36
|
+
successEvent: 'kt.clipboard.success',
|
|
37
|
+
errorEvent: 'kt.clipboard.error',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
protected override _config: KTClipboardConfigInterface = this
|
|
41
|
+
._defaultConfig as KTClipboardConfigInterface;
|
|
42
|
+
|
|
43
|
+
private _activateHandler: ((event: Event) => void) | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
element: HTMLElement,
|
|
47
|
+
config: KTClipboardConfigInterface | null = null,
|
|
48
|
+
) {
|
|
49
|
+
super();
|
|
50
|
+
|
|
51
|
+
// Ensure we don't double bind handlers on the same trigger.
|
|
52
|
+
if (this._shouldSkipInit(element)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._init(element);
|
|
57
|
+
this._buildConfig(config);
|
|
58
|
+
|
|
59
|
+
if (!this._element) return;
|
|
60
|
+
|
|
61
|
+
this._activateHandler = this._handleActivate.bind(this);
|
|
62
|
+
this._element.addEventListener('click', this._activateHandler);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private _getSuccessEventName(): string {
|
|
66
|
+
const eventName = this._getOption('successEvent');
|
|
67
|
+
return typeof eventName === 'string' && eventName.length > 0
|
|
68
|
+
? eventName
|
|
69
|
+
: 'kt.clipboard.success';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private _getErrorEventName(): string {
|
|
73
|
+
const eventName = this._getOption('errorEvent');
|
|
74
|
+
return typeof eventName === 'string' && eventName.length > 0
|
|
75
|
+
? eventName
|
|
76
|
+
: 'kt.clipboard.error';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private _getAction(): KTClipboardActionType {
|
|
80
|
+
const action = this._getOption('action');
|
|
81
|
+
return action === 'cut' ? 'cut' : 'copy';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private _getTrimmedString(optionValue: unknown): string {
|
|
85
|
+
return typeof optionValue === 'string' ? optionValue.trim() : '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private _getPredefinedText(): string {
|
|
89
|
+
const text = this._getOption('text');
|
|
90
|
+
return this._getTrimmedString(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _getTargetSelector(): string {
|
|
94
|
+
const target = this._getOption('target');
|
|
95
|
+
return this._getTrimmedString(target);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private _getCopiedClass(): string {
|
|
99
|
+
const copiedClass = this._getOption('copiedClass');
|
|
100
|
+
return this._getTrimmedString(copiedClass);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private _setCopiedClass(shouldSet: boolean): void {
|
|
104
|
+
const copiedClass = this._getCopiedClass();
|
|
105
|
+
if (!copiedClass || !this._element) return;
|
|
106
|
+
|
|
107
|
+
// Keep deterministic behavior: remove any previous state before toggling.
|
|
108
|
+
KTDom.removeClass(this._element, copiedClass);
|
|
109
|
+
if (shouldSet) {
|
|
110
|
+
KTDom.addClass(this._element, copiedClass);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _isInputLike(element: HTMLElement): boolean {
|
|
115
|
+
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private _readTargetValue(target: HTMLElement): string {
|
|
119
|
+
if (this._isInputLike(target)) {
|
|
120
|
+
return (target as HTMLInputElement | HTMLTextAreaElement).value ?? '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return target.textContent ?? '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _execCommandCopy(text: string): boolean {
|
|
127
|
+
const textarea = document.createElement('textarea');
|
|
128
|
+
textarea.value = text;
|
|
129
|
+
|
|
130
|
+
// Avoid scrolling to bottom on iOS/Safari and keep it out of layout.
|
|
131
|
+
textarea.style.position = 'fixed';
|
|
132
|
+
textarea.style.top = '0';
|
|
133
|
+
textarea.style.left = '0';
|
|
134
|
+
textarea.style.opacity = '0';
|
|
135
|
+
textarea.style.pointerEvents = 'none';
|
|
136
|
+
|
|
137
|
+
document.body.appendChild(textarea);
|
|
138
|
+
textarea.focus();
|
|
139
|
+
textarea.select();
|
|
140
|
+
// Some browsers require explicit range selection.
|
|
141
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
return document.execCommand('copy');
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
} finally {
|
|
148
|
+
textarea.remove();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async _writeText(text: string): Promise<boolean> {
|
|
153
|
+
// Native Clipboard API (requires secure context).
|
|
154
|
+
const clipboard =
|
|
155
|
+
typeof navigator !== 'undefined' ? navigator.clipboard : null;
|
|
156
|
+
const writeText =
|
|
157
|
+
clipboard && typeof clipboard.writeText === 'function'
|
|
158
|
+
? clipboard.writeText.bind(clipboard)
|
|
159
|
+
: null;
|
|
160
|
+
|
|
161
|
+
if (writeText) {
|
|
162
|
+
await writeText(text);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ok = this._execCommandCopy(text);
|
|
167
|
+
if (!ok) {
|
|
168
|
+
throw new Error('Clipboard copy failed.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async _handleActivate(event: Event): Promise<void> {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
|
|
177
|
+
const action = this._getAction();
|
|
178
|
+
const successEventName = this._getSuccessEventName();
|
|
179
|
+
const errorEventName = this._getErrorEventName();
|
|
180
|
+
const textFromConfig = this._getPredefinedText();
|
|
181
|
+
const targetSelector = this._getTargetSelector();
|
|
182
|
+
const hasPredefinedText = Boolean(
|
|
183
|
+
this._element?.hasAttribute('data-kt-clipboard-text'),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Deterministic precedence:
|
|
187
|
+
// - If `data-kt-clipboard-text` attribute is present, it wins over target.
|
|
188
|
+
if (hasPredefinedText) {
|
|
189
|
+
let targetElForCut: HTMLElement | null = null;
|
|
190
|
+
|
|
191
|
+
// `cut` requires an editable target (input/textarea) even when predefined text is used.
|
|
192
|
+
if (action === 'cut') {
|
|
193
|
+
if (!targetSelector) {
|
|
194
|
+
this._setCopiedClass(false);
|
|
195
|
+
|
|
196
|
+
const payload: KTClipboardEventPayload = {
|
|
197
|
+
action,
|
|
198
|
+
text: null,
|
|
199
|
+
target: null,
|
|
200
|
+
error:
|
|
201
|
+
'Cut action requires data-kt-clipboard-target pointing to an input/textarea.',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this._fireEvent(errorEventName, payload);
|
|
205
|
+
this._dispatchEvent(errorEventName, payload);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
targetElForCut = KTDom.getElement(targetSelector) as HTMLElement | null;
|
|
210
|
+
|
|
211
|
+
if (!targetElForCut || !this._isInputLike(targetElForCut)) {
|
|
212
|
+
this._setCopiedClass(false);
|
|
213
|
+
|
|
214
|
+
const payload: KTClipboardEventPayload = {
|
|
215
|
+
action,
|
|
216
|
+
text: null,
|
|
217
|
+
target: targetSelector,
|
|
218
|
+
error: 'Cut action is only supported for input/textarea targets.',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this._fireEvent(errorEventName, payload);
|
|
222
|
+
this._dispatchEvent(errorEventName, payload);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Treat empty/whitespace-only predefined text as invalid.
|
|
228
|
+
if (!textFromConfig) {
|
|
229
|
+
this._setCopiedClass(false);
|
|
230
|
+
|
|
231
|
+
const payload: KTClipboardEventPayload = {
|
|
232
|
+
action,
|
|
233
|
+
text: null,
|
|
234
|
+
target: targetSelector || null,
|
|
235
|
+
error: 'Predefined clipboard text is empty.',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
this._fireEvent(errorEventName, payload);
|
|
239
|
+
this._dispatchEvent(errorEventName, payload);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await this._writeText(textFromConfig);
|
|
245
|
+
|
|
246
|
+
// For `cut`, clear the editable target after successful clipboard write.
|
|
247
|
+
if (action === 'cut' && targetElForCut) {
|
|
248
|
+
(targetElForCut as HTMLInputElement | HTMLTextAreaElement).value = '';
|
|
249
|
+
targetElForCut.dispatchEvent(new Event('input', { bubbles: true }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._setCopiedClass(true);
|
|
253
|
+
|
|
254
|
+
const payload: KTClipboardEventPayload = {
|
|
255
|
+
action,
|
|
256
|
+
text: textFromConfig,
|
|
257
|
+
target: targetSelector || null,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
this._fireEvent(successEventName, payload);
|
|
261
|
+
this._dispatchEvent(successEventName, payload);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this._setCopiedClass(false);
|
|
264
|
+
|
|
265
|
+
const payload: KTClipboardEventPayload = {
|
|
266
|
+
action,
|
|
267
|
+
text: textFromConfig,
|
|
268
|
+
target: targetSelector || null,
|
|
269
|
+
error:
|
|
270
|
+
error instanceof Error ? error.message : String(error ?? 'Unknown'),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
this._fireEvent(errorEventName, payload);
|
|
274
|
+
this._dispatchEvent(errorEventName, payload);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// No usable predefined text; copy from target.
|
|
281
|
+
if (!targetSelector) {
|
|
282
|
+
this._setCopiedClass(false);
|
|
283
|
+
|
|
284
|
+
const payload: KTClipboardEventPayload = {
|
|
285
|
+
action,
|
|
286
|
+
text: null,
|
|
287
|
+
target: null,
|
|
288
|
+
error:
|
|
289
|
+
'Missing clipboard source (provide data-kt-clipboard-text or data-kt-clipboard-target).',
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
this._fireEvent(errorEventName, payload);
|
|
293
|
+
this._dispatchEvent(errorEventName, payload);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const targetEl = KTDom.getElement(targetSelector) as HTMLElement | null;
|
|
298
|
+
if (!targetEl) {
|
|
299
|
+
this._setCopiedClass(false);
|
|
300
|
+
|
|
301
|
+
const payload: KTClipboardEventPayload = {
|
|
302
|
+
action,
|
|
303
|
+
text: null,
|
|
304
|
+
target: targetSelector,
|
|
305
|
+
error: `Clipboard target not found: ${targetSelector}`,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
this._fireEvent(errorEventName, payload);
|
|
309
|
+
this._dispatchEvent(errorEventName, payload);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const value = this._readTargetValue(targetEl).trim();
|
|
314
|
+
if (!value) {
|
|
315
|
+
this._setCopiedClass(false);
|
|
316
|
+
|
|
317
|
+
const payload: KTClipboardEventPayload = {
|
|
318
|
+
action,
|
|
319
|
+
text: null,
|
|
320
|
+
target: targetSelector,
|
|
321
|
+
error: 'Target content is empty.',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
this._fireEvent(errorEventName, payload);
|
|
325
|
+
this._dispatchEvent(errorEventName, payload);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Cut action: only allowed for input/textarea.
|
|
330
|
+
if (action === 'cut' && !this._isInputLike(targetEl)) {
|
|
331
|
+
this._setCopiedClass(false);
|
|
332
|
+
|
|
333
|
+
const payload: KTClipboardEventPayload = {
|
|
334
|
+
action,
|
|
335
|
+
text: null,
|
|
336
|
+
target: targetSelector,
|
|
337
|
+
error: 'Cut action is only supported for input/textarea targets.',
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this._fireEvent(errorEventName, payload);
|
|
341
|
+
this._dispatchEvent(errorEventName, payload);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await this._writeText(value);
|
|
347
|
+
this._setCopiedClass(true);
|
|
348
|
+
|
|
349
|
+
if (action === 'cut') {
|
|
350
|
+
(targetEl as HTMLInputElement | HTMLTextAreaElement).value = '';
|
|
351
|
+
targetEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const payload: KTClipboardEventPayload = {
|
|
355
|
+
action,
|
|
356
|
+
text: value,
|
|
357
|
+
target: targetSelector,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
this._fireEvent(successEventName, payload);
|
|
361
|
+
this._dispatchEvent(successEventName, payload);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this._setCopiedClass(false);
|
|
364
|
+
|
|
365
|
+
const payload: KTClipboardEventPayload = {
|
|
366
|
+
action,
|
|
367
|
+
text: null,
|
|
368
|
+
target: targetSelector,
|
|
369
|
+
error:
|
|
370
|
+
error instanceof Error ? error.message : String(error ?? 'Unknown'),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
this._fireEvent(errorEventName, payload);
|
|
374
|
+
this._dispatchEvent(errorEventName, payload);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
public override dispose(): void {
|
|
379
|
+
if (this._element && this._activateHandler) {
|
|
380
|
+
this._element.removeEventListener('click', this._activateHandler);
|
|
381
|
+
}
|
|
382
|
+
this._activateHandler = null;
|
|
383
|
+
super.dispose();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public static getInstance(element: HTMLElement): KTClipboard | null {
|
|
387
|
+
if (!element) return null;
|
|
388
|
+
if (KTData.has(element, 'clipboard')) {
|
|
389
|
+
return KTData.get(element, 'clipboard') as KTClipboard;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
public static getOrCreateInstance(
|
|
395
|
+
element: HTMLElement,
|
|
396
|
+
config?: KTClipboardConfigInterface,
|
|
397
|
+
): KTClipboard {
|
|
398
|
+
return (
|
|
399
|
+
this.getInstance(element) || new KTClipboard(element, config ?? undefined)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
public static createInstances(): void {
|
|
404
|
+
document.querySelectorAll('[data-kt-clipboard]').forEach((el) => {
|
|
405
|
+
new KTClipboard(el as HTMLElement);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public static init(): void {
|
|
410
|
+
KTClipboard.createInstances();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (typeof window !== 'undefined') {
|
|
415
|
+
window.KTClipboard = KTClipboard;
|
|
416
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
|
|
3
|
+
* Copyright 2025 by Keenthemes Inc
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type KTClipboardActionType = 'copy' | 'cut';
|
|
7
|
+
|
|
8
|
+
export interface KTClipboardConfigInterface {
|
|
9
|
+
/**
|
|
10
|
+
* CSS selector for the element to read from when copying from target.
|
|
11
|
+
* For `input`/`textarea`, reads `.value`; for other elements, reads `.textContent`.
|
|
12
|
+
*/
|
|
13
|
+
target?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Static string to copy. Takes precedence over `target` when both are present.
|
|
17
|
+
*/
|
|
18
|
+
text?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clipboard action to perform.
|
|
22
|
+
* - `copy` (default)
|
|
23
|
+
* - `cut` (only valid for `input`/`textarea` targets)
|
|
24
|
+
*/
|
|
25
|
+
action?: KTClipboardActionType;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional class toggled on the trigger when copy/cut succeeds.
|
|
29
|
+
*/
|
|
30
|
+
copiedClass?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional DOM event name dispatched on success.
|
|
34
|
+
* Defaults to `kt.clipboard.success`.
|
|
35
|
+
*/
|
|
36
|
+
successEvent?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional DOM event name dispatched on failure.
|
|
40
|
+
* Defaults to `kt.clipboard.error`.
|
|
41
|
+
*/
|
|
42
|
+
errorEvent?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface KTClipboardInterface {
|
|
46
|
+
getOption(name: string): unknown;
|
|
47
|
+
getElement(): HTMLElement | null;
|
|
48
|
+
on(eventType: string, callback: CallableFunction): string;
|
|
49
|
+
off(eventType: string, eventId: string): void;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
@@ -67,11 +67,7 @@ describe('KTDataTable - Currency/numeric sort', () => {
|
|
|
67
67
|
noop,
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
-
const data = [
|
|
71
|
-
{ price: '£5' },
|
|
72
|
-
{ price: '£20' },
|
|
73
|
-
{ price: '£123' },
|
|
74
|
-
];
|
|
70
|
+
const data = [{ price: '£5' }, { price: '£20' }, { price: '£123' }];
|
|
75
71
|
const sorted = handler.sortData(data, 'price', 'desc');
|
|
76
72
|
|
|
77
73
|
const numericOrder = sorted.map((row) =>
|
|
@@ -92,11 +88,7 @@ describe('KTDataTable - Currency/numeric sort', () => {
|
|
|
92
88
|
noop,
|
|
93
89
|
);
|
|
94
90
|
|
|
95
|
-
const data = [
|
|
96
|
-
{ price: '£123' },
|
|
97
|
-
{ price: '£20' },
|
|
98
|
-
{ price: '£5' },
|
|
99
|
-
];
|
|
91
|
+
const data = [{ price: '£123' }, { price: '£20' }, { price: '£5' }];
|
|
100
92
|
const sorted = handler.sortData(data, 'price', 'asc');
|
|
101
93
|
|
|
102
94
|
// String sort: "£123" < "£20" < "£5" (1 < 2 < 5)
|
|
@@ -89,7 +89,7 @@ describe('KTDataTable - Multi-row header column count', () => {
|
|
|
89
89
|
|
|
90
90
|
it('should render exactly 17 columns when thead has multi-row headers and no data-kt-datatable-column', async () => {
|
|
91
91
|
createMultiRowHeaderTable(2);
|
|
92
|
-
|
|
92
|
+
new KTDataTable(container, { stateSave: false });
|
|
93
93
|
await vi.runAllTimersAsync();
|
|
94
94
|
|
|
95
95
|
const tbody = tableElement.tBodies[0];
|
|
@@ -107,7 +107,7 @@ describe('KTDataTable - Multi-row header column count', () => {
|
|
|
107
107
|
createMultiRowHeaderTable(0);
|
|
108
108
|
const tbody = tableElement.querySelector('tbody');
|
|
109
109
|
expect(tbody).toBeDefined();
|
|
110
|
-
|
|
110
|
+
new KTDataTable(container, { stateSave: false });
|
|
111
111
|
await vi.runAllTimersAsync();
|
|
112
112
|
|
|
113
113
|
const noticeRow = tableElement.tBodies[0].querySelector('tr');
|
|
@@ -43,7 +43,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
43
43
|
|
|
44
44
|
// Mock fetch to track requests and signals
|
|
45
45
|
abortSignals = [];
|
|
46
|
-
mockFetch = vi.fn<typeof fetch>((
|
|
46
|
+
mockFetch = vi.fn<typeof fetch>((_url, options) => {
|
|
47
47
|
// Store abort signal for verification
|
|
48
48
|
if (options?.signal) {
|
|
49
49
|
abortSignals.push(options.signal);
|
|
@@ -98,12 +98,9 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
98
98
|
|
|
99
99
|
describe('AbortController Integration', () => {
|
|
100
100
|
it('should create AbortController for remote data requests', async () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
apiEndpoint: '/api/data',
|
|
105
|
-
},
|
|
106
|
-
);
|
|
101
|
+
new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
|
|
102
|
+
apiEndpoint: '/api/data',
|
|
103
|
+
});
|
|
107
104
|
|
|
108
105
|
await waitFor(150);
|
|
109
106
|
|
|
@@ -186,7 +183,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
186
183
|
|
|
187
184
|
// Mock to capture request sequence
|
|
188
185
|
mockFetch.mockImplementation(
|
|
189
|
-
(
|
|
186
|
+
(_url: RequestInfo | URL, _options?: RequestInit) => {
|
|
190
187
|
callCount++;
|
|
191
188
|
const id = callCount;
|
|
192
189
|
requestIds.push(id);
|
|
@@ -293,7 +290,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
293
290
|
it('should reset _isFetching flag even after fetch error', async () => {
|
|
294
291
|
let callCount = 0;
|
|
295
292
|
mockFetch.mockImplementation(
|
|
296
|
-
(
|
|
293
|
+
(_url: RequestInfo | URL, _options?: RequestInit) => {
|
|
297
294
|
callCount++;
|
|
298
295
|
if (callCount === 1) {
|
|
299
296
|
// Return invalid JSON to trigger parse error
|
|
@@ -333,7 +330,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
333
330
|
const element = container.querySelector(
|
|
334
331
|
'[data-kt-datatable="true"]',
|
|
335
332
|
) as HTMLElement;
|
|
336
|
-
|
|
333
|
+
new KTDataTable(element, {
|
|
337
334
|
apiEndpoint: '/api/data',
|
|
338
335
|
});
|
|
339
336
|
|
|
@@ -410,7 +407,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
410
407
|
|
|
411
408
|
describe('Event Handling During Race Conditions', () => {
|
|
412
409
|
it('should fire fetch event for successful requests', async () => {
|
|
413
|
-
const fetchEvents:
|
|
410
|
+
const fetchEvents: Event[] = [];
|
|
414
411
|
|
|
415
412
|
const element = container.querySelector(
|
|
416
413
|
'[data-kt-datatable="true"]',
|
|
@@ -433,7 +430,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
433
430
|
});
|
|
434
431
|
|
|
435
432
|
it('should fire fetched event after successful data load', async () => {
|
|
436
|
-
const fetchedEvents:
|
|
433
|
+
const fetchedEvents: Event[] = [];
|
|
437
434
|
|
|
438
435
|
const element = container.querySelector(
|
|
439
436
|
'[data-kt-datatable="true"]',
|
|
@@ -442,7 +439,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
442
439
|
fetchedEvents.push(e);
|
|
443
440
|
});
|
|
444
441
|
|
|
445
|
-
|
|
442
|
+
new KTDataTable(element, {
|
|
446
443
|
apiEndpoint: '/api/data',
|
|
447
444
|
});
|
|
448
445
|
|
|
@@ -453,7 +450,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
453
450
|
});
|
|
454
451
|
|
|
455
452
|
it('should not fire error events for AbortError', async () => {
|
|
456
|
-
const errorEvents:
|
|
453
|
+
const errorEvents: Event[] = [];
|
|
457
454
|
|
|
458
455
|
const element = container.querySelector(
|
|
459
456
|
'[data-kt-datatable="true"]',
|
|
@@ -746,9 +746,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
|
|
|
746
746
|
return Object.keys(originalData[0]).length;
|
|
747
747
|
}
|
|
748
748
|
if (this._tbodyElement) {
|
|
749
|
-
const firstRow =
|
|
750
|
-
'tr'
|
|
751
|
-
);
|
|
749
|
+
const firstRow =
|
|
750
|
+
this._tbodyElement.querySelector<HTMLTableRowElement>('tr');
|
|
752
751
|
if (firstRow) {
|
|
753
752
|
return firstRow.querySelectorAll<HTMLTableCellElement>('td').length;
|
|
754
753
|
}
|
|
@@ -1052,8 +1051,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
|
|
|
1052
1051
|
th.hasAttribute('data-kt-datatable-column'),
|
|
1053
1052
|
);
|
|
1054
1053
|
// When no th has data-kt-datatable-column (e.g. multi-row headers), use logical column count from tbody so we don't overcount thead cells
|
|
1055
|
-
const columnsToRender: HTMLTableCellElement[] =
|
|
1056
|
-
ths.length > 0 ? ths : [];
|
|
1054
|
+
const columnsToRender: HTMLTableCellElement[] = ths.length > 0 ? ths : [];
|
|
1057
1055
|
const logicalColumnCount =
|
|
1058
1056
|
ths.length > 0 ? ths.length : this._getLogicalColumnCount();
|
|
1059
1057
|
|