-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathtable-scrolling.js
323 lines (283 loc) · 9.37 KB
/
table-scrolling.js
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
// See init-utils.js
if (TEST_MODE) {
// This is used at the end of the hereby file
// Stubs to be able to test without having to load anything else in Node.js
// Normally I keep this disabled cause it interferes with my IDE.
function createGeneTableBody () {}
function createEvidenceTableBody () {}
}
/**
* An helper to manage table infinite scrolling and paging.
*
* It has a {@link setupTableScroller scroll event handler} and methods to manage a table
* pagination view.
*
* We currently use with the gene and evidence table, but the design is generic enough to be
* able to support virtually any table.
*
* @author Marco Brandizi
*
* @since 5.7 (2023-05-14)
*
*/
class InfiniteScrollManager
{
#tableData = null
/**
* The HTML ID of the element that represents the table we manage.
*
* This is used by the {@link setupTableScroller scroll event handler}.
*
* We don't expose this to the outside, since we don't want this class to deal with
* the non-pertinent concern of holding its table reference for others.
*
* Also, this is table-type specific and set by subclasses, by invoking the constructor.
*/
#tableId = null
/**
* The method that {@link setupTableScroller} has to use to display additional rows when necessary.
*
* This is table-type specific and set by subclasses, by invoking the constructor.
*
*/
#tableBodyGenerator = null
#page = 0
#pageSize = 30
/**
* This class is abstract, the constructor raises an exception if you try to
* instantiate it directly. You need to define concrete sublcasses, ad done below.
*/
constructor ( tableId, tableBodyGenerator )
{
if ( this.constructor === InfiniteScrollManager )
throw new TypeError ( "Can't instantiate abstract class InfiniteScrollManager" )
if ( !( typeof tableId == 'string' || tableId == "" ) ) throw TypeError (
"Can't set InfiniteScrollManager with null, empty or non string tableId"
)
if ( typeof tableBodyGenerator == 'function' )
this.#tableId = tableId
this.#tableBodyGenerator = tableBodyGenerator
}
getTableId ()
{
return this.#tableId
}
/**
* The 'raw data' table we have to manage. This is the array of data, not the HTML
* correspondant.
*
* We don't expose this to the outside, since we don't want this class to deal with
* the non-pertinent concern of holding its table reference for others.
*
*/
setTableData ( tableData )
{
if ( !Array.isArray ( tableData ) ) throw new TypeError (
"Can't set InfiniteScrollManager with null or non-array table"
)
if ( tableData.length == 0 ) throw new RangeError (
"Can't set InfiniteScrollManager with empty table."
)
this.#tableData = tableData
this.setPage ( 0 )
}
/**
* Internal method used pretty much everywhere, do some preliminary sanity checks (eg,
* the manage table is set).
*
*/
#validateTableData () {
if ( !this.#tableData ) throw new TypeError (
"InfiniteScrollManager, table not set"
)
}
/**
* The page size for paging and auto-scrolling functions. Default is 30.
*/
getPageSize () {
this.#validateTableData ()
return this.#pageSize
}
/**
* It raises a RangeError if < 1
*/
setPageSize ( pageSize )
{
this.#validateTableData ()
if ( pageSize < 1 ) throw new RangeError (
`Invalid page size of ${pageSize}, must be a positive value`
)
$this.#pageSize = pageSize
}
/**
* The no of available pages for the current table, which, of course is obtained by
* dividing the table size by {@link getPageSize}.
*
*/
getPagesCount ()
{
this.#validateTableData ()
return Math.ceil ( this.#tableData.length / this.#pageSize )
}
/**
* These accessors allows for moving (managing) through the different table pages to
* visusalise.
*
* As usually, this is a page index, ie, it ranges from 0 to the pages count - 1.
*
* In Knetminer, page switching is done by the {@link setupTableScroller scroll event handler}.
*
*/
getPage () {
this.#validateTableData ()
return this.#page
}
/**
* @see getPage
*
* This raises an error if the parameter doesn't fall within
* 0 and {@link getPagesCount getPagesCount() - 1}
*/
setPage ( page )
{
this.#validateTableData ()
const pagesCt = this.getPagesCount ()
const tableId = this.getTableId ()
if ( page >= pagesCt && pagesCt ) throw new RangeError (
`Invalid page value #${page} for table '${tableId}', which has ${pagesCt} page(s)`
)
this.#page = page
}
/**
* Based on the current table, tells, if there is still one more page after the current one.
*/
hasNextPage () {
this.#validateTableData ()
return ( this.#page + 1 ) < this.getPagesCount ()
}
/**
* Helper that uses {@link setPage} to advance from the current page to the next.
*
* Which implies it raises an exception if you're already at the last page.
*/
goNextPage ()
{
this.#validateTableData ()
this.setPage ( this.#page + 1 )
return this.getPage ()
}
/**
* The table row index at which the {@link getPage current page} starts.
*
* As usually for indices, this starts at 0.
*
*/
getPageStart ()
{
this.#validateTableData ()
return this.#page * this.#pageSize
}
/**
* The table row *limit* at which the {@link getPage current page} ends.
*
* As usually for indices, this is the index of the last row in the page *plus* 1. This
* inclusive/exclusive range ease the use of these methods in loops and the like.
*/
getPageEnd ()
{
this.#validateTableData ()
const result = ( this.#page + 1 ) * this.#pageSize
return Math.min ( result, this.#tableData.length )
}
/**
* Deploys the table scroll event handler that is used to rendere one more page each time the
* table's scroll bar reaches the bottom.
*
* This uses {@link #tableId} and {@link #tableBodyGenerator} to manipulate the current table,
* and the paging-related methods to establing when new rows need to be displayed and how many.
*/
setupScrollHandler ()
{
this.#validateTableData ()
const jqTable = $(`#${ this.#tableId }` )
var timer = null
// Else, they're not visible below
// #tableData also requires dynamic access, for it might change (eg, via filtering) after
// the hereby scroll handler has been already set and the timer below launched.
const tableId = this.#tableId
const tableDataAccessor = () => this.#tableData
const tableBodyGenerator = this.#tableBodyGenerator
jqTable.scroll ( () => {
// Clear/set used to throttle the scroll event firing:
// https://stackoverflow.com/a/29654601/529286
// The scroll event clears any previous timeout for a while, nothing happens until the
// delayed timeout handler below fires too.
//
clearTimeout ( timer )
timer = setTimeout ( () =>
{
// TODO: can't we use jqTable here?
const tableElem = document.getElementById ( tableId );
const needsMoreRows = tableElem.scrollTop + tableElem.offsetHeight >= tableElem.scrollHeight
if ( !needsMoreRows ) return
if ( !this.hasNextPage () ) return
this.goNextPage ()
tableBodyGenerator ( tableDataAccessor(), true )
}, 300 ) // setTimeout
}) // scroll()
} // scrollHandler ()
}
/**
* As said above, we have specific subclasses and singletons for the gene table and
* the evidence table.
*/
class GenesTableScroller extends InfiniteScrollManager
{
constructor () {
super ( 'geneViewTable', createGeneTableBody )
}
}
/**
* As said above, we have specific subclasses and singletons for the gene table and
* the evidence table.
*/
class EvidenceTableScroller extends InfiniteScrollManager
{
constructor () {
super ( 'evidenceViewTable', createEvidenceTableBody )
}
}
/**
* As said above, we have specific subclasses and singletons for the gene table and
* the evidence table.
*/
const genesTableScroller = Object.freeze ( new GenesTableScroller () )
const evidenceTableScroller = Object.freeze ( new EvidenceTableScroller () )
/**
* TODO: I wrote this to run them manually, needs to be rewritten for Jest (or other framework) and integrated
* into Maven with the Jasmine plug-in
*
*/
if ( TEST_MODE )
{
const testTable = Array ( 100 ).fill ( [ 'fooRowValue' ] )
genesTableScroller.setTableData ( testTable )
console.assert ( genesTableScroller.getPageSize () == 30, "page size wrong!" )
console.assert (
genesTableScroller.getPagesCount () == Math.ceil ( testTable.length / 30 ), "pages count wrong!"
)
console.assert (
genesTableScroller.getPagesCount () == Math.ceil ( testTable.length / 30 ), "pages count wrong!"
)
console.assert ( genesTableScroller.getPageStart () == 0, "page start wrong!" )
console.assert ( genesTableScroller.getPageEnd () == 30, "page end wrong!" )
genesTableScroller.setPage ( 2 )
console.assert ( genesTableScroller.getPageStart () == 30 * 2, "page start at page 2 is wrong!" )
console.assert ( genesTableScroller.getPageEnd () == 30 * 3, "page end at page 2 is wrong!" )
console.assert ( genesTableScroller.hasNextPage (), "hasNextPage() is wrong!" )
console.assert ( genesTableScroller.goNextPage () == 3, "goNextPage() is wrong!" )
console.assert ( genesTableScroller.getPage () == 3, "getPage () after goNextPage() is wrong!" )
genesTableScroller.setPage ( genesTableScroller.getPagesCount () - 1 )
console.assert ( genesTableScroller.getPageEnd () == testTable.length, "page end at pageCount is wrong!" )
console.assert ( !genesTableScroller.hasNextPage (), "hasNextPage() at last page is true!" )
}