-
Notifications
You must be signed in to change notification settings - Fork 14
/
virtualizedTable.ts
422 lines (386 loc) · 13.6 KB
/
virtualizedTable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
import type React from "react";
import type ReactDOM from "react-dom";
import type { createRoot } from "react-dom/client";
import type { IntlProvider } from "react-intl";
import { BasicTool } from "../basic.js";
/**
* VirtualizedTable helper.
*/
export class VirtualizedTableHelper extends BasicTool {
public props: VirtualizedTableProps;
public localeStrings: { [name: string]: string };
public containerId!: string;
public treeInstance!: VirtualizedTable;
private window: Window;
private React: typeof React;
private ReactDOM: typeof ReactDOM & { createRoot: typeof createRoot };
private VirtualizedTable: VirtualizedTableConstructor;
private IntlProvider: typeof IntlProvider;
constructor(win: Window) {
super();
this.window = win;
const Zotero = this.getGlobal("Zotero");
// eslint-disable-next-line ts/no-unsafe-function-type
const _require = (win as any).require as Function;
// Don't actually use any React instance, so that it won't be actually compiled.
this.React = _require("react");
this.ReactDOM = _require("react-dom");
this.VirtualizedTable = _require("components/virtualized-table");
this.IntlProvider = _require("react-intl").IntlProvider;
this.props = {
id: `${Zotero.Utilities.randomString()}-${new Date().getTime()}`,
getRowCount: () => 0,
};
this.localeStrings = Zotero.Intl.strings;
}
/**
* Set properties by name.
* @remarks
* `id` and `getRowCount` are required.
* If `id` is not set, it's a random string.
* @param propName Property name
* @param propValue Property value
*/
public setProp<
K extends keyof VirtualizedTableProps,
V extends VirtualizedTableProps[K],
>(propName: K, propValue: V): VirtualizedTableHelper;
/**
* Set properties object.
* @remarks
* `id` and `getRowCount` are required.
* If `id` is not set, it's a random string.
* @param data property object.
* @remarks
* All available properties:
* ```ts
* interface VirtualizedTableProps {
* id: string;
* getRowCount: () => number;
* getRowData?: (index: number) => { [dataKey: string]: string };
* // Use `getRowData` instead. This property is generated automatically.
* renderItem?: (
* index: number,
* selection: TreeSelection,
* oldElem: HTMLElement,
* columns: ColumnOptions[]
* ) => Node;
* // Row height specified as lines of text per row. Defaults to 1
* linesPerRow?: number;
* // Do not adjust for Zotero-defined font scaling
* disableFontSizeScaling?: boolean;
* // An array of two elements for alternating row colors
* alternatingRowColors?: Array<string>;
* // For screen-readers
* label?: string;
* role?: string;
* showHeader?: boolean;
* // Array of column objects like the ones in itemTreeColumns.js
* columns?: Array<ColumnOptions>;
* onColumnPickerMenu?: (event: Event) => void;
* onColumnSort?: (columnIndex: number, ascending: 1 | -1) => void;
* getColumnPrefs?: () => { [dataKey: string]: any };
* storeColumnPrefs?: (prefs: { [dataKey: string]: any }) => void;
* getDefaultColumnOrder?: () => { [dataKey: string]: any };
* // Makes columns unmovable, unsortable, etc
* staticColumns?: boolean;
* // Used for initial column widths calculation
* containerWidth?: number;
* // Internal windowed-list ref
* treeboxRef?: (innerWindowedList: WindowedList) => any;
* // Render with display?: none
* hide?: boolean;
* multiSelect?: boolean;
* onSelectionChange?: (
* selection: TreeSelection,
* shouldDebounce: boolean
* ) => void;
* // The below are for arrow-key navigation
* isSelectable?: (index: number) => boolean;
* getParentIndex?: (index: number) => number;
* isContainer?: (index: number) => boolean;
* isContainerEmpty?: (index: number) => boolean;
* isContainerOpen?: (index: number) => boolean;
* toggleOpenState?: (index: number) => void;
* // A function with signature (index?:Number) => result?:String which will be used
* // for find-as-you-type navigation. Find-as-you-type is disabled if prop is undefined.
* getRowString?: (index: number) => string;
* // If you want to perform custom key handling it should be in this function
* // if it returns false then virtualized-table's own key handler won't run
* onKeyDown?: (e: Event) => boolean;
* onKeyUp?: (e: Event) => boolean;
* onDragOver?: (e: Event) => boolean;
* onDrop?: (e: Event) => boolean;
* // Enter, double-clicking
* onActivate?: (e: Event) => boolean;
* onFocus?: (e: Event) => boolean;
* onItemContextMenu?: (e: Event, x: number, y: number) => boolean;
* }
* ```
*/
public setProp(data: Partial<VirtualizedTableProps>): VirtualizedTableHelper;
public setProp(...args: any[]) {
if (args.length === 1) {
Object.assign(this.props, args[0]);
} else if (args.length === 2) {
(this.props[args[0] as keyof VirtualizedTableProps] as unknown) = args[1];
}
return this;
}
/**
* Set locale strings, which replaces the table header's label if matches. Default it's `Zotero.Intl.strings`
* @param localeStrings
*/
public setLocale(localeStrings: { [name: string]: string }) {
Object.assign(this.localeStrings, localeStrings);
return this;
}
/**
* Set container element id that the table will be rendered on.
* @param id element id
*/
public setContainerId(id: string) {
this.containerId = id;
return this;
}
/**
* Render the table.
* @param selectId Which row to select after rendering
* @param onfulfilled callback after successfully rendered
* @param onrejected callback after rendering with error
*/
public render(
selectId?: number,
onfulfilled?: (value: unknown) => unknown,
onrejected?: (reason: any) => PromiseLike<never>,
) {
const refreshSelection = () => {
this.treeInstance.invalidate();
if (typeof selectId !== "undefined" && selectId >= 0) {
this.treeInstance.selection.select(selectId);
} else {
this.treeInstance.selection.clearSelection();
}
};
if (!this.treeInstance) {
new Promise((resolve) => {
const vtableProps = Object.assign({}, this.props, {
ref: (ref: VirtualizedTable) => {
this.treeInstance = ref;
resolve(void 0);
},
});
if (vtableProps.getRowData && !vtableProps.renderItem) {
Object.assign(vtableProps, {
renderItem: this.VirtualizedTable.makeRowRenderer(
vtableProps.getRowData,
),
});
}
const elem = this.React.createElement(
this.IntlProvider,
{ locale: Zotero.locale, messages: Zotero.Intl.strings },
this.React.createElement(this.VirtualizedTable, vtableProps),
);
const container = this.window.document.getElementById(this.containerId);
this.ReactDOM.createRoot(container!).render(elem);
})
.then(() => {
// Fix style manager showing partially blank until scrolled
this.getGlobal("setTimeout")(() => {
refreshSelection();
});
})
.then(onfulfilled, onrejected);
} else {
refreshSelection();
}
return this;
}
}
export interface ColumnOptions {
dataKey: string;
label: string;
iconLabel?: React.ReactElement;
defaultSort?: 1 | -1;
flex?: number;
width?: number;
fixedWidth?: boolean;
staticWidth?: boolean;
minWidth?: number;
ignoreInColumnPicker?: boolean;
submenu?: boolean;
}
interface VirtualizedTableProps {
id: string;
getRowCount: () => number;
getRowData?: (index: number) => { [dataKey: string]: string };
/**
* Use `getRowData` instead. This property is generated automatically.
* @param index
* @param selection
* @param oldElem
* @param columns
*/
renderItem?: (
index: number,
selection: TreeSelection,
oldElem: HTMLElement,
columns: ColumnOptions[],
) => Node;
// Row height specified as lines of text per row. Defaults to 1
linesPerRow?: number;
// Do not adjust for Zotero-defined font scaling
disableFontSizeScaling?: boolean;
// An array of two elements for alternating row colors
alternatingRowColors?: Array<string>;
// For screen-readers
label?: string;
role?: string;
showHeader?: boolean;
// Array of column objects like the ones in itemTreeColumns.js
columns?: Array<ColumnOptions>;
onColumnPickerMenu?: (event: Event) => void;
onColumnSort?: (columnIndex: number, ascending: 1 | -1) => void;
getColumnPrefs?: () => { [dataKey: string]: any };
storeColumnPrefs?: (prefs: { [dataKey: string]: any }) => void;
getDefaultColumnOrder?: () => { [dataKey: string]: any };
// Makes columns unmovable, unsortable, etc
staticColumns?: boolean;
// Used for initial column widths calculation
containerWidth?: number;
// Internal windowed-list ref
treeboxRef?: (innerWindowedList: WindowedList) => any;
// Render with display?: none
hide?: boolean;
multiSelect?: boolean;
onSelectionChange?: (
selection: TreeSelection,
shouldDebounce: boolean,
) => void;
// The below are for arrow-key navigation
isSelectable?: (index: number) => boolean;
getParentIndex?: (index: number) => number;
isContainer?: (index: number) => boolean;
isContainerEmpty?: (index: number) => boolean;
isContainerOpen?: (index: number) => boolean;
toggleOpenState?: (index: number) => void;
// A function with signature (index?:Number) => result?:String which will be used
// for find-as-you-type navigation. Find-as-you-type is disabled if prop is undefined.
getRowString?: (index: number) => string;
// If you want to perform custom key handling it should be in this function
// if it returns false then virtualized-table's own key handler won't run
onKeyDown?: (e: KeyboardEvent) => boolean;
onKeyUp?: (e: KeyboardEvent) => boolean;
onDragOver?: (e: DragEvent) => boolean;
onDrop?: (e: DragEvent) => boolean;
// Enter, double-clicking
onActivate?: (e: MouseEvent) => boolean;
onFocus?: (e: FocusEvent) => boolean;
onItemContextMenu?: (
e: MouseEvent | KeyboardEvent,
x: number,
y: number,
) => boolean;
}
interface VirtualizedTableConstructor
extends React.ComponentClass<VirtualizedTableProps, object> {
renderCell: (
index: number,
data: string,
column: HTMLElement,
dir?: string,
) => HTMLSpanElement;
renderCheckboxCell: (
index: number,
data: string,
column: HTMLElement,
dir?: string,
) => HTMLSpanElement;
makeRowRenderer: (
getRowData: (index: number) => { [dataKey: string]: string },
) => (
index: number,
selection: any,
oldDiv: HTMLDivElement,
columns: HTMLElement,
) => any;
formatColumnName: (column: HTMLElement) => string;
}
interface VirtualizedTable
extends React.Component<VirtualizedTableProps, object> {
selection: TreeSelection;
invalidate: () => void;
}
/**
* Somewhat corresponds to nsITreeSelection
* https://udn.realityripple.com/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsITreeSelection
*
* @property pivot {Number} The selection "pivot". This is the first item the user selected as part of
* a ranged select (i.e. shift-select).
* @property focused {Number} The currently selected/focused item.
* @property count {Number} The number of selected items
* @property selected {Set} The set of currently selected items
* @property selectEventsSuppressed {Boolean} Controls whether select events are triggered on selection change.
*/
interface TreeSelection {
_tree: VirtualizedTable;
pivot: number;
focused: number;
selected: Set<number>;
_selectEventsSuppressed: boolean;
/**
* @param tree {VirtualizedTable} The tree where selection occurs. Will be used to issue
* updates.
*/
new (tree: VirtualizedTable): this;
/**
* Returns whether the given index is selected.
* @param index {Number} The index is 0-clamped.
* @returns {boolean}
*/
isSelected: (index: number) => boolean;
/**
* Toggles an item's selection state, updates focused item to index.
* @param index {Number} The index is 0-clamped.
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
*/
toggleSelect: (index: number, shouldDebounce?: boolean) => void;
clearSelection: () => void;
/**
* Selects an item, updates focused item to index.
* @param index {Number} The index is 0-clamped.
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
* @returns {boolean} False if nothing to select and select handlers won't be called
*/
select: (index: number, shouldDebounce?: boolean) => boolean;
rangedSelect: (
from: number,
to: number,
augment: boolean,
isSelectAll: boolean,
) => void;
/**
* Performs a shift-select from current pivot to provided index. Updates focused item to index.
* @param index {Number} The index is 0-clamped.
* @param augment {Boolean} Adds to existing selection if true
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
*/
shiftSelect: (
index: number,
augment: boolean,
shouldDebounce?: boolean,
) => void;
/**
* Calls the onSelectionChange prop on the tree
* @param shouldDebounce {Boolean} Whether the update to the tree should be debounced
* @private
*/
_updateTree: (shouldDebounce?: boolean) => void;
get count(): number;
get selectEventsSuppressed(): boolean;
set selectEventsSuppressed(val: boolean);
}
interface WindowedList {
[key: string | number | symbol]: any;
}