juxscript 1.1.227 → 1.1.230

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.
@@ -4,9 +4,9 @@ import { TabularDriver } from '../storage/TabularDriver.js';
4
4
  import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
6
  import { Tabs } from './tabs.js';
7
- import { Modal } from './modal.js';
8
7
  import { Button } from './button.js';
9
8
  import { renderIcon } from './icons.js';
9
+ import { ImportSettingsModal } from './dataframe/ImportSettingsModal.js';
10
10
  const TRIGGER_EVENTS = [];
11
11
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
12
12
  export class DataFrameComponent extends BaseComponent {
@@ -30,15 +30,11 @@ export class DataFrameComponent extends BaseComponent {
30
30
  this._storageKey = null;
31
31
  this._pendingSource = null;
32
32
  this._inlineUpload = null;
33
- this._showStatus = true;
34
33
  this._icon = '';
35
34
  this._maxSheetSize = 100000;
36
35
  this._sheetChunkSize = 10000;
37
36
  this._maxFileSize = 50;
38
- this._showReshapeWarning = true;
39
37
  this._rawFileData = null;
40
- this._reshapeModal = null;
41
- this._reshapeModalRendered = false;
42
38
  this._persistToIndexedDB = false;
43
39
  this._clearStorageOnFileRemove = true;
44
40
  this._uploadButtonLabel = 'Upload File';
@@ -47,14 +43,7 @@ export class DataFrameComponent extends BaseComponent {
47
43
  this._uploadAccept = '.csv,.tsv,.txt,.xlsx,.xls';
48
44
  this._uploadDescription = '';
49
45
  this._showUploadIcon = true;
50
- // ✅ Collapsible state
51
- this._collapsible = false;
52
- this._collapsed = false;
53
- this._summaryTemplate = null;
54
- this._detailsElement = null;
55
- this._settingsModal = null;
56
46
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
57
- this._showStatus = options.showStatus ?? true;
58
47
  this._icon = options.icon ?? '';
59
48
  this._tableOptions = {
60
49
  striped: options.striped ?? true,
@@ -67,13 +56,13 @@ export class DataFrameComponent extends BaseComponent {
67
56
  this._maxSheetSize = options.maxSheetSize ?? 100000;
68
57
  this._sheetChunkSize = options.sheetChunkSize ?? 10000;
69
58
  this._maxFileSize = options.maxFileSize ?? 50;
70
- this._showReshapeWarning = options.showReshapeWarning ?? true;
71
59
  this._persistToIndexedDB = options.persistToIndexedDB ?? false;
72
60
  this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
73
- // Collapsible options
74
- this._collapsible = options.collapsible ?? false;
75
- this._collapsed = options.collapsed ?? false;
76
- this._summaryTemplate = options.summaryTemplate ?? null;
61
+ // Initialize the import settings modal
62
+ this._importSettingsModal = new ImportSettingsModal(id, this._driver, {
63
+ maxSheetSize: this._maxSheetSize,
64
+ sheetChunkSize: this._sheetChunkSize
65
+ });
77
66
  }
78
67
  getTriggerEvents() { return TRIGGER_EVENTS; }
79
68
  getCallbackEvents() { return CALLBACK_EVENTS; }
@@ -149,18 +138,12 @@ export class DataFrameComponent extends BaseComponent {
149
138
  this._uploadButtonIcon = icon;
150
139
  return this;
151
140
  }
152
- /**
153
- * Set upload button label
154
- */
155
141
  uploadLabel(label) {
156
142
  this._uploadButtonLabel = label;
157
143
  if (this._inlineUpload)
158
144
  this._inlineUpload.label = label;
159
145
  return this;
160
146
  }
161
- /**
162
- * Set upload button icon
163
- */
164
147
  uploadIcon(icon) {
165
148
  this._uploadButtonIcon = icon;
166
149
  this._showUploadIcon = !!icon;
@@ -168,49 +151,31 @@ export class DataFrameComponent extends BaseComponent {
168
151
  this._inlineUpload.icon = icon;
169
152
  return this;
170
153
  }
171
- /**
172
- * Set upload button variant (outline, primary, ghost, etc.)
173
- */
174
154
  uploadVariant(variant) {
175
155
  this._uploadButtonVariant = variant;
176
156
  return this;
177
157
  }
178
- /**
179
- * Set accepted file types
180
- */
181
158
  uploadAccept(accept) {
182
159
  this._uploadAccept = accept;
183
160
  if (this._inlineUpload)
184
161
  this._inlineUpload.accept = accept;
185
162
  return this;
186
163
  }
187
- /**
188
- * Set description text below the upload button
189
- */
190
164
  uploadDescription(description) {
191
165
  this._uploadDescription = description;
192
166
  return this;
193
167
  }
194
- /**
195
- * Show/hide upload icon
196
- */
197
168
  showUploadIcon(show) {
198
169
  this._showUploadIcon = show;
199
170
  return this;
200
171
  }
201
172
  /* ═══════════════════════════════════════════════════
202
- * STORAGE OPTIONS (fluent API)
173
+ * STORAGE OPTIONS
203
174
  * ═══════════════════════════════════════════════════ */
204
- /**
205
- * Enable/disable IndexedDB persistence (default: false = session only)
206
- */
207
175
  persistToIndexedDB(enabled) {
208
176
  this._persistToIndexedDB = enabled;
209
177
  return this;
210
178
  }
211
- /**
212
- * Clear stored data when file is removed (default: true)
213
- */
214
179
  clearStorageOnFileRemove(enabled) {
215
180
  this._clearStorageOnFileRemove = enabled;
216
181
  return this;
@@ -218,11 +183,7 @@ export class DataFrameComponent extends BaseComponent {
218
183
  /* ═══════════════════════════════════════════════════
219
184
  * CLEAR / RESET
220
185
  * ═══════════════════════════════════════════════════ */
221
- /**
222
- * Clear the current data and reset the table
223
- */
224
186
  async clear() {
225
- // Clear storage if enabled
226
187
  if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
227
188
  try {
228
189
  const tables = await this._driver.list();
@@ -235,7 +196,6 @@ export class DataFrameComponent extends BaseComponent {
235
196
  console.warn('[DataFrame] Failed to clear storage:', err);
236
197
  }
237
198
  }
238
- // Reset state
239
199
  this._df = null;
240
200
  this._rawFileData = null;
241
201
  this._sheets.clear();
@@ -243,24 +203,16 @@ export class DataFrameComponent extends BaseComponent {
243
203
  this.state.sourceName = '';
244
204
  this.state.rowCount = 0;
245
205
  this.state.colCount = 0;
246
- // Clear table display
247
206
  if (this._table) {
248
207
  this._table.columns([]).rows([]);
249
208
  }
250
- // Remove tabs if present
251
209
  const wrapper = document.getElementById(this._id);
252
210
  if (wrapper) {
253
211
  const existingTabs = wrapper.querySelector('.jux-tabs');
254
212
  if (existingTabs)
255
213
  existingTabs.remove();
256
214
  }
257
- // ✅ Hide data view, show upload
258
215
  this._hideDataView();
259
- // ✅ Hide collapsible details
260
- if (this._collapsible && this._detailsElement) {
261
- this._detailsElement.style.display = 'none';
262
- }
263
- // ✅ Clear file from upload component
264
216
  if (this._uploadRef) {
265
217
  this._uploadRef.clear();
266
218
  }
@@ -269,7 +221,6 @@ export class DataFrameComponent extends BaseComponent {
269
221
  /* ═══════════════════════════════════════════════════
270
222
  * UI TOGGLES
271
223
  * ═══════════════════════════════════════════════════ */
272
- showStatus(v) { this._showStatus = v; return this; }
273
224
  statusIcon(v) { this._icon = v; return this; }
274
225
  /* ═══════════════════════════════════════════════════
275
226
  * TRANSFORM API
@@ -321,7 +272,7 @@ export class DataFrameComponent extends BaseComponent {
321
272
  return this._driver.store(name, this._df);
322
273
  }
323
274
  /* ═══════════════════════════════════════════════════
324
- * TABLE OPTIONS (fluent, pre-render)
275
+ * TABLE OPTIONS
325
276
  * ═══════════════════════════════════════════════════ */
326
277
  striped(v) { this._tableOptions.striped = v; return this; }
327
278
  hoverable(v) { this._tableOptions.hoverable = v; return this; }
@@ -333,76 +284,7 @@ export class DataFrameComponent extends BaseComponent {
333
284
  sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
334
285
  maxFileSize(mb) { this._maxFileSize = mb; return this; }
335
286
  /* ═══════════════════════════════════════════════════
336
- * COLLAPSIBLE OPTIONS (fluent API)
337
- * ═══════════════════════════════════════════════════ */
338
- /**
339
- * Enable/disable collapsible mode
340
- */
341
- collapsible(enabled) {
342
- this._collapsible = enabled;
343
- return this;
344
- }
345
- /**
346
- * Set initial collapsed state (true = collapsed, false = expanded)
347
- */
348
- collapsed(value) {
349
- this._collapsed = value;
350
- if (this._detailsElement) {
351
- this._detailsElement.open = !value;
352
- }
353
- return this;
354
- }
355
- /**
356
- * Expand the details (show table)
357
- */
358
- expand() {
359
- this._collapsed = false;
360
- if (this._detailsElement) {
361
- this._detailsElement.open = true;
362
- }
363
- return this;
364
- }
365
- /**
366
- * Collapse the details (hide table)
367
- */
368
- collapse() {
369
- this._collapsed = true;
370
- if (this._detailsElement) {
371
- this._detailsElement.open = false;
372
- }
373
- return this;
374
- }
375
- /**
376
- * Check if collapsible mode is enabled
377
- */
378
- isCollapsible() {
379
- return this._collapsible;
380
- }
381
- /**
382
- * Toggle collapsed state
383
- */
384
- toggle() {
385
- this._collapsed = !this._collapsed;
386
- if (this._detailsElement) {
387
- this._detailsElement.open = !this._collapsed;
388
- }
389
- return this;
390
- }
391
- /**
392
- * Set custom summary template
393
- */
394
- summaryTemplate(fn) {
395
- this._summaryTemplate = fn;
396
- return this;
397
- }
398
- /**
399
- * Get current collapsed state
400
- */
401
- isCollapsed() {
402
- return this._collapsed;
403
- }
404
- /* ═══════════════════════════════════════════════════
405
- * FILE HANDLING (modified to use storage options)
287
+ * FILE HANDLING
406
288
  * ═══════════════════════════════════════════════════ */
407
289
  async _handleFile(file) {
408
290
  const fileSizeMB = file.size / (1024 * 1024);
@@ -431,7 +313,6 @@ export class DataFrameComponent extends BaseComponent {
431
313
  this.state.loading = false;
432
314
  return;
433
315
  }
434
- // ✅ Only store if persistence is enabled
435
316
  if (this._persistToIndexedDB) {
436
317
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
437
318
  }
@@ -449,7 +330,6 @@ export class DataFrameComponent extends BaseComponent {
449
330
  autoDetectDelimiter: true,
450
331
  hasHeader: true
451
332
  });
452
- // ✅ Only store if persistence is enabled
453
333
  if (this._persistToIndexedDB) {
454
334
  await this._driver.store(file.name, df, { source: file.name });
455
335
  }
@@ -481,7 +361,6 @@ export class DataFrameComponent extends BaseComponent {
481
361
  this._sheets.set(name, df);
482
362
  });
483
363
  const sheetNames = Object.keys(sheets);
484
- // Sanitize sheet names for use as DOM IDs
485
364
  const sanitizeId = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
486
365
  const tabDefs = sheetNames.map(name => ({
487
366
  id: sanitizeId(name),
@@ -492,26 +371,28 @@ export class DataFrameComponent extends BaseComponent {
492
371
  tabs: tabDefs,
493
372
  activeTab: sanitizeId(sheetNames[0])
494
373
  });
495
- // Map sanitized IDs back to original sheet names
496
374
  const idToSheetName = new Map();
497
375
  sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
498
376
  this._tabs.bind('tabChange', (tabId) => {
499
377
  const originalName = idToSheetName.get(tabId) || tabId;
500
378
  this._df = this._sheets.get(originalName) || null;
501
379
  });
380
+ const dataContainer = wrapper.querySelector('.jux-dataframe-data');
381
+ if (dataContainer) {
382
+ dataContainer.style.display = '';
383
+ }
502
384
  const tabsContainer = document.createElement('div');
503
385
  tabsContainer.className = 'jux-dataframe-tabs';
504
- wrapper.appendChild(tabsContainer);
386
+ if (dataContainer) {
387
+ dataContainer.appendChild(tabsContainer);
388
+ }
389
+ else {
390
+ wrapper.appendChild(tabsContainer);
391
+ }
505
392
  this._tabs.render(tabsContainer);
506
393
  sheetNames.forEach((sheetName) => {
507
394
  const df = sheets[sheetName];
508
395
  const safeId = sanitizeId(sheetName);
509
- // ✅ DEBUG: Log table options
510
- console.log(`[DataFrame] Creating table for sheet "${sheetName}" with options:`, {
511
- filterable: this._tableOptions.filterable,
512
- paginated: this._tableOptions.paginated,
513
- sortable: this._tableOptions.sortable
514
- });
515
396
  const table = new Table(`${this._id}-table-${safeId}`, {
516
397
  striped: this._tableOptions.striped,
517
398
  hoverable: this._tableOptions.hoverable,
@@ -530,8 +411,7 @@ export class DataFrameComponent extends BaseComponent {
530
411
  settingsBtn.bind('click', () => this._showReshapeModal());
531
412
  this._tabs.addTabContent(safeId, [settingsBtn, table]);
532
413
  });
533
- const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
534
- this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
414
+ this._showDataView();
535
415
  this._df = sheets[sheetNames[0]];
536
416
  this._triggerCallback('load', this._df, null, this);
537
417
  }
@@ -542,7 +422,6 @@ export class DataFrameComponent extends BaseComponent {
542
422
  const el = document.getElementById(`${this._id}-status`);
543
423
  if (!el)
544
424
  return;
545
- // ✅ Only show status during loading/error states
546
425
  if (type === 'loading' || type === 'error') {
547
426
  el.style.display = '';
548
427
  el.className = 'jux-dataframe-status';
@@ -561,7 +440,6 @@ export class DataFrameComponent extends BaseComponent {
561
440
  el.appendChild(span);
562
441
  }
563
442
  else {
564
- // ✅ Hide status after successful load - table speaks for itself
565
443
  el.style.display = 'none';
566
444
  }
567
445
  }
@@ -572,7 +450,6 @@ export class DataFrameComponent extends BaseComponent {
572
450
  this.state.sourceName = sourceName;
573
451
  this.state.rowCount = df.height;
574
452
  this.state.colCount = df.width;
575
- // Only strip __EMPTY columns that are ENTIRELY null/empty
576
453
  const cols = df.columns;
577
454
  const rows = df.toRows();
578
455
  const emptyColumns = cols.filter(c => {
@@ -593,58 +470,37 @@ export class DataFrameComponent extends BaseComponent {
593
470
  const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
594
471
  this._table.columns(columnDefs).rows(this._df.toRows());
595
472
  }
596
- // ✅ Show the table container, hide upload area
597
473
  this._showDataView();
598
- // ✅ Update collapsible summary if enabled
599
- if (this._collapsible && this._detailsElement) {
600
- this._detailsElement.style.display = '';
601
- this._updateSummary();
602
- }
603
- // ✅ Hide status - data is loaded
604
474
  this._updateStatus('', 'success');
605
475
  this._triggerCallback('load', this._df, null, this);
606
476
  }
607
- /**
608
- * Show the data view (table + settings gear), hide upload area
609
- */
610
477
  _showDataView() {
611
478
  const wrapper = document.getElementById(this._id);
612
479
  if (!wrapper)
613
480
  return;
614
- // Hide upload area
615
481
  const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
616
482
  if (uploadArea) {
617
483
  uploadArea.style.display = 'none';
618
484
  }
619
- // Show data container (table + toolbar)
620
485
  const dataContainer = wrapper.querySelector('.jux-dataframe-data');
621
486
  if (dataContainer) {
622
487
  dataContainer.style.display = '';
623
488
  }
624
- // Update settings gear with file info
625
489
  this._updateSettingsGear();
626
490
  }
627
- /**
628
- * Hide data view, show upload area
629
- */
630
491
  _hideDataView() {
631
492
  const wrapper = document.getElementById(this._id);
632
493
  if (!wrapper)
633
494
  return;
634
- // Show upload area
635
495
  const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
636
496
  if (uploadArea) {
637
497
  uploadArea.style.display = '';
638
498
  }
639
- // Hide data container
640
499
  const dataContainer = wrapper.querySelector('.jux-dataframe-data');
641
500
  if (dataContainer) {
642
501
  dataContainer.style.display = 'none';
643
502
  }
644
503
  }
645
- /**
646
- * Update the settings gear tooltip/info
647
- */
648
504
  _updateSettingsGear() {
649
505
  const gear = document.getElementById(`${this._id}-settings-gear`);
650
506
  if (!gear || !this._df)
@@ -660,159 +516,29 @@ export class DataFrameComponent extends BaseComponent {
660
516
  }
661
517
  }
662
518
  /**
663
- * Show the unified settings modal
519
+ * Show the unified settings modal (now delegated to ImportSettingsModal)
664
520
  */
665
521
  _showSettingsModal() {
666
- this._cleanupReshapeModal();
667
- const fileInfo = this._rawFileData?.file;
668
522
  const isMalformed = this._df ? this._detectMalformedData(this._df) : false;
669
- this._settingsModal = new Modal(`${this._id}-settings-modal`, {
670
- title: 'Data Settings',
671
- size: 'medium',
672
- close: true,
673
- backdropClose: true
674
- });
675
- const fileSizeKB = fileInfo ? (fileInfo.size / 1024).toFixed(1) : '0';
676
- const fileName = fileInfo?.name || this.state.sourceName || 'Unknown';
677
- let contentHTML = `
678
- <div class="jux-dataframe-settings-content">
679
- <!-- File Info Section -->
680
- <div class="jux-dataframe-settings-section">
681
- <div class="jux-dataframe-settings-label">Source</div>
682
- <div class="jux-dataframe-settings-value">
683
- <strong>${this._escapeHtml(fileName)}</strong>
684
- ${fileInfo ? `<span class="jux-muted" style="margin-left: 8px;">${fileSizeKB} KB</span>` : ''}
685
- </div>
686
- </div>
687
-
688
- <!-- Data Info Section -->
689
- <div class="jux-dataframe-settings-section">
690
- <div class="jux-dataframe-settings-label">Data</div>
691
- <div class="jux-dataframe-settings-value">
692
- ${this._df ? `${this._df.height} rows × ${this._df.width} columns` : 'No data loaded'}
693
- ${isMalformed ? '<span style="color: hsl(var(--warning)); margin-left: 8px;">⚠️ May need reformatting</span>' : ''}
694
- </div>
695
- </div>
696
- `;
697
- // Import Settings button (only if we have raw file data)
698
- if (this._rawFileData) {
699
- contentHTML += `
700
- <div class="jux-dataframe-settings-section">
701
- <div class="jux-dataframe-settings-label">Import</div>
702
- <div class="jux-dataframe-settings-value">
703
- <button id="${this._id}-adjust-import" class="jux-button jux-button-outline jux-button-sm">
704
- ⚙️ Adjust Header Row / Delimiter
705
- </button>
706
- </div>
707
- </div>
708
- `;
709
- }
710
- contentHTML += `
711
- </div>
712
- `;
713
- this._settingsModal
714
- .content(contentHTML)
715
- .actions([
716
- {
717
- label: 'Remove Data',
718
- variant: 'secondary',
719
- click: async () => {
720
- await this.clear();
721
- this._settingsModal.closeModal();
722
- }
523
+ this._importSettingsModal.showSettings({
524
+ fileName: this._rawFileData?.file?.name || this.state.sourceName || 'Unknown',
525
+ fileSize: this._rawFileData?.file?.size,
526
+ rowCount: this._df?.height ?? 0,
527
+ colCount: this._df?.width ?? 0,
528
+ isMalformed,
529
+ hasRawFileData: !!this._rawFileData
530
+ }, {
531
+ onClear: async () => {
532
+ await this.clear();
723
533
  },
724
- {
725
- label: 'Done',
726
- variant: 'primary',
727
- click: () => this._settingsModal.closeModal()
728
- }
729
- ]);
730
- this._settingsModal.render(document.body);
731
- this._settingsModal.open();
732
- // Wire up import settings button
733
- requestAnimationFrame(() => {
734
- const adjustBtn = document.getElementById(`${this._id}-adjust-import`);
735
- if (adjustBtn) {
736
- adjustBtn.addEventListener('click', () => {
737
- this._settingsModal.closeModal();
738
- this._showReshapeModal();
739
- });
534
+ onAdjustImport: () => {
535
+ this._showReshapeModal();
740
536
  }
741
537
  });
742
538
  }
743
- /**
744
- * Update the collapsible summary text
745
- */
746
- _updateSummary(isMalformed) {
747
- if (!this._collapsible || !this._detailsElement)
748
- return;
749
- const summaryEl = this._detailsElement.querySelector('.jux-dataframe-summary');
750
- if (!summaryEl)
751
- return;
752
- const malformed = isMalformed ?? (this._df ? this._detectMalformedData(this._df) : false);
753
- const summaryTextEl = summaryEl.querySelector('.jux-dataframe-summary-text');
754
- if (summaryTextEl) {
755
- if (!this._df) {
756
- summaryTextEl.textContent = 'No data loaded';
757
- }
758
- else if (this._summaryTemplate) {
759
- summaryTextEl.textContent = this._summaryTemplate(this._df);
760
- }
761
- else {
762
- const suffix = malformed ? ' ⚠️' : '';
763
- summaryTextEl.textContent = `${this.state.sourceName || 'Data'} — ${this._df.height} rows × ${this._df.width} cols${suffix}`;
764
- }
765
- }
766
- // ✅ Add/update settings gear in summary
767
- let gearBtn = summaryEl.querySelector('.jux-dataframe-summary-gear');
768
- if (this._df) {
769
- if (!gearBtn) {
770
- gearBtn = document.createElement('button');
771
- gearBtn.className = 'jux-dataframe-summary-gear';
772
- gearBtn.type = 'button';
773
- gearBtn.innerHTML = '⚙️';
774
- gearBtn.addEventListener('click', (e) => {
775
- e.stopPropagation();
776
- this._showSettingsModal();
777
- });
778
- summaryEl.appendChild(gearBtn);
779
- }
780
- if (malformed) {
781
- gearBtn.classList.add('jux-dataframe-gear-warning');
782
- }
783
- else {
784
- gearBtn.classList.remove('jux-dataframe-gear-warning');
785
- }
786
- }
787
- else if (gearBtn) {
788
- gearBtn.remove();
789
- }
790
- }
791
539
  /* ═══════════════════════════════════════════════════
792
540
  * MALFORMED DATA DETECTION
793
541
  * ═══════════════════════════════════════════════════ */
794
- _appendSettingsButton(label, variant) {
795
- const statusEl = document.getElementById(`${this._id}-status`);
796
- if (!statusEl)
797
- return;
798
- // Remove existing settings button if any
799
- const existing = statusEl.querySelector('.jux-dataframe-settings-btn');
800
- if (existing)
801
- existing.remove();
802
- const btn = document.createElement('button');
803
- btn.className = `jux-dataframe-settings-btn jux-dataframe-settings-btn-${variant}`;
804
- btn.type = 'button';
805
- btn.textContent = `⚙️ ${label}`;
806
- btn.style.marginLeft = '8px';
807
- btn.style.cursor = 'pointer';
808
- btn.style.fontSize = '0.8rem';
809
- btn.style.padding = '2px 8px';
810
- btn.style.borderRadius = 'var(--radius, 4px)';
811
- btn.style.border = '1px solid hsl(var(--border))';
812
- btn.style.background = variant === 'warning' ? 'hsl(38 92% 50% / 0.15)' : 'transparent';
813
- btn.addEventListener('click', () => this._showReshapeModal());
814
- statusEl.appendChild(btn);
815
- }
816
542
  _detectMalformedData(df) {
817
543
  const columns = df.columns;
818
544
  const rows = df.toRows();
@@ -836,367 +562,38 @@ export class DataFrameComponent extends BaseComponent {
836
562
  return false;
837
563
  }
838
564
  /* ═══════════════════════════════════════════════════
839
- * RESHAPE MODAL
565
+ * RESHAPE MODAL (delegated to ImportSettingsModal)
840
566
  * ═══════════════════════════════════════════════════ */
841
567
  _showReshapeModal() {
842
568
  if (!this._rawFileData)
843
569
  return;
844
- if (this._rawFileData.isExcel) {
845
- this._showExcelReshapeModal();
846
- }
847
- else {
848
- this._showCSVReshapeModal();
849
- }
850
- }
851
- _cleanupReshapeModal() {
852
- if (this._reshapeModal && this._reshapeModalRendered) {
853
- const oldEl = document.getElementById(`${this._id}-reshape-modal`);
854
- if (oldEl)
855
- oldEl.remove();
856
- this._reshapeModal = null;
857
- this._reshapeModalRendered = false;
858
- }
859
- }
860
- _escapeHtml(text) {
861
- const div = document.createElement('div');
862
- div.textContent = text;
863
- return div.innerHTML;
864
- }
865
- /**
866
- * Build a clickable preview table from raw row data.
867
- * Each row stores its actual sheet row index via data-sheet-row attribute.
868
- * Returns the table HTML string.
869
- */
870
- _buildClickablePreviewHTML(rawRows, selectedSheetRow) {
871
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
872
- for (const { sheetRow, values } of rawRows) {
873
- const isHeader = (sheetRow === selectedSheetRow);
874
- const isSkipped = (sheetRow < selectedSheetRow);
875
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
876
- if (isHeader) {
877
- rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
878
- }
879
- else if (isSkipped) {
880
- rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
881
- }
882
- html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
883
- // Row index cell
884
- 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;">`;
885
- if (isHeader) {
886
- html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
887
- }
888
- else {
889
- html += `${sheetRow}`;
890
- }
891
- html += '</td>';
892
- // Show first 6 columns
893
- const displayCols = values.slice(0, 6);
894
- displayCols.forEach(val => {
895
- const displayVal = val != null ? String(val).substring(0, 20) : '';
896
- const cellStyle = isHeader
897
- ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
898
- : 'padding: 8px 12px;';
899
- html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
900
- });
901
- if (values.length > 6) {
902
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
903
- }
904
- // Status badge
905
- html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
906
- if (isHeader) {
907
- html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
908
- }
909
- else if (isSkipped) {
910
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
911
- }
912
- else {
913
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
914
- }
915
- html += '</td></tr>';
916
- }
917
- html += '</table>';
918
- return html;
919
- }
920
- async _showExcelReshapeModal() {
921
- if (!this._rawFileData?.file)
922
- return;
923
- this._cleanupReshapeModal();
924
- // ✅ Use the SAME cell-reading method as the parser
925
- const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
926
- if (rawRows.length === 0)
927
- return;
928
- // Log what we got so we can verify alignment
929
- console.log('[DataFrame Preview] Raw rows from readRawExcelRows:');
930
- rawRows.forEach(r => console.log(` sheetRow ${r.sheetRow}:`, r.values.slice(0, 5)));
931
- // Auto-detect best header row
932
- let selectedSheetRow = rawRows[0].sheetRow;
933
- for (const { sheetRow, values } of rawRows) {
934
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
935
- if (nonEmpty.length < values.length * 0.5)
936
- continue;
937
- const nonNumeric = nonEmpty.filter(v => {
938
- const trimmed = v.trim();
939
- return isNaN(Number(trimmed)) && trimmed !== '';
940
- }).length;
941
- if (nonNumeric >= nonEmpty.length * 0.7) {
942
- selectedSheetRow = sheetRow;
943
- break;
944
- }
945
- }
946
- this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
947
- title: 'Excel Import Settings',
948
- size: 'large',
949
- close: true,
950
- backdropClose: false
951
- });
952
- const modalContentHTML = `
953
- <div style="margin-bottom: 1rem;">
954
- <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
955
- <input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
956
- </div>
957
- <div>
958
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
959
- <div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
960
- </div>
961
- `;
962
- this._reshapeModal
963
- .content(modalContentHTML)
964
- .actions([
965
- {
966
- label: 'Cancel',
967
- variant: 'secondary',
968
- click: () => this._reshapeModal.closeModal()
969
- },
970
- {
971
- label: 'Apply & Re-import',
972
- variant: 'primary',
973
- click: async () => {
974
- const input = document.getElementById(`${this._id}-header-row`);
975
- const headerRow = parseInt(input.value) || 0;
976
- console.log(`[DataFrame] Apply clicked: headerRow=${headerRow}`);
977
- this.state.loading = true;
978
- this._updateStatus('Re-parsing with new settings...', 'loading');
979
- try {
980
- const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
981
- headerRow,
982
- maxSheetSize: this._maxSheetSize,
983
- sheetChunkSize: this._sheetChunkSize
984
- });
985
- const sheetNames = Object.keys(sheets);
986
- if (sheetNames.length === 0) {
987
- this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
988
- this.state.loading = false;
989
- return;
990
- }
991
- await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
992
- if (sheetNames.length > 1) {
993
- this._renderMultiSheet(sheets, this._rawFileData.file.name);
994
- }
995
- else {
996
- this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
997
- }
998
- this._reshapeModal.closeModal();
999
- }
1000
- catch (err) {
1001
- this._updateStatus(`Error: ${err.message}`, 'error');
1002
- this.state.loading = false;
1003
- }
1004
- }
1005
- }
1006
- ]);
1007
- this._reshapeModal.render(document.body);
1008
- this._reshapeModalRendered = true;
1009
- await new Promise(resolve => requestAnimationFrame(resolve));
1010
- const previewDiv = document.getElementById(`${this._id}-preview`);
1011
- const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
1012
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
1013
- const updateHint = (row) => {
1014
- if (!hintDiv)
1015
- return;
1016
- const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
1017
- const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
1018
- const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
1019
- if (row > rawRows[0].sheetRow) {
1020
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
1021
- }
1022
- else {
1023
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
1024
- }
1025
- };
1026
- const renderPreview = (selected) => {
1027
- if (!previewDiv)
1028
- return;
1029
- previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
1030
- previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
1031
- tr.addEventListener('click', () => {
1032
- const rowIdx = parseInt(tr.dataset.sheetRow);
1033
- hiddenInput.value = String(rowIdx);
1034
- console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
1035
- updateHint(rowIdx);
1036
- renderPreview(rowIdx);
1037
- });
1038
- });
1039
- };
1040
- updateHint(selectedSheetRow);
1041
- renderPreview(selectedSheetRow);
1042
- this._reshapeModal.open();
1043
- }
1044
- _showCSVReshapeModal() {
1045
- if (!this._rawFileData?.text)
1046
- return;
1047
- this._cleanupReshapeModal();
1048
- const text = this._rawFileData.text;
1049
- const detected = this._driver._detectDelimiter(text);
1050
- // Parse raw lines
1051
- const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
1052
- const rawRows = [];
1053
- const maxPreviewRows = Math.min(lines.length, 15);
1054
- for (let i = 0; i < maxPreviewRows; i++) {
1055
- if (!lines[i]) {
1056
- rawRows.push({ sheetRow: i, values: [''] });
1057
- continue;
1058
- }
1059
- const values = this._driver._parseLine(lines[i], detected);
1060
- rawRows.push({ sheetRow: i, values });
1061
- }
1062
- // Auto-detect header row
1063
- let selectedRow = 0;
1064
- for (const { sheetRow, values } of rawRows) {
1065
- const nonEmpty = values.filter((v) => v.trim() !== '');
1066
- if (nonEmpty.length < values.length * 0.5)
1067
- continue;
1068
- const nonNumeric = nonEmpty.filter((v) => {
1069
- const trimmed = v.trim();
1070
- return isNaN(Number(trimmed)) && trimmed !== '';
1071
- }).length;
1072
- if (nonNumeric >= nonEmpty.length * 0.7) {
1073
- selectedRow = sheetRow;
1074
- break;
1075
- }
1076
- }
1077
- this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
1078
- title: 'CSV Import Settings',
1079
- size: 'large',
1080
- close: true,
1081
- backdropClose: false
1082
- });
1083
- const modalContentHTML = `
1084
- <div style="margin-bottom: 1rem;">
1085
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
1086
- <select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
1087
- <option value=",">Comma (,)</option>
1088
- <option value="|">Pipe (|)</option>
1089
- <option value="&#9;">Tab (\\t)</option>
1090
- <option value=";">Semicolon (;)</option>
1091
- </select>
1092
- </div>
1093
- <div style="margin-bottom: 1rem;">
1094
- <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
1095
- <input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
1096
- </div>
1097
- <div>
1098
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
1099
- <div id="${this._id}-preview" style="font-family: monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
1100
- </div>
1101
- `;
1102
- this._reshapeModal
1103
- .content(modalContentHTML)
1104
- .actions([
1105
- {
1106
- label: 'Cancel',
1107
- variant: 'secondary',
1108
- click: () => this._reshapeModal.closeModal()
1109
- },
1110
- {
1111
- label: 'Apply & Re-import',
1112
- variant: 'primary',
1113
- click: async () => {
1114
- const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
1115
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
1116
- const delim = delimiterSelect.value;
1117
- const headerRow = parseInt(hiddenInput.value) || 0;
1118
- this.state.loading = true;
1119
- this._updateStatus('Re-parsing with new settings...', 'loading');
1120
- try {
1121
- const df = this._driver.parseCSV(this._rawFileData.text, {
1122
- delimiter: delim,
1123
- headerRow,
1124
- hasHeader: true
1125
- });
1126
- await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
1127
- this._setDataFrame(df, this._rawFileData.file.name);
1128
- this._reshapeModal.closeModal();
1129
- }
1130
- catch (err) {
1131
- this._updateStatus(`Error: ${err.message}`, 'error');
1132
- this.state.loading = false;
1133
- }
1134
- }
1135
- }
1136
- ]);
1137
- this._reshapeModal.render(document.body);
1138
- this._reshapeModalRendered = true;
1139
- requestAnimationFrame(() => {
1140
- const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
1141
- const previewDiv = document.getElementById(`${this._id}-preview`);
1142
- const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
1143
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
1144
- if (delimiterSelect)
1145
- delimiterSelect.value = detected;
1146
- const updateHint = (row) => {
1147
- if (!hintDiv)
1148
- return;
1149
- const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
1150
- const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
1151
- const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
1152
- if (row > rawRows[0].sheetRow) {
1153
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
570
+ this._importSettingsModal.showImportSettings(this._rawFileData, {
571
+ onReimport: (result, sourceName) => {
572
+ if (result instanceof DataFrame) {
573
+ this._setDataFrame(result, sourceName);
1154
574
  }
1155
575
  else {
1156
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
1157
- }
1158
- };
1159
- const reparse = () => {
1160
- const delim = delimiterSelect?.value || ',';
1161
- rawRows.length = 0;
1162
- for (let i = 0; i < maxPreviewRows; i++) {
1163
- if (!lines[i]) {
1164
- rawRows.push({ sheetRow: i, values: [''] });
1165
- continue;
576
+ const sheetNames = Object.keys(result);
577
+ if (sheetNames.length > 1) {
578
+ this._renderMultiSheet(result, sourceName);
579
+ }
580
+ else {
581
+ this._setDataFrame(result[sheetNames[0]], sourceName);
1166
582
  }
1167
- const values = this._driver._parseLine(lines[i], delim);
1168
- rawRows.push({ sheetRow: i, values });
1169
583
  }
1170
- };
1171
- const renderPreview = (selected) => {
1172
- if (!previewDiv)
1173
- return;
1174
- previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
1175
- previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
1176
- tr.addEventListener('click', () => {
1177
- const rowIdx = parseInt(tr.dataset.sheetRow);
1178
- hiddenInput.value = String(rowIdx);
1179
- console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
1180
- updateHint(rowIdx);
1181
- renderPreview(rowIdx);
1182
- });
1183
- });
1184
- };
1185
- if (delimiterSelect) {
1186
- delimiterSelect.addEventListener('change', () => {
1187
- reparse();
1188
- const current = parseInt(hiddenInput.value) || 0;
1189
- updateHint(current);
1190
- renderPreview(current);
1191
- });
584
+ },
585
+ onError: (message) => {
586
+ this._updateStatus(message, 'error');
587
+ this.state.loading = false;
588
+ },
589
+ onStatusUpdate: (text, type) => {
590
+ this.state.loading = type === 'loading';
591
+ this._updateStatus(text, type);
1192
592
  }
1193
- updateHint(selectedRow);
1194
- renderPreview(selectedRow);
1195
- this._reshapeModal.open();
1196
593
  });
1197
594
  }
1198
595
  /* ═══════════════════════════════════════════════════
1199
- * UPDATE & RENDER (modified for better upload styling)
596
+ * UPDATE & RENDER
1200
597
  * ═══════════════════════════════════════════════════ */
1201
598
  update(_prop, _value) { }
1202
599
  render(targetId) {
@@ -1209,9 +606,7 @@ export class DataFrameComponent extends BaseComponent {
1209
606
  wrapper.className += ` ${className}`;
1210
607
  if (style)
1211
608
  wrapper.setAttribute('style', style);
1212
- // ═══════════════════════════════════════════════════
1213
- // UPLOAD AREA (shown initially, hidden after data loads)
1214
- // ═══════════════════════════════════════════════════
609
+ // Upload area
1215
610
  if (this._inlineUpload) {
1216
611
  const uploadArea = document.createElement('div');
1217
612
  uploadArea.className = 'jux-dataframe-upload-area';
@@ -1239,14 +634,11 @@ export class DataFrameComponent extends BaseComponent {
1239
634
  uploadContainer.className = 'jux-dataframe-upload';
1240
635
  uploadContainer.id = `${this._id}-upload-container`;
1241
636
  uploadArea.appendChild(uploadContainer);
1242
- // Status bar (for loading/error states only)
1243
- if (this._showStatus) {
1244
- const statusBar = document.createElement('div');
1245
- statusBar.className = 'jux-dataframe-status';
1246
- statusBar.id = `${this._id}-status`;
1247
- statusBar.style.display = 'none';
1248
- uploadArea.appendChild(statusBar);
1249
- }
637
+ const statusBar = document.createElement('div');
638
+ statusBar.className = 'jux-dataframe-status';
639
+ statusBar.id = `${this._id}-status`;
640
+ statusBar.style.display = 'none';
641
+ uploadArea.appendChild(statusBar);
1250
642
  if (this._uploadDescription) {
1251
643
  const descEl = document.createElement('div');
1252
644
  descEl.className = 'jux-dataframe-upload-description';
@@ -1260,13 +652,10 @@ export class DataFrameComponent extends BaseComponent {
1260
652
  else {
1261
653
  container.appendChild(wrapper);
1262
654
  }
1263
- // ═══════════════════════════════════════════════════
1264
- // DATA CONTAINER (hidden initially, shown after data loads)
1265
- // ═══════════════════════════════════════════════════
655
+ // Data container
1266
656
  const dataContainer = document.createElement('div');
1267
657
  dataContainer.className = 'jux-dataframe-data';
1268
- dataContainer.style.display = 'none'; // Hidden until data loads
1269
- // ✅ Toolbar with settings gear
658
+ dataContainer.style.display = 'none';
1270
659
  const toolbar = document.createElement('div');
1271
660
  toolbar.className = 'jux-dataframe-toolbar';
1272
661
  const settingsGear = document.createElement('button');
@@ -1278,42 +667,8 @@ export class DataFrameComponent extends BaseComponent {
1278
667
  settingsGear.addEventListener('click', () => this._showSettingsModal());
1279
668
  toolbar.appendChild(settingsGear);
1280
669
  dataContainer.appendChild(toolbar);
1281
- // ═══════════════════════════════════════════════════
1282
- // TABLE CONTAINER (inside data container or collapsible)
1283
- // ═══════════════════════════════════════════════════
1284
- let tableContainer;
1285
- if (this._collapsible) {
1286
- const details = document.createElement('details');
1287
- details.className = 'jux-dataframe-details';
1288
- details.open = !this._collapsed;
1289
- details.style.display = 'none'; // Hidden until data loads
1290
- this._detailsElement = details;
1291
- const summary = document.createElement('summary');
1292
- summary.className = 'jux-dataframe-summary';
1293
- const chevron = document.createElement('span');
1294
- chevron.className = 'jux-dataframe-summary-chevron';
1295
- chevron.innerHTML = '▶';
1296
- summary.appendChild(chevron);
1297
- const textSpan = document.createElement('span');
1298
- textSpan.className = 'jux-dataframe-summary-text';
1299
- summary.appendChild(textSpan);
1300
- details.appendChild(summary);
1301
- const content = document.createElement('div');
1302
- content.className = 'jux-dataframe-details-content';
1303
- details.appendChild(content);
1304
- dataContainer.appendChild(details);
1305
- tableContainer = content;
1306
- details.addEventListener('toggle', () => {
1307
- this._collapsed = !details.open;
1308
- });
1309
- }
1310
- else {
1311
- tableContainer = dataContainer;
1312
- }
1313
670
  wrapper.appendChild(dataContainer);
1314
- // ═══════════════════════════════════════════════════
1315
- // TABLE
1316
- // ═══════════════════════════════════════════════════
671
+ // Table
1317
672
  const tbl = new Table(`${this._id}-table`, {
1318
673
  striped: this._tableOptions.striped,
1319
674
  hoverable: this._tableOptions.hoverable,
@@ -1322,7 +677,7 @@ export class DataFrameComponent extends BaseComponent {
1322
677
  paginated: this._tableOptions.paginated,
1323
678
  rowsPerPage: this._tableOptions.rowsPerPage
1324
679
  });
1325
- tbl.render(tableContainer);
680
+ tbl.render(dataContainer);
1326
681
  this._table = tbl;
1327
682
  if (this._pendingSource) {
1328
683
  const fn = this._pendingSource;