juxscript 1.1.227 → 1.1.228
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 +0 -88
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +23 -300
- package/lib/components/dataframe.ts +25 -332
- package/lib/styles/shadcn.css +0 -100
- package/package.json +1 -1
|
@@ -30,10 +30,6 @@ export interface DataFrameOptions {
|
|
|
30
30
|
class?: string;
|
|
31
31
|
persistToIndexedDB?: boolean;
|
|
32
32
|
clearStorageOnFileRemove?: boolean;
|
|
33
|
-
// ✅ Collapsible options
|
|
34
|
-
collapsible?: boolean;
|
|
35
|
-
collapsed?: boolean;
|
|
36
|
-
summaryTemplate?: (df: DataFrame) => string;
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
type DataFrameState = BaseState & {
|
|
@@ -78,11 +74,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
78
74
|
private _uploadAccept: string = '.csv,.tsv,.txt,.xlsx,.xls';
|
|
79
75
|
private _uploadDescription: string = '';
|
|
80
76
|
private _showUploadIcon: boolean = true;
|
|
81
|
-
// ✅ Collapsible state
|
|
82
|
-
private _collapsible: boolean = false;
|
|
83
|
-
private _collapsed: boolean = false;
|
|
84
|
-
private _summaryTemplate: ((df: DataFrame) => string) | null = null;
|
|
85
|
-
private _detailsElement: HTMLDetailsElement | null = null;
|
|
86
77
|
private _settingsModal: Modal | null = null;
|
|
87
78
|
|
|
88
79
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
@@ -120,10 +111,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
120
111
|
this._showReshapeWarning = options.showReshapeWarning ?? true;
|
|
121
112
|
this._persistToIndexedDB = options.persistToIndexedDB ?? false;
|
|
122
113
|
this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
|
|
123
|
-
// ✅ Collapsible options
|
|
124
|
-
this._collapsible = options.collapsible ?? false;
|
|
125
|
-
this._collapsed = options.collapsed ?? false;
|
|
126
|
-
this._summaryTemplate = options.summaryTemplate ?? null;
|
|
127
114
|
}
|
|
128
115
|
|
|
129
116
|
protected getTriggerEvents(): readonly string[] { return TRIGGER_EVENTS; }
|
|
@@ -197,18 +184,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
197
184
|
return this;
|
|
198
185
|
}
|
|
199
186
|
|
|
200
|
-
/**
|
|
201
|
-
* Set upload button label
|
|
202
|
-
*/
|
|
203
187
|
uploadLabel(label: string): this {
|
|
204
188
|
this._uploadButtonLabel = label;
|
|
205
189
|
if (this._inlineUpload) this._inlineUpload.label = label;
|
|
206
190
|
return this;
|
|
207
191
|
}
|
|
208
192
|
|
|
209
|
-
/**
|
|
210
|
-
* Set upload button icon
|
|
211
|
-
*/
|
|
212
193
|
uploadIcon(icon: string): this {
|
|
213
194
|
this._uploadButtonIcon = icon;
|
|
214
195
|
this._showUploadIcon = !!icon;
|
|
@@ -216,54 +197,36 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
216
197
|
return this;
|
|
217
198
|
}
|
|
218
199
|
|
|
219
|
-
/**
|
|
220
|
-
* Set upload button variant (outline, primary, ghost, etc.)
|
|
221
|
-
*/
|
|
222
200
|
uploadVariant(variant: string): this {
|
|
223
201
|
this._uploadButtonVariant = variant;
|
|
224
202
|
return this;
|
|
225
203
|
}
|
|
226
204
|
|
|
227
|
-
/**
|
|
228
|
-
* Set accepted file types
|
|
229
|
-
*/
|
|
230
205
|
uploadAccept(accept: string): this {
|
|
231
206
|
this._uploadAccept = accept;
|
|
232
207
|
if (this._inlineUpload) this._inlineUpload.accept = accept;
|
|
233
208
|
return this;
|
|
234
209
|
}
|
|
235
210
|
|
|
236
|
-
/**
|
|
237
|
-
* Set description text below the upload button
|
|
238
|
-
*/
|
|
239
211
|
uploadDescription(description: string): this {
|
|
240
212
|
this._uploadDescription = description;
|
|
241
213
|
return this;
|
|
242
214
|
}
|
|
243
215
|
|
|
244
|
-
/**
|
|
245
|
-
* Show/hide upload icon
|
|
246
|
-
*/
|
|
247
216
|
showUploadIcon(show: boolean): this {
|
|
248
217
|
this._showUploadIcon = show;
|
|
249
218
|
return this;
|
|
250
219
|
}
|
|
251
220
|
|
|
252
221
|
/* ═══════════════════════════════════════════════════
|
|
253
|
-
* STORAGE OPTIONS
|
|
222
|
+
* STORAGE OPTIONS
|
|
254
223
|
* ═══════════════════════════════════════════════════ */
|
|
255
224
|
|
|
256
|
-
/**
|
|
257
|
-
* Enable/disable IndexedDB persistence (default: false = session only)
|
|
258
|
-
*/
|
|
259
225
|
persistToIndexedDB(enabled: boolean): this {
|
|
260
226
|
this._persistToIndexedDB = enabled;
|
|
261
227
|
return this;
|
|
262
228
|
}
|
|
263
229
|
|
|
264
|
-
/**
|
|
265
|
-
* Clear stored data when file is removed (default: true)
|
|
266
|
-
*/
|
|
267
230
|
clearStorageOnFileRemove(enabled: boolean): this {
|
|
268
231
|
this._clearStorageOnFileRemove = enabled;
|
|
269
232
|
return this;
|
|
@@ -273,11 +236,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
273
236
|
* CLEAR / RESET
|
|
274
237
|
* ═══════════════════════════════════════════════════ */
|
|
275
238
|
|
|
276
|
-
/**
|
|
277
|
-
* Clear the current data and reset the table
|
|
278
|
-
*/
|
|
279
239
|
async clear(): Promise<this> {
|
|
280
|
-
// Clear storage if enabled
|
|
281
240
|
if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
|
|
282
241
|
try {
|
|
283
242
|
const tables = await this._driver.list();
|
|
@@ -290,7 +249,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
290
249
|
}
|
|
291
250
|
}
|
|
292
251
|
|
|
293
|
-
// Reset state
|
|
294
252
|
this._df = null;
|
|
295
253
|
this._rawFileData = null;
|
|
296
254
|
this._sheets.clear();
|
|
@@ -299,27 +257,18 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
299
257
|
this.state.rowCount = 0;
|
|
300
258
|
this.state.colCount = 0;
|
|
301
259
|
|
|
302
|
-
// Clear table display
|
|
303
260
|
if (this._table) {
|
|
304
261
|
this._table.columns([]).rows([]);
|
|
305
262
|
}
|
|
306
263
|
|
|
307
|
-
// Remove tabs if present
|
|
308
264
|
const wrapper = document.getElementById(this._id);
|
|
309
265
|
if (wrapper) {
|
|
310
266
|
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
311
267
|
if (existingTabs) existingTabs.remove();
|
|
312
268
|
}
|
|
313
269
|
|
|
314
|
-
// ✅ Hide data view, show upload
|
|
315
270
|
this._hideDataView();
|
|
316
271
|
|
|
317
|
-
// ✅ Hide collapsible details
|
|
318
|
-
if (this._collapsible && this._detailsElement) {
|
|
319
|
-
this._detailsElement.style.display = 'none';
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ✅ Clear file from upload component
|
|
323
272
|
if (this._uploadRef) {
|
|
324
273
|
this._uploadRef.clear();
|
|
325
274
|
}
|
|
@@ -394,7 +343,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
394
343
|
}
|
|
395
344
|
|
|
396
345
|
/* ═══════════════════════════════════════════════════
|
|
397
|
-
* TABLE OPTIONS
|
|
346
|
+
* TABLE OPTIONS
|
|
398
347
|
* ═══════════════════════════════════════════════════ */
|
|
399
348
|
|
|
400
349
|
striped(v: boolean): this { this._tableOptions.striped = v; return this; }
|
|
@@ -408,85 +357,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
408
357
|
maxFileSize(mb: number): this { this._maxFileSize = mb; return this; }
|
|
409
358
|
|
|
410
359
|
/* ═══════════════════════════════════════════════════
|
|
411
|
-
*
|
|
412
|
-
* ═══════════════════════════════════════════════════ */
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Enable/disable collapsible mode
|
|
416
|
-
*/
|
|
417
|
-
collapsible(enabled: boolean): this {
|
|
418
|
-
this._collapsible = enabled;
|
|
419
|
-
return this;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Set initial collapsed state (true = collapsed, false = expanded)
|
|
424
|
-
*/
|
|
425
|
-
collapsed(value: boolean): this {
|
|
426
|
-
this._collapsed = value;
|
|
427
|
-
if (this._detailsElement) {
|
|
428
|
-
this._detailsElement.open = !value;
|
|
429
|
-
}
|
|
430
|
-
return this;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Expand the details (show table)
|
|
435
|
-
*/
|
|
436
|
-
expand(): this {
|
|
437
|
-
this._collapsed = false;
|
|
438
|
-
if (this._detailsElement) {
|
|
439
|
-
this._detailsElement.open = true;
|
|
440
|
-
}
|
|
441
|
-
return this;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Collapse the details (hide table)
|
|
446
|
-
*/
|
|
447
|
-
collapse(): this {
|
|
448
|
-
this._collapsed = true;
|
|
449
|
-
if (this._detailsElement) {
|
|
450
|
-
this._detailsElement.open = false;
|
|
451
|
-
}
|
|
452
|
-
return this;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Check if collapsible mode is enabled
|
|
457
|
-
*/
|
|
458
|
-
isCollapsible(): boolean {
|
|
459
|
-
return this._collapsible;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Toggle collapsed state
|
|
464
|
-
*/
|
|
465
|
-
toggle(): this {
|
|
466
|
-
this._collapsed = !this._collapsed;
|
|
467
|
-
if (this._detailsElement) {
|
|
468
|
-
this._detailsElement.open = !this._collapsed;
|
|
469
|
-
}
|
|
470
|
-
return this;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Set custom summary template
|
|
475
|
-
*/
|
|
476
|
-
summaryTemplate(fn: (df: DataFrame) => string): this {
|
|
477
|
-
this._summaryTemplate = fn;
|
|
478
|
-
return this;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Get current collapsed state
|
|
483
|
-
*/
|
|
484
|
-
isCollapsed(): boolean {
|
|
485
|
-
return this._collapsed;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/* ═══════════════════════════════════════════════════
|
|
489
|
-
* FILE HANDLING (modified to use storage options)
|
|
360
|
+
* FILE HANDLING
|
|
490
361
|
* ═══════════════════════════════════════════════════ */
|
|
491
362
|
|
|
492
363
|
private async _handleFile(file: File): Promise<void> {
|
|
@@ -523,7 +394,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
523
394
|
return;
|
|
524
395
|
}
|
|
525
396
|
|
|
526
|
-
// ✅ Only store if persistence is enabled
|
|
527
397
|
if (this._persistToIndexedDB) {
|
|
528
398
|
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
529
399
|
}
|
|
@@ -542,7 +412,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
542
412
|
hasHeader: true
|
|
543
413
|
});
|
|
544
414
|
|
|
545
|
-
// ✅ Only store if persistence is enabled
|
|
546
415
|
if (this._persistToIndexedDB) {
|
|
547
416
|
await this._driver.store(file.name, df, { source: file.name });
|
|
548
417
|
}
|
|
@@ -578,8 +447,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
578
447
|
});
|
|
579
448
|
|
|
580
449
|
const sheetNames = Object.keys(sheets);
|
|
581
|
-
|
|
582
|
-
// Sanitize sheet names for use as DOM IDs
|
|
583
450
|
const sanitizeId = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
584
451
|
|
|
585
452
|
const tabDefs = sheetNames.map(name => ({
|
|
@@ -593,7 +460,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
593
460
|
activeTab: sanitizeId(sheetNames[0])
|
|
594
461
|
});
|
|
595
462
|
|
|
596
|
-
// Map sanitized IDs back to original sheet names
|
|
597
463
|
const idToSheetName = new Map<string, string>();
|
|
598
464
|
sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
|
|
599
465
|
|
|
@@ -602,9 +468,19 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
602
468
|
this._df = this._sheets.get(originalName) || null;
|
|
603
469
|
});
|
|
604
470
|
|
|
471
|
+
const dataContainer = wrapper.querySelector('.jux-dataframe-data') as HTMLElement;
|
|
472
|
+
if (dataContainer) {
|
|
473
|
+
dataContainer.style.display = '';
|
|
474
|
+
}
|
|
475
|
+
|
|
605
476
|
const tabsContainer = document.createElement('div');
|
|
606
477
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
607
|
-
|
|
478
|
+
|
|
479
|
+
if (dataContainer) {
|
|
480
|
+
dataContainer.appendChild(tabsContainer);
|
|
481
|
+
} else {
|
|
482
|
+
wrapper.appendChild(tabsContainer);
|
|
483
|
+
}
|
|
608
484
|
|
|
609
485
|
this._tabs.render(tabsContainer);
|
|
610
486
|
|
|
@@ -612,13 +488,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
612
488
|
const df = sheets[sheetName];
|
|
613
489
|
const safeId = sanitizeId(sheetName);
|
|
614
490
|
|
|
615
|
-
// ✅ DEBUG: Log table options
|
|
616
|
-
console.log(`[DataFrame] Creating table for sheet "${sheetName}" with options:`, {
|
|
617
|
-
filterable: this._tableOptions.filterable,
|
|
618
|
-
paginated: this._tableOptions.paginated,
|
|
619
|
-
sortable: this._tableOptions.sortable
|
|
620
|
-
});
|
|
621
|
-
|
|
622
491
|
const table = new Table(`${this._id}-table-${safeId}`, {
|
|
623
492
|
striped: this._tableOptions.striped,
|
|
624
493
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -647,6 +516,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
647
516
|
'success'
|
|
648
517
|
);
|
|
649
518
|
|
|
519
|
+
this._showDataView();
|
|
520
|
+
|
|
650
521
|
this._df = sheets[sheetNames[0]];
|
|
651
522
|
this._triggerCallback('load', this._df, null, this);
|
|
652
523
|
}
|
|
@@ -659,7 +530,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
659
530
|
const el = document.getElementById(`${this._id}-status`);
|
|
660
531
|
if (!el) return;
|
|
661
532
|
|
|
662
|
-
// ✅ Only show status during loading/error states
|
|
663
533
|
if (type === 'loading' || type === 'error') {
|
|
664
534
|
el.style.display = '';
|
|
665
535
|
el.className = 'jux-dataframe-status';
|
|
@@ -679,7 +549,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
679
549
|
span.textContent = text;
|
|
680
550
|
el.appendChild(span);
|
|
681
551
|
} else {
|
|
682
|
-
// ✅ Hide status after successful load - table speaks for itself
|
|
683
552
|
el.style.display = 'none';
|
|
684
553
|
}
|
|
685
554
|
}
|
|
@@ -692,7 +561,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
692
561
|
this.state.rowCount = df.height;
|
|
693
562
|
this.state.colCount = df.width;
|
|
694
563
|
|
|
695
|
-
// Only strip __EMPTY columns that are ENTIRELY null/empty
|
|
696
564
|
const cols = df.columns;
|
|
697
565
|
const rows = df.toRows();
|
|
698
566
|
const emptyColumns = cols.filter(c => {
|
|
@@ -715,67 +583,43 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
715
583
|
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
716
584
|
}
|
|
717
585
|
|
|
718
|
-
// ✅ Show the table container, hide upload area
|
|
719
586
|
this._showDataView();
|
|
720
|
-
|
|
721
|
-
// ✅ Update collapsible summary if enabled
|
|
722
|
-
if (this._collapsible && this._detailsElement) {
|
|
723
|
-
this._detailsElement.style.display = '';
|
|
724
|
-
this._updateSummary();
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// ✅ Hide status - data is loaded
|
|
728
587
|
this._updateStatus('', 'success');
|
|
729
|
-
|
|
730
588
|
this._triggerCallback('load', this._df, null, this);
|
|
731
589
|
}
|
|
732
590
|
|
|
733
|
-
/**
|
|
734
|
-
* Show the data view (table + settings gear), hide upload area
|
|
735
|
-
*/
|
|
736
591
|
private _showDataView(): void {
|
|
737
592
|
const wrapper = document.getElementById(this._id);
|
|
738
593
|
if (!wrapper) return;
|
|
739
594
|
|
|
740
|
-
// Hide upload area
|
|
741
595
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area') as HTMLElement;
|
|
742
596
|
if (uploadArea) {
|
|
743
597
|
uploadArea.style.display = 'none';
|
|
744
598
|
}
|
|
745
599
|
|
|
746
|
-
// Show data container (table + toolbar)
|
|
747
600
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data') as HTMLElement;
|
|
748
601
|
if (dataContainer) {
|
|
749
602
|
dataContainer.style.display = '';
|
|
750
603
|
}
|
|
751
604
|
|
|
752
|
-
// Update settings gear with file info
|
|
753
605
|
this._updateSettingsGear();
|
|
754
606
|
}
|
|
755
607
|
|
|
756
|
-
/**
|
|
757
|
-
* Hide data view, show upload area
|
|
758
|
-
*/
|
|
759
608
|
private _hideDataView(): void {
|
|
760
609
|
const wrapper = document.getElementById(this._id);
|
|
761
610
|
if (!wrapper) return;
|
|
762
611
|
|
|
763
|
-
// Show upload area
|
|
764
612
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area') as HTMLElement;
|
|
765
613
|
if (uploadArea) {
|
|
766
614
|
uploadArea.style.display = '';
|
|
767
615
|
}
|
|
768
616
|
|
|
769
|
-
// Hide data container
|
|
770
617
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data') as HTMLElement;
|
|
771
618
|
if (dataContainer) {
|
|
772
619
|
dataContainer.style.display = 'none';
|
|
773
620
|
}
|
|
774
621
|
}
|
|
775
622
|
|
|
776
|
-
/**
|
|
777
|
-
* Update the settings gear tooltip/info
|
|
778
|
-
*/
|
|
779
623
|
private _updateSettingsGear(): void {
|
|
780
624
|
const gear = document.getElementById(`${this._id}-settings-gear`);
|
|
781
625
|
if (!gear || !this._df) return;
|
|
@@ -791,9 +635,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
791
635
|
}
|
|
792
636
|
}
|
|
793
637
|
|
|
794
|
-
/**
|
|
795
|
-
* Show the unified settings modal
|
|
796
|
-
*/
|
|
797
638
|
private _showSettingsModal(): void {
|
|
798
639
|
this._cleanupReshapeModal();
|
|
799
640
|
|
|
@@ -812,7 +653,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
812
653
|
|
|
813
654
|
let contentHTML = `
|
|
814
655
|
<div class="jux-dataframe-settings-content">
|
|
815
|
-
<!-- File Info Section -->
|
|
816
656
|
<div class="jux-dataframe-settings-section">
|
|
817
657
|
<div class="jux-dataframe-settings-label">Source</div>
|
|
818
658
|
<div class="jux-dataframe-settings-value">
|
|
@@ -820,8 +660,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
820
660
|
${fileInfo ? `<span class="jux-muted" style="margin-left: 8px;">${fileSizeKB} KB</span>` : ''}
|
|
821
661
|
</div>
|
|
822
662
|
</div>
|
|
823
|
-
|
|
824
|
-
<!-- Data Info Section -->
|
|
825
663
|
<div class="jux-dataframe-settings-section">
|
|
826
664
|
<div class="jux-dataframe-settings-label">Data</div>
|
|
827
665
|
<div class="jux-dataframe-settings-value">
|
|
@@ -831,7 +669,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
831
669
|
</div>
|
|
832
670
|
`;
|
|
833
671
|
|
|
834
|
-
// Import Settings button (only if we have raw file data)
|
|
835
672
|
if (this._rawFileData) {
|
|
836
673
|
contentHTML += `
|
|
837
674
|
<div class="jux-dataframe-settings-section">
|
|
@@ -845,9 +682,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
845
682
|
`;
|
|
846
683
|
}
|
|
847
684
|
|
|
848
|
-
contentHTML +=
|
|
849
|
-
</div>
|
|
850
|
-
`;
|
|
685
|
+
contentHTML += `</div>`;
|
|
851
686
|
|
|
852
687
|
this._settingsModal
|
|
853
688
|
.content(contentHTML)
|
|
@@ -870,7 +705,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
870
705
|
this._settingsModal.render(document.body);
|
|
871
706
|
this._settingsModal.open();
|
|
872
707
|
|
|
873
|
-
// Wire up import settings button
|
|
874
708
|
requestAnimationFrame(() => {
|
|
875
709
|
const adjustBtn = document.getElementById(`${this._id}-adjust-import`);
|
|
876
710
|
if (adjustBtn) {
|
|
@@ -882,82 +716,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
882
716
|
});
|
|
883
717
|
}
|
|
884
718
|
|
|
885
|
-
/**
|
|
886
|
-
* Update the collapsible summary text
|
|
887
|
-
*/
|
|
888
|
-
private _updateSummary(isMalformed?: boolean): void {
|
|
889
|
-
if (!this._collapsible || !this._detailsElement) return;
|
|
890
|
-
|
|
891
|
-
const summaryEl = this._detailsElement.querySelector('.jux-dataframe-summary');
|
|
892
|
-
if (!summaryEl) return;
|
|
893
|
-
|
|
894
|
-
const malformed = isMalformed ?? (this._df ? this._detectMalformedData(this._df) : false);
|
|
895
|
-
|
|
896
|
-
const summaryTextEl = summaryEl.querySelector('.jux-dataframe-summary-text');
|
|
897
|
-
if (summaryTextEl) {
|
|
898
|
-
if (!this._df) {
|
|
899
|
-
summaryTextEl.textContent = 'No data loaded';
|
|
900
|
-
} else if (this._summaryTemplate) {
|
|
901
|
-
summaryTextEl.textContent = this._summaryTemplate(this._df);
|
|
902
|
-
} else {
|
|
903
|
-
const suffix = malformed ? ' ⚠️' : '';
|
|
904
|
-
summaryTextEl.textContent = `${this.state.sourceName || 'Data'} — ${this._df.height} rows × ${this._df.width} cols${suffix}`;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// ✅ Add/update settings gear in summary
|
|
909
|
-
let gearBtn = summaryEl.querySelector('.jux-dataframe-summary-gear') as HTMLButtonElement;
|
|
910
|
-
|
|
911
|
-
if (this._df) {
|
|
912
|
-
if (!gearBtn) {
|
|
913
|
-
gearBtn = document.createElement('button');
|
|
914
|
-
gearBtn.className = 'jux-dataframe-summary-gear';
|
|
915
|
-
gearBtn.type = 'button';
|
|
916
|
-
gearBtn.innerHTML = '⚙️';
|
|
917
|
-
gearBtn.addEventListener('click', (e) => {
|
|
918
|
-
e.stopPropagation();
|
|
919
|
-
this._showSettingsModal();
|
|
920
|
-
});
|
|
921
|
-
summaryEl.appendChild(gearBtn);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (malformed) {
|
|
925
|
-
gearBtn.classList.add('jux-dataframe-gear-warning');
|
|
926
|
-
} else {
|
|
927
|
-
gearBtn.classList.remove('jux-dataframe-gear-warning');
|
|
928
|
-
}
|
|
929
|
-
} else if (gearBtn) {
|
|
930
|
-
gearBtn.remove();
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
719
|
/* ═══════════════════════════════════════════════════
|
|
935
720
|
* MALFORMED DATA DETECTION
|
|
936
721
|
* ═══════════════════════════════════════════════════ */
|
|
937
722
|
|
|
938
|
-
private _appendSettingsButton(label: string, variant: string): void {
|
|
939
|
-
const statusEl = document.getElementById(`${this._id}-status`);
|
|
940
|
-
if (!statusEl) return;
|
|
941
|
-
|
|
942
|
-
// Remove existing settings button if any
|
|
943
|
-
const existing = statusEl.querySelector('.jux-dataframe-settings-btn');
|
|
944
|
-
if (existing) existing.remove();
|
|
945
|
-
|
|
946
|
-
const btn = document.createElement('button');
|
|
947
|
-
btn.className = `jux-dataframe-settings-btn jux-dataframe-settings-btn-${variant}`;
|
|
948
|
-
btn.type = 'button';
|
|
949
|
-
btn.textContent = `⚙️ ${label}`;
|
|
950
|
-
btn.style.marginLeft = '8px';
|
|
951
|
-
btn.style.cursor = 'pointer';
|
|
952
|
-
btn.style.fontSize = '0.8rem';
|
|
953
|
-
btn.style.padding = '2px 8px';
|
|
954
|
-
btn.style.borderRadius = 'var(--radius, 4px)';
|
|
955
|
-
btn.style.border = '1px solid hsl(var(--border))';
|
|
956
|
-
btn.style.background = variant === 'warning' ? 'hsl(38 92% 50% / 0.15)' : 'transparent';
|
|
957
|
-
btn.addEventListener('click', () => this._showReshapeModal());
|
|
958
|
-
statusEl.appendChild(btn);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
723
|
private _detectMalformedData(df: DataFrame): boolean {
|
|
962
724
|
const columns = df.columns;
|
|
963
725
|
const rows = df.toRows();
|
|
@@ -1016,11 +778,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1016
778
|
return div.innerHTML;
|
|
1017
779
|
}
|
|
1018
780
|
|
|
1019
|
-
/**
|
|
1020
|
-
* Build a clickable preview table from raw row data.
|
|
1021
|
-
* Each row stores its actual sheet row index via data-sheet-row attribute.
|
|
1022
|
-
* Returns the table HTML string.
|
|
1023
|
-
*/
|
|
1024
781
|
private _buildClickablePreviewHTML(
|
|
1025
782
|
rawRows: { sheetRow: number; values: any[] }[],
|
|
1026
783
|
selectedSheetRow: number
|
|
@@ -1041,7 +798,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1041
798
|
|
|
1042
799
|
html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
|
|
1043
800
|
|
|
1044
|
-
// Row index cell
|
|
1045
801
|
html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center; user-select: none;">`;
|
|
1046
802
|
if (isHeader) {
|
|
1047
803
|
html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
|
|
@@ -1050,7 +806,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1050
806
|
}
|
|
1051
807
|
html += '</td>';
|
|
1052
808
|
|
|
1053
|
-
// Show first 6 columns
|
|
1054
809
|
const displayCols = values.slice(0, 6);
|
|
1055
810
|
displayCols.forEach(val => {
|
|
1056
811
|
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
@@ -1064,7 +819,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1064
819
|
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
1065
820
|
}
|
|
1066
821
|
|
|
1067
|
-
// Status badge
|
|
1068
822
|
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
|
|
1069
823
|
if (isHeader) {
|
|
1070
824
|
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
@@ -1085,23 +839,17 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1085
839
|
|
|
1086
840
|
this._cleanupReshapeModal();
|
|
1087
841
|
|
|
1088
|
-
// ✅ Use the SAME cell-reading method as the parser
|
|
1089
842
|
const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
|
|
1090
843
|
|
|
1091
844
|
if (rawRows.length === 0) return;
|
|
1092
845
|
|
|
1093
|
-
// Log what we got so we can verify alignment
|
|
1094
|
-
console.log('[DataFrame Preview] Raw rows from readRawExcelRows:');
|
|
1095
|
-
rawRows.forEach(r => console.log(` sheetRow ${r.sheetRow}:`, r.values.slice(0, 5)));
|
|
1096
|
-
|
|
1097
|
-
// Auto-detect best header row
|
|
1098
846
|
let selectedSheetRow = rawRows[0].sheetRow;
|
|
1099
847
|
for (const { sheetRow, values } of rawRows) {
|
|
1100
848
|
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
1101
849
|
if (nonEmpty.length < values.length * 0.5) continue;
|
|
1102
850
|
const nonNumeric = nonEmpty.filter(v => {
|
|
1103
|
-
const
|
|
1104
|
-
return isNaN(Number(
|
|
851
|
+
const str = String(v).trim();
|
|
852
|
+
return isNaN(Number(str)) && str !== '';
|
|
1105
853
|
}).length;
|
|
1106
854
|
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
1107
855
|
selectedSheetRow = sheetRow;
|
|
@@ -1142,8 +890,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1142
890
|
const input = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
1143
891
|
const headerRow = parseInt(input.value) || 0;
|
|
1144
892
|
|
|
1145
|
-
console.log(`[DataFrame] Apply clicked: headerRow=${headerRow}`);
|
|
1146
|
-
|
|
1147
893
|
this.state.loading = true;
|
|
1148
894
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
1149
895
|
|
|
@@ -1208,7 +954,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1208
954
|
tr.addEventListener('click', () => {
|
|
1209
955
|
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
1210
956
|
hiddenInput.value = String(rowIdx);
|
|
1211
|
-
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
1212
957
|
updateHint(rowIdx);
|
|
1213
958
|
renderPreview(rowIdx);
|
|
1214
959
|
});
|
|
@@ -1228,7 +973,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1228
973
|
const text = this._rawFileData.text;
|
|
1229
974
|
const detected = (this._driver as any)._detectDelimiter(text);
|
|
1230
975
|
|
|
1231
|
-
// Parse raw lines
|
|
1232
976
|
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
1233
977
|
const rawRows: { sheetRow: number; values: any[] }[] = [];
|
|
1234
978
|
const maxPreviewRows = Math.min(lines.length, 15);
|
|
@@ -1242,7 +986,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1242
986
|
rawRows.push({ sheetRow: i, values });
|
|
1243
987
|
}
|
|
1244
988
|
|
|
1245
|
-
// Auto-detect header row
|
|
1246
989
|
let selectedRow = 0;
|
|
1247
990
|
for (const { sheetRow, values } of rawRows) {
|
|
1248
991
|
const nonEmpty = values.filter((v: string) => v.trim() !== '');
|
|
@@ -1367,7 +1110,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1367
1110
|
tr.addEventListener('click', () => {
|
|
1368
1111
|
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
1369
1112
|
hiddenInput.value = String(rowIdx);
|
|
1370
|
-
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
1371
1113
|
updateHint(rowIdx);
|
|
1372
1114
|
renderPreview(rowIdx);
|
|
1373
1115
|
});
|
|
@@ -1390,7 +1132,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1390
1132
|
}
|
|
1391
1133
|
|
|
1392
1134
|
/* ═══════════════════════════════════════════════════
|
|
1393
|
-
* UPDATE & RENDER
|
|
1135
|
+
* UPDATE & RENDER
|
|
1394
1136
|
* ═══════════════════════════════════════════════════ */
|
|
1395
1137
|
|
|
1396
1138
|
update(_prop: string, _value: any): void { }
|
|
@@ -1405,9 +1147,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1405
1147
|
if (className) wrapper.className += ` ${className}`;
|
|
1406
1148
|
if (style) wrapper.setAttribute('style', style);
|
|
1407
1149
|
|
|
1408
|
-
//
|
|
1409
|
-
// UPLOAD AREA (shown initially, hidden after data loads)
|
|
1410
|
-
// ═══════════════════════════════════════════════════
|
|
1150
|
+
// Upload area
|
|
1411
1151
|
if (this._inlineUpload) {
|
|
1412
1152
|
const uploadArea = document.createElement('div');
|
|
1413
1153
|
uploadArea.className = 'jux-dataframe-upload-area';
|
|
@@ -1440,7 +1180,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1440
1180
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
1441
1181
|
uploadArea.appendChild(uploadContainer);
|
|
1442
1182
|
|
|
1443
|
-
// Status bar (for loading/error states only)
|
|
1444
1183
|
if (this._showStatus) {
|
|
1445
1184
|
const statusBar = document.createElement('div');
|
|
1446
1185
|
statusBar.className = 'jux-dataframe-status';
|
|
@@ -1463,14 +1202,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1463
1202
|
container.appendChild(wrapper);
|
|
1464
1203
|
}
|
|
1465
1204
|
|
|
1466
|
-
//
|
|
1467
|
-
// DATA CONTAINER (hidden initially, shown after data loads)
|
|
1468
|
-
// ═══════════════════════════════════════════════════
|
|
1205
|
+
// Data container
|
|
1469
1206
|
const dataContainer = document.createElement('div');
|
|
1470
1207
|
dataContainer.className = 'jux-dataframe-data';
|
|
1471
|
-
dataContainer.style.display = 'none';
|
|
1208
|
+
dataContainer.style.display = 'none';
|
|
1472
1209
|
|
|
1473
|
-
// ✅ Toolbar with settings gear
|
|
1474
1210
|
const toolbar = document.createElement('div');
|
|
1475
1211
|
toolbar.className = 'jux-dataframe-toolbar';
|
|
1476
1212
|
|
|
@@ -1484,52 +1220,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1484
1220
|
toolbar.appendChild(settingsGear);
|
|
1485
1221
|
|
|
1486
1222
|
dataContainer.appendChild(toolbar);
|
|
1487
|
-
|
|
1488
|
-
// ═══════════════════════════════════════════════════
|
|
1489
|
-
// TABLE CONTAINER (inside data container or collapsible)
|
|
1490
|
-
// ═══════════════════════════════════════════════════
|
|
1491
|
-
let tableContainer: HTMLElement;
|
|
1492
|
-
|
|
1493
|
-
if (this._collapsible) {
|
|
1494
|
-
const details = document.createElement('details');
|
|
1495
|
-
details.className = 'jux-dataframe-details';
|
|
1496
|
-
details.open = !this._collapsed;
|
|
1497
|
-
details.style.display = 'none'; // Hidden until data loads
|
|
1498
|
-
this._detailsElement = details;
|
|
1499
|
-
|
|
1500
|
-
const summary = document.createElement('summary');
|
|
1501
|
-
summary.className = 'jux-dataframe-summary';
|
|
1502
|
-
|
|
1503
|
-
const chevron = document.createElement('span');
|
|
1504
|
-
chevron.className = 'jux-dataframe-summary-chevron';
|
|
1505
|
-
chevron.innerHTML = '▶';
|
|
1506
|
-
summary.appendChild(chevron);
|
|
1507
|
-
|
|
1508
|
-
const textSpan = document.createElement('span');
|
|
1509
|
-
textSpan.className = 'jux-dataframe-summary-text';
|
|
1510
|
-
summary.appendChild(textSpan);
|
|
1511
|
-
|
|
1512
|
-
details.appendChild(summary);
|
|
1513
|
-
|
|
1514
|
-
const content = document.createElement('div');
|
|
1515
|
-
content.className = 'jux-dataframe-details-content';
|
|
1516
|
-
details.appendChild(content);
|
|
1517
|
-
|
|
1518
|
-
dataContainer.appendChild(details);
|
|
1519
|
-
tableContainer = content;
|
|
1520
|
-
|
|
1521
|
-
details.addEventListener('toggle', () => {
|
|
1522
|
-
this._collapsed = !details.open;
|
|
1523
|
-
});
|
|
1524
|
-
} else {
|
|
1525
|
-
tableContainer = dataContainer;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
1223
|
wrapper.appendChild(dataContainer);
|
|
1529
1224
|
|
|
1530
|
-
//
|
|
1531
|
-
// TABLE
|
|
1532
|
-
// ═══════════════════════════════════════════════════
|
|
1225
|
+
// Table
|
|
1533
1226
|
const tbl = new Table(`${this._id}-table`, {
|
|
1534
1227
|
striped: this._tableOptions.striped,
|
|
1535
1228
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -1538,7 +1231,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1538
1231
|
paginated: this._tableOptions.paginated,
|
|
1539
1232
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
1540
1233
|
});
|
|
1541
|
-
tbl.render(
|
|
1234
|
+
tbl.render(dataContainer);
|
|
1542
1235
|
this._table = tbl;
|
|
1543
1236
|
|
|
1544
1237
|
if (this._pendingSource) {
|