juxscript 1.1.165 → 1.1.167
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 +16 -40
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +178 -98
- package/lib/components/dataframe.ts +192 -106
- package/lib/styles/shadcn.css +354 -0
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@ export interface DataFrameOptions {
|
|
|
12
12
|
filterable?: boolean;
|
|
13
13
|
paginated?: boolean;
|
|
14
14
|
rowsPerPage?: number;
|
|
15
|
+
showStatus?: boolean;
|
|
16
|
+
icon?: string;
|
|
15
17
|
style?: string;
|
|
16
18
|
class?: string;
|
|
17
19
|
}
|
|
@@ -29,70 +31,42 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
29
31
|
private _uploadRef;
|
|
30
32
|
private _storageKey;
|
|
31
33
|
private _pendingSource;
|
|
34
|
+
private _inlineUpload;
|
|
35
|
+
private _showStatus;
|
|
36
|
+
private _icon;
|
|
32
37
|
constructor(id: string, options?: DataFrameOptions);
|
|
33
38
|
protected getTriggerEvents(): readonly string[];
|
|
34
39
|
protected getCallbackEvents(): readonly string[];
|
|
35
|
-
/**
|
|
36
|
-
* Load from IndexedDB by storage key
|
|
37
|
-
*/
|
|
38
40
|
fromStorage(key: string): this;
|
|
39
|
-
/**
|
|
40
|
-
* Load from a FileUpload component — auto-wires change event
|
|
41
|
-
*/
|
|
42
41
|
fromUpload(upload: FileUpload): this;
|
|
43
|
-
/**
|
|
44
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
45
|
-
*/
|
|
46
42
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this;
|
|
47
43
|
/**
|
|
48
|
-
*
|
|
44
|
+
* Add an inline file upload control.
|
|
45
|
+
* @param label - Button label (default: 'Upload File')
|
|
46
|
+
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
47
|
+
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
49
48
|
*/
|
|
49
|
+
withUpload(label?: string, accept?: string, icon?: string): this;
|
|
50
|
+
/** Show/hide the status bar */
|
|
51
|
+
showStatus(v: boolean): this;
|
|
52
|
+
/** Set a custom icon for the status bar */
|
|
53
|
+
statusIcon(v: string): this;
|
|
50
54
|
apply(fn: (df: DataFrame) => DataFrame): this;
|
|
51
|
-
/**
|
|
52
|
-
* Filter rows
|
|
53
|
-
*/
|
|
54
55
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
|
|
55
|
-
/**
|
|
56
|
-
* Select columns
|
|
57
|
-
*/
|
|
58
56
|
select(...cols: string[]): this;
|
|
59
|
-
/**
|
|
60
|
-
* Sort by column
|
|
61
|
-
*/
|
|
62
57
|
sort(col: string, descending?: boolean): this;
|
|
63
|
-
/**
|
|
64
|
-
* Show first N rows
|
|
65
|
-
*/
|
|
66
58
|
head(n?: number): this;
|
|
67
|
-
/**
|
|
68
|
-
* Show last N rows
|
|
69
|
-
*/
|
|
70
59
|
tail(n?: number): this;
|
|
71
|
-
/**
|
|
72
|
-
* Add a computed column
|
|
73
|
-
*/
|
|
74
60
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
|
|
75
|
-
/**
|
|
76
|
-
* Where clause
|
|
77
|
-
*/
|
|
78
61
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
|
|
79
|
-
/** Get the underlying DataFrame */
|
|
80
62
|
get df(): DataFrame | null;
|
|
81
|
-
/** Get the underlying TabularDriver */
|
|
82
63
|
get driver(): TabularDriver;
|
|
83
|
-
/** Get the internal Table component */
|
|
84
64
|
get table(): Table | null;
|
|
85
|
-
/** Get describe() stats */
|
|
86
65
|
describe(): Record<string, any> | null;
|
|
87
|
-
/** Export to CSV string */
|
|
88
66
|
toCSV(delimiter?: string): string;
|
|
89
|
-
/** Export to row objects */
|
|
90
67
|
toRows(): Record<string, any>[];
|
|
91
|
-
/** Get shape */
|
|
92
68
|
get shape(): [number, number];
|
|
93
|
-
/** Get column names */
|
|
94
69
|
get columns(): string[];
|
|
95
|
-
/** Save current DataFrame to IndexedDB */
|
|
96
70
|
save(key?: string): Promise<string | null>;
|
|
97
71
|
striped(v: boolean): this;
|
|
98
72
|
hoverable(v: boolean): this;
|
|
@@ -100,8 +74,10 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
100
74
|
filterable(v: boolean): this;
|
|
101
75
|
paginated(v: boolean): this;
|
|
102
76
|
rowsPerPage(v: number): this;
|
|
77
|
+
private _updateStatus;
|
|
103
78
|
private _setDataFrame;
|
|
104
79
|
private _updateTable;
|
|
80
|
+
private _showFilterInput;
|
|
105
81
|
update(prop: string, value: any): void;
|
|
106
82
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
107
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
2
|
import { DataFrame } from '../storage/DataFrame.js';
|
|
3
3
|
import { TabularDriver } from '../storage/TabularDriver.js';
|
|
4
|
+
import { FileUpload } from './fileupload.js';
|
|
4
5
|
import { Table } from './table.js';
|
|
6
|
+
import { renderIcon } from './icons.js';
|
|
5
7
|
const TRIGGER_EVENTS = [];
|
|
6
8
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
7
9
|
export class DataFrameComponent extends BaseComponent {
|
|
@@ -22,12 +24,17 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
22
24
|
this._uploadRef = null;
|
|
23
25
|
this._storageKey = null;
|
|
24
26
|
this._pendingSource = null;
|
|
27
|
+
this._inlineUpload = null;
|
|
28
|
+
this._showStatus = true;
|
|
29
|
+
this._icon = '';
|
|
25
30
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
31
|
+
this._showStatus = options.showStatus ?? true;
|
|
32
|
+
this._icon = options.icon ?? '';
|
|
26
33
|
this._tableOptions = {
|
|
27
34
|
striped: options.striped ?? true,
|
|
28
35
|
hoverable: options.hoverable ?? true,
|
|
29
36
|
sortable: options.sortable ?? true,
|
|
30
|
-
filterable: options.filterable ??
|
|
37
|
+
filterable: options.filterable ?? false, // defer until data loaded
|
|
31
38
|
paginated: options.paginated ?? true,
|
|
32
39
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
33
40
|
};
|
|
@@ -37,18 +44,17 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
37
44
|
/* ═══════════════════════════════════════════════════
|
|
38
45
|
* DATA SOURCES
|
|
39
46
|
* ═══════════════════════════════════════════════════ */
|
|
40
|
-
/**
|
|
41
|
-
* Load from IndexedDB by storage key
|
|
42
|
-
*/
|
|
43
47
|
fromStorage(key) {
|
|
44
48
|
this._storageKey = key;
|
|
45
|
-
|
|
49
|
+
const loadFn = async () => {
|
|
46
50
|
this.state.loading = true;
|
|
51
|
+
this._updateStatus('⏳ Loading...', 'loading');
|
|
47
52
|
try {
|
|
48
|
-
|
|
53
|
+
const df = await this._driver.loadByName(key);
|
|
49
54
|
if (!df) {
|
|
50
55
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
51
56
|
this.state.loading = false;
|
|
57
|
+
this._updateStatus('No table found: ' + key, 'empty');
|
|
52
58
|
return;
|
|
53
59
|
}
|
|
54
60
|
this._setDataFrame(df, 'storage: ' + key);
|
|
@@ -56,42 +62,44 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
56
62
|
catch (err) {
|
|
57
63
|
this._triggerCallback('error', err.message, null, this);
|
|
58
64
|
this.state.loading = false;
|
|
65
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
59
66
|
}
|
|
60
67
|
};
|
|
68
|
+
if (this._table) {
|
|
69
|
+
loadFn();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this._pendingSource = loadFn;
|
|
73
|
+
}
|
|
61
74
|
return this;
|
|
62
75
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Load from a FileUpload component — auto-wires change event
|
|
65
|
-
*/
|
|
66
76
|
fromUpload(upload) {
|
|
67
77
|
this._uploadRef = upload;
|
|
68
78
|
this._pendingSource = async () => {
|
|
69
|
-
// Wire upload's change event to parse incoming files
|
|
70
79
|
upload.bind('change', async (files) => {
|
|
71
80
|
if (!files || files.length === 0)
|
|
72
81
|
return;
|
|
73
82
|
const file = files[0];
|
|
74
83
|
this.state.loading = true;
|
|
84
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
75
85
|
try {
|
|
76
86
|
const df = await this._driver.streamFile(file);
|
|
77
|
-
// Auto-persist to IndexedDB
|
|
78
87
|
await this._driver.store(file.name, df, { source: file.name });
|
|
79
|
-
this._setDataFrame(df,
|
|
88
|
+
this._setDataFrame(df, file.name);
|
|
80
89
|
}
|
|
81
90
|
catch (err) {
|
|
82
91
|
this._triggerCallback('error', err.message, null, this);
|
|
83
92
|
this.state.loading = false;
|
|
93
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
84
94
|
}
|
|
85
95
|
});
|
|
86
96
|
};
|
|
87
97
|
return this;
|
|
88
98
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
91
|
-
*/
|
|
92
99
|
fromData(data) {
|
|
93
|
-
|
|
100
|
+
const loadFn = async () => {
|
|
94
101
|
this.state.loading = true;
|
|
102
|
+
this._updateStatus('⏳ Loading data...', 'loading');
|
|
95
103
|
try {
|
|
96
104
|
const df = new DataFrame(data);
|
|
97
105
|
this._setDataFrame(df, 'inline data');
|
|
@@ -99,16 +107,37 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
99
107
|
catch (err) {
|
|
100
108
|
this._triggerCallback('error', err.message, null, this);
|
|
101
109
|
this.state.loading = false;
|
|
110
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
102
111
|
}
|
|
103
112
|
};
|
|
113
|
+
if (this._table) {
|
|
114
|
+
loadFn();
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this._pendingSource = loadFn;
|
|
118
|
+
}
|
|
104
119
|
return this;
|
|
105
120
|
}
|
|
106
|
-
/* ═══════════════════════════════════════════════════
|
|
107
|
-
* TRANSFORM API (returns new DataFrameComponent view)
|
|
108
|
-
* ═══════════════════════════════════════════════════ */
|
|
109
121
|
/**
|
|
110
|
-
*
|
|
122
|
+
* Add an inline file upload control.
|
|
123
|
+
* @param label - Button label (default: 'Upload File')
|
|
124
|
+
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
125
|
+
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
111
126
|
*/
|
|
127
|
+
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
|
|
128
|
+
this._inlineUpload = { label, accept, icon };
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
/* ═══════════════════════════════════════════════════
|
|
132
|
+
* UI TOGGLES
|
|
133
|
+
* ═══════════════════════════════════════════════════ */
|
|
134
|
+
/** Show/hide the status bar */
|
|
135
|
+
showStatus(v) { this._showStatus = v; return this; }
|
|
136
|
+
/** Set a custom icon for the status bar */
|
|
137
|
+
statusIcon(v) { this._icon = v; return this; }
|
|
138
|
+
/* ═══════════════════════════════════════════════════
|
|
139
|
+
* TRANSFORM API
|
|
140
|
+
* ═══════════════════════════════════════════════════ */
|
|
112
141
|
apply(fn) {
|
|
113
142
|
if (!this._df)
|
|
114
143
|
return this;
|
|
@@ -117,78 +146,38 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
117
146
|
this._triggerCallback('transform', result, null, this);
|
|
118
147
|
return this;
|
|
119
148
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Filter rows
|
|
122
|
-
*/
|
|
123
149
|
filter(predicate) {
|
|
124
150
|
return this.apply(df => df.filter(predicate));
|
|
125
151
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Select columns
|
|
128
|
-
*/
|
|
129
152
|
select(...cols) {
|
|
130
153
|
return this.apply(df => df.select(...cols));
|
|
131
154
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Sort by column
|
|
134
|
-
*/
|
|
135
155
|
sort(col, descending) {
|
|
136
156
|
return this.apply(df => df.sort(col, descending));
|
|
137
157
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Show first N rows
|
|
140
|
-
*/
|
|
141
158
|
head(n = 5) {
|
|
142
159
|
return this.apply(df => df.head(n));
|
|
143
160
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Show last N rows
|
|
146
|
-
*/
|
|
147
161
|
tail(n = 5) {
|
|
148
162
|
return this.apply(df => df.tail(n));
|
|
149
163
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Add a computed column
|
|
152
|
-
*/
|
|
153
164
|
withColumn(name, fn) {
|
|
154
165
|
return this.apply(df => df.withColumn(name, fn));
|
|
155
166
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Where clause
|
|
158
|
-
*/
|
|
159
167
|
where(col, op, value) {
|
|
160
168
|
return this.apply(df => df.where(col, op, value));
|
|
161
169
|
}
|
|
162
170
|
/* ═══════════════════════════════════════════════════
|
|
163
171
|
* ACCESSORS
|
|
164
172
|
* ═══════════════════════════════════════════════════ */
|
|
165
|
-
/** Get the underlying DataFrame */
|
|
166
173
|
get df() { return this._df; }
|
|
167
|
-
/** Get the underlying TabularDriver */
|
|
168
174
|
get driver() { return this._driver; }
|
|
169
|
-
/** Get the internal Table component */
|
|
170
175
|
get table() { return this._table; }
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
toCSV(delimiter) {
|
|
177
|
-
return this._df?.toCSV(delimiter) ?? '';
|
|
178
|
-
}
|
|
179
|
-
/** Export to row objects */
|
|
180
|
-
toRows() {
|
|
181
|
-
return this._df?.toRows() ?? [];
|
|
182
|
-
}
|
|
183
|
-
/** Get shape */
|
|
184
|
-
get shape() {
|
|
185
|
-
return this._df?.shape ?? [0, 0];
|
|
186
|
-
}
|
|
187
|
-
/** Get column names */
|
|
188
|
-
get columns() {
|
|
189
|
-
return this._df?.columns ?? [];
|
|
190
|
-
}
|
|
191
|
-
/** Save current DataFrame to IndexedDB */
|
|
176
|
+
describe() { return this._df?.describe() ?? null; }
|
|
177
|
+
toCSV(delimiter) { return this._df?.toCSV(delimiter) ?? ''; }
|
|
178
|
+
toRows() { return this._df?.toRows() ?? []; }
|
|
179
|
+
get shape() { return this._df?.shape ?? [0, 0]; }
|
|
180
|
+
get columns() { return this._df?.columns ?? []; }
|
|
192
181
|
async save(key) {
|
|
193
182
|
if (!this._df)
|
|
194
183
|
return null;
|
|
@@ -207,6 +196,27 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
207
196
|
/* ═══════════════════════════════════════════════════
|
|
208
197
|
* INTERNAL
|
|
209
198
|
* ═══════════════════════════════════════════════════ */
|
|
199
|
+
_updateStatus(text, type = 'empty') {
|
|
200
|
+
const el = document.getElementById(`${this._id}-status`);
|
|
201
|
+
if (!el)
|
|
202
|
+
return;
|
|
203
|
+
el.className = 'jux-dataframe-status';
|
|
204
|
+
if (type)
|
|
205
|
+
el.classList.add(`jux-dataframe-status-${type}`);
|
|
206
|
+
// Clear and rebuild
|
|
207
|
+
el.innerHTML = '';
|
|
208
|
+
if (this._icon && type === 'success') {
|
|
209
|
+
const iconEl = renderIcon(this._icon);
|
|
210
|
+
iconEl.style.width = '16px';
|
|
211
|
+
iconEl.style.height = '16px';
|
|
212
|
+
iconEl.style.marginRight = '6px';
|
|
213
|
+
iconEl.style.verticalAlign = 'middle';
|
|
214
|
+
el.appendChild(iconEl);
|
|
215
|
+
}
|
|
216
|
+
const span = document.createElement('span');
|
|
217
|
+
span.textContent = text;
|
|
218
|
+
el.appendChild(span);
|
|
219
|
+
}
|
|
210
220
|
_setDataFrame(df, sourceName) {
|
|
211
221
|
this._df = df;
|
|
212
222
|
this.state.loaded = true;
|
|
@@ -214,12 +224,16 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
214
224
|
this.state.sourceName = sourceName;
|
|
215
225
|
this.state.rowCount = df.height;
|
|
216
226
|
this.state.colCount = df.width;
|
|
217
|
-
// Clean __EMPTY columns from xlsx artifacts
|
|
218
227
|
const cleanCols = df.columns.filter(c => !c.startsWith('__EMPTY'));
|
|
219
228
|
if (cleanCols.length < df.columns.length) {
|
|
220
229
|
this._df = df.select(...cleanCols);
|
|
221
230
|
}
|
|
222
231
|
this._updateTable();
|
|
232
|
+
this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
|
|
233
|
+
// Enable filter now that data exists
|
|
234
|
+
if (this._tableOptions.filterable) {
|
|
235
|
+
this._showFilterInput();
|
|
236
|
+
}
|
|
223
237
|
this._triggerCallback('load', this._df, null, this);
|
|
224
238
|
}
|
|
225
239
|
_updateTable() {
|
|
@@ -227,6 +241,49 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
227
241
|
return;
|
|
228
242
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
229
243
|
}
|
|
244
|
+
_showFilterInput() {
|
|
245
|
+
const wrapper = document.getElementById(this._id);
|
|
246
|
+
if (!wrapper)
|
|
247
|
+
return;
|
|
248
|
+
// Only add once
|
|
249
|
+
if (wrapper.querySelector('.jux-dataframe-filter'))
|
|
250
|
+
return;
|
|
251
|
+
const filterContainer = document.createElement('div');
|
|
252
|
+
filterContainer.className = 'jux-dataframe-filter';
|
|
253
|
+
const input = document.createElement('input');
|
|
254
|
+
input.type = 'text';
|
|
255
|
+
input.placeholder = 'Filter rows...';
|
|
256
|
+
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
257
|
+
const iconEl = renderIcon('search');
|
|
258
|
+
iconEl.style.width = '16px';
|
|
259
|
+
iconEl.style.height = '16px';
|
|
260
|
+
const iconWrap = document.createElement('span');
|
|
261
|
+
iconWrap.className = 'jux-dataframe-filter-icon';
|
|
262
|
+
iconWrap.appendChild(iconEl);
|
|
263
|
+
filterContainer.appendChild(iconWrap);
|
|
264
|
+
filterContainer.appendChild(input);
|
|
265
|
+
// Insert before the table
|
|
266
|
+
const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
|
|
267
|
+
if (tableWrapper) {
|
|
268
|
+
wrapper.insertBefore(filterContainer, tableWrapper);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
wrapper.appendChild(filterContainer);
|
|
272
|
+
}
|
|
273
|
+
input.addEventListener('input', () => {
|
|
274
|
+
if (!this._df)
|
|
275
|
+
return;
|
|
276
|
+
const text = input.value.toLowerCase();
|
|
277
|
+
if (!text) {
|
|
278
|
+
this._table?.rows(this._df.toRows());
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const filtered = this._df.filter((row) => {
|
|
282
|
+
return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
|
|
283
|
+
});
|
|
284
|
+
this._table?.rows(filtered.toRows());
|
|
285
|
+
});
|
|
286
|
+
}
|
|
230
287
|
update(prop, value) { }
|
|
231
288
|
/* ═══════════════════════════════════════════════════
|
|
232
289
|
* RENDER
|
|
@@ -241,47 +298,70 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
241
298
|
wrapper.className += ` ${className}`;
|
|
242
299
|
if (style)
|
|
243
300
|
wrapper.setAttribute('style', style);
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
301
|
+
// Inline upload
|
|
302
|
+
if (this._inlineUpload) {
|
|
303
|
+
const uploadOpts = {
|
|
304
|
+
label: this._inlineUpload.label,
|
|
305
|
+
accept: this._inlineUpload.accept,
|
|
306
|
+
};
|
|
307
|
+
if (this._inlineUpload.icon) {
|
|
308
|
+
uploadOpts.icon = this._inlineUpload.icon;
|
|
309
|
+
}
|
|
310
|
+
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
311
|
+
this._uploadRef = upload;
|
|
312
|
+
this._pendingSource = async () => {
|
|
313
|
+
upload.bind('change', async (files) => {
|
|
314
|
+
if (!files || files.length === 0)
|
|
315
|
+
return;
|
|
316
|
+
const file = files[0];
|
|
317
|
+
this.state.loading = true;
|
|
318
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
319
|
+
try {
|
|
320
|
+
const df = await this._driver.streamFile(file);
|
|
321
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
322
|
+
this._setDataFrame(df, file.name);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
this._triggerCallback('error', err.message, null, this);
|
|
326
|
+
this.state.loading = false;
|
|
327
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
const uploadContainer = document.createElement('div');
|
|
332
|
+
uploadContainer.className = 'jux-dataframe-upload';
|
|
333
|
+
uploadContainer.id = `${this._id}-upload-container`;
|
|
334
|
+
wrapper.appendChild(uploadContainer);
|
|
335
|
+
container.appendChild(wrapper);
|
|
336
|
+
upload.render(uploadContainer);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
container.appendChild(wrapper);
|
|
340
|
+
}
|
|
341
|
+
// Status bar (conditional)
|
|
342
|
+
if (this._showStatus) {
|
|
343
|
+
const statusBar = document.createElement('div');
|
|
344
|
+
statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
|
|
345
|
+
statusBar.id = `${this._id}-status`;
|
|
346
|
+
statusBar.textContent = 'No data loaded.';
|
|
347
|
+
wrapper.appendChild(statusBar);
|
|
348
|
+
}
|
|
349
|
+
// Table — filterable is false initially; we enable it after data loads
|
|
350
|
+
const tbl = new Table(`${this._id}-table`, {
|
|
254
351
|
striped: this._tableOptions.striped,
|
|
255
352
|
hoverable: this._tableOptions.hoverable,
|
|
256
353
|
sortable: this._tableOptions.sortable,
|
|
257
|
-
filterable:
|
|
354
|
+
filterable: false, // we handle filtering ourselves
|
|
258
355
|
paginated: this._tableOptions.paginated,
|
|
259
356
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
260
357
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const origUpdate = this.update.bind(this);
|
|
264
|
-
this.update = (prop, value) => {
|
|
265
|
-
origUpdate(prop, value);
|
|
266
|
-
if (prop === 'loaded' || prop === 'loading' || prop === 'rowCount' || prop === 'colCount' || prop === 'sourceName') {
|
|
267
|
-
const el = document.getElementById(`${this._id}-status`);
|
|
268
|
-
if (el) {
|
|
269
|
-
if (this.state.loading) {
|
|
270
|
-
el.textContent = '⏳ Loading...';
|
|
271
|
-
}
|
|
272
|
-
else if (this.state.loaded) {
|
|
273
|
-
el.textContent = `✅ ${this.state.sourceName} — ${this.state.rowCount} rows × ${this.state.colCount} cols`;
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
el.textContent = 'No data loaded.';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
};
|
|
358
|
+
tbl.render(wrapper);
|
|
359
|
+
this._table = tbl;
|
|
281
360
|
// Execute pending data source
|
|
282
361
|
if (this._pendingSource) {
|
|
283
|
-
this._pendingSource
|
|
362
|
+
const fn = this._pendingSource;
|
|
284
363
|
this._pendingSource = null;
|
|
364
|
+
fn();
|
|
285
365
|
}
|
|
286
366
|
this._wireStandardEvents(wrapper);
|
|
287
367
|
return this;
|