juxscript 1.1.228 → 1.1.231

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.
@@ -1,12 +1,11 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
2
  import { DataFrame } from '../storage/DataFrame.js';
3
- import { TabularDriver } from '../storage/TabularDriver.js';
4
3
  import { FileUpload } from './fileupload.js';
5
4
  import { Table } from './table.js';
6
5
  import { Tabs } from './tabs.js';
7
- import { Modal } from './modal.js';
8
6
  import { Button } from './button.js';
9
- import { renderIcon } from './icons.js';
7
+ import { dataFrameSource } from './dataframe/DataFrameSource.js';
8
+ import { ImportSettingsModal } from './dataframe/ImportSettingsModal.js';
10
9
  const TRIGGER_EVENTS = [];
11
10
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
12
11
  export class DataFrameComponent extends BaseComponent {
@@ -22,35 +21,35 @@ export class DataFrameComponent extends BaseComponent {
22
21
  rowCount: 0,
23
22
  colCount: 0
24
23
  });
25
- this._df = null;
26
24
  this._table = null;
27
25
  this._tabs = null;
28
- this._sheets = new Map();
29
26
  this._uploadRef = null;
30
- this._storageKey = null;
31
- this._pendingSource = null;
32
27
  this._inlineUpload = null;
33
- this._showStatus = true;
34
- this._icon = '';
35
- this._maxSheetSize = 100000;
36
- this._sheetChunkSize = 10000;
37
- this._maxFileSize = 50;
38
- this._showReshapeWarning = true;
39
- this._rawFileData = null;
40
- this._reshapeModal = null;
41
- this._reshapeModalRendered = false;
42
- this._persistToIndexedDB = false;
43
- this._clearStorageOnFileRemove = true;
44
28
  this._uploadButtonLabel = 'Upload File';
45
29
  this._uploadButtonIcon = 'upload';
46
30
  this._uploadButtonVariant = 'outline';
47
31
  this._uploadAccept = '.csv,.tsv,.txt,.xlsx,.xls';
48
32
  this._uploadDescription = '';
49
33
  this._showUploadIcon = true;
50
- this._settingsModal = null;
51
- this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
52
- this._showStatus = options.showStatus ?? true;
53
- this._icon = options.icon ?? '';
34
+ this._pendingSource = null;
35
+ // Initialize data source
36
+ this._source = dataFrameSource({
37
+ maxFileSize: options.maxFileSize ?? 50,
38
+ maxSheetSize: options.maxSheetSize ?? 100000,
39
+ sheetChunkSize: options.sheetChunkSize ?? 10000,
40
+ persistToIndexedDB: options.persistToIndexedDB ?? false,
41
+ dbName: options.dbName ?? 'jux-dataframes',
42
+ storeName: options.storeName ?? 'frames'
43
+ });
44
+ // Wire up source callbacks
45
+ this._source.onLoad((result) => this._handleLoadResult(result));
46
+ this._source.onError((message) => this._handleError(message));
47
+ this._source.onProgress((loaded, total) => this._handleProgress(loaded, total));
48
+ // Initialize import settings modal
49
+ this._importModal = new ImportSettingsModal(id, this._source.driver, {
50
+ maxSheetSize: options.maxSheetSize ?? 100000,
51
+ sheetChunkSize: options.sheetChunkSize ?? 10000
52
+ });
54
53
  this._tableOptions = {
55
54
  striped: options.striped ?? true,
56
55
  hoverable: options.hoverable ?? true,
@@ -59,38 +58,20 @@ export class DataFrameComponent extends BaseComponent {
59
58
  paginated: options.paginated ?? true,
60
59
  rowsPerPage: options.rowsPerPage ?? 25
61
60
  };
62
- this._maxSheetSize = options.maxSheetSize ?? 100000;
63
- this._sheetChunkSize = options.sheetChunkSize ?? 10000;
64
- this._maxFileSize = options.maxFileSize ?? 50;
65
- this._showReshapeWarning = options.showReshapeWarning ?? true;
66
- this._persistToIndexedDB = options.persistToIndexedDB ?? false;
67
- this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
68
61
  }
69
62
  getTriggerEvents() { return TRIGGER_EVENTS; }
70
63
  getCallbackEvents() { return CALLBACK_EVENTS; }
71
64
  /* ═══════════════════════════════════════════════════
72
- * DATA SOURCES
65
+ * DATA SOURCE METHODS (Delegate to DataFrameSource)
73
66
  * ═══════════════════════════════════════════════════ */
67
+ /**
68
+ * Load from IndexedDB storage
69
+ */
74
70
  fromStorage(key) {
75
- this._storageKey = key;
76
71
  const loadFn = async () => {
77
72
  this.state.loading = true;
78
73
  this._updateStatus('Loading...', 'loading');
79
- try {
80
- const df = await this._driver.loadByName(key);
81
- if (!df) {
82
- this._triggerCallback('error', 'No table found with key: ' + key, null, this);
83
- this.state.loading = false;
84
- this._updateStatus('No table found: ' + key, 'empty');
85
- return;
86
- }
87
- this._setDataFrame(df, 'storage: ' + key);
88
- }
89
- catch (err) {
90
- this._triggerCallback('error', err.message, null, this);
91
- this.state.loading = false;
92
- this._updateStatus(err.message, 'error');
93
- }
74
+ await this._source.fromStorage(key);
94
75
  };
95
76
  if (this._table) {
96
77
  loadFn();
@@ -100,30 +81,24 @@ export class DataFrameComponent extends BaseComponent {
100
81
  }
101
82
  return this;
102
83
  }
84
+ /**
85
+ * Load from a FileUpload component
86
+ */
103
87
  fromUpload(upload) {
104
88
  this._uploadRef = upload;
105
89
  this._pendingSource = async () => {
106
- upload.bind('change', async (files) => {
107
- if (!files || files.length === 0)
108
- return;
109
- await this._handleFile(files[0]);
110
- });
90
+ this._source.fromUpload(upload);
111
91
  };
112
92
  return this;
113
93
  }
94
+ /**
95
+ * Load from inline data
96
+ */
114
97
  fromData(data) {
115
98
  const loadFn = async () => {
116
99
  this.state.loading = true;
117
100
  this._updateStatus('Loading data...', 'loading');
118
- try {
119
- const df = new DataFrame(data);
120
- this._setDataFrame(df, 'inline data');
121
- }
122
- catch (err) {
123
- this._triggerCallback('error', err.message, null, this);
124
- this.state.loading = false;
125
- this._updateStatus(err.message, 'error');
126
- }
101
+ this._source.fromData(data);
127
102
  };
128
103
  if (this._table) {
129
104
  loadFn();
@@ -133,6 +108,26 @@ export class DataFrameComponent extends BaseComponent {
133
108
  }
134
109
  return this;
135
110
  }
111
+ /**
112
+ * Load from a URL
113
+ */
114
+ fromUrl(url) {
115
+ const loadFn = async () => {
116
+ this.state.loading = true;
117
+ this._updateStatus('Fetching...', 'loading');
118
+ await this._source.fromUrl(url);
119
+ };
120
+ if (this._table) {
121
+ loadFn();
122
+ }
123
+ else {
124
+ this._pendingSource = loadFn;
125
+ }
126
+ return this;
127
+ }
128
+ /**
129
+ * Enable inline file upload UI
130
+ */
136
131
  withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
137
132
  this._inlineUpload = { label, accept, icon };
138
133
  this._uploadButtonLabel = label;
@@ -140,99 +135,47 @@ export class DataFrameComponent extends BaseComponent {
140
135
  this._uploadButtonIcon = icon;
141
136
  return this;
142
137
  }
143
- uploadLabel(label) {
144
- this._uploadButtonLabel = label;
145
- if (this._inlineUpload)
146
- this._inlineUpload.label = label;
147
- return this;
148
- }
149
- uploadIcon(icon) {
150
- this._uploadButtonIcon = icon;
151
- this._showUploadIcon = !!icon;
152
- if (this._inlineUpload)
153
- this._inlineUpload.icon = icon;
154
- return this;
155
- }
156
- uploadVariant(variant) {
157
- this._uploadButtonVariant = variant;
158
- return this;
159
- }
160
- uploadAccept(accept) {
161
- this._uploadAccept = accept;
162
- if (this._inlineUpload)
163
- this._inlineUpload.accept = accept;
164
- return this;
165
- }
166
- uploadDescription(description) {
167
- this._uploadDescription = description;
168
- return this;
169
- }
170
- showUploadIcon(show) {
171
- this._showUploadIcon = show;
172
- return this;
173
- }
174
138
  /* ═══════════════════════════════════════════════════
175
- * STORAGE OPTIONS
139
+ * UPLOAD UI CONFIGURATION
176
140
  * ═══════════════════════════════════════════════════ */
177
- persistToIndexedDB(enabled) {
178
- this._persistToIndexedDB = enabled;
179
- return this;
180
- }
181
- clearStorageOnFileRemove(enabled) {
182
- this._clearStorageOnFileRemove = enabled;
183
- return this;
184
- }
141
+ uploadLabel(label) { this._uploadButtonLabel = label; return this; }
142
+ uploadIcon(icon) { this._uploadButtonIcon = icon; this._showUploadIcon = !!icon; return this; }
143
+ uploadVariant(variant) { this._uploadButtonVariant = variant; return this; }
144
+ uploadAccept(accept) { this._uploadAccept = accept; return this; }
145
+ uploadDescription(description) { this._uploadDescription = description; return this; }
146
+ showUploadIcon(show) { this._showUploadIcon = show; return this; }
185
147
  /* ═══════════════════════════════════════════════════
186
- * CLEAR / RESET
148
+ * TABLE OPTIONS
187
149
  * ═══════════════════════════════════════════════════ */
188
- async clear() {
189
- if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
190
- try {
191
- const tables = await this._driver.list();
192
- const matchingTables = tables.filter(t => t.name === this._rawFileData.file.name);
193
- for (const table of matchingTables) {
194
- await this._driver.delete(table.id);
195
- }
196
- }
197
- catch (err) {
198
- console.warn('[DataFrame] Failed to clear storage:', err);
199
- }
200
- }
201
- this._df = null;
202
- this._rawFileData = null;
203
- this._sheets.clear();
204
- this.state.loaded = false;
205
- this.state.sourceName = '';
206
- this.state.rowCount = 0;
207
- this.state.colCount = 0;
208
- if (this._table) {
209
- this._table.columns([]).rows([]);
210
- }
211
- const wrapper = document.getElementById(this._id);
212
- if (wrapper) {
213
- const existingTabs = wrapper.querySelector('.jux-tabs');
214
- if (existingTabs)
215
- existingTabs.remove();
216
- }
217
- this._hideDataView();
218
- if (this._uploadRef) {
219
- this._uploadRef.clear();
220
- }
221
- return this;
222
- }
150
+ striped(v) { this._tableOptions.striped = v; return this; }
151
+ hoverable(v) { this._tableOptions.hoverable = v; return this; }
152
+ sortable(v) { this._tableOptions.sortable = v; return this; }
153
+ filterable(v) { this._tableOptions.filterable = v; return this; }
154
+ paginated(v) { this._tableOptions.paginated = v; return this; }
155
+ rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
223
156
  /* ═══════════════════════════════════════════════════
224
- * UI TOGGLES
157
+ * ACCESSORS (Delegate to DataFrameSource)
225
158
  * ═══════════════════════════════════════════════════ */
226
- showStatus(v) { this._showStatus = v; return this; }
227
- statusIcon(v) { this._icon = v; return this; }
159
+ get df() { return this._source.df; }
160
+ get source() { return this._source; }
161
+ get table() { return this._table; }
162
+ get shape() { return this._source.shape; }
163
+ get columns() { return this._source.columns; }
164
+ toRows() { return this._source.df?.toRows() ?? []; }
165
+ toCSV(delimiter) { return this._source.df?.toCSV(delimiter) ?? ''; }
166
+ describe() { return this._source.df?.describe() ?? null; }
167
+ async save(key) {
168
+ return this._source.save(key);
169
+ }
228
170
  /* ═══════════════════════════════════════════════════
229
- * TRANSFORM API
171
+ * TRANSFORM API (Operate on underlying DataFrame)
230
172
  * ═══════════════════════════════════════════════════ */
231
173
  apply(fn) {
232
- if (!this._df)
174
+ const df = this._source.df;
175
+ if (!df)
233
176
  return this;
234
- const result = fn(this._df);
235
- this._setDataFrame(result, this.state.sourceName + ' (transformed)');
177
+ const result = fn(df);
178
+ this._source.fromData(result.toRows(), this.state.sourceName + ' (transformed)');
236
179
  this._triggerCallback('transform', result, null, this);
237
180
  return this;
238
181
  }
@@ -251,118 +194,69 @@ export class DataFrameComponent extends BaseComponent {
251
194
  tail(n = 5) {
252
195
  return this.apply(df => df.tail(n));
253
196
  }
254
- withColumn(name, fn) {
255
- return this.apply(df => df.withColumn(name, fn));
256
- }
257
- where(col, op, value) {
258
- return this.apply(df => df.where(col, op, value));
259
- }
260
197
  /* ═══════════════════════════════════════════════════
261
- * ACCESSORS
198
+ * CLEAR / RESET
262
199
  * ═══════════════════════════════════════════════════ */
263
- get df() { return this._df; }
264
- get driver() { return this._driver; }
265
- get table() { return this._table; }
266
- describe() { return this._df?.describe() ?? null; }
267
- toCSV(delimiter) { return this._df?.toCSV(delimiter) ?? ''; }
268
- toRows() { return this._df?.toRows() ?? []; }
269
- get shape() { return this._df?.shape ?? [0, 0]; }
270
- get columns() { return this._df?.columns ?? []; }
271
- async save(key) {
272
- if (!this._df)
273
- return null;
274
- const name = key ?? this._storageKey ?? this._id;
275
- return this._driver.store(name, this._df);
200
+ async clear() {
201
+ await this._source.clearStorage();
202
+ this._source.clear();
203
+ this.state.loaded = false;
204
+ this.state.sourceName = '';
205
+ this.state.rowCount = 0;
206
+ this.state.colCount = 0;
207
+ if (this._table) {
208
+ this._table.columns([]).rows([]);
209
+ }
210
+ this._hideDataView();
211
+ this._uploadRef?.clear();
212
+ return this;
276
213
  }
277
214
  /* ═══════════════════════════════════════════════════
278
- * TABLE OPTIONS
279
- * ═══════════════════════════════════════════════════ */
280
- striped(v) { this._tableOptions.striped = v; return this; }
281
- hoverable(v) { this._tableOptions.hoverable = v; return this; }
282
- sortable(v) { this._tableOptions.sortable = v; return this; }
283
- filterable(v) { this._tableOptions.filterable = v; return this; }
284
- paginated(v) { this._tableOptions.paginated = v; return this; }
285
- rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
286
- maxSheetSize(v) { this._maxSheetSize = v; return this; }
287
- sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
288
- maxFileSize(mb) { this._maxFileSize = mb; return this; }
289
- /* ═══════════════════════════════════════════════════
290
- * FILE HANDLING
215
+ * INTERNAL HANDLERS
291
216
  * ═══════════════════════════════════════════════════ */
292
- async _handleFile(file) {
293
- const fileSizeMB = file.size / (1024 * 1024);
294
- if (fileSizeMB > this._maxFileSize) {
295
- this._updateStatus(`File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
217
+ _handleLoadResult(result) {
218
+ if (result.error) {
219
+ this._handleError(result.error);
296
220
  return;
297
221
  }
298
- this.state.loading = true;
299
- this._updateStatus('Parsing ' + file.name + '...', 'loading');
300
- try {
301
- const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
302
- file.name.toLowerCase().endsWith('.xls');
303
- if (isExcel) {
304
- this._rawFileData = { file, isExcel: true };
305
- const sheets = await this._driver.streamFileMultiSheet(file, {
306
- maxSheetSize: this._maxSheetSize,
307
- sheetChunkSize: this._sheetChunkSize,
308
- onProgress: (loaded, total) => {
309
- const pct = total ? Math.round((loaded / total) * 100) : 0;
310
- this._updateStatus(`Parsing ${file.name}... ${pct}%`, 'loading');
311
- }
312
- });
313
- const sheetNames = Object.keys(sheets);
314
- if (sheetNames.length === 0) {
315
- this._updateStatus('No data found in file', 'error');
316
- this.state.loading = false;
317
- return;
318
- }
319
- if (this._persistToIndexedDB) {
320
- await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
321
- }
322
- if (sheetNames.length > 1) {
323
- this._renderMultiSheet(sheets, file.name);
324
- }
325
- else {
326
- this._setDataFrame(sheets[sheetNames[0]], file.name);
327
- }
328
- }
329
- else {
330
- const text = await file.text();
331
- this._rawFileData = { file, text, isExcel: false };
332
- const df = this._driver.parseCSV(text, {
333
- autoDetectDelimiter: true,
334
- hasHeader: true
335
- });
336
- if (this._persistToIndexedDB) {
337
- await this._driver.store(file.name, df, { source: file.name });
338
- }
339
- this._setDataFrame(df, file.name);
340
- }
222
+ this.state.loading = false;
223
+ this.state.loaded = true;
224
+ this.state.sourceName = result.sourceName;
225
+ this.state.rowCount = result.df?.height ?? 0;
226
+ this.state.colCount = result.df?.width ?? 0;
227
+ if (result.isMultiSheet && result.sheets) {
228
+ this._renderMultiSheet(result.sheets, result.sourceName);
341
229
  }
342
- catch (err) {
343
- this._triggerCallback('error', err.message, null, this);
344
- this.state.loading = false;
345
- this._updateStatus('Error: ' + err.message, 'error');
230
+ else if (result.df) {
231
+ this._renderSingleSheet(result.df);
346
232
  }
233
+ this._updateStatus('', 'success');
234
+ this._triggerCallback('load', result.df, null, this);
347
235
  }
348
- /* ═══════════════════════════════════════════════════
349
- * MULTI-SHEET RENDERING
350
- * ═══════════════════════════════════════════════════ */
351
- _renderMultiSheet(sheets, sourceName) {
236
+ _handleError(message) {
352
237
  this.state.loading = false;
353
- this._sheets.clear();
238
+ this._updateStatus(message, 'error');
239
+ this._triggerCallback('error', message, null, this);
240
+ }
241
+ _handleProgress(loaded, total) {
242
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
243
+ this._updateStatus(`Loading... ${pct}%`, 'loading');
244
+ }
245
+ _renderSingleSheet(df) {
246
+ if (this._table) {
247
+ const columnDefs = df.columns.map(col => ({ key: col, label: col }));
248
+ this._table.columns(columnDefs).rows(df.toRows());
249
+ }
250
+ this._showDataView();
251
+ }
252
+ _renderMultiSheet(sheets, sourceName) {
354
253
  const wrapper = document.getElementById(this._id);
355
254
  if (!wrapper)
356
255
  return;
357
- const existingTable = wrapper.querySelector('.jux-table-wrapper');
358
- if (existingTable)
359
- existingTable.remove();
256
+ // Clear existing tabs
360
257
  const existingTabs = wrapper.querySelector('.jux-tabs');
361
258
  if (existingTabs)
362
259
  existingTabs.remove();
363
- Object.entries(sheets).forEach(([name, df]) => {
364
- this._sheets.set(name, df);
365
- });
366
260
  const sheetNames = Object.keys(sheets);
367
261
  const sanitizeId = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
368
262
  const tabDefs = sheetNames.map(name => ({
@@ -374,36 +268,18 @@ export class DataFrameComponent extends BaseComponent {
374
268
  tabs: tabDefs,
375
269
  activeTab: sanitizeId(sheetNames[0])
376
270
  });
377
- const idToSheetName = new Map();
378
- sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
379
- this._tabs.bind('tabChange', (tabId) => {
380
- const originalName = idToSheetName.get(tabId) || tabId;
381
- this._df = this._sheets.get(originalName) || null;
382
- });
383
271
  const dataContainer = wrapper.querySelector('.jux-dataframe-data');
384
272
  if (dataContainer) {
385
273
  dataContainer.style.display = '';
386
- }
387
- const tabsContainer = document.createElement('div');
388
- tabsContainer.className = 'jux-dataframe-tabs';
389
- if (dataContainer) {
274
+ const tabsContainer = document.createElement('div');
275
+ tabsContainer.className = 'jux-dataframe-tabs';
390
276
  dataContainer.appendChild(tabsContainer);
277
+ this._tabs.render(tabsContainer);
391
278
  }
392
- else {
393
- wrapper.appendChild(tabsContainer);
394
- }
395
- this._tabs.render(tabsContainer);
396
- sheetNames.forEach((sheetName) => {
279
+ sheetNames.forEach(sheetName => {
397
280
  const df = sheets[sheetName];
398
281
  const safeId = sanitizeId(sheetName);
399
- const table = new Table(`${this._id}-table-${safeId}`, {
400
- striped: this._tableOptions.striped,
401
- hoverable: this._tableOptions.hoverable,
402
- sortable: this._tableOptions.sortable,
403
- filterable: this._tableOptions.filterable,
404
- paginated: this._tableOptions.paginated,
405
- rowsPerPage: this._tableOptions.rowsPerPage
406
- });
282
+ const table = new Table(`${this._id}-table-${safeId}`, this._tableOptions);
407
283
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
408
284
  table.columns(columnDefs).rows(df.toRows());
409
285
  const settingsBtn = new Button(`${this._id}-settings-${safeId}`, {
@@ -411,86 +287,37 @@ export class DataFrameComponent extends BaseComponent {
411
287
  variant: 'ghost',
412
288
  size: 'small'
413
289
  });
414
- settingsBtn.bind('click', () => this._showReshapeModal());
290
+ settingsBtn.bind('click', () => this._showImportSettings());
415
291
  this._tabs.addTabContent(safeId, [settingsBtn, table]);
416
292
  });
417
- const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
418
- this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
419
293
  this._showDataView();
420
- this._df = sheets[sheetNames[0]];
421
- this._triggerCallback('load', this._df, null, this);
422
294
  }
423
295
  /* ═══════════════════════════════════════════════════
424
- * INTERNAL
296
+ * UI HELPERS
425
297
  * ═══════════════════════════════════════════════════ */
426
- _updateStatus(text, type = 'empty') {
298
+ _updateStatus(text, type) {
427
299
  const el = document.getElementById(`${this._id}-status`);
428
300
  if (!el)
429
301
  return;
430
302
  if (type === 'loading' || type === 'error') {
431
303
  el.style.display = '';
432
- el.className = 'jux-dataframe-status';
433
- el.classList.add(`jux-dataframe-status-${type}`);
434
- el.innerHTML = '';
435
- if (this._icon && type === 'error') {
436
- const iconEl = renderIcon(this._icon);
437
- iconEl.style.width = '16px';
438
- iconEl.style.height = '16px';
439
- iconEl.style.marginRight = '6px';
440
- iconEl.style.verticalAlign = 'middle';
441
- el.appendChild(iconEl);
442
- }
443
- const span = document.createElement('span');
444
- span.textContent = text;
445
- el.appendChild(span);
304
+ el.className = `jux-dataframe-status jux-dataframe-status-${type}`;
305
+ el.textContent = text;
446
306
  }
447
307
  else {
448
308
  el.style.display = 'none';
449
309
  }
450
310
  }
451
- _setDataFrame(df, sourceName) {
452
- this._df = df;
453
- this.state.loaded = true;
454
- this.state.loading = false;
455
- this.state.sourceName = sourceName;
456
- this.state.rowCount = df.height;
457
- this.state.colCount = df.width;
458
- const cols = df.columns;
459
- const rows = df.toRows();
460
- const emptyColumns = cols.filter(c => {
461
- if (!c.startsWith('__EMPTY'))
462
- return false;
463
- return rows.every(row => {
464
- const val = row[c];
465
- return val === null || val === undefined || String(val).trim() === '';
466
- });
467
- });
468
- if (emptyColumns.length > 0) {
469
- const keepCols = cols.filter(c => !emptyColumns.includes(c));
470
- if (keepCols.length > 0) {
471
- this._df = df.select(...keepCols);
472
- }
473
- }
474
- if (this._table && this._df) {
475
- const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
476
- this._table.columns(columnDefs).rows(this._df.toRows());
477
- }
478
- this._showDataView();
479
- this._updateStatus('', 'success');
480
- this._triggerCallback('load', this._df, null, this);
481
- }
482
311
  _showDataView() {
483
312
  const wrapper = document.getElementById(this._id);
484
313
  if (!wrapper)
485
314
  return;
486
315
  const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
487
- if (uploadArea) {
316
+ if (uploadArea)
488
317
  uploadArea.style.display = 'none';
489
- }
490
318
  const dataContainer = wrapper.querySelector('.jux-dataframe-data');
491
- if (dataContainer) {
319
+ if (dataContainer)
492
320
  dataContainer.style.display = '';
493
- }
494
321
  this._updateSettingsGear();
495
322
  }
496
323
  _hideDataView() {
@@ -498,468 +325,61 @@ export class DataFrameComponent extends BaseComponent {
498
325
  if (!wrapper)
499
326
  return;
500
327
  const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
501
- if (uploadArea) {
328
+ if (uploadArea)
502
329
  uploadArea.style.display = '';
503
- }
504
330
  const dataContainer = wrapper.querySelector('.jux-dataframe-data');
505
- if (dataContainer) {
331
+ if (dataContainer)
506
332
  dataContainer.style.display = 'none';
507
- }
508
333
  }
509
334
  _updateSettingsGear() {
510
335
  const gear = document.getElementById(`${this._id}-settings-gear`);
511
- if (!gear || !this._df)
336
+ if (!gear || !this._source.df)
512
337
  return;
513
- const isMalformed = this._detectMalformedData(this._df);
338
+ const isMalformed = this._source.detectMalformed();
514
339
  if (isMalformed) {
515
340
  gear.classList.add('jux-dataframe-gear-warning');
516
341
  gear.title = `${this.state.sourceName} — May need reformatting`;
517
342
  }
518
343
  else {
519
344
  gear.classList.remove('jux-dataframe-gear-warning');
520
- gear.title = `${this.state.sourceName} — ${this._df.height} rows × ${this._df.width} cols`;
345
+ gear.title = `${this.state.sourceName} — ${this._source.height} rows × ${this._source.width} cols`;
521
346
  }
522
347
  }
523
348
  _showSettingsModal() {
524
- this._cleanupReshapeModal();
525
- const fileInfo = this._rawFileData?.file;
526
- const isMalformed = this._df ? this._detectMalformedData(this._df) : false;
527
- this._settingsModal = new Modal(`${this._id}-settings-modal`, {
528
- title: 'Data Settings',
529
- size: 'medium',
530
- close: true,
531
- backdropClose: true
532
- });
533
- const fileSizeKB = fileInfo ? (fileInfo.size / 1024).toFixed(1) : '0';
534
- const fileName = fileInfo?.name || this.state.sourceName || 'Unknown';
535
- let contentHTML = `
536
- <div class="jux-dataframe-settings-content">
537
- <div class="jux-dataframe-settings-section">
538
- <div class="jux-dataframe-settings-label">Source</div>
539
- <div class="jux-dataframe-settings-value">
540
- <strong>${this._escapeHtml(fileName)}</strong>
541
- ${fileInfo ? `<span class="jux-muted" style="margin-left: 8px;">${fileSizeKB} KB</span>` : ''}
542
- </div>
543
- </div>
544
- <div class="jux-dataframe-settings-section">
545
- <div class="jux-dataframe-settings-label">Data</div>
546
- <div class="jux-dataframe-settings-value">
547
- ${this._df ? `${this._df.height} rows × ${this._df.width} columns` : 'No data loaded'}
548
- ${isMalformed ? '<span style="color: hsl(var(--warning)); margin-left: 8px;">⚠️ May need reformatting</span>' : ''}
549
- </div>
550
- </div>
551
- `;
552
- if (this._rawFileData) {
553
- contentHTML += `
554
- <div class="jux-dataframe-settings-section">
555
- <div class="jux-dataframe-settings-label">Import</div>
556
- <div class="jux-dataframe-settings-value">
557
- <button id="${this._id}-adjust-import" class="jux-button jux-button-outline jux-button-sm">
558
- ⚙️ Adjust Header Row / Delimiter
559
- </button>
560
- </div>
561
- </div>
562
- `;
563
- }
564
- contentHTML += `</div>`;
565
- this._settingsModal
566
- .content(contentHTML)
567
- .actions([
568
- {
569
- label: 'Remove Data',
570
- variant: 'secondary',
571
- click: async () => {
572
- await this.clear();
573
- this._settingsModal.closeModal();
574
- }
575
- },
576
- {
577
- label: 'Done',
578
- variant: 'primary',
579
- click: () => this._settingsModal.closeModal()
580
- }
581
- ]);
582
- this._settingsModal.render(document.body);
583
- this._settingsModal.open();
584
- requestAnimationFrame(() => {
585
- const adjustBtn = document.getElementById(`${this._id}-adjust-import`);
586
- if (adjustBtn) {
587
- adjustBtn.addEventListener('click', () => {
588
- this._settingsModal.closeModal();
589
- this._showReshapeModal();
590
- });
591
- }
592
- });
593
- }
594
- /* ═══════════════════════════════════════════════════
595
- * MALFORMED DATA DETECTION
596
- * ═══════════════════════════════════════════════════ */
597
- _detectMalformedData(df) {
598
- const columns = df.columns;
599
- const rows = df.toRows();
600
- const hasGenericColumns = columns.some(col => col.startsWith('__EMPTY') ||
601
- col.match(/^_\d+$/) ||
602
- col.match(/^col_\d+$/));
603
- if (hasGenericColumns)
604
- return true;
605
- if (rows.length > 0) {
606
- const firstRow = rows[0];
607
- const values = Object.values(firstRow);
608
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
609
- if (nonEmpty.length < columns.length * 0.5)
610
- return true;
611
- const hasMetadata = values.some(v => String(v).includes('Exported') ||
612
- String(v).includes('Generated') ||
613
- String(v).includes('Report'));
614
- if (hasMetadata)
615
- return true;
616
- }
617
- return false;
618
- }
619
- /* ═══════════════════════════════════════════════════
620
- * RESHAPE MODAL
621
- * ═══════════════════════════════════════════════════ */
622
- _showReshapeModal() {
623
- if (!this._rawFileData)
624
- return;
625
- if (this._rawFileData.isExcel) {
626
- this._showExcelReshapeModal();
627
- }
628
- else {
629
- this._showCSVReshapeModal();
630
- }
631
- }
632
- _cleanupReshapeModal() {
633
- if (this._reshapeModal && this._reshapeModalRendered) {
634
- const oldEl = document.getElementById(`${this._id}-reshape-modal`);
635
- if (oldEl)
636
- oldEl.remove();
637
- this._reshapeModal = null;
638
- this._reshapeModalRendered = false;
639
- }
640
- }
641
- _escapeHtml(text) {
642
- const div = document.createElement('div');
643
- div.textContent = text;
644
- return div.innerHTML;
645
- }
646
- _buildClickablePreviewHTML(rawRows, selectedSheetRow) {
647
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
648
- for (const { sheetRow, values } of rawRows) {
649
- const isHeader = (sheetRow === selectedSheetRow);
650
- const isSkipped = (sheetRow < selectedSheetRow);
651
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
652
- if (isHeader) {
653
- rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
654
- }
655
- else if (isSkipped) {
656
- rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
657
- }
658
- html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
659
- 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;">`;
660
- if (isHeader) {
661
- html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
662
- }
663
- else {
664
- html += `${sheetRow}`;
665
- }
666
- html += '</td>';
667
- const displayCols = values.slice(0, 6);
668
- displayCols.forEach(val => {
669
- const displayVal = val != null ? String(val).substring(0, 20) : '';
670
- const cellStyle = isHeader
671
- ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
672
- : 'padding: 8px 12px;';
673
- html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
674
- });
675
- if (values.length > 6) {
676
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
677
- }
678
- html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
679
- if (isHeader) {
680
- html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
681
- }
682
- else if (isSkipped) {
683
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
684
- }
685
- else {
686
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
687
- }
688
- html += '</td></tr>';
689
- }
690
- html += '</table>';
691
- return html;
692
- }
693
- async _showExcelReshapeModal() {
694
- if (!this._rawFileData?.file)
695
- return;
696
- this._cleanupReshapeModal();
697
- const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
698
- if (rawRows.length === 0)
699
- return;
700
- let selectedSheetRow = rawRows[0].sheetRow;
701
- for (const { sheetRow, values } of rawRows) {
702
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
703
- if (nonEmpty.length < values.length * 0.5)
704
- continue;
705
- const nonNumeric = nonEmpty.filter(v => {
706
- const str = String(v).trim();
707
- return isNaN(Number(str)) && str !== '';
708
- }).length;
709
- if (nonNumeric >= nonEmpty.length * 0.7) {
710
- selectedSheetRow = sheetRow;
711
- break;
712
- }
713
- }
714
- this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
715
- title: 'Excel Import Settings',
716
- size: 'large',
717
- close: true,
718
- backdropClose: false
349
+ this._importModal.showSettings({
350
+ fileName: this._source.rawFileData?.file?.name || this.state.sourceName,
351
+ fileSize: this._source.rawFileData?.file?.size,
352
+ rowCount: this._source.height,
353
+ colCount: this._source.width,
354
+ isMalformed: this._source.detectMalformed(),
355
+ hasRawFileData: !!this._source.rawFileData
356
+ }, {
357
+ onClear: async () => { await this.clear(); },
358
+ onAdjustImport: () => { this._showImportSettings(); }
719
359
  });
720
- const modalContentHTML = `
721
- <div style="margin-bottom: 1rem;">
722
- <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>
723
- <input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
724
- </div>
725
- <div>
726
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
727
- <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>
728
- </div>
729
- `;
730
- this._reshapeModal
731
- .content(modalContentHTML)
732
- .actions([
733
- {
734
- label: 'Cancel',
735
- variant: 'secondary',
736
- click: () => this._reshapeModal.closeModal()
737
- },
738
- {
739
- label: 'Apply & Re-import',
740
- variant: 'primary',
741
- click: async () => {
742
- const input = document.getElementById(`${this._id}-header-row`);
743
- const headerRow = parseInt(input.value) || 0;
744
- this.state.loading = true;
745
- this._updateStatus('Re-parsing with new settings...', 'loading');
746
- try {
747
- const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
748
- headerRow,
749
- maxSheetSize: this._maxSheetSize,
750
- sheetChunkSize: this._sheetChunkSize
751
- });
752
- const sheetNames = Object.keys(sheets);
753
- if (sheetNames.length === 0) {
754
- this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
755
- this.state.loading = false;
756
- return;
757
- }
758
- await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
759
- if (sheetNames.length > 1) {
760
- this._renderMultiSheet(sheets, this._rawFileData.file.name);
761
- }
762
- else {
763
- this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
764
- }
765
- this._reshapeModal.closeModal();
766
- }
767
- catch (err) {
768
- this._updateStatus(`Error: ${err.message}`, 'error');
769
- this.state.loading = false;
770
- }
771
- }
772
- }
773
- ]);
774
- this._reshapeModal.render(document.body);
775
- this._reshapeModalRendered = true;
776
- await new Promise(resolve => requestAnimationFrame(resolve));
777
- const previewDiv = document.getElementById(`${this._id}-preview`);
778
- const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
779
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
780
- const updateHint = (row) => {
781
- if (!hintDiv)
782
- return;
783
- const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
784
- const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
785
- const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
786
- if (row > rawRows[0].sheetRow) {
787
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
788
- }
789
- else {
790
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
791
- }
792
- };
793
- const renderPreview = (selected) => {
794
- if (!previewDiv)
795
- return;
796
- previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
797
- previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
798
- tr.addEventListener('click', () => {
799
- const rowIdx = parseInt(tr.dataset.sheetRow);
800
- hiddenInput.value = String(rowIdx);
801
- updateHint(rowIdx);
802
- renderPreview(rowIdx);
803
- });
804
- });
805
- };
806
- updateHint(selectedSheetRow);
807
- renderPreview(selectedSheetRow);
808
- this._reshapeModal.open();
809
360
  }
810
- _showCSVReshapeModal() {
811
- if (!this._rawFileData?.text)
361
+ _showImportSettings() {
362
+ const rawData = this._source.rawFileData;
363
+ if (!rawData)
812
364
  return;
813
- this._cleanupReshapeModal();
814
- const text = this._rawFileData.text;
815
- const detected = this._driver._detectDelimiter(text);
816
- const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
817
- const rawRows = [];
818
- const maxPreviewRows = Math.min(lines.length, 15);
819
- for (let i = 0; i < maxPreviewRows; i++) {
820
- if (!lines[i]) {
821
- rawRows.push({ sheetRow: i, values: [''] });
822
- continue;
823
- }
824
- const values = this._driver._parseLine(lines[i], detected);
825
- rawRows.push({ sheetRow: i, values });
826
- }
827
- let selectedRow = 0;
828
- for (const { sheetRow, values } of rawRows) {
829
- const nonEmpty = values.filter((v) => v.trim() !== '');
830
- if (nonEmpty.length < values.length * 0.5)
831
- continue;
832
- const nonNumeric = nonEmpty.filter((v) => {
833
- const trimmed = v.trim();
834
- return isNaN(Number(trimmed)) && trimmed !== '';
835
- }).length;
836
- if (nonNumeric >= nonEmpty.length * 0.7) {
837
- selectedRow = sheetRow;
838
- break;
839
- }
840
- }
841
- this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
842
- title: 'CSV Import Settings',
843
- size: 'large',
844
- close: true,
845
- backdropClose: false
846
- });
847
- const modalContentHTML = `
848
- <div style="margin-bottom: 1rem;">
849
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
850
- <select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
851
- <option value=",">Comma (,)</option>
852
- <option value="|">Pipe (|)</option>
853
- <option value="&#9;">Tab (\\t)</option>
854
- <option value=";">Semicolon (;)</option>
855
- </select>
856
- </div>
857
- <div style="margin-bottom: 1rem;">
858
- <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>
859
- <input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
860
- </div>
861
- <div>
862
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
863
- <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>
864
- </div>
865
- `;
866
- this._reshapeModal
867
- .content(modalContentHTML)
868
- .actions([
869
- {
870
- label: 'Cancel',
871
- variant: 'secondary',
872
- click: () => this._reshapeModal.closeModal()
873
- },
874
- {
875
- label: 'Apply & Re-import',
876
- variant: 'primary',
877
- click: async () => {
878
- const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
879
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
880
- const delim = delimiterSelect.value;
881
- const headerRow = parseInt(hiddenInput.value) || 0;
882
- this.state.loading = true;
883
- this._updateStatus('Re-parsing with new settings...', 'loading');
884
- try {
885
- const df = this._driver.parseCSV(this._rawFileData.text, {
886
- delimiter: delim,
887
- headerRow,
888
- hasHeader: true
889
- });
890
- await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
891
- this._setDataFrame(df, this._rawFileData.file.name);
892
- this._reshapeModal.closeModal();
893
- }
894
- catch (err) {
895
- this._updateStatus(`Error: ${err.message}`, 'error');
896
- this.state.loading = false;
897
- }
898
- }
899
- }
900
- ]);
901
- this._reshapeModal.render(document.body);
902
- this._reshapeModalRendered = true;
903
- requestAnimationFrame(() => {
904
- const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
905
- const previewDiv = document.getElementById(`${this._id}-preview`);
906
- const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
907
- const hiddenInput = document.getElementById(`${this._id}-header-row`);
908
- if (delimiterSelect)
909
- delimiterSelect.value = detected;
910
- const updateHint = (row) => {
911
- if (!hintDiv)
912
- return;
913
- const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
914
- const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
915
- const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
916
- if (row > rawRows[0].sheetRow) {
917
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
365
+ this._importModal.showImportSettings(rawData, {
366
+ onReimport: async (result, sourceName) => {
367
+ if (result instanceof DataFrame) {
368
+ this._source.fromData(result.toRows(), sourceName);
918
369
  }
919
370
  else {
920
- hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
921
- }
922
- };
923
- const reparse = () => {
924
- const delim = delimiterSelect?.value || ',';
925
- rawRows.length = 0;
926
- for (let i = 0; i < maxPreviewRows; i++) {
927
- if (!lines[i]) {
928
- rawRows.push({ sheetRow: i, values: [''] });
929
- continue;
930
- }
931
- const values = this._driver._parseLine(lines[i], delim);
932
- rawRows.push({ sheetRow: i, values });
371
+ // Multi-sheet - handled by source callbacks
933
372
  }
934
- };
935
- const renderPreview = (selected) => {
936
- if (!previewDiv)
937
- return;
938
- previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
939
- previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
940
- tr.addEventListener('click', () => {
941
- const rowIdx = parseInt(tr.dataset.sheetRow);
942
- hiddenInput.value = String(rowIdx);
943
- updateHint(rowIdx);
944
- renderPreview(rowIdx);
945
- });
946
- });
947
- };
948
- if (delimiterSelect) {
949
- delimiterSelect.addEventListener('change', () => {
950
- reparse();
951
- const current = parseInt(hiddenInput.value) || 0;
952
- updateHint(current);
953
- renderPreview(current);
954
- });
373
+ },
374
+ onError: (message) => this._handleError(message),
375
+ onStatusUpdate: (text, type) => {
376
+ this.state.loading = type === 'loading';
377
+ this._updateStatus(text, type);
955
378
  }
956
- updateHint(selectedRow);
957
- renderPreview(selectedRow);
958
- this._reshapeModal.open();
959
379
  });
960
380
  }
961
381
  /* ═══════════════════════════════════════════════════
962
- * UPDATE & RENDER
382
+ * RENDER
963
383
  * ═══════════════════════════════════════════════════ */
964
384
  update(_prop, _value) { }
965
385
  render(targetId) {
@@ -976,37 +396,33 @@ export class DataFrameComponent extends BaseComponent {
976
396
  if (this._inlineUpload) {
977
397
  const uploadArea = document.createElement('div');
978
398
  uploadArea.className = 'jux-dataframe-upload-area';
979
- uploadArea.id = `${this._id}-upload-area`;
980
399
  const uploadOpts = {
981
400
  label: this._uploadButtonLabel,
982
401
  accept: this._uploadAccept,
983
402
  variant: this._uploadButtonVariant,
984
403
  };
985
- if (this._showUploadIcon && this._uploadButtonIcon) {
404
+ if (this._showUploadIcon)
986
405
  uploadOpts.icon = this._uploadButtonIcon;
987
- }
988
406
  const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
989
407
  this._uploadRef = upload;
990
- this._pendingSource = async () => {
991
- upload.bind('change', async (files) => {
992
- if (!files || files.length === 0) {
993
- await this.clear();
994
- return;
995
- }
996
- await this._handleFile(files[0]);
997
- });
998
- };
408
+ // Wire upload to source
409
+ upload.bind('change', async (files) => {
410
+ if (!files || files.length === 0) {
411
+ await this.clear();
412
+ return;
413
+ }
414
+ this.state.loading = true;
415
+ this._updateStatus(`Parsing ${files[0].name}...`, 'loading');
416
+ await this._source.fromFile(files[0]);
417
+ });
999
418
  const uploadContainer = document.createElement('div');
1000
419
  uploadContainer.className = 'jux-dataframe-upload';
1001
- uploadContainer.id = `${this._id}-upload-container`;
1002
420
  uploadArea.appendChild(uploadContainer);
1003
- if (this._showStatus) {
1004
- const statusBar = document.createElement('div');
1005
- statusBar.className = 'jux-dataframe-status';
1006
- statusBar.id = `${this._id}-status`;
1007
- statusBar.style.display = 'none';
1008
- uploadArea.appendChild(statusBar);
1009
- }
421
+ const statusBar = document.createElement('div');
422
+ statusBar.className = 'jux-dataframe-status';
423
+ statusBar.id = `${this._id}-status`;
424
+ statusBar.style.display = 'none';
425
+ uploadArea.appendChild(statusBar);
1010
426
  if (this._uploadDescription) {
1011
427
  const descEl = document.createElement('div');
1012
428
  descEl.className = 'jux-dataframe-upload-description';
@@ -1037,14 +453,7 @@ export class DataFrameComponent extends BaseComponent {
1037
453
  dataContainer.appendChild(toolbar);
1038
454
  wrapper.appendChild(dataContainer);
1039
455
  // Table
1040
- const tbl = new Table(`${this._id}-table`, {
1041
- striped: this._tableOptions.striped,
1042
- hoverable: this._tableOptions.hoverable,
1043
- sortable: this._tableOptions.sortable,
1044
- filterable: this._tableOptions.filterable,
1045
- paginated: this._tableOptions.paginated,
1046
- rowsPerPage: this._tableOptions.rowsPerPage
1047
- });
456
+ const tbl = new Table(`${this._id}-table`, this._tableOptions);
1048
457
  tbl.render(dataContainer);
1049
458
  this._table = tbl;
1050
459
  if (this._pendingSource) {