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
|
@@ -3,6 +3,7 @@ import { DataFrame } from '../storage/DataFrame.js';
|
|
|
3
3
|
import { TabularDriver } from '../storage/TabularDriver.js';
|
|
4
4
|
import { FileUpload } from './fileupload.js';
|
|
5
5
|
import { Table } from './table.js';
|
|
6
|
+
import { renderIcon } from './icons.js';
|
|
6
7
|
|
|
7
8
|
const TRIGGER_EVENTS = [] as const;
|
|
8
9
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'] as const;
|
|
@@ -16,6 +17,8 @@ export interface DataFrameOptions {
|
|
|
16
17
|
filterable?: boolean;
|
|
17
18
|
paginated?: boolean;
|
|
18
19
|
rowsPerPage?: number;
|
|
20
|
+
showStatus?: boolean;
|
|
21
|
+
icon?: string;
|
|
19
22
|
style?: string;
|
|
20
23
|
class?: string;
|
|
21
24
|
}
|
|
@@ -42,6 +45,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
42
45
|
private _uploadRef: FileUpload | null = null;
|
|
43
46
|
private _storageKey: string | null = null;
|
|
44
47
|
private _pendingSource: (() => Promise<void>) | null = null;
|
|
48
|
+
private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
|
|
49
|
+
private _showStatus: boolean = true;
|
|
50
|
+
private _icon: string = '';
|
|
45
51
|
|
|
46
52
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
47
53
|
super(id, {
|
|
@@ -61,11 +67,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
61
67
|
options.storeName ?? 'frames'
|
|
62
68
|
);
|
|
63
69
|
|
|
70
|
+
this._showStatus = options.showStatus ?? true;
|
|
71
|
+
this._icon = options.icon ?? '';
|
|
72
|
+
|
|
64
73
|
this._tableOptions = {
|
|
65
74
|
striped: options.striped ?? true,
|
|
66
75
|
hoverable: options.hoverable ?? true,
|
|
67
76
|
sortable: options.sortable ?? true,
|
|
68
|
-
filterable: options.filterable ??
|
|
77
|
+
filterable: options.filterable ?? false, // defer until data loaded
|
|
69
78
|
paginated: options.paginated ?? true,
|
|
70
79
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
71
80
|
};
|
|
@@ -78,79 +87,98 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
78
87
|
* DATA SOURCES
|
|
79
88
|
* ═══════════════════════════════════════════════════ */
|
|
80
89
|
|
|
81
|
-
/**
|
|
82
|
-
* Load from IndexedDB by storage key
|
|
83
|
-
*/
|
|
84
90
|
fromStorage(key: string): this {
|
|
85
91
|
this._storageKey = key;
|
|
86
|
-
|
|
92
|
+
const loadFn = async () => {
|
|
87
93
|
this.state.loading = true;
|
|
94
|
+
this._updateStatus('⏳ Loading...', 'loading');
|
|
88
95
|
try {
|
|
89
|
-
|
|
96
|
+
const df = await this._driver.loadByName(key);
|
|
90
97
|
if (!df) {
|
|
91
98
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
92
99
|
this.state.loading = false;
|
|
100
|
+
this._updateStatus('No table found: ' + key, 'empty');
|
|
93
101
|
return;
|
|
94
102
|
}
|
|
95
103
|
this._setDataFrame(df, 'storage: ' + key);
|
|
96
104
|
} catch (err: any) {
|
|
97
105
|
this._triggerCallback('error', err.message, null, this);
|
|
98
106
|
this.state.loading = false;
|
|
107
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
99
108
|
}
|
|
100
109
|
};
|
|
110
|
+
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
101
111
|
return this;
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
/**
|
|
105
|
-
* Load from a FileUpload component — auto-wires change event
|
|
106
|
-
*/
|
|
107
114
|
fromUpload(upload: FileUpload): this {
|
|
108
115
|
this._uploadRef = upload;
|
|
109
116
|
this._pendingSource = async () => {
|
|
110
|
-
// Wire upload's change event to parse incoming files
|
|
111
117
|
upload.bind('change', async (files: File[]) => {
|
|
112
118
|
if (!files || files.length === 0) return;
|
|
113
119
|
const file = files[0];
|
|
114
120
|
this.state.loading = true;
|
|
115
|
-
|
|
121
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
116
122
|
try {
|
|
117
123
|
const df = await this._driver.streamFile(file);
|
|
118
|
-
// Auto-persist to IndexedDB
|
|
119
124
|
await this._driver.store(file.name, df, { source: file.name });
|
|
120
|
-
this._setDataFrame(df,
|
|
125
|
+
this._setDataFrame(df, file.name);
|
|
121
126
|
} catch (err: any) {
|
|
122
127
|
this._triggerCallback('error', err.message, null, this);
|
|
123
128
|
this.state.loading = false;
|
|
129
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
124
130
|
}
|
|
125
131
|
});
|
|
126
132
|
};
|
|
127
133
|
return this;
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
/**
|
|
131
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
132
|
-
*/
|
|
133
136
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this {
|
|
134
|
-
|
|
137
|
+
const loadFn = async () => {
|
|
135
138
|
this.state.loading = true;
|
|
139
|
+
this._updateStatus('⏳ Loading data...', 'loading');
|
|
136
140
|
try {
|
|
137
141
|
const df = new DataFrame(data);
|
|
138
142
|
this._setDataFrame(df, 'inline data');
|
|
139
143
|
} catch (err: any) {
|
|
140
144
|
this._triggerCallback('error', err.message, null, this);
|
|
141
145
|
this.state.loading = false;
|
|
146
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
142
147
|
}
|
|
143
148
|
};
|
|
149
|
+
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Add an inline file upload control.
|
|
155
|
+
* @param label - Button label (default: 'Upload File')
|
|
156
|
+
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
157
|
+
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
158
|
+
*/
|
|
159
|
+
withUpload(
|
|
160
|
+
label: string = 'Upload File',
|
|
161
|
+
accept: string = '.csv,.tsv,.txt,.xlsx,.xls',
|
|
162
|
+
icon: string = 'upload'
|
|
163
|
+
): this {
|
|
164
|
+
this._inlineUpload = { label, accept, icon };
|
|
144
165
|
return this;
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
/* ═══════════════════════════════════════════════════
|
|
148
|
-
*
|
|
169
|
+
* UI TOGGLES
|
|
170
|
+
* ═══════════════════════════════════════════════════ */
|
|
171
|
+
|
|
172
|
+
/** Show/hide the status bar */
|
|
173
|
+
showStatus(v: boolean): this { this._showStatus = v; return this; }
|
|
174
|
+
|
|
175
|
+
/** Set a custom icon for the status bar */
|
|
176
|
+
statusIcon(v: string): this { this._icon = v; return this; }
|
|
177
|
+
|
|
178
|
+
/* ═══════════════════════════════════════════════════
|
|
179
|
+
* TRANSFORM API
|
|
149
180
|
* ═══════════════════════════════════════════════════ */
|
|
150
181
|
|
|
151
|
-
/**
|
|
152
|
-
* Apply a transform to the current DataFrame and update the table
|
|
153
|
-
*/
|
|
154
182
|
apply(fn: (df: DataFrame) => DataFrame): this {
|
|
155
183
|
if (!this._df) return this;
|
|
156
184
|
const result = fn(this._df);
|
|
@@ -159,51 +187,30 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
159
187
|
return this;
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
/**
|
|
163
|
-
* Filter rows
|
|
164
|
-
*/
|
|
165
190
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
|
|
166
191
|
return this.apply(df => df.filter(predicate));
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
/**
|
|
170
|
-
* Select columns
|
|
171
|
-
*/
|
|
172
194
|
select(...cols: string[]): this {
|
|
173
195
|
return this.apply(df => df.select(...cols));
|
|
174
196
|
}
|
|
175
197
|
|
|
176
|
-
/**
|
|
177
|
-
* Sort by column
|
|
178
|
-
*/
|
|
179
198
|
sort(col: string, descending?: boolean): this {
|
|
180
199
|
return this.apply(df => df.sort(col, descending));
|
|
181
200
|
}
|
|
182
201
|
|
|
183
|
-
/**
|
|
184
|
-
* Show first N rows
|
|
185
|
-
*/
|
|
186
202
|
head(n: number = 5): this {
|
|
187
203
|
return this.apply(df => df.head(n));
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
/**
|
|
191
|
-
* Show last N rows
|
|
192
|
-
*/
|
|
193
206
|
tail(n: number = 5): this {
|
|
194
207
|
return this.apply(df => df.tail(n));
|
|
195
208
|
}
|
|
196
209
|
|
|
197
|
-
/**
|
|
198
|
-
* Add a computed column
|
|
199
|
-
*/
|
|
200
210
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
|
|
201
211
|
return this.apply(df => df.withColumn(name, fn));
|
|
202
212
|
}
|
|
203
213
|
|
|
204
|
-
/**
|
|
205
|
-
* Where clause
|
|
206
|
-
*/
|
|
207
214
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this {
|
|
208
215
|
return this.apply(df => df.where(col, op, value));
|
|
209
216
|
}
|
|
@@ -212,41 +219,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
212
219
|
* ACCESSORS
|
|
213
220
|
* ═══════════════════════════════════════════════════ */
|
|
214
221
|
|
|
215
|
-
/** Get the underlying DataFrame */
|
|
216
222
|
get df(): DataFrame | null { return this._df; }
|
|
217
|
-
|
|
218
|
-
/** Get the underlying TabularDriver */
|
|
219
223
|
get driver(): TabularDriver { return this._driver; }
|
|
220
|
-
|
|
221
|
-
/** Get the internal Table component */
|
|
222
224
|
get table(): Table | null { return this._table; }
|
|
225
|
+
describe(): Record<string, any> | null { return this._df?.describe() ?? null; }
|
|
226
|
+
toCSV(delimiter?: string): string { return this._df?.toCSV(delimiter) ?? ''; }
|
|
227
|
+
toRows(): Record<string, any>[] { return this._df?.toRows() ?? []; }
|
|
228
|
+
get shape(): [number, number] { return this._df?.shape ?? [0, 0]; }
|
|
229
|
+
get columns(): string[] { return this._df?.columns ?? []; }
|
|
223
230
|
|
|
224
|
-
/** Get describe() stats */
|
|
225
|
-
describe(): Record<string, any> | null {
|
|
226
|
-
return this._df?.describe() ?? null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Export to CSV string */
|
|
230
|
-
toCSV(delimiter?: string): string {
|
|
231
|
-
return this._df?.toCSV(delimiter) ?? '';
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Export to row objects */
|
|
235
|
-
toRows(): Record<string, any>[] {
|
|
236
|
-
return this._df?.toRows() ?? [];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Get shape */
|
|
240
|
-
get shape(): [number, number] {
|
|
241
|
-
return this._df?.shape ?? [0, 0];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Get column names */
|
|
245
|
-
get columns(): string[] {
|
|
246
|
-
return this._df?.columns ?? [];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Save current DataFrame to IndexedDB */
|
|
250
231
|
async save(key?: string): Promise<string | null> {
|
|
251
232
|
if (!this._df) return null;
|
|
252
233
|
const name = key ?? this._storageKey ?? this._id;
|
|
@@ -268,6 +249,30 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
268
249
|
* INTERNAL
|
|
269
250
|
* ═══════════════════════════════════════════════════ */
|
|
270
251
|
|
|
252
|
+
private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' = 'empty'): void {
|
|
253
|
+
const el = document.getElementById(`${this._id}-status`);
|
|
254
|
+
if (!el) return;
|
|
255
|
+
|
|
256
|
+
el.className = 'jux-dataframe-status';
|
|
257
|
+
if (type) el.classList.add(`jux-dataframe-status-${type}`);
|
|
258
|
+
|
|
259
|
+
// Clear and rebuild
|
|
260
|
+
el.innerHTML = '';
|
|
261
|
+
|
|
262
|
+
if (this._icon && type === 'success') {
|
|
263
|
+
const iconEl = renderIcon(this._icon);
|
|
264
|
+
iconEl.style.width = '16px';
|
|
265
|
+
iconEl.style.height = '16px';
|
|
266
|
+
iconEl.style.marginRight = '6px';
|
|
267
|
+
iconEl.style.verticalAlign = 'middle';
|
|
268
|
+
el.appendChild(iconEl);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const span = document.createElement('span');
|
|
272
|
+
span.textContent = text;
|
|
273
|
+
el.appendChild(span);
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
private _setDataFrame(df: DataFrame, sourceName: string): void {
|
|
272
277
|
this._df = df;
|
|
273
278
|
this.state.loaded = true;
|
|
@@ -276,13 +281,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
276
281
|
this.state.rowCount = df.height;
|
|
277
282
|
this.state.colCount = df.width;
|
|
278
283
|
|
|
279
|
-
// Clean __EMPTY columns from xlsx artifacts
|
|
280
284
|
const cleanCols = df.columns.filter(c => !c.startsWith('__EMPTY'));
|
|
281
285
|
if (cleanCols.length < df.columns.length) {
|
|
282
286
|
this._df = df.select(...cleanCols);
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
this._updateTable();
|
|
290
|
+
this._updateStatus(
|
|
291
|
+
`${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
|
|
292
|
+
'success'
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Enable filter now that data exists
|
|
296
|
+
if (this._tableOptions.filterable) {
|
|
297
|
+
this._showFilterInput();
|
|
298
|
+
}
|
|
299
|
+
|
|
286
300
|
this._triggerCallback('load', this._df, null, this);
|
|
287
301
|
}
|
|
288
302
|
|
|
@@ -291,6 +305,55 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
291
305
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
292
306
|
}
|
|
293
307
|
|
|
308
|
+
private _showFilterInput(): void {
|
|
309
|
+
const wrapper = document.getElementById(this._id);
|
|
310
|
+
if (!wrapper) return;
|
|
311
|
+
// Only add once
|
|
312
|
+
if (wrapper.querySelector('.jux-dataframe-filter')) return;
|
|
313
|
+
|
|
314
|
+
const filterContainer = document.createElement('div');
|
|
315
|
+
filterContainer.className = 'jux-dataframe-filter';
|
|
316
|
+
|
|
317
|
+
const input = document.createElement('input');
|
|
318
|
+
input.type = 'text';
|
|
319
|
+
input.placeholder = 'Filter rows...';
|
|
320
|
+
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
321
|
+
|
|
322
|
+
const iconEl = renderIcon('search');
|
|
323
|
+
iconEl.style.width = '16px';
|
|
324
|
+
iconEl.style.height = '16px';
|
|
325
|
+
|
|
326
|
+
const iconWrap = document.createElement('span');
|
|
327
|
+
iconWrap.className = 'jux-dataframe-filter-icon';
|
|
328
|
+
iconWrap.appendChild(iconEl);
|
|
329
|
+
|
|
330
|
+
filterContainer.appendChild(iconWrap);
|
|
331
|
+
filterContainer.appendChild(input);
|
|
332
|
+
|
|
333
|
+
// Insert before the table
|
|
334
|
+
const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
|
|
335
|
+
if (tableWrapper) {
|
|
336
|
+
wrapper.insertBefore(filterContainer, tableWrapper);
|
|
337
|
+
} else {
|
|
338
|
+
wrapper.appendChild(filterContainer);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
input.addEventListener('input', () => {
|
|
342
|
+
if (!this._df) return;
|
|
343
|
+
const text = input.value.toLowerCase();
|
|
344
|
+
if (!text) {
|
|
345
|
+
this._table?.rows(this._df.toRows());
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const filtered = this._df.filter((row) => {
|
|
349
|
+
return Object.values(row).some(v =>
|
|
350
|
+
v !== null && v !== undefined && String(v).toLowerCase().includes(text)
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
this._table?.rows(filtered.toRows());
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
294
357
|
update(prop: string, value: any): void { }
|
|
295
358
|
|
|
296
359
|
/* ═══════════════════════════════════════════════════
|
|
@@ -307,53 +370,76 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
307
370
|
if (className) wrapper.className += ` ${className}`;
|
|
308
371
|
if (style) wrapper.setAttribute('style', style);
|
|
309
372
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
373
|
+
// Inline upload
|
|
374
|
+
if (this._inlineUpload) {
|
|
375
|
+
const uploadOpts: any = {
|
|
376
|
+
label: this._inlineUpload.label,
|
|
377
|
+
accept: this._inlineUpload.accept,
|
|
378
|
+
};
|
|
379
|
+
if (this._inlineUpload.icon) {
|
|
380
|
+
uploadOpts.icon = this._inlineUpload.icon;
|
|
381
|
+
}
|
|
317
382
|
|
|
318
|
-
|
|
383
|
+
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
384
|
+
|
|
385
|
+
this._uploadRef = upload;
|
|
386
|
+
this._pendingSource = async () => {
|
|
387
|
+
upload.bind('change', async (files: File[]) => {
|
|
388
|
+
if (!files || files.length === 0) return;
|
|
389
|
+
const file = files[0];
|
|
390
|
+
this.state.loading = true;
|
|
391
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
392
|
+
try {
|
|
393
|
+
const df = await this._driver.streamFile(file);
|
|
394
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
395
|
+
this._setDataFrame(df, file.name);
|
|
396
|
+
} catch (err: any) {
|
|
397
|
+
this._triggerCallback('error', err.message, null, this);
|
|
398
|
+
this.state.loading = false;
|
|
399
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const uploadContainer = document.createElement('div');
|
|
405
|
+
uploadContainer.className = 'jux-dataframe-upload';
|
|
406
|
+
uploadContainer.id = `${this._id}-upload-container`;
|
|
407
|
+
wrapper.appendChild(uploadContainer);
|
|
408
|
+
container.appendChild(wrapper);
|
|
409
|
+
upload.render(uploadContainer);
|
|
410
|
+
} else {
|
|
411
|
+
container.appendChild(wrapper);
|
|
412
|
+
}
|
|
319
413
|
|
|
320
|
-
//
|
|
321
|
-
|
|
414
|
+
// Status bar (conditional)
|
|
415
|
+
if (this._showStatus) {
|
|
416
|
+
const statusBar = document.createElement('div');
|
|
417
|
+
statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
|
|
418
|
+
statusBar.id = `${this._id}-status`;
|
|
419
|
+
statusBar.textContent = 'No data loaded.';
|
|
420
|
+
wrapper.appendChild(statusBar);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Table — filterable is false initially; we enable it after data loads
|
|
424
|
+
const tbl = new Table(`${this._id}-table`, {
|
|
322
425
|
striped: this._tableOptions.striped,
|
|
323
426
|
hoverable: this._tableOptions.hoverable,
|
|
324
427
|
sortable: this._tableOptions.sortable,
|
|
325
|
-
filterable:
|
|
428
|
+
filterable: false, // we handle filtering ourselves
|
|
326
429
|
paginated: this._tableOptions.paginated,
|
|
327
430
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
328
431
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Watch state for status updates
|
|
332
|
-
const origUpdate = this.update.bind(this);
|
|
333
|
-
this.update = (prop: string, value: any) => {
|
|
334
|
-
origUpdate(prop, value);
|
|
335
|
-
if (prop === 'loaded' || prop === 'loading' || prop === 'rowCount' || prop === 'colCount' || prop === 'sourceName') {
|
|
336
|
-
const el = document.getElementById(`${this._id}-status`);
|
|
337
|
-
if (el) {
|
|
338
|
-
if (this.state.loading) {
|
|
339
|
-
el.textContent = '⏳ Loading...';
|
|
340
|
-
} else if (this.state.loaded) {
|
|
341
|
-
el.textContent = `✅ ${this.state.sourceName} — ${this.state.rowCount} rows × ${this.state.colCount} cols`;
|
|
342
|
-
} else {
|
|
343
|
-
el.textContent = 'No data loaded.';
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
};
|
|
432
|
+
tbl.render(wrapper);
|
|
433
|
+
this._table = tbl;
|
|
348
434
|
|
|
349
435
|
// Execute pending data source
|
|
350
436
|
if (this._pendingSource) {
|
|
351
|
-
this._pendingSource
|
|
437
|
+
const fn = this._pendingSource;
|
|
352
438
|
this._pendingSource = null;
|
|
439
|
+
fn();
|
|
353
440
|
}
|
|
354
441
|
|
|
355
442
|
this._wireStandardEvents(wrapper);
|
|
356
|
-
|
|
357
443
|
return this;
|
|
358
444
|
}
|
|
359
445
|
}
|