@jupyterlab/csvviewer 4.0.0-alpha.19 → 4.0.0-alpha.20
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/parse.d.ts +1 -1
- package/lib/toolbar.d.ts +1 -1
- package/lib/widget.d.ts +8 -6
- package/lib/widget.js +55 -25
- package/lib/widget.js.map +1 -1
- package/package.json +16 -15
- package/src/index.ts +11 -0
- package/src/model.ts +740 -0
- package/src/parse.ts +577 -0
- package/src/toolbar.ts +157 -0
- package/src/widget.ts +563 -0
package/src/toolbar.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
5
|
+
import { Styling } from '@jupyterlab/ui-components';
|
|
6
|
+
import { Message } from '@lumino/messaging';
|
|
7
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
|
+
import { Widget } from '@lumino/widgets';
|
|
9
|
+
import type { CSVViewer } from './widget';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The class name added to a csv toolbar widget.
|
|
13
|
+
*/
|
|
14
|
+
const CSV_DELIMITER_CLASS = 'jp-CSVDelimiter';
|
|
15
|
+
|
|
16
|
+
const CSV_DELIMITER_LABEL_CLASS = 'jp-CSVDelimiter-label';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The class name added to a csv toolbar's dropdown element.
|
|
20
|
+
*/
|
|
21
|
+
const CSV_DELIMITER_DROPDOWN_CLASS = 'jp-CSVDelimiter-dropdown';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A widget for selecting a delimiter.
|
|
25
|
+
*/
|
|
26
|
+
export class CSVDelimiter extends Widget {
|
|
27
|
+
/**
|
|
28
|
+
* Construct a new csv table widget.
|
|
29
|
+
*/
|
|
30
|
+
constructor(options: CSVToolbar.IOptions) {
|
|
31
|
+
super({
|
|
32
|
+
node: Private.createNode(options.widget.delimiter, options.translator)
|
|
33
|
+
});
|
|
34
|
+
this._widget = options.widget;
|
|
35
|
+
this.addClass(CSV_DELIMITER_CLASS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A signal emitted when the delimiter selection has changed.
|
|
40
|
+
*
|
|
41
|
+
* @deprecated since v3.2
|
|
42
|
+
* This is dead code now.
|
|
43
|
+
*/
|
|
44
|
+
get delimiterChanged(): ISignal<this, string> {
|
|
45
|
+
return this._delimiterChanged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The delimiter dropdown menu.
|
|
50
|
+
*/
|
|
51
|
+
get selectNode(): HTMLSelectElement {
|
|
52
|
+
return this.node.getElementsByTagName('select')![0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle the DOM events for the widget.
|
|
57
|
+
*
|
|
58
|
+
* @param event - The DOM event sent to the widget.
|
|
59
|
+
*
|
|
60
|
+
* #### Notes
|
|
61
|
+
* This method implements the DOM `EventListener` interface and is
|
|
62
|
+
* called in response to events on the dock panel's node. It should
|
|
63
|
+
* not be called directly by user code.
|
|
64
|
+
*/
|
|
65
|
+
handleEvent(event: Event): void {
|
|
66
|
+
switch (event.type) {
|
|
67
|
+
case 'change':
|
|
68
|
+
this._delimiterChanged.emit(this.selectNode.value);
|
|
69
|
+
this._widget.delimiter = this.selectNode.value;
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handle `after-attach` messages for the widget.
|
|
78
|
+
*/
|
|
79
|
+
protected onAfterAttach(msg: Message): void {
|
|
80
|
+
this.selectNode.addEventListener('change', this);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle `before-detach` messages for the widget.
|
|
85
|
+
*/
|
|
86
|
+
protected onBeforeDetach(msg: Message): void {
|
|
87
|
+
this.selectNode.removeEventListener('change', this);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private _delimiterChanged = new Signal<this, string>(this);
|
|
91
|
+
protected _widget: CSVViewer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A namespace for `CSVToolbar` statics.
|
|
96
|
+
*/
|
|
97
|
+
export namespace CSVToolbar {
|
|
98
|
+
/**
|
|
99
|
+
* The instantiation options for a CSV toolbar.
|
|
100
|
+
*/
|
|
101
|
+
export interface IOptions {
|
|
102
|
+
/**
|
|
103
|
+
* Document widget for this toolbar
|
|
104
|
+
*/
|
|
105
|
+
widget: CSVViewer;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The application language translator.
|
|
109
|
+
*/
|
|
110
|
+
translator?: ITranslator;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A namespace for private toolbar methods.
|
|
116
|
+
*/
|
|
117
|
+
namespace Private {
|
|
118
|
+
/**
|
|
119
|
+
* Create the node for the delimiter switcher.
|
|
120
|
+
*/
|
|
121
|
+
export function createNode(
|
|
122
|
+
selected: string,
|
|
123
|
+
translator?: ITranslator
|
|
124
|
+
): HTMLElement {
|
|
125
|
+
translator = translator || nullTranslator;
|
|
126
|
+
const trans = translator?.load('jupyterlab');
|
|
127
|
+
|
|
128
|
+
// The supported parsing delimiters and labels.
|
|
129
|
+
const delimiters = [
|
|
130
|
+
[',', ','],
|
|
131
|
+
[';', ';'],
|
|
132
|
+
['\t', trans.__('tab')],
|
|
133
|
+
['|', trans.__('pipe')],
|
|
134
|
+
['#', trans.__('hash')]
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const div = document.createElement('div');
|
|
138
|
+
const label = document.createElement('span');
|
|
139
|
+
const select = document.createElement('select');
|
|
140
|
+
label.textContent = trans.__('Delimiter: ');
|
|
141
|
+
label.className = CSV_DELIMITER_LABEL_CLASS;
|
|
142
|
+
for (const [delimiter, label] of delimiters) {
|
|
143
|
+
const option = document.createElement('option');
|
|
144
|
+
option.value = delimiter;
|
|
145
|
+
option.textContent = label;
|
|
146
|
+
if (delimiter === selected) {
|
|
147
|
+
option.selected = true;
|
|
148
|
+
}
|
|
149
|
+
select.appendChild(option);
|
|
150
|
+
}
|
|
151
|
+
div.appendChild(label);
|
|
152
|
+
const node = Styling.wrapSelect(select);
|
|
153
|
+
node.classList.add(CSV_DELIMITER_DROPDOWN_CLASS);
|
|
154
|
+
div.appendChild(node);
|
|
155
|
+
return div;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { ActivityMonitor } from '@jupyterlab/coreutils';
|
|
5
|
+
import {
|
|
6
|
+
ABCWidgetFactory,
|
|
7
|
+
DocumentRegistry,
|
|
8
|
+
DocumentWidget,
|
|
9
|
+
IDocumentWidget
|
|
10
|
+
} from '@jupyterlab/docregistry';
|
|
11
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
12
|
+
import type * as DataGridModule from '@lumino/datagrid';
|
|
13
|
+
import { Message } from '@lumino/messaging';
|
|
14
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
15
|
+
import { PanelLayout, Widget } from '@lumino/widgets';
|
|
16
|
+
import type * as DSVModelModule from './model';
|
|
17
|
+
import { CSVDelimiter } from './toolbar';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The class name added to a CSV viewer.
|
|
21
|
+
*/
|
|
22
|
+
const CSV_CLASS = 'jp-CSVViewer';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The class name added to a CSV viewer datagrid.
|
|
26
|
+
*/
|
|
27
|
+
const CSV_GRID_CLASS = 'jp-CSVViewer-grid';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The timeout to wait for change activity to have ceased before rendering.
|
|
31
|
+
*/
|
|
32
|
+
const RENDER_TIMEOUT = 1000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for cells textrenderer.
|
|
36
|
+
*/
|
|
37
|
+
export class TextRenderConfig {
|
|
38
|
+
/**
|
|
39
|
+
* default text color
|
|
40
|
+
*/
|
|
41
|
+
textColor: string;
|
|
42
|
+
/**
|
|
43
|
+
* background color for a search match
|
|
44
|
+
*/
|
|
45
|
+
matchBackgroundColor: string;
|
|
46
|
+
/**
|
|
47
|
+
* background color for the current search match.
|
|
48
|
+
*/
|
|
49
|
+
currentMatchBackgroundColor: string;
|
|
50
|
+
/**
|
|
51
|
+
* horizontalAlignment of the text
|
|
52
|
+
*/
|
|
53
|
+
horizontalAlignment: DataGridModule.TextRenderer.HorizontalAlignment;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Search service remembers the search state and the location of the last
|
|
58
|
+
* match, for incremental searching.
|
|
59
|
+
* Search service is also responsible of providing a cell renderer function
|
|
60
|
+
* to set the background color of cells matching the search text.
|
|
61
|
+
*/
|
|
62
|
+
export class GridSearchService {
|
|
63
|
+
constructor(grid: DataGridModule.DataGrid) {
|
|
64
|
+
this._grid = grid;
|
|
65
|
+
this._query = null;
|
|
66
|
+
this._row = 0;
|
|
67
|
+
this._column = -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A signal fired when the grid changes.
|
|
72
|
+
*/
|
|
73
|
+
get changed(): ISignal<GridSearchService, void> {
|
|
74
|
+
return this._changed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns a cellrenderer config function to render each cell background.
|
|
79
|
+
* If cell match, background is matchBackgroundColor, if it's the current
|
|
80
|
+
* match, background is currentMatchBackgroundColor.
|
|
81
|
+
*/
|
|
82
|
+
cellBackgroundColorRendererFunc(
|
|
83
|
+
config: TextRenderConfig
|
|
84
|
+
): DataGridModule.CellRenderer.ConfigFunc<string> {
|
|
85
|
+
return ({ value, row, column }) => {
|
|
86
|
+
if (this._query) {
|
|
87
|
+
if ((value as string).match(this._query)) {
|
|
88
|
+
if (this._row === row && this._column === column) {
|
|
89
|
+
return config.currentMatchBackgroundColor;
|
|
90
|
+
}
|
|
91
|
+
return config.matchBackgroundColor;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return '';
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear the search.
|
|
100
|
+
*/
|
|
101
|
+
clear(): void {
|
|
102
|
+
this._query = null;
|
|
103
|
+
this._row = 0;
|
|
104
|
+
this._column = -1;
|
|
105
|
+
this._changed.emit(undefined);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* incrementally look for searchText.
|
|
110
|
+
*/
|
|
111
|
+
find(query: RegExp, reverse = false): boolean {
|
|
112
|
+
const model = this._grid.dataModel!;
|
|
113
|
+
const rowCount = model.rowCount('body');
|
|
114
|
+
const columnCount = model.columnCount('body');
|
|
115
|
+
|
|
116
|
+
if (this._query !== query) {
|
|
117
|
+
// reset search
|
|
118
|
+
this._row = 0;
|
|
119
|
+
this._column = -1;
|
|
120
|
+
}
|
|
121
|
+
this._query = query;
|
|
122
|
+
|
|
123
|
+
// check if the match is in current viewport
|
|
124
|
+
|
|
125
|
+
const minRow = this._grid.scrollY / this._grid.defaultSizes.rowHeight;
|
|
126
|
+
const maxRow =
|
|
127
|
+
(this._grid.scrollY + this._grid.pageHeight) /
|
|
128
|
+
this._grid.defaultSizes.rowHeight;
|
|
129
|
+
const minColumn =
|
|
130
|
+
this._grid.scrollX / this._grid.defaultSizes.columnHeaderHeight;
|
|
131
|
+
const maxColumn =
|
|
132
|
+
(this._grid.scrollX + this._grid.pageWidth) /
|
|
133
|
+
this._grid.defaultSizes.columnHeaderHeight;
|
|
134
|
+
const isInViewport = (row: number, column: number) => {
|
|
135
|
+
return (
|
|
136
|
+
row >= minRow &&
|
|
137
|
+
row <= maxRow &&
|
|
138
|
+
column >= minColumn &&
|
|
139
|
+
column <= maxColumn
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const increment = reverse ? -1 : 1;
|
|
144
|
+
this._column += increment;
|
|
145
|
+
for (
|
|
146
|
+
let row = this._row;
|
|
147
|
+
reverse ? row >= 0 : row < rowCount;
|
|
148
|
+
row += increment
|
|
149
|
+
) {
|
|
150
|
+
for (
|
|
151
|
+
let col = this._column;
|
|
152
|
+
reverse ? col >= 0 : col < columnCount;
|
|
153
|
+
col += increment
|
|
154
|
+
) {
|
|
155
|
+
const cellData = model.data('body', row, col) as string;
|
|
156
|
+
if (cellData.match(query)) {
|
|
157
|
+
// to update the background of matching cells.
|
|
158
|
+
|
|
159
|
+
// TODO: we only really need to invalidate the previous and current
|
|
160
|
+
// cell rects, not the entire grid.
|
|
161
|
+
this._changed.emit(undefined);
|
|
162
|
+
|
|
163
|
+
if (!isInViewport(row, col)) {
|
|
164
|
+
this._grid.scrollToRow(row);
|
|
165
|
+
}
|
|
166
|
+
this._row = row;
|
|
167
|
+
this._column = col;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this._column = reverse ? columnCount - 1 : 0;
|
|
172
|
+
}
|
|
173
|
+
// We've finished searching all the way to the limits of the grid. If this
|
|
174
|
+
// is the first time through (looping is true), wrap the indices and search
|
|
175
|
+
// again. Otherwise, give up.
|
|
176
|
+
if (this._looping) {
|
|
177
|
+
this._looping = false;
|
|
178
|
+
this._row = reverse ? 0 : rowCount - 1;
|
|
179
|
+
this._wrapRows(reverse);
|
|
180
|
+
try {
|
|
181
|
+
return this.find(query, reverse);
|
|
182
|
+
} finally {
|
|
183
|
+
this._looping = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wrap indices if needed to just before the start or just after the end.
|
|
191
|
+
*/
|
|
192
|
+
private _wrapRows(reverse = false) {
|
|
193
|
+
const model = this._grid.dataModel!;
|
|
194
|
+
const rowCount = model.rowCount('body');
|
|
195
|
+
const columnCount = model.columnCount('body');
|
|
196
|
+
|
|
197
|
+
if (reverse && this._row <= 0) {
|
|
198
|
+
// if we are at the front, wrap to just past the end.
|
|
199
|
+
this._row = rowCount - 1;
|
|
200
|
+
this._column = columnCount;
|
|
201
|
+
} else if (!reverse && this._row >= rowCount - 1) {
|
|
202
|
+
// if we are at the end, wrap to just before the front.
|
|
203
|
+
this._row = 0;
|
|
204
|
+
this._column = -1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get query(): RegExp | null {
|
|
209
|
+
return this._query;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _grid: DataGridModule.DataGrid;
|
|
213
|
+
private _query: RegExp | null;
|
|
214
|
+
private _row: number;
|
|
215
|
+
private _column: number;
|
|
216
|
+
private _looping = true;
|
|
217
|
+
private _changed = new Signal<GridSearchService, void>(this);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A viewer for CSV tables.
|
|
222
|
+
*/
|
|
223
|
+
export class CSVViewer extends Widget {
|
|
224
|
+
/**
|
|
225
|
+
* Construct a new CSV viewer.
|
|
226
|
+
*/
|
|
227
|
+
constructor(options: CSVViewer.IOptions) {
|
|
228
|
+
super();
|
|
229
|
+
|
|
230
|
+
this._context = options.context;
|
|
231
|
+
this.layout = new PanelLayout();
|
|
232
|
+
|
|
233
|
+
this.addClass(CSV_CLASS);
|
|
234
|
+
|
|
235
|
+
void this.initialize();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected async initialize(): Promise<void> {
|
|
239
|
+
const layout = this.layout as PanelLayout;
|
|
240
|
+
if (this.isDisposed || !layout) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const { BasicKeyHandler, BasicMouseHandler, DataGrid } =
|
|
244
|
+
await Private.ensureDataGrid();
|
|
245
|
+
this._defaultStyle = DataGrid.defaultStyle;
|
|
246
|
+
this._grid = new DataGrid({
|
|
247
|
+
defaultSizes: {
|
|
248
|
+
rowHeight: 24,
|
|
249
|
+
columnWidth: 144,
|
|
250
|
+
rowHeaderWidth: 64,
|
|
251
|
+
columnHeaderHeight: 36
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
this._grid.addClass(CSV_GRID_CLASS);
|
|
255
|
+
this._grid.headerVisibility = 'all';
|
|
256
|
+
this._grid.keyHandler = new BasicKeyHandler();
|
|
257
|
+
this._grid.mouseHandler = new BasicMouseHandler();
|
|
258
|
+
this._grid.copyConfig = {
|
|
259
|
+
separator: '\t',
|
|
260
|
+
format: DataGrid.copyFormatGeneric,
|
|
261
|
+
headers: 'all',
|
|
262
|
+
warningThreshold: 1e6
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
layout.addWidget(this._grid);
|
|
266
|
+
|
|
267
|
+
this._searchService = new GridSearchService(this._grid);
|
|
268
|
+
this._searchService.changed.connect(this._updateRenderer, this);
|
|
269
|
+
|
|
270
|
+
await this._context.ready;
|
|
271
|
+
await this._updateGrid();
|
|
272
|
+
this._revealed.resolve(undefined);
|
|
273
|
+
// Throttle the rendering rate of the widget.
|
|
274
|
+
this._monitor = new ActivityMonitor({
|
|
275
|
+
signal: this._context.model.contentChanged,
|
|
276
|
+
timeout: RENDER_TIMEOUT
|
|
277
|
+
});
|
|
278
|
+
this._monitor.activityStopped.connect(this._updateGrid, this);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The CSV widget's context.
|
|
283
|
+
*/
|
|
284
|
+
get context(): DocumentRegistry.Context {
|
|
285
|
+
return this._context;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A promise that resolves when the csv viewer is ready to be revealed.
|
|
290
|
+
*/
|
|
291
|
+
get revealed(): Promise<void> {
|
|
292
|
+
return this._revealed.promise;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The delimiter for the file.
|
|
297
|
+
*/
|
|
298
|
+
get delimiter(): string {
|
|
299
|
+
return this._delimiter;
|
|
300
|
+
}
|
|
301
|
+
set delimiter(value: string) {
|
|
302
|
+
if (value === this._delimiter) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this._delimiter = value;
|
|
306
|
+
void this._updateGrid();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The style used by the data grid.
|
|
311
|
+
*/
|
|
312
|
+
get style(): DataGridModule.DataGrid.Style {
|
|
313
|
+
return this._grid.style;
|
|
314
|
+
}
|
|
315
|
+
set style(value: DataGridModule.DataGrid.Style) {
|
|
316
|
+
this._grid.style = { ...this._defaultStyle, ...value };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The config used to create text renderer.
|
|
321
|
+
*/
|
|
322
|
+
set rendererConfig(rendererConfig: TextRenderConfig) {
|
|
323
|
+
this._baseRenderer = rendererConfig;
|
|
324
|
+
void this._updateRenderer();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The search service
|
|
329
|
+
*/
|
|
330
|
+
get searchService(): GridSearchService {
|
|
331
|
+
return this._searchService;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Dispose of the resources used by the widget.
|
|
336
|
+
*/
|
|
337
|
+
dispose(): void {
|
|
338
|
+
if (this._monitor) {
|
|
339
|
+
this._monitor.dispose();
|
|
340
|
+
}
|
|
341
|
+
super.dispose();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Go to line
|
|
346
|
+
*/
|
|
347
|
+
goToLine(lineNumber: number): void {
|
|
348
|
+
this._grid.scrollToRow(lineNumber);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Handle `'activate-request'` messages.
|
|
353
|
+
*/
|
|
354
|
+
protected onActivateRequest(msg: Message): void {
|
|
355
|
+
this.node.tabIndex = -1;
|
|
356
|
+
this.node.focus();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Create the model for the grid.
|
|
361
|
+
*/
|
|
362
|
+
private async _updateGrid(): Promise<void> {
|
|
363
|
+
const { BasicSelectionModel } = await Private.ensureDataGrid();
|
|
364
|
+
const { DSVModel } = await Private.ensureDSVModel();
|
|
365
|
+
const data: string = this._context.model.toString();
|
|
366
|
+
const delimiter = this._delimiter;
|
|
367
|
+
const oldModel = this._grid.dataModel as DSVModelModule.DSVModel;
|
|
368
|
+
const dataModel = (this._grid.dataModel = new DSVModel({
|
|
369
|
+
data,
|
|
370
|
+
delimiter
|
|
371
|
+
}));
|
|
372
|
+
this._grid.selectionModel = new BasicSelectionModel({ dataModel });
|
|
373
|
+
if (oldModel) {
|
|
374
|
+
oldModel.dispose();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Update the renderer for the grid.
|
|
380
|
+
*/
|
|
381
|
+
private async _updateRenderer(): Promise<void> {
|
|
382
|
+
if (this._baseRenderer === null) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const { TextRenderer } = await Private.ensureDataGrid();
|
|
386
|
+
const rendererConfig = this._baseRenderer;
|
|
387
|
+
const renderer = new TextRenderer({
|
|
388
|
+
textColor: rendererConfig.textColor,
|
|
389
|
+
horizontalAlignment: rendererConfig.horizontalAlignment,
|
|
390
|
+
backgroundColor:
|
|
391
|
+
this._searchService.cellBackgroundColorRendererFunc(rendererConfig)
|
|
392
|
+
});
|
|
393
|
+
this._grid.cellRenderers.update({
|
|
394
|
+
body: renderer,
|
|
395
|
+
'column-header': renderer,
|
|
396
|
+
'corner-header': renderer,
|
|
397
|
+
'row-header': renderer
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private _context: DocumentRegistry.Context;
|
|
402
|
+
private _grid: DataGridModule.DataGrid;
|
|
403
|
+
private _defaultStyle: typeof DataGridModule.DataGrid.defaultStyle;
|
|
404
|
+
private _searchService: GridSearchService;
|
|
405
|
+
private _monitor: ActivityMonitor<DocumentRegistry.IModel, void> | null =
|
|
406
|
+
null;
|
|
407
|
+
private _delimiter = ',';
|
|
408
|
+
private _revealed = new PromiseDelegate<void>();
|
|
409
|
+
private _baseRenderer: TextRenderConfig | null = null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A namespace for `CSVViewer` statics.
|
|
414
|
+
*/
|
|
415
|
+
export namespace CSVViewer {
|
|
416
|
+
/**
|
|
417
|
+
* Instantiation options for CSV widgets.
|
|
418
|
+
*/
|
|
419
|
+
export interface IOptions {
|
|
420
|
+
/**
|
|
421
|
+
* The document context for the CSV being rendered by the widget.
|
|
422
|
+
*/
|
|
423
|
+
context: DocumentRegistry.Context;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A document widget for CSV content widgets.
|
|
429
|
+
*/
|
|
430
|
+
export class CSVDocumentWidget extends DocumentWidget<CSVViewer> {
|
|
431
|
+
constructor(options: CSVDocumentWidget.IOptions) {
|
|
432
|
+
let { content, context, delimiter, reveal, ...other } = options;
|
|
433
|
+
content = content || Private.createContent(context);
|
|
434
|
+
reveal = Promise.all([reveal, content.revealed]);
|
|
435
|
+
super({ content, context, reveal, ...other });
|
|
436
|
+
|
|
437
|
+
if (delimiter) {
|
|
438
|
+
content.delimiter = delimiter;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Set URI fragment identifier for rows
|
|
444
|
+
*/
|
|
445
|
+
setFragment(fragment: string): void {
|
|
446
|
+
const parseFragments = fragment.split('=');
|
|
447
|
+
|
|
448
|
+
// TODO: expand to allow columns and cells to be selected
|
|
449
|
+
// reference: https://tools.ietf.org/html/rfc7111#section-3
|
|
450
|
+
if (parseFragments[0] !== '#row') {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// multiple rows, separated by semi-colons can be provided, we will just
|
|
455
|
+
// go to the top one
|
|
456
|
+
let topRow = parseFragments[1].split(';')[0];
|
|
457
|
+
|
|
458
|
+
// a range of rows can be provided, we will take the first value
|
|
459
|
+
topRow = topRow.split('-')[0];
|
|
460
|
+
|
|
461
|
+
// go to that row
|
|
462
|
+
void this.context.ready.then(() => {
|
|
463
|
+
this.content.goToLine(Number(topRow));
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export namespace CSVDocumentWidget {
|
|
469
|
+
// TODO: In TypeScript 2.8, we can make just the content property optional
|
|
470
|
+
// using something like https://stackoverflow.com/a/46941824, instead of
|
|
471
|
+
// inheriting from this IOptionsOptionalContent.
|
|
472
|
+
|
|
473
|
+
export interface IOptions
|
|
474
|
+
extends DocumentWidget.IOptionsOptionalContent<CSVViewer> {
|
|
475
|
+
/**
|
|
476
|
+
* Data delimiter character
|
|
477
|
+
*/
|
|
478
|
+
delimiter?: string;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* A widget factory for CSV widgets.
|
|
484
|
+
*/
|
|
485
|
+
export class CSVViewerFactory extends ABCWidgetFactory<
|
|
486
|
+
IDocumentWidget<CSVViewer>
|
|
487
|
+
> {
|
|
488
|
+
/**
|
|
489
|
+
* Create a new widget given a context.
|
|
490
|
+
*/
|
|
491
|
+
protected createNewWidget(
|
|
492
|
+
context: DocumentRegistry.Context
|
|
493
|
+
): IDocumentWidget<CSVViewer> {
|
|
494
|
+
const translator = this.translator;
|
|
495
|
+
return new CSVDocumentWidget({ context, translator });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Default factory for toolbar items to be added after the widget is created.
|
|
500
|
+
*/
|
|
501
|
+
protected defaultToolbarFactory(
|
|
502
|
+
widget: IDocumentWidget<CSVViewer>
|
|
503
|
+
): DocumentRegistry.IToolbarItem[] {
|
|
504
|
+
return [
|
|
505
|
+
{
|
|
506
|
+
name: 'delimiter',
|
|
507
|
+
widget: new CSVDelimiter({
|
|
508
|
+
widget: widget.content,
|
|
509
|
+
translator: this.translator
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* A widget factory for TSV widgets.
|
|
518
|
+
*/
|
|
519
|
+
export class TSVViewerFactory extends CSVViewerFactory {
|
|
520
|
+
/**
|
|
521
|
+
* Create a new widget given a context.
|
|
522
|
+
*/
|
|
523
|
+
protected createNewWidget(
|
|
524
|
+
context: DocumentRegistry.Context
|
|
525
|
+
): IDocumentWidget<CSVViewer> {
|
|
526
|
+
const delimiter = '\t';
|
|
527
|
+
return new CSVDocumentWidget({
|
|
528
|
+
context,
|
|
529
|
+
delimiter,
|
|
530
|
+
translator: this.translator
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
namespace Private {
|
|
536
|
+
let gridLoaded: PromiseDelegate<typeof DataGridModule> | null = null;
|
|
537
|
+
let modelLoaded: PromiseDelegate<typeof DSVModelModule> | null = null;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Lazily load the datagrid module when the first grid is requested.
|
|
541
|
+
*/
|
|
542
|
+
export async function ensureDataGrid(): Promise<typeof DataGridModule> {
|
|
543
|
+
if (gridLoaded == null) {
|
|
544
|
+
gridLoaded = new PromiseDelegate();
|
|
545
|
+
gridLoaded.resolve(await import('@lumino/datagrid'));
|
|
546
|
+
}
|
|
547
|
+
return gridLoaded.promise;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function ensureDSVModel(): Promise<typeof DSVModelModule> {
|
|
551
|
+
if (modelLoaded == null) {
|
|
552
|
+
modelLoaded = new PromiseDelegate();
|
|
553
|
+
modelLoaded.resolve(await import('./model'));
|
|
554
|
+
}
|
|
555
|
+
return modelLoaded.promise;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function createContent(
|
|
559
|
+
context: DocumentRegistry.IContext<DocumentRegistry.IModel>
|
|
560
|
+
): CSVViewer {
|
|
561
|
+
return new CSVViewer({ context });
|
|
562
|
+
}
|
|
563
|
+
}
|