juxscript 1.1.209 → 1.1.212
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/lib/components/dataframe.d.ts +46 -0
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +171 -16
- package/lib/components/dataframe.ts +197 -17
- package/lib/components/fileupload.d.ts +19 -8
- package/lib/components/fileupload.d.ts.map +1 -1
- package/lib/components/fileupload.js +50 -39
- package/lib/components/fileupload.ts +70 -51
- package/lib/styles/shadcn.css +69 -0
- package/package.json +1 -1
|
@@ -20,6 +20,8 @@ export interface DataFrameOptions {
|
|
|
20
20
|
showReshapeWarning?: boolean;
|
|
21
21
|
style?: string;
|
|
22
22
|
class?: string;
|
|
23
|
+
persistToIndexedDB?: boolean;
|
|
24
|
+
clearStorageOnFileRemove?: boolean;
|
|
23
25
|
}
|
|
24
26
|
type DataFrameState = BaseState & {
|
|
25
27
|
loaded: boolean;
|
|
@@ -47,6 +49,14 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
47
49
|
private _rawFileData;
|
|
48
50
|
private _reshapeModal;
|
|
49
51
|
private _reshapeModalRendered;
|
|
52
|
+
private _persistToIndexedDB;
|
|
53
|
+
private _clearStorageOnFileRemove;
|
|
54
|
+
private _uploadButtonLabel;
|
|
55
|
+
private _uploadButtonIcon;
|
|
56
|
+
private _uploadButtonVariant;
|
|
57
|
+
private _uploadAccept;
|
|
58
|
+
private _uploadDescription;
|
|
59
|
+
private _showUploadIcon;
|
|
50
60
|
constructor(id: string, options?: DataFrameOptions);
|
|
51
61
|
protected getTriggerEvents(): readonly string[];
|
|
52
62
|
protected getCallbackEvents(): readonly string[];
|
|
@@ -54,6 +64,42 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
54
64
|
fromUpload(upload: FileUpload): this;
|
|
55
65
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this;
|
|
56
66
|
withUpload(label?: string, accept?: string, icon?: string): this;
|
|
67
|
+
/**
|
|
68
|
+
* Set upload button label
|
|
69
|
+
*/
|
|
70
|
+
uploadLabel(label: string): this;
|
|
71
|
+
/**
|
|
72
|
+
* Set upload button icon
|
|
73
|
+
*/
|
|
74
|
+
uploadIcon(icon: string): this;
|
|
75
|
+
/**
|
|
76
|
+
* Set upload button variant (outline, primary, ghost, etc.)
|
|
77
|
+
*/
|
|
78
|
+
uploadVariant(variant: string): this;
|
|
79
|
+
/**
|
|
80
|
+
* Set accepted file types
|
|
81
|
+
*/
|
|
82
|
+
uploadAccept(accept: string): this;
|
|
83
|
+
/**
|
|
84
|
+
* Set description text below the upload button
|
|
85
|
+
*/
|
|
86
|
+
uploadDescription(description: string): this;
|
|
87
|
+
/**
|
|
88
|
+
* Show/hide upload icon
|
|
89
|
+
*/
|
|
90
|
+
showUploadIcon(show: boolean): this;
|
|
91
|
+
/**
|
|
92
|
+
* Enable/disable IndexedDB persistence (default: false = session only)
|
|
93
|
+
*/
|
|
94
|
+
persistToIndexedDB(enabled: boolean): this;
|
|
95
|
+
/**
|
|
96
|
+
* Clear stored data when file is removed (default: true)
|
|
97
|
+
*/
|
|
98
|
+
clearStorageOnFileRemove(enabled: boolean): this;
|
|
99
|
+
/**
|
|
100
|
+
* Clear the current data and reset the table
|
|
101
|
+
*/
|
|
102
|
+
clear(): Promise<this>;
|
|
57
103
|
showStatus(v: boolean): this;
|
|
58
104
|
statusIcon(v: string): this;
|
|
59
105
|
apply(fn: (df: DataFrame) => DataFrame): this;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AASnC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,6 +39,16 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
39
39
|
this._rawFileData = null;
|
|
40
40
|
this._reshapeModal = null;
|
|
41
41
|
this._reshapeModalRendered = false;
|
|
42
|
+
// ✅ NEW: Storage configuration
|
|
43
|
+
this._persistToIndexedDB = false;
|
|
44
|
+
this._clearStorageOnFileRemove = true;
|
|
45
|
+
// ✅ NEW: Upload button styling
|
|
46
|
+
this._uploadButtonLabel = 'Upload File';
|
|
47
|
+
this._uploadButtonIcon = 'upload';
|
|
48
|
+
this._uploadButtonVariant = 'outline';
|
|
49
|
+
this._uploadAccept = '.csv,.tsv,.txt,.xlsx,.xls';
|
|
50
|
+
this._uploadDescription = '';
|
|
51
|
+
this._showUploadIcon = true;
|
|
42
52
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
43
53
|
this._showStatus = options.showStatus ?? true;
|
|
44
54
|
this._icon = options.icon ?? '';
|
|
@@ -54,6 +64,9 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
54
64
|
this._sheetChunkSize = options.sheetChunkSize ?? 10000;
|
|
55
65
|
this._maxFileSize = options.maxFileSize ?? 50;
|
|
56
66
|
this._showReshapeWarning = options.showReshapeWarning ?? true;
|
|
67
|
+
// ✅ NEW: Storage options
|
|
68
|
+
this._persistToIndexedDB = options.persistToIndexedDB ?? false;
|
|
69
|
+
this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
|
|
57
70
|
}
|
|
58
71
|
getTriggerEvents() { return TRIGGER_EVENTS; }
|
|
59
72
|
getCallbackEvents() { return CALLBACK_EVENTS; }
|
|
@@ -124,6 +137,118 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
124
137
|
}
|
|
125
138
|
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
|
|
126
139
|
this._inlineUpload = { label, accept, icon };
|
|
140
|
+
this._uploadButtonLabel = label;
|
|
141
|
+
this._uploadAccept = accept;
|
|
142
|
+
this._uploadButtonIcon = icon;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Set upload button label
|
|
147
|
+
*/
|
|
148
|
+
uploadLabel(label) {
|
|
149
|
+
this._uploadButtonLabel = label;
|
|
150
|
+
if (this._inlineUpload)
|
|
151
|
+
this._inlineUpload.label = label;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Set upload button icon
|
|
156
|
+
*/
|
|
157
|
+
uploadIcon(icon) {
|
|
158
|
+
this._uploadButtonIcon = icon;
|
|
159
|
+
this._showUploadIcon = !!icon;
|
|
160
|
+
if (this._inlineUpload)
|
|
161
|
+
this._inlineUpload.icon = icon;
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Set upload button variant (outline, primary, ghost, etc.)
|
|
166
|
+
*/
|
|
167
|
+
uploadVariant(variant) {
|
|
168
|
+
this._uploadButtonVariant = variant;
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Set accepted file types
|
|
173
|
+
*/
|
|
174
|
+
uploadAccept(accept) {
|
|
175
|
+
this._uploadAccept = accept;
|
|
176
|
+
if (this._inlineUpload)
|
|
177
|
+
this._inlineUpload.accept = accept;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Set description text below the upload button
|
|
182
|
+
*/
|
|
183
|
+
uploadDescription(description) {
|
|
184
|
+
this._uploadDescription = description;
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Show/hide upload icon
|
|
189
|
+
*/
|
|
190
|
+
showUploadIcon(show) {
|
|
191
|
+
this._showUploadIcon = show;
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
/* ═══════════════════════════════════════════════════
|
|
195
|
+
* STORAGE OPTIONS (fluent API)
|
|
196
|
+
* ═══════════════════════════════════════════════════ */
|
|
197
|
+
/**
|
|
198
|
+
* Enable/disable IndexedDB persistence (default: false = session only)
|
|
199
|
+
*/
|
|
200
|
+
persistToIndexedDB(enabled) {
|
|
201
|
+
this._persistToIndexedDB = enabled;
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Clear stored data when file is removed (default: true)
|
|
206
|
+
*/
|
|
207
|
+
clearStorageOnFileRemove(enabled) {
|
|
208
|
+
this._clearStorageOnFileRemove = enabled;
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
/* ═══════════════════════════════════════════════════
|
|
212
|
+
* CLEAR / RESET
|
|
213
|
+
* ═══════════════════════════════════════════════════ */
|
|
214
|
+
/**
|
|
215
|
+
* Clear the current data and reset the table
|
|
216
|
+
*/
|
|
217
|
+
async clear() {
|
|
218
|
+
// Clear storage if enabled
|
|
219
|
+
if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
|
|
220
|
+
try {
|
|
221
|
+
const tables = await this._driver.list();
|
|
222
|
+
const matchingTables = tables.filter(t => t.name === this._rawFileData.file.name);
|
|
223
|
+
for (const table of matchingTables) {
|
|
224
|
+
await this._driver.delete(table.id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.warn('[DataFrame] Failed to clear storage:', err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Reset state
|
|
232
|
+
this._df = null;
|
|
233
|
+
this._rawFileData = null;
|
|
234
|
+
this._sheets.clear();
|
|
235
|
+
this.state.loaded = false;
|
|
236
|
+
this.state.sourceName = '';
|
|
237
|
+
this.state.rowCount = 0;
|
|
238
|
+
this.state.colCount = 0;
|
|
239
|
+
// Clear table display
|
|
240
|
+
if (this._table) {
|
|
241
|
+
this._table.columns([]).rows([]);
|
|
242
|
+
}
|
|
243
|
+
// Remove tabs if present
|
|
244
|
+
const wrapper = document.getElementById(this._id);
|
|
245
|
+
if (wrapper) {
|
|
246
|
+
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
247
|
+
if (existingTabs)
|
|
248
|
+
existingTabs.remove();
|
|
249
|
+
}
|
|
250
|
+
// Reset status
|
|
251
|
+
this._updateStatus('No data loaded.', 'empty');
|
|
127
252
|
return this;
|
|
128
253
|
}
|
|
129
254
|
/* ═══════════════════════════════════════════════════
|
|
@@ -193,7 +318,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
193
318
|
sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
|
|
194
319
|
maxFileSize(mb) { this._maxFileSize = mb; return this; }
|
|
195
320
|
/* ═══════════════════════════════════════════════════
|
|
196
|
-
* FILE HANDLING
|
|
321
|
+
* FILE HANDLING (modified to use storage options)
|
|
197
322
|
* ═══════════════════════════════════════════════════ */
|
|
198
323
|
async _handleFile(file) {
|
|
199
324
|
const fileSizeMB = file.size / (1024 * 1024);
|
|
@@ -222,7 +347,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
222
347
|
this.state.loading = false;
|
|
223
348
|
return;
|
|
224
349
|
}
|
|
225
|
-
|
|
350
|
+
// ✅ Only store if persistence is enabled
|
|
351
|
+
if (this._persistToIndexedDB) {
|
|
352
|
+
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
353
|
+
}
|
|
226
354
|
if (sheetNames.length > 1) {
|
|
227
355
|
this._renderMultiSheet(sheets, file.name);
|
|
228
356
|
}
|
|
@@ -237,7 +365,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
237
365
|
autoDetectDelimiter: true,
|
|
238
366
|
hasHeader: true
|
|
239
367
|
});
|
|
240
|
-
|
|
368
|
+
// ✅ Only store if persistence is enabled
|
|
369
|
+
if (this._persistToIndexedDB) {
|
|
370
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
371
|
+
}
|
|
241
372
|
this._setDataFrame(df, file.name);
|
|
242
373
|
}
|
|
243
374
|
}
|
|
@@ -266,17 +397,23 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
266
397
|
this._sheets.set(name, df);
|
|
267
398
|
});
|
|
268
399
|
const sheetNames = Object.keys(sheets);
|
|
400
|
+
// Sanitize sheet names for use as DOM IDs
|
|
401
|
+
const sanitizeId = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
269
402
|
const tabDefs = sheetNames.map(name => ({
|
|
270
|
-
id: name,
|
|
403
|
+
id: sanitizeId(name),
|
|
271
404
|
label: name,
|
|
272
405
|
content: ''
|
|
273
406
|
}));
|
|
274
407
|
this._tabs = new Tabs(`${this._id}-tabs`, {
|
|
275
408
|
tabs: tabDefs,
|
|
276
|
-
activeTab: sheetNames[0]
|
|
409
|
+
activeTab: sanitizeId(sheetNames[0])
|
|
277
410
|
});
|
|
411
|
+
// Map sanitized IDs back to original sheet names
|
|
412
|
+
const idToSheetName = new Map();
|
|
413
|
+
sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
|
|
278
414
|
this._tabs.bind('tabChange', (tabId) => {
|
|
279
|
-
|
|
415
|
+
const originalName = idToSheetName.get(tabId) || tabId;
|
|
416
|
+
this._df = this._sheets.get(originalName) || null;
|
|
280
417
|
});
|
|
281
418
|
const tabsContainer = document.createElement('div');
|
|
282
419
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
@@ -284,7 +421,8 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
284
421
|
this._tabs.render(tabsContainer);
|
|
285
422
|
sheetNames.forEach((sheetName) => {
|
|
286
423
|
const df = sheets[sheetName];
|
|
287
|
-
const
|
|
424
|
+
const safeId = sanitizeId(sheetName);
|
|
425
|
+
const table = new Table(`${this._id}-table-${safeId}`, {
|
|
288
426
|
striped: this._tableOptions.striped,
|
|
289
427
|
hoverable: this._tableOptions.hoverable,
|
|
290
428
|
sortable: this._tableOptions.sortable,
|
|
@@ -294,13 +432,13 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
294
432
|
});
|
|
295
433
|
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
296
434
|
table.columns(columnDefs).rows(df.toRows());
|
|
297
|
-
const settingsBtn = new Button(`${this._id}-settings-${
|
|
435
|
+
const settingsBtn = new Button(`${this._id}-settings-${safeId}`, {
|
|
298
436
|
label: '⚙️ Import Settings',
|
|
299
437
|
variant: 'ghost',
|
|
300
438
|
size: 'small'
|
|
301
439
|
});
|
|
302
440
|
settingsBtn.bind('click', () => this._showReshapeModal());
|
|
303
|
-
this._tabs.addTabContent(
|
|
441
|
+
this._tabs.addTabContent(safeId, [settingsBtn, table]);
|
|
304
442
|
});
|
|
305
443
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
306
444
|
this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
|
|
@@ -777,7 +915,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
777
915
|
});
|
|
778
916
|
}
|
|
779
917
|
/* ═══════════════════════════════════════════════════
|
|
780
|
-
* UPDATE & RENDER
|
|
918
|
+
* UPDATE & RENDER (modified for better upload styling)
|
|
781
919
|
* ═══════════════════════════════════════════════════ */
|
|
782
920
|
update(_prop, _value) { }
|
|
783
921
|
render(targetId) {
|
|
@@ -791,26 +929,43 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
791
929
|
if (style)
|
|
792
930
|
wrapper.setAttribute('style', style);
|
|
793
931
|
if (this._inlineUpload) {
|
|
932
|
+
// ✅ NEW: Enhanced upload area with better styling
|
|
933
|
+
const uploadArea = document.createElement('div');
|
|
934
|
+
uploadArea.className = 'jux-dataframe-upload-area';
|
|
935
|
+
uploadArea.id = `${this._id}-upload-area`;
|
|
794
936
|
const uploadOpts = {
|
|
795
|
-
label: this.
|
|
796
|
-
accept: this.
|
|
937
|
+
label: this._uploadButtonLabel,
|
|
938
|
+
accept: this._uploadAccept,
|
|
939
|
+
variant: this._uploadButtonVariant,
|
|
797
940
|
};
|
|
798
|
-
if (this.
|
|
799
|
-
uploadOpts.icon = this.
|
|
941
|
+
if (this._showUploadIcon && this._uploadButtonIcon) {
|
|
942
|
+
uploadOpts.icon = this._uploadButtonIcon;
|
|
800
943
|
}
|
|
801
944
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
802
945
|
this._uploadRef = upload;
|
|
946
|
+
// ✅ Handle file change AND file clear
|
|
803
947
|
this._pendingSource = async () => {
|
|
804
948
|
upload.bind('change', async (files) => {
|
|
805
|
-
if (!files || files.length === 0)
|
|
949
|
+
if (!files || files.length === 0) {
|
|
950
|
+
// File was cleared - reset the table
|
|
951
|
+
await this.clear();
|
|
806
952
|
return;
|
|
953
|
+
}
|
|
807
954
|
await this._handleFile(files[0]);
|
|
808
955
|
});
|
|
809
956
|
};
|
|
810
957
|
const uploadContainer = document.createElement('div');
|
|
811
958
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
812
959
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
813
|
-
|
|
960
|
+
uploadArea.appendChild(uploadContainer);
|
|
961
|
+
// ✅ Optional description text
|
|
962
|
+
if (this._uploadDescription) {
|
|
963
|
+
const descEl = document.createElement('div');
|
|
964
|
+
descEl.className = 'jux-dataframe-upload-description';
|
|
965
|
+
descEl.textContent = this._uploadDescription;
|
|
966
|
+
uploadArea.appendChild(descEl);
|
|
967
|
+
}
|
|
968
|
+
wrapper.appendChild(uploadArea);
|
|
814
969
|
container.appendChild(wrapper);
|
|
815
970
|
upload.render(uploadContainer);
|
|
816
971
|
}
|
|
@@ -28,6 +28,9 @@ export interface DataFrameOptions {
|
|
|
28
28
|
showReshapeWarning?: boolean;
|
|
29
29
|
style?: string;
|
|
30
30
|
class?: string;
|
|
31
|
+
// ✅ NEW: Storage options
|
|
32
|
+
persistToIndexedDB?: boolean; // Default: false (session only)
|
|
33
|
+
clearStorageOnFileRemove?: boolean; // Default: true
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
type DataFrameState = BaseState & {
|
|
@@ -64,6 +67,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
64
67
|
private _rawFileData: { file: File; text?: string; isExcel?: boolean } | null = null;
|
|
65
68
|
private _reshapeModal: Modal | null = null;
|
|
66
69
|
private _reshapeModalRendered: boolean = false;
|
|
70
|
+
// ✅ NEW: Storage configuration
|
|
71
|
+
private _persistToIndexedDB: boolean = false;
|
|
72
|
+
private _clearStorageOnFileRemove: boolean = true;
|
|
73
|
+
// ✅ NEW: Upload button styling
|
|
74
|
+
private _uploadButtonLabel: string = 'Upload File';
|
|
75
|
+
private _uploadButtonIcon: string = 'upload';
|
|
76
|
+
private _uploadButtonVariant: string = 'outline';
|
|
77
|
+
private _uploadAccept: string = '.csv,.tsv,.txt,.xlsx,.xls';
|
|
78
|
+
private _uploadDescription: string = '';
|
|
79
|
+
private _showUploadIcon: boolean = true;
|
|
67
80
|
|
|
68
81
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
69
82
|
super(id, {
|
|
@@ -98,6 +111,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
98
111
|
this._sheetChunkSize = options.sheetChunkSize ?? 10000;
|
|
99
112
|
this._maxFileSize = options.maxFileSize ?? 50;
|
|
100
113
|
this._showReshapeWarning = options.showReshapeWarning ?? true;
|
|
114
|
+
// ✅ NEW: Storage options
|
|
115
|
+
this._persistToIndexedDB = options.persistToIndexedDB ?? false;
|
|
116
|
+
this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
protected getTriggerEvents(): readonly string[] { return TRIGGER_EVENTS; }
|
|
@@ -159,8 +175,135 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
159
175
|
return this;
|
|
160
176
|
}
|
|
161
177
|
|
|
162
|
-
withUpload(
|
|
178
|
+
withUpload(
|
|
179
|
+
label: string = 'Upload File',
|
|
180
|
+
accept: string = '.csv,.tsv,.txt,.xlsx,.xls',
|
|
181
|
+
icon: string = 'upload'
|
|
182
|
+
): this {
|
|
163
183
|
this._inlineUpload = { label, accept, icon };
|
|
184
|
+
this._uploadButtonLabel = label;
|
|
185
|
+
this._uploadAccept = accept;
|
|
186
|
+
this._uploadButtonIcon = icon;
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set upload button label
|
|
192
|
+
*/
|
|
193
|
+
uploadLabel(label: string): this {
|
|
194
|
+
this._uploadButtonLabel = label;
|
|
195
|
+
if (this._inlineUpload) this._inlineUpload.label = label;
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set upload button icon
|
|
201
|
+
*/
|
|
202
|
+
uploadIcon(icon: string): this {
|
|
203
|
+
this._uploadButtonIcon = icon;
|
|
204
|
+
this._showUploadIcon = !!icon;
|
|
205
|
+
if (this._inlineUpload) this._inlineUpload.icon = icon;
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set upload button variant (outline, primary, ghost, etc.)
|
|
211
|
+
*/
|
|
212
|
+
uploadVariant(variant: string): this {
|
|
213
|
+
this._uploadButtonVariant = variant;
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Set accepted file types
|
|
219
|
+
*/
|
|
220
|
+
uploadAccept(accept: string): this {
|
|
221
|
+
this._uploadAccept = accept;
|
|
222
|
+
if (this._inlineUpload) this._inlineUpload.accept = accept;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Set description text below the upload button
|
|
228
|
+
*/
|
|
229
|
+
uploadDescription(description: string): this {
|
|
230
|
+
this._uploadDescription = description;
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Show/hide upload icon
|
|
236
|
+
*/
|
|
237
|
+
showUploadIcon(show: boolean): this {
|
|
238
|
+
this._showUploadIcon = show;
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ═══════════════════════════════════════════════════
|
|
243
|
+
* STORAGE OPTIONS (fluent API)
|
|
244
|
+
* ═══════════════════════════════════════════════════ */
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Enable/disable IndexedDB persistence (default: false = session only)
|
|
248
|
+
*/
|
|
249
|
+
persistToIndexedDB(enabled: boolean): this {
|
|
250
|
+
this._persistToIndexedDB = enabled;
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear stored data when file is removed (default: true)
|
|
256
|
+
*/
|
|
257
|
+
clearStorageOnFileRemove(enabled: boolean): this {
|
|
258
|
+
this._clearStorageOnFileRemove = enabled;
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* ═══════════════════════════════════════════════════
|
|
263
|
+
* CLEAR / RESET
|
|
264
|
+
* ═══════════════════════════════════════════════════ */
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Clear the current data and reset the table
|
|
268
|
+
*/
|
|
269
|
+
async clear(): Promise<this> {
|
|
270
|
+
// Clear storage if enabled
|
|
271
|
+
if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
|
|
272
|
+
try {
|
|
273
|
+
const tables = await this._driver.list();
|
|
274
|
+
const matchingTables = tables.filter(t => t.name === this._rawFileData!.file.name);
|
|
275
|
+
for (const table of matchingTables) {
|
|
276
|
+
await this._driver.delete(table.id);
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.warn('[DataFrame] Failed to clear storage:', err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Reset state
|
|
284
|
+
this._df = null;
|
|
285
|
+
this._rawFileData = null;
|
|
286
|
+
this._sheets.clear();
|
|
287
|
+
this.state.loaded = false;
|
|
288
|
+
this.state.sourceName = '';
|
|
289
|
+
this.state.rowCount = 0;
|
|
290
|
+
this.state.colCount = 0;
|
|
291
|
+
|
|
292
|
+
// Clear table display
|
|
293
|
+
if (this._table) {
|
|
294
|
+
this._table.columns([]).rows([]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Remove tabs if present
|
|
298
|
+
const wrapper = document.getElementById(this._id);
|
|
299
|
+
if (wrapper) {
|
|
300
|
+
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
301
|
+
if (existingTabs) existingTabs.remove();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Reset status
|
|
305
|
+
this._updateStatus('No data loaded.', 'empty');
|
|
306
|
+
|
|
164
307
|
return this;
|
|
165
308
|
}
|
|
166
309
|
|
|
@@ -245,7 +388,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
245
388
|
maxFileSize(mb: number): this { this._maxFileSize = mb; return this; }
|
|
246
389
|
|
|
247
390
|
/* ═══════════════════════════════════════════════════
|
|
248
|
-
* FILE HANDLING
|
|
391
|
+
* FILE HANDLING (modified to use storage options)
|
|
249
392
|
* ═══════════════════════════════════════════════════ */
|
|
250
393
|
|
|
251
394
|
private async _handleFile(file: File): Promise<void> {
|
|
@@ -282,7 +425,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
282
425
|
return;
|
|
283
426
|
}
|
|
284
427
|
|
|
285
|
-
|
|
428
|
+
// ✅ Only store if persistence is enabled
|
|
429
|
+
if (this._persistToIndexedDB) {
|
|
430
|
+
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
431
|
+
}
|
|
286
432
|
|
|
287
433
|
if (sheetNames.length > 1) {
|
|
288
434
|
this._renderMultiSheet(sheets, file.name);
|
|
@@ -298,7 +444,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
298
444
|
hasHeader: true
|
|
299
445
|
});
|
|
300
446
|
|
|
301
|
-
|
|
447
|
+
// ✅ Only store if persistence is enabled
|
|
448
|
+
if (this._persistToIndexedDB) {
|
|
449
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
450
|
+
}
|
|
451
|
+
|
|
302
452
|
this._setDataFrame(df, file.name);
|
|
303
453
|
}
|
|
304
454
|
} catch (err: any) {
|
|
@@ -331,19 +481,27 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
331
481
|
|
|
332
482
|
const sheetNames = Object.keys(sheets);
|
|
333
483
|
|
|
484
|
+
// Sanitize sheet names for use as DOM IDs
|
|
485
|
+
const sanitizeId = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
486
|
+
|
|
334
487
|
const tabDefs = sheetNames.map(name => ({
|
|
335
|
-
id: name,
|
|
488
|
+
id: sanitizeId(name),
|
|
336
489
|
label: name,
|
|
337
490
|
content: ''
|
|
338
491
|
}));
|
|
339
492
|
|
|
340
493
|
this._tabs = new Tabs(`${this._id}-tabs`, {
|
|
341
494
|
tabs: tabDefs,
|
|
342
|
-
activeTab: sheetNames[0]
|
|
495
|
+
activeTab: sanitizeId(sheetNames[0])
|
|
343
496
|
});
|
|
344
497
|
|
|
498
|
+
// Map sanitized IDs back to original sheet names
|
|
499
|
+
const idToSheetName = new Map<string, string>();
|
|
500
|
+
sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
|
|
501
|
+
|
|
345
502
|
this._tabs.bind('tabChange', (tabId: string) => {
|
|
346
|
-
|
|
503
|
+
const originalName = idToSheetName.get(tabId) || tabId;
|
|
504
|
+
this._df = this._sheets.get(originalName) || null;
|
|
347
505
|
});
|
|
348
506
|
|
|
349
507
|
const tabsContainer = document.createElement('div');
|
|
@@ -354,8 +512,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
354
512
|
|
|
355
513
|
sheetNames.forEach((sheetName) => {
|
|
356
514
|
const df = sheets[sheetName];
|
|
515
|
+
const safeId = sanitizeId(sheetName);
|
|
357
516
|
|
|
358
|
-
const table = new Table(`${this._id}-table-${
|
|
517
|
+
const table = new Table(`${this._id}-table-${safeId}`, {
|
|
359
518
|
striped: this._tableOptions.striped,
|
|
360
519
|
hoverable: this._tableOptions.hoverable,
|
|
361
520
|
sortable: this._tableOptions.sortable,
|
|
@@ -367,14 +526,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
367
526
|
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
368
527
|
table.columns(columnDefs).rows(df.toRows());
|
|
369
528
|
|
|
370
|
-
const settingsBtn = new Button(`${this._id}-settings-${
|
|
529
|
+
const settingsBtn = new Button(`${this._id}-settings-${safeId}`, {
|
|
371
530
|
label: '⚙️ Import Settings',
|
|
372
531
|
variant: 'ghost',
|
|
373
532
|
size: 'small'
|
|
374
533
|
});
|
|
375
534
|
settingsBtn.bind('click', () => this._showReshapeModal());
|
|
376
535
|
|
|
377
|
-
this._tabs!.addTabContent(
|
|
536
|
+
this._tabs!.addTabContent(safeId, [settingsBtn, table]);
|
|
378
537
|
});
|
|
379
538
|
|
|
380
539
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
@@ -922,7 +1081,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
922
1081
|
}
|
|
923
1082
|
|
|
924
1083
|
/* ═══════════════════════════════════════════════════
|
|
925
|
-
* UPDATE & RENDER
|
|
1084
|
+
* UPDATE & RENDER (modified for better upload styling)
|
|
926
1085
|
* ═══════════════════════════════════════════════════ */
|
|
927
1086
|
|
|
928
1087
|
update(_prop: string, _value: any): void { }
|
|
@@ -938,20 +1097,31 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
938
1097
|
if (style) wrapper.setAttribute('style', style);
|
|
939
1098
|
|
|
940
1099
|
if (this._inlineUpload) {
|
|
1100
|
+
// ✅ NEW: Enhanced upload area with better styling
|
|
1101
|
+
const uploadArea = document.createElement('div');
|
|
1102
|
+
uploadArea.className = 'jux-dataframe-upload-area';
|
|
1103
|
+
uploadArea.id = `${this._id}-upload-area`;
|
|
1104
|
+
|
|
941
1105
|
const uploadOpts: any = {
|
|
942
|
-
label: this.
|
|
943
|
-
accept: this.
|
|
1106
|
+
label: this._uploadButtonLabel,
|
|
1107
|
+
accept: this._uploadAccept,
|
|
1108
|
+
variant: this._uploadButtonVariant,
|
|
944
1109
|
};
|
|
945
|
-
if (this.
|
|
946
|
-
uploadOpts.icon = this.
|
|
1110
|
+
if (this._showUploadIcon && this._uploadButtonIcon) {
|
|
1111
|
+
uploadOpts.icon = this._uploadButtonIcon;
|
|
947
1112
|
}
|
|
948
1113
|
|
|
949
1114
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
950
1115
|
this._uploadRef = upload;
|
|
951
1116
|
|
|
1117
|
+
// ✅ Handle file change AND file clear
|
|
952
1118
|
this._pendingSource = async () => {
|
|
953
1119
|
upload.bind('change', async (files: File[]) => {
|
|
954
|
-
if (!files || files.length === 0)
|
|
1120
|
+
if (!files || files.length === 0) {
|
|
1121
|
+
// File was cleared - reset the table
|
|
1122
|
+
await this.clear();
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
955
1125
|
await this._handleFile(files[0]);
|
|
956
1126
|
});
|
|
957
1127
|
};
|
|
@@ -959,7 +1129,17 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
959
1129
|
const uploadContainer = document.createElement('div');
|
|
960
1130
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
961
1131
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
962
|
-
|
|
1132
|
+
uploadArea.appendChild(uploadContainer);
|
|
1133
|
+
|
|
1134
|
+
// ✅ Optional description text
|
|
1135
|
+
if (this._uploadDescription) {
|
|
1136
|
+
const descEl = document.createElement('div');
|
|
1137
|
+
descEl.className = 'jux-dataframe-upload-description';
|
|
1138
|
+
descEl.textContent = this._uploadDescription;
|
|
1139
|
+
uploadArea.appendChild(descEl);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
wrapper.appendChild(uploadArea);
|
|
963
1143
|
container.appendChild(wrapper);
|
|
964
1144
|
upload.render(uploadContainer);
|
|
965
1145
|
} else {
|
|
@@ -4,22 +4,31 @@ export interface FileUploadOptions {
|
|
|
4
4
|
label?: string;
|
|
5
5
|
accept?: string;
|
|
6
6
|
multiple?: boolean;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
name?: string;
|
|
9
7
|
icon?: string;
|
|
8
|
+
variant?: 'outline' | 'primary' | 'ghost' | 'secondary';
|
|
9
|
+
showFileList?: boolean;
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
maxFileSize?: number;
|
|
12
|
+
disabled?: boolean;
|
|
10
13
|
required?: boolean;
|
|
11
|
-
|
|
12
|
-
class?: string;
|
|
14
|
+
name?: string;
|
|
13
15
|
onValidate?: (files: File[]) => boolean | string;
|
|
14
16
|
storage?: FileStorage;
|
|
15
17
|
metadata?: Record<string, any>;
|
|
18
|
+
style?: string;
|
|
19
|
+
class?: string;
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
type FileUploadState = BaseState & {
|
|
22
|
+
label: string;
|
|
19
23
|
accept: string;
|
|
20
24
|
multiple: boolean;
|
|
21
25
|
icon: string;
|
|
22
|
-
|
|
26
|
+
variant: string;
|
|
27
|
+
showFileList: boolean;
|
|
28
|
+
maxFiles: number;
|
|
29
|
+
maxFileSize: number;
|
|
30
|
+
files: File[];
|
|
31
|
+
};
|
|
23
32
|
export declare class FileUpload extends BaseComponent<FileUploadState> {
|
|
24
33
|
private _fileListElement;
|
|
25
34
|
private _storage;
|
|
@@ -30,16 +39,18 @@ export declare class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
30
39
|
accept(value: string): this;
|
|
31
40
|
multiple(value: boolean): this;
|
|
32
41
|
icon(value: string): this;
|
|
42
|
+
variant(value: 'outline' | 'primary' | 'ghost' | 'secondary'): this;
|
|
33
43
|
storage(store: FileStorage, metadata?: Record<string, any>): this;
|
|
34
44
|
clear(): this;
|
|
45
|
+
clearFiles(): this;
|
|
35
46
|
getValue(): File[];
|
|
36
47
|
setValue(files: File[]): this;
|
|
37
48
|
validate(): boolean;
|
|
38
49
|
isValid(): boolean;
|
|
39
50
|
protected _validateValue(files: File[]): boolean | string;
|
|
40
|
-
protected _buildInputElement(): HTMLElement;
|
|
41
51
|
private _updateFileList;
|
|
42
52
|
private _formatFileSize;
|
|
53
|
+
update(prop: string, value: any): void;
|
|
43
54
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
44
55
|
}
|
|
45
56
|
export declare function fileupload(id: string, options?: FileUploadOptions): FileUpload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileupload.d.ts","sourceRoot":"","sources":["fileupload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAMxD,MAAM,WAAW,iBAAiB;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,
|
|
1
|
+
{"version":3,"file":"fileupload.d.ts","sourceRoot":"","sources":["fileupload.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,11 @@ export class FileUpload extends BaseComponent {
|
|
|
20
20
|
files: [],
|
|
21
21
|
accept: options.accept ?? '',
|
|
22
22
|
multiple: options.multiple ?? false,
|
|
23
|
-
icon: options.icon ?? 'upload'
|
|
23
|
+
icon: options.icon ?? 'upload',
|
|
24
|
+
variant: options.variant ?? 'outline',
|
|
25
|
+
showFileList: options.showFileList ?? true,
|
|
26
|
+
maxFiles: options.maxFiles ?? 10,
|
|
27
|
+
maxFileSize: options.maxFileSize ?? 10,
|
|
24
28
|
});
|
|
25
29
|
this._fileListElement = null;
|
|
26
30
|
this._storage = null;
|
|
@@ -51,6 +55,10 @@ export class FileUpload extends BaseComponent {
|
|
|
51
55
|
this.state.icon = value;
|
|
52
56
|
return this;
|
|
53
57
|
}
|
|
58
|
+
variant(value) {
|
|
59
|
+
this.state.variant = value;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
54
62
|
storage(store, metadata) {
|
|
55
63
|
this._storage = store;
|
|
56
64
|
this._metadata = metadata;
|
|
@@ -67,6 +75,12 @@ export class FileUpload extends BaseComponent {
|
|
|
67
75
|
this._triggerCallback('clear', null, null, this);
|
|
68
76
|
return this;
|
|
69
77
|
}
|
|
78
|
+
clearFiles() {
|
|
79
|
+
this.state.files = [];
|
|
80
|
+
this._updateFileList();
|
|
81
|
+
this._triggerCallback('change', [], null, this);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
70
84
|
/* ═════════════════════════════════════════════════════════════════
|
|
71
85
|
* FORM INPUT IMPLEMENTATION
|
|
72
86
|
* ═════════════════════════════════════════════════════════════════ */
|
|
@@ -108,20 +122,6 @@ export class FileUpload extends BaseComponent {
|
|
|
108
122
|
}
|
|
109
123
|
return true;
|
|
110
124
|
}
|
|
111
|
-
_buildInputElement() {
|
|
112
|
-
const { accept, multiple, required, disabled, name } = this.state;
|
|
113
|
-
const input = document.createElement('input');
|
|
114
|
-
input.type = 'file';
|
|
115
|
-
input.className = 'jux-fileupload-input';
|
|
116
|
-
input.id = `${this._id}-input`;
|
|
117
|
-
input.name = name;
|
|
118
|
-
input.accept = accept;
|
|
119
|
-
input.multiple = multiple;
|
|
120
|
-
input.required = required;
|
|
121
|
-
input.disabled = disabled;
|
|
122
|
-
input.style.display = 'none';
|
|
123
|
-
return input;
|
|
124
|
-
}
|
|
125
125
|
_updateFileList() {
|
|
126
126
|
if (!this._fileListElement)
|
|
127
127
|
return;
|
|
@@ -157,49 +157,58 @@ export class FileUpload extends BaseComponent {
|
|
|
157
157
|
/* ═════════════════════════════════════════════════════════════════
|
|
158
158
|
* RENDER
|
|
159
159
|
* ═════════════════════════════════════════════════════════════════ */
|
|
160
|
+
update(prop, value) {
|
|
161
|
+
// No reactive updates needed
|
|
162
|
+
}
|
|
160
163
|
render(targetId) {
|
|
161
164
|
const container = this._setupContainer(targetId);
|
|
162
|
-
const { label, accept, multiple, icon, style, class: className } = this.state;
|
|
165
|
+
const { label, accept, multiple, icon, variant, showFileList, style, class: className } = this.state;
|
|
163
166
|
const wrapper = document.createElement('div');
|
|
164
|
-
wrapper.className = 'jux-
|
|
167
|
+
wrapper.className = 'jux-fileupload';
|
|
165
168
|
wrapper.id = this._id;
|
|
166
169
|
if (className)
|
|
167
170
|
wrapper.className += ` ${className}`;
|
|
168
171
|
if (style)
|
|
169
172
|
wrapper.setAttribute('style', style);
|
|
170
|
-
|
|
171
|
-
wrapper.appendChild(this._renderLabel());
|
|
172
|
-
}
|
|
173
|
-
// File input
|
|
173
|
+
// Hidden file input
|
|
174
174
|
const fileInput = document.createElement('input');
|
|
175
175
|
fileInput.type = 'file';
|
|
176
|
-
fileInput.id = `${this._id}-input`;
|
|
177
|
-
fileInput.style.display = 'none';
|
|
178
|
-
fileInput.multiple = multiple;
|
|
179
176
|
fileInput.accept = accept;
|
|
177
|
+
fileInput.multiple = multiple;
|
|
178
|
+
fileInput.style.display = 'none';
|
|
179
|
+
fileInput.id = `${this._id}-input`;
|
|
180
180
|
this._inputElement = fileInput;
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
181
|
+
// Button
|
|
182
|
+
const btn = document.createElement('button');
|
|
183
|
+
btn.className = `jux-fileupload-button jux-button-${variant}`;
|
|
184
|
+
btn.type = 'button';
|
|
184
185
|
if (icon) {
|
|
185
186
|
const iconEl = document.createElement('span');
|
|
186
187
|
iconEl.className = 'jux-fileupload-icon';
|
|
187
188
|
iconEl.appendChild(renderIcon(icon));
|
|
188
|
-
|
|
189
|
+
btn.appendChild(iconEl);
|
|
189
190
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
buttonContainer.
|
|
191
|
+
const labelSpan = document.createElement('span');
|
|
192
|
+
labelSpan.textContent = label;
|
|
193
|
+
btn.appendChild(labelSpan);
|
|
194
|
+
// Button container
|
|
195
|
+
const buttonContainer = document.createElement('div');
|
|
196
|
+
buttonContainer.className = 'jux-fileupload-button-container';
|
|
197
|
+
buttonContainer.appendChild(fileInput);
|
|
198
|
+
buttonContainer.appendChild(btn);
|
|
196
199
|
wrapper.appendChild(buttonContainer);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
// File list
|
|
201
|
+
if (showFileList) {
|
|
202
|
+
const fileList = document.createElement('div');
|
|
203
|
+
fileList.className = 'jux-fileupload-filelist';
|
|
204
|
+
this._fileListElement = fileList;
|
|
205
|
+
wrapper.appendChild(fileList);
|
|
206
|
+
}
|
|
207
|
+
// Error element
|
|
201
208
|
wrapper.appendChild(this._renderError());
|
|
202
|
-
button
|
|
209
|
+
// Wire up button click to trigger file input
|
|
210
|
+
btn.addEventListener('click', () => fileInput.click());
|
|
211
|
+
// Wire up file input change
|
|
203
212
|
fileInput.addEventListener('change', (e) => {
|
|
204
213
|
const files = Array.from(fileInput.files || []);
|
|
205
214
|
this.state.files = files;
|
|
@@ -216,11 +225,13 @@ export class FileUpload extends BaseComponent {
|
|
|
216
225
|
}
|
|
217
226
|
});
|
|
218
227
|
this._wireStandardEvents(wrapper);
|
|
228
|
+
// Validate on blur if already validated
|
|
219
229
|
fileInput.addEventListener('blur', () => {
|
|
220
230
|
if (this._hasBeenValidated) {
|
|
221
231
|
this.validate();
|
|
222
232
|
}
|
|
223
233
|
});
|
|
234
|
+
// Wire sync bindings for label
|
|
224
235
|
const labelSync = this._syncBindings.find(b => b.property === 'label');
|
|
225
236
|
if (labelSync) {
|
|
226
237
|
const transform = labelSync.toComponent || ((v) => String(v));
|
|
@@ -11,23 +11,32 @@ export interface FileUploadOptions {
|
|
|
11
11
|
label?: string;
|
|
12
12
|
accept?: string;
|
|
13
13
|
multiple?: boolean;
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
name?: string;
|
|
16
14
|
icon?: string;
|
|
15
|
+
variant?: 'outline' | 'primary' | 'ghost' | 'secondary';
|
|
16
|
+
showFileList?: boolean;
|
|
17
|
+
maxFiles?: number;
|
|
18
|
+
maxFileSize?: number;
|
|
19
|
+
disabled?: boolean;
|
|
17
20
|
required?: boolean;
|
|
18
|
-
|
|
19
|
-
class?: string;
|
|
21
|
+
name?: string;
|
|
20
22
|
onValidate?: (files: File[]) => boolean | string;
|
|
21
23
|
storage?: FileStorage;
|
|
22
24
|
metadata?: Record<string, any>;
|
|
25
|
+
style?: string;
|
|
26
|
+
class?: string;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
type FileUploadState = BaseState & {
|
|
30
|
+
label: string;
|
|
27
31
|
accept: string;
|
|
28
32
|
multiple: boolean;
|
|
29
33
|
icon: string;
|
|
30
|
-
|
|
34
|
+
variant: string;
|
|
35
|
+
showFileList: boolean;
|
|
36
|
+
maxFiles: number;
|
|
37
|
+
maxFileSize: number;
|
|
38
|
+
files: File[];
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
export class FileUpload extends BaseComponent<FileUploadState> {
|
|
33
42
|
private _fileListElement: HTMLElement | null = null;
|
|
@@ -49,7 +58,11 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
49
58
|
files: [],
|
|
50
59
|
accept: options.accept ?? '',
|
|
51
60
|
multiple: options.multiple ?? false,
|
|
52
|
-
icon: options.icon ?? 'upload'
|
|
61
|
+
icon: options.icon ?? 'upload',
|
|
62
|
+
variant: options.variant ?? 'outline',
|
|
63
|
+
showFileList: options.showFileList ?? true,
|
|
64
|
+
maxFiles: options.maxFiles ?? 10,
|
|
65
|
+
maxFileSize: options.maxFileSize ?? 10,
|
|
53
66
|
});
|
|
54
67
|
|
|
55
68
|
if (options.onValidate) {
|
|
@@ -87,6 +100,11 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
87
100
|
return this;
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
variant(value: 'outline' | 'primary' | 'ghost' | 'secondary'): this {
|
|
104
|
+
this.state.variant = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
storage(store: FileStorage, metadata?: Record<string, any>): this {
|
|
91
109
|
this._storage = store;
|
|
92
110
|
this._metadata = metadata;
|
|
@@ -105,6 +123,13 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
105
123
|
return this;
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
clearFiles(): this {
|
|
127
|
+
this.state.files = [];
|
|
128
|
+
this._updateFileList();
|
|
129
|
+
this._triggerCallback('change', [], null, this);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
/* ═════════════════════════════════════════════════════════════════
|
|
109
134
|
* FORM INPUT IMPLEMENTATION
|
|
110
135
|
* ═════════════════════════════════════════════════════════════════ */
|
|
@@ -155,23 +180,6 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
155
180
|
return true;
|
|
156
181
|
}
|
|
157
182
|
|
|
158
|
-
protected _buildInputElement(): HTMLElement {
|
|
159
|
-
const { accept, multiple, required, disabled, name } = this.state;
|
|
160
|
-
|
|
161
|
-
const input = document.createElement('input');
|
|
162
|
-
input.type = 'file';
|
|
163
|
-
input.className = 'jux-fileupload-input';
|
|
164
|
-
input.id = `${this._id}-input`;
|
|
165
|
-
input.name = name!;
|
|
166
|
-
input.accept = accept;
|
|
167
|
-
input.multiple = multiple;
|
|
168
|
-
input.required = required!;
|
|
169
|
-
input.disabled = disabled!;
|
|
170
|
-
input.style.display = 'none';
|
|
171
|
-
|
|
172
|
-
return input;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
183
|
private _updateFileList(): void {
|
|
176
184
|
if (!this._fileListElement) return;
|
|
177
185
|
|
|
@@ -213,59 +221,68 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
213
221
|
* RENDER
|
|
214
222
|
* ═════════════════════════════════════════════════════════════════ */
|
|
215
223
|
|
|
224
|
+
update(prop: string, value: any): void {
|
|
225
|
+
// No reactive updates needed
|
|
226
|
+
}
|
|
227
|
+
|
|
216
228
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
217
229
|
const container = this._setupContainer(targetId);
|
|
218
|
-
|
|
230
|
+
|
|
231
|
+
const { label, accept, multiple, icon, variant, showFileList, style, class: className } = this.state;
|
|
219
232
|
|
|
220
233
|
const wrapper = document.createElement('div');
|
|
221
|
-
wrapper.className = 'jux-
|
|
234
|
+
wrapper.className = 'jux-fileupload';
|
|
222
235
|
wrapper.id = this._id;
|
|
223
236
|
if (className) wrapper.className += ` ${className}`;
|
|
224
237
|
if (style) wrapper.setAttribute('style', style);
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
wrapper.appendChild(this._renderLabel());
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// File input
|
|
239
|
+
// Hidden file input
|
|
231
240
|
const fileInput = document.createElement('input');
|
|
232
241
|
fileInput.type = 'file';
|
|
233
|
-
fileInput.id = `${this._id}-input`;
|
|
234
|
-
fileInput.style.display = 'none';
|
|
235
|
-
fileInput.multiple = multiple;
|
|
236
242
|
fileInput.accept = accept;
|
|
237
|
-
|
|
243
|
+
fileInput.multiple = multiple;
|
|
244
|
+
fileInput.style.display = 'none';
|
|
245
|
+
fileInput.id = `${this._id}-input`;
|
|
238
246
|
this._inputElement = fileInput;
|
|
239
|
-
wrapper.appendChild(fileInput);
|
|
240
247
|
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
// Button
|
|
249
|
+
const btn = document.createElement('button');
|
|
250
|
+
btn.className = `jux-fileupload-button jux-button-${variant}`;
|
|
251
|
+
btn.type = 'button';
|
|
243
252
|
|
|
244
253
|
if (icon) {
|
|
245
254
|
const iconEl = document.createElement('span');
|
|
246
255
|
iconEl.className = 'jux-fileupload-icon';
|
|
247
256
|
iconEl.appendChild(renderIcon(icon));
|
|
248
|
-
|
|
257
|
+
btn.appendChild(iconEl);
|
|
249
258
|
}
|
|
250
259
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
button.textContent = 'Choose File(s)';
|
|
255
|
-
button.disabled = this.state.disabled!;
|
|
260
|
+
const labelSpan = document.createElement('span');
|
|
261
|
+
labelSpan.textContent = label;
|
|
262
|
+
btn.appendChild(labelSpan);
|
|
256
263
|
|
|
257
|
-
|
|
264
|
+
// Button container
|
|
265
|
+
const buttonContainer = document.createElement('div');
|
|
266
|
+
buttonContainer.className = 'jux-fileupload-button-container';
|
|
267
|
+
buttonContainer.appendChild(fileInput);
|
|
268
|
+
buttonContainer.appendChild(btn);
|
|
258
269
|
wrapper.appendChild(buttonContainer);
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
271
|
+
// File list
|
|
272
|
+
if (showFileList) {
|
|
273
|
+
const fileList = document.createElement('div');
|
|
274
|
+
fileList.className = 'jux-fileupload-filelist';
|
|
275
|
+
this._fileListElement = fileList;
|
|
276
|
+
wrapper.appendChild(fileList);
|
|
277
|
+
}
|
|
264
278
|
|
|
279
|
+
// Error element
|
|
265
280
|
wrapper.appendChild(this._renderError());
|
|
266
281
|
|
|
267
|
-
button
|
|
282
|
+
// Wire up button click to trigger file input
|
|
283
|
+
btn.addEventListener('click', () => fileInput.click());
|
|
268
284
|
|
|
285
|
+
// Wire up file input change
|
|
269
286
|
fileInput.addEventListener('change', (e) => {
|
|
270
287
|
const files = Array.from(fileInput.files || []);
|
|
271
288
|
this.state.files = files;
|
|
@@ -286,12 +303,14 @@ export class FileUpload extends BaseComponent<FileUploadState> {
|
|
|
286
303
|
|
|
287
304
|
this._wireStandardEvents(wrapper);
|
|
288
305
|
|
|
306
|
+
// Validate on blur if already validated
|
|
289
307
|
fileInput.addEventListener('blur', () => {
|
|
290
308
|
if (this._hasBeenValidated) {
|
|
291
309
|
this.validate();
|
|
292
310
|
}
|
|
293
311
|
});
|
|
294
312
|
|
|
313
|
+
// Wire sync bindings for label
|
|
295
314
|
const labelSync = this._syncBindings.find(b => b.property === 'label');
|
|
296
315
|
if (labelSync) {
|
|
297
316
|
const transform = labelSync.toComponent || ((v: any) => String(v));
|
package/lib/styles/shadcn.css
CHANGED
|
@@ -160,6 +160,75 @@ p { line-height: 1.75; color: hsl(var(--foreground)); }
|
|
|
160
160
|
width: 100%;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/* ✅ NEW: Enhanced upload area */
|
|
164
|
+
.jux-dataframe-upload-area {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
align-items: flex-start;
|
|
168
|
+
gap: 0.5rem;
|
|
169
|
+
padding: 1rem;
|
|
170
|
+
background: hsl(var(--muted) / 0.2);
|
|
171
|
+
border: 1px dashed hsl(var(--border));
|
|
172
|
+
border-radius: var(--radius);
|
|
173
|
+
transition: all 0.2s;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.jux-dataframe-upload-area:hover {
|
|
177
|
+
background: hsl(var(--muted) / 0.3);
|
|
178
|
+
border-color: hsl(var(--muted-foreground) / 0.3);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.jux-dataframe-upload-area:has(.jux-fileupload-filelist:not(:empty)) {
|
|
182
|
+
background: hsl(var(--success) / 0.05);
|
|
183
|
+
border-color: hsl(var(--success) / 0.2);
|
|
184
|
+
border-style: solid;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.jux-dataframe-upload {
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
gap: 0.75rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.jux-dataframe-upload-description {
|
|
194
|
+
font-size: 0.8125rem;
|
|
195
|
+
color: hsl(var(--muted-foreground));
|
|
196
|
+
line-height: 1.4;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ✅ Style the fileupload button inside dataframe */
|
|
200
|
+
.jux-dataframe-upload .jux-fileupload {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 0.75rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.jux-dataframe-upload .jux-fileupload-button {
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 0.5rem;
|
|
210
|
+
padding: 0.5rem 1rem;
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
border-radius: var(--radius);
|
|
213
|
+
transition: all 0.15s;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.jux-dataframe-upload .jux-fileupload-button:hover {
|
|
217
|
+
background: hsl(var(--accent));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ✅ File list styling inside dataframe */
|
|
221
|
+
.jux-dataframe-upload .jux-fileupload-filelist {
|
|
222
|
+
margin-top: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.jux-dataframe-upload .jux-fileupload-item {
|
|
226
|
+
background: hsl(var(--background));
|
|
227
|
+
border: 1px solid hsl(var(--border));
|
|
228
|
+
padding: 0.5rem 0.75rem;
|
|
229
|
+
font-size: 0.8125rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
163
232
|
.jux-dataframe-status {
|
|
164
233
|
display: flex;
|
|
165
234
|
align-items: center;
|