datavis-glide 4.0.0-PRE.0

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.
Files changed (62) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +129 -0
  3. package/datavis.js +101 -0
  4. package/dist/wcdatavis.css +1957 -0
  5. package/dist/wcdatavis.min.js +1 -0
  6. package/global-jquery.js +4 -0
  7. package/ie-fixes.js +13 -0
  8. package/index.js +70 -0
  9. package/meteor.js +1 -0
  10. package/package.json +102 -0
  11. package/src/flags.js +6 -0
  12. package/src/graph.js +1079 -0
  13. package/src/graph_renderer.js +85 -0
  14. package/src/grid.js +2777 -0
  15. package/src/grid_control.js +1957 -0
  16. package/src/grid_filter.js +1073 -0
  17. package/src/grid_renderer.js +276 -0
  18. package/src/group_fun_win.js +121 -0
  19. package/src/lang/en-US.js +188 -0
  20. package/src/lang/es-MX.js +188 -0
  21. package/src/lang/fr-FR.js +188 -0
  22. package/src/lang/id-ID.js +188 -0
  23. package/src/lang/nl-NL.js +188 -0
  24. package/src/lang/pt-BR.js +188 -0
  25. package/src/lang/ru-RU.js +188 -0
  26. package/src/lang/th-TH.js +188 -0
  27. package/src/lang/vi-VN.js +188 -0
  28. package/src/lang/zh-Hans-CN.js +188 -0
  29. package/src/operations_palette.js +176 -0
  30. package/src/prefs_modules.js +132 -0
  31. package/src/reg/graph_renderer.js +17 -0
  32. package/src/renderers/graph/chartjs.js +457 -0
  33. package/src/renderers/graph/google.js +584 -0
  34. package/src/renderers/graph/jit.js +61 -0
  35. package/src/renderers/graph/svelte-gantt.js +168 -0
  36. package/src/renderers/grid/dummy.js +79 -0
  37. package/src/renderers/grid/handlebars.js +217 -0
  38. package/src/renderers/grid/squirrelly.js +215 -0
  39. package/src/renderers/grid/table/group_detail.js +1404 -0
  40. package/src/renderers/grid/table/group_summary.js +380 -0
  41. package/src/renderers/grid/table/pivot.js +915 -0
  42. package/src/renderers/grid/table/plain.js +1592 -0
  43. package/src/renderers/grid/table.js +2510 -0
  44. package/src/trans.js +101 -0
  45. package/src/ui/collapsible.js +234 -0
  46. package/src/ui/filters/date.js +283 -0
  47. package/src/ui/grid_filter.js +398 -0
  48. package/src/ui/popup_menu.js +224 -0
  49. package/src/ui/popup_window.js +572 -0
  50. package/src/ui/slider.js +156 -0
  51. package/src/ui/tabs.js +202 -0
  52. package/src/ui/templates.js +131 -0
  53. package/src/ui/toolbar.js +63 -0
  54. package/src/ui/toolbars/grid.js +873 -0
  55. package/src/ui/windows/col_config.js +341 -0
  56. package/src/ui/windows/debug.js +164 -0
  57. package/src/ui/windows/grid_table_opts.js +139 -0
  58. package/src/util/handlebars.js +158 -0
  59. package/src/util/jquery.js +630 -0
  60. package/src/util/misc.js +1058 -0
  61. package/src/util/squirrelly.js +155 -0
  62. package/wcdatavis.css +1601 -0
@@ -0,0 +1,1592 @@
1
+ // Imports {{{1
2
+
3
+ import _ from 'underscore';
4
+ import sprintf from 'sprintf-js';
5
+ import jQuery from 'jquery';
6
+
7
+ import { trans } from '../../../trans.js';
8
+ import {
9
+ addFocusHandler,
10
+ removeFocusHandler,
11
+ determineColumns,
12
+ icon,
13
+ format,
14
+ gensym,
15
+ getProp,
16
+ getPropDef,
17
+ isElement,
18
+ isElementInViewport,
19
+ isVisible,
20
+ makeOperationButton,
21
+ makeSubclass,
22
+ mixinLogging,
23
+ onVisibilityChange,
24
+ setTableCell,
25
+ } from '../../../util/misc.js';
26
+
27
+ import {AggregateInfo, ComputedView, Source} from 'datavis-ace';
28
+ import {GridFilterSet} from '../../../grid_filter.js';
29
+ import {GridRenderer} from '../../../grid_renderer.js';
30
+
31
+ import GridTable from '../table.js';
32
+ import Slider from '../../../ui/slider.js';
33
+
34
+ // GridTablePlain {{{1
35
+ // Constructor {{{2
36
+
37
+ /**
38
+ * The GridTablePlain is in charge of displaying the HTML table of data.
39
+ *
40
+ * @class
41
+ * @extends GridTable
42
+ *
43
+ * @property {Grid~Features} features
44
+ *
45
+ * @property {object} defn
46
+ *
47
+ * @property {ComputedView} view
48
+ *
49
+ * @property {Element} root
50
+ *
51
+ * @property {object} colConfig Map associating field name with the configuration of the
52
+ * corresponding column in this grid table.
53
+ *
54
+ * @property {Timing} timing
55
+ *
56
+ * @property {boolean} needsRedraw True if the grid needs to redraw itself when the view is done
57
+ * working.
58
+ */
59
+
60
+ var GridTablePlain = makeSubclass('GridTablePlain', GridTable, function (grid, defn, view, features, opts, timing, id) {
61
+ var self = this;
62
+
63
+ self.super['GridTable'].ctor.apply(self, arguments);
64
+
65
+ self.features.filter = false;
66
+
67
+ self._focusEventId = gensym('grid-plain-');
68
+
69
+ // Pagination state.
70
+ self._paginationPage = 0;
71
+ self._paginationRowsPerPage = getPropDef(40, self.defn, 'table', 'pagination', 'rowsPerPage');
72
+
73
+ self.logDebug(self.makeLogTag() + ' DataVis // %s // Constructing grid table; features = %O', self.toString(), features);
74
+
75
+ self.addFilterHandler();
76
+ });
77
+
78
+ mixinLogging(GridTablePlain);
79
+
80
+ // #canRender {{{2
81
+
82
+ /**
83
+ * Responds whether or not this grid table can render the type of data requested.
84
+ *
85
+ * @param {string} what
86
+ * The kind of data the caller wants us to show. Must be one of: plain, group, or pivot.
87
+ *
88
+ * @return {boolean}
89
+ * True if this grid table can render that kind of data, false if it can't.
90
+ */
91
+
92
+ GridTablePlain.prototype.canRender = function (what) {
93
+ return ['plain'].indexOf(what) >= 0;
94
+ };
95
+
96
+ GridTablePlain.prototype.draw = function (root, opts, cont) {
97
+ var self = this;
98
+
99
+ GridTable.prototype.draw.call(self, root, opts, function () {
100
+ if (self.features.activeRow || self.features.omnifilter) {
101
+ self._hasFocus = false;
102
+ addFocusHandler(root, self._focusEventId, function (isFocused) {
103
+ self._hasFocus = isFocused;
104
+ });
105
+ }
106
+
107
+ if (self.features.omnifilter) {
108
+ jQuery(document).on('keydown.omnifilter-' + self._focusEventId, function (evt) {
109
+ var avoidElts = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
110
+
111
+ if (avoidElts.indexOf(evt.target.tagName) >= 0) {
112
+ return; // These elements don't count for turning on the omnifilter.
113
+ }
114
+
115
+ if (!self._hasFocus) {
116
+ return;
117
+ }
118
+
119
+ if (evt.key === 'f') {
120
+ evt.preventDefault();
121
+ evt.stopPropagation();
122
+ if (!self.grid.ui.omnifilter.is(':visible')) {
123
+ self.grid.ui.omnifilterToggle.addClass('wcdv_omnifilter_active');
124
+ self.grid.ui.omnifilter.show();
125
+ }
126
+ self.grid.ui.omnifilterInput.focus();
127
+ }
128
+ });
129
+ }
130
+
131
+ if (self.features.activeRow) {
132
+ if (getProp(self.defn, 'table', 'activeRow', 'slider')) {
133
+ self.ui.slider = new Slider();
134
+ self.ui.slider.on('hide', function () {
135
+ self.clearActiveRow();
136
+ });
137
+ self.ui.slider.draw(root);
138
+ }
139
+
140
+ self.ui.tbody.on('click', 'td', function (evt) {
141
+ var avoidElts = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
142
+
143
+ if (avoidElts.indexOf(evt.target.tagName) >= 0) {
144
+ return; // These elements don't count for setting the active row.
145
+ }
146
+
147
+ self.setActiveRow(jQuery(this).closest('tr'));
148
+ });
149
+
150
+ jQuery(document).on('keydown.active-row-' + self._focusEventId, function (evt) {
151
+ var avoidElts = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
152
+
153
+ if (avoidElts.indexOf(evt.target.tagName) >= 0) {
154
+ return; // These elements don't count for setting the active row.
155
+ }
156
+
157
+ if (!self._hasFocus) {
158
+ return;
159
+ }
160
+
161
+ switch(evt.key.toLowerCase()) {
162
+ case 'j':
163
+ if (self.activeRow) {
164
+ evt.preventDefault();
165
+ self.activeRowNext();
166
+ }
167
+ break;
168
+ case 'k':
169
+ if (self.activeRow) {
170
+ evt.preventDefault();
171
+ self.activeRowPrev();
172
+ }
173
+ break;
174
+ case 'escape':
175
+ self.clearActiveRow();
176
+ break;
177
+ }
178
+ });
179
+ }
180
+
181
+ return typeof cont === 'function' ? cont() : null;
182
+ });
183
+ };
184
+
185
+ // #setActiveRow {{{2
186
+
187
+ GridTablePlain.prototype.setActiveRow = function (which) {
188
+ var self = this
189
+ , rowId
190
+ , tr;
191
+
192
+ if (!self.features.activeRow) {
193
+ console.warn('[DataVis // %s // Set Active Row] Active row feature is disabled', self.toString());
194
+ return;
195
+ }
196
+
197
+ if (typeof which === 'number') {
198
+ rowId = which;
199
+ tr = self.ui.tbody.find('tr[data-row-num=' + which + ']');
200
+ }
201
+ else if (which instanceof jQuery) {
202
+ tr = which;
203
+ rowId = +tr.attr('data-row-num');
204
+ }
205
+
206
+ self.activeRow = {
207
+ rowId: rowId,
208
+ tr: tr
209
+ };
210
+
211
+ self.ui.tbody.find('tr.wcdv-active-row').removeClass('wcdv-active-row');
212
+ tr.addClass('wcdv-active-row');
213
+ if (!isElementInViewport(self.opts.fixedHeight ? self.root : window, tr)) {
214
+ tr.get(0).scrollIntoView({
215
+ block: 'nearest'
216
+ });
217
+ }
218
+
219
+ var rowData = self.view.data.dataByRowId[rowId];
220
+ var cbObj = {
221
+ rowId: rowId,
222
+ rowData: rowData,
223
+ colConfig: self.colConfig,
224
+ tableRow: tr,
225
+ tableRenderer: self
226
+ };
227
+ if (getProp(self.defn, 'table', 'activeRow', 'slider')) {
228
+ if (getProp(self.defn, 'table', 'activeRow', 'callback')) {
229
+ cbObj.slider = self.ui.slider;
230
+ self.defn.table.activeRow.callback(cbObj);
231
+ }
232
+ else {
233
+ var dataHtml = jQuery('<dl>');
234
+ self.colConfig.each(function (v, k) {
235
+ jQuery('<dt>').text(v.displayText || v.field).appendTo(dataHtml);
236
+ var dd = jQuery('<dd>').appendTo(dataHtml);
237
+ var cr = rowData[k].cachedRender || rowData[k].value || rowData[k].orig;
238
+ if (cr instanceof Element || cr instanceof jQuery) {
239
+ dd.append(cr);
240
+ }
241
+ else if (cr === '') {
242
+ dd.html('&nbsp;');
243
+ }
244
+ else if (v.allowHtml) {
245
+ dd.html(cr);
246
+ }
247
+ else {
248
+ dd.text(cr);
249
+ }
250
+ });
251
+ self.ui.slider.setHeader('Row Info');
252
+ self.ui.slider.setBody(dataHtml);
253
+ }
254
+ self.ui.slider.show();
255
+ }
256
+ else if (getProp(self.defn, 'table', 'activeRow', 'callback')) {
257
+ self.defn.table.activeRow.callback(cbObj);
258
+ }
259
+ };
260
+
261
+ // #clearActiveRow {{{2
262
+
263
+ GridTablePlain.prototype.clearActiveRow = function () {
264
+ var self = this;
265
+
266
+ if (!self.features.activeRow) {
267
+ console.warn('[DataVis // %s // Clear Active Row] Active row feature is disabled', self.toString());
268
+ return;
269
+ }
270
+
271
+ if (getProp(self.defn, 'table', 'activeRow', 'slider')) {
272
+ self.ui.slider.hide();
273
+ }
274
+
275
+ self.ui.tbody.find('tr.wcdv-active-row').removeClass('wcdv-active-row');
276
+
277
+ self.activeRow = null;
278
+ };
279
+
280
+ // #activeRowPrev {{{2
281
+
282
+ GridTablePlain.prototype.activeRowPrev = function () {
283
+ var self = this;
284
+
285
+ var activeRowId = self.activeRow.rowId - 1;
286
+ if (activeRowId < 0) {
287
+ activeRowId = self.view.data.dataByRowId.length - 1;
288
+ }
289
+ self.setActiveRow(activeRowId);
290
+ };
291
+
292
+ // #activeRowNext {{{2
293
+
294
+ GridTablePlain.prototype.activeRowNext = function () {
295
+ var self = this;
296
+
297
+ var activeRowId = self.activeRow.rowId + 1;
298
+ if (activeRowId >= self.view.data.dataByRowId.length) {
299
+ activeRowId = 0;
300
+ }
301
+ self.setActiveRow(activeRowId);
302
+ };
303
+
304
+ // #drawHeader {{{2
305
+
306
+ /**
307
+ * Render the header columns of a GridTablePlain.
308
+ *
309
+ * @param {Array.<string>} columns A list of the fields that are to be included as columns within
310
+ * the GridTablePlain.
311
+ *
312
+ * @param {ComputedView~Data} data
313
+ *
314
+ * @param {Source~TypeInfo} typeInfo
315
+ *
316
+ * @param {object} opts
317
+ */
318
+
319
+ GridTablePlain.prototype.drawHeader = function (columns, data, typeInfo, opts) {
320
+ var self = this;
321
+
322
+ var headingTr, headingSpan, headingTh, filterTr;
323
+
324
+ var headingThCss = {
325
+ 'white-space': 'nowrap'
326
+ };
327
+
328
+ var filterThCss = {
329
+ 'white-space': 'nowrap',
330
+ 'padding-top': 4,
331
+ 'vertical-align': 'top'
332
+ };
333
+
334
+ headingTr = jQuery('<tr>');
335
+ filterTr = jQuery('<tr>', {
336
+ 'class': 'wcdv_grid_filterrow'
337
+ });
338
+
339
+ /*
340
+ * Create the checkbox that allows the user to select all rows.
341
+ */
342
+
343
+ if (self.features.rowSelect) {
344
+ self.ui.checkAll_thead = jQuery('<input>', { 'name': 'checkAll', 'type': 'checkbox' })
345
+ .on('change', function (evt) {
346
+ self.checkAll(evt);
347
+ });
348
+
349
+ headingTh = jQuery('<th>', { scope: 'col' })
350
+ .addClass('wcdv_group_col_spacer')
351
+ .append(self.ui.checkAll_thead)
352
+ .appendTo(headingTr);
353
+ if (self.opts.drawInternalBorders) {
354
+ headingTh.addClass('wcdv_pivot_colval_boundary');
355
+ }
356
+
357
+ if (self.features.filter) {
358
+ filterTr.append(jQuery('<th>').css(filterThCss));
359
+ }
360
+ }
361
+
362
+ // Create the column for row-based operations.
363
+
364
+ if (self.hasOperations('row')) {
365
+ headingTh = jQuery('<th>', {
366
+ 'class': 'wcdv_group_col_spacer'
367
+ });
368
+ headingTr.append(headingTh);
369
+ }
370
+
371
+ var progress = self.makeProgress('Filter');
372
+
373
+ /*
374
+ * Set up the GridFilterSet instance that manages the (potentially multiple) filters on each
375
+ * column of the ComputedView that belongs to this GridTablePlain.
376
+ */
377
+
378
+ if (self.features.filter) {
379
+ self.defn.gridFilterSet = new GridFilterSet(self.view, null, self, progress);
380
+ }
381
+
382
+ /*
383
+ * Configure every column which comes from the data (i.e. not the "select all" checkbox, and not
384
+ * the editing "options" column).
385
+ */
386
+
387
+ _.each(columns, function (field, colIndex) {
388
+ var fcc = self.colConfig.get(field) || {};
389
+
390
+ if (self.features.rowSelect) {
391
+ colIndex += 1; // Add a column for the row selection checkbox.
392
+ }
393
+
394
+ if (self.hasOperations('row')) {
395
+ colIndex += 1; // Add a column for row-based operations.
396
+ }
397
+
398
+ var headingText = fcc.displayText || field;
399
+
400
+ // headingTh <TH>
401
+ // headingThContainer <DIV>
402
+ // headingThSpan <SPAN>
403
+ // headingThControls <DIV>
404
+
405
+ var headingSpan = jQuery('<span>', {
406
+ 'class': 'wcdv_heading_title',
407
+ 'data-wcdv-field': field,
408
+ 'data-wcdv-draggable-origin': 'GRID_TABLE_HEADER',
409
+ })
410
+ .text(headingText)
411
+ ._makeDraggableField();
412
+
413
+ var headingThControls = jQuery('<div>');
414
+
415
+ var headingThContainer = jQuery('<div>')
416
+ .addClass('wcdv_heading_container')
417
+ .append(headingSpan, headingThControls);
418
+
419
+ var headingTh = jQuery('<th>', { id: gensym(), scope: 'col' })
420
+ .css(headingThCss)
421
+ .append(headingThContainer);
422
+
423
+ var fti = typeInfo.get(field);
424
+
425
+ if (fti != null && fti.type != null) {
426
+ headingTh.attr('data-wcdv-field-type', fti.type);
427
+ }
428
+
429
+ // In the plain grid table output, the only way to sort is vertically by field.
430
+
431
+ self._addSortingToHeader(data, 'vertical', {field: field}, headingThControls.get(0));
432
+
433
+ self._addFilterToHeader(headingThControls, field, headingText);
434
+
435
+
436
+ if (self.opts.drawInternalBorders) {
437
+ headingTh.addClass('wcdv_pivot_colval_boundary');
438
+ }
439
+
440
+ /*
441
+ * Configure filtering for this column. This mainly involves creating a button, which when
442
+ * clicked adds (for this column) a filter to the GridFilterSet instance.
443
+ */
444
+
445
+ if (self.features.filter) {
446
+
447
+ // Add a TH to the TR that will contain the filters. Every filter will actually be a DIV
448
+ // inside this TH.
449
+ //
450
+ // The ID attribute here is used to provide a selector to NProgress, so the progress bar
451
+ // will be drawn in the header cell for the column we're filtering by. You can't pass an
452
+ // element to NProgress for this, it needs to be a selector string. Passing ('#' + id) was
453
+ // the easiest way to do it.
454
+ //
455
+ // Unfortunately, the ID attribute is copied when using TableTool so this might mess us up.
456
+
457
+ var filterThId = gensym();
458
+ var filterTh = jQuery('<th>', { id: filterThId }).addClass('wcdv_grid_filtercol filter_col_' + colIndex).css(filterThCss);
459
+ self.setCss(filterTh, field);
460
+ filterTr.append(filterTh);
461
+
462
+ // Create the "button" (really a SPAN) that will add the filter to the grid, and stick it
463
+ // onto the end of the column heading TH.
464
+
465
+ jQuery(icon('filter', null, 'Click to add a filter on this column'))
466
+ .css({'cursor': 'pointer', 'margin-left': '0.5ex'})
467
+ .on('click', function () {
468
+ // When using TableTool, we need to put the filter UI into the floating (clone) header,
469
+ // instead of the original (variable `filterTh` holds the original). This jQuery will
470
+ // always do the right thing.
471
+
472
+ var thead = jQuery(this).closest('thead');
473
+ var tr = thead.children('tr:eq(1)');
474
+ var th = tr.children('th.filter_col_' + colIndex);
475
+
476
+ var adjustTableToolHeight = function () {
477
+ if (self.features.floatingHeader) {
478
+ // Update the height of the original, non-floating header to be the same as that of
479
+ // the floating header. This is needed because otherwise the floating header will
480
+ // cover up the first rows of the table body as we add filters. TableTool does not
481
+ // keep the heights of the original and clone in sync on its own (using the `update`
482
+ // function only synchronizes the widths).
483
+
484
+ var trHeight = tr.innerHeight();
485
+
486
+ self.logDebug(self.makeLogTag() + ' Adjusting original table header height to ' + trHeight + 'px to match floating header height', self.toString());
487
+ filterTr.innerHeight(trHeight);
488
+ }
489
+ };
490
+
491
+ var onRemove = adjustTableToolHeight;
492
+
493
+ self.defn.gridFilterSet.add(field, th, {
494
+ filterType: fcc.filter,
495
+ filterButton: jQuery(this),
496
+ makeRemoveButton: true,
497
+ onRemove: onRemove,
498
+ autoUpdateInputWidth: true,
499
+ sizingElement: filterTh
500
+ });
501
+
502
+ adjustTableToolHeight();
503
+ })
504
+ .appendTo(headingTh);
505
+ }
506
+
507
+ self.setCss(headingTh, field);
508
+ self.setAlignment(headingTh, fcc, typeInfo.get(field));
509
+
510
+ // Add column resize handle
511
+ if (self.features.columnResize !== false) {
512
+ self._addColumnResizeHandle(headingTh, field, colIndex);
513
+ }
514
+
515
+ // Add column reorder handler
516
+ if (self.features.columnReorder !== false) {
517
+ self._addColumnReorderHandler(headingTh, field, colIndex, columns);
518
+ }
519
+
520
+ self.ui.thMap[field] = headingTh;
521
+ headingTr.append(headingTh);
522
+ });
523
+
524
+ if (self.opts.addCols) {
525
+ self.drawHeader_addCols(headingTr, typeInfo, opts);
526
+ }
527
+
528
+ /*
529
+ * Create a column with buttons that allows the user to reorder the rows.
530
+ */
531
+
532
+ if (self.features.rowReorder) {
533
+ headingTh = jQuery('<th>', { scope: 'col' })
534
+ .text('Options')
535
+ .appendTo(headingTr);
536
+ if (self.opts.drawInternalBorders) {
537
+ headingTh.addClass('wcdv_pivot_colval_boundary');
538
+ }
539
+
540
+ if (self.features.filter) {
541
+ headingTh = jQuery('<th>').css(filterThCss).appendTo(filterTr);
542
+ if (self.opts.drawInternalBorders) {
543
+ headingTh.addClass('wcdv_pivot_colval_boundary');
544
+ }
545
+ }
546
+ }
547
+
548
+ self.ui.thead.append(headingTr);
549
+
550
+ if (self.features.filter) {
551
+ self.ui.thead.append(filterTr);
552
+ }
553
+ };
554
+
555
+ // #drawBody {{{2
556
+
557
+ GridTablePlain.prototype.drawBody = function (data, typeInfo, columns, cont, opts) {
558
+ var self = this;
559
+
560
+ // When pagination is enabled, disable the limit feature so that all rows are rendered into the
561
+ // DOM. Pagination controls visibility by showing/hiding TR elements per page.
562
+ var useLimit = self.features.pagination ? false : self.features.limit;
563
+ var limitConfig = getPropDef({}, self.defn, 'table', 'limit');
564
+ var usingTableTool = self.features.floatingHeader && getProp(self.defn, 'table', 'floatingHeader', 'method') === 'tabletool';
565
+
566
+ if (self.features.limit && !self.features.pagination && limitConfig && data.data.length > limitConfig.threshold) {
567
+ self.logDebug(self.makeLogTag() + ' Limiting output to first ' + limitConfig.threshold + ' rows', self.toString());
568
+ }
569
+
570
+ if (self.opts.generateCsv) {
571
+ self.addDataToCsv(data);
572
+ }
573
+
574
+ // When pagination is enabled, wrap the continuation so that page visibility and pagination
575
+ // controls are applied after all rows have been rendered. The originalCont must run first
576
+ // because it appends the tbody to the table and runs the full draw chain (including
577
+ // omnifilter, which sets all rows visible). Pagination is then applied last.
578
+
579
+ if (self.features.pagination) {
580
+ var originalCont = cont;
581
+ cont = function () {
582
+ if (typeof originalCont === 'function') {
583
+ originalCont();
584
+ }
585
+ self._paginationApply();
586
+ self._paginationDrawControls();
587
+ };
588
+ }
589
+
590
+ // Clear out the body of the table. We do this in case somebody invokes this function multiple
591
+ // times. This function draws the entirety of the data, we certainly don't want to just tack rows
592
+ // on to the end.
593
+
594
+ self.ui.tbody.children().remove();
595
+
596
+ // Reset pagination to page 0 when redrawing all rows.
597
+ if (self.features.pagination) {
598
+ self._paginationPage = 0;
599
+ }
600
+
601
+ self._setupFullValueWin(data);
602
+
603
+ var renderDataRow = function (row, idx) {
604
+ var tr, td;
605
+
606
+ tr = document.createElement('tr');
607
+ tr.setAttribute('id', self.defn.table.id + '_' + row.rowNum);
608
+ tr.setAttribute('data-row-num', row.rowNum);
609
+ tr.classList.add(idx % 2 === 0 ? 'even' : 'odd');
610
+
611
+ // Create the check box which selects the row.
612
+
613
+ if (self.features.rowSelect) {
614
+ var checkbox = jQuery('<input>', {
615
+ 'type': 'checkbox',
616
+ 'data-row-num': row.rowNum,
617
+ });
618
+ td = jQuery('<td>').addClass('wcdv_group_col_spacer').append(checkbox).appendTo(tr);
619
+ if (self.opts.drawInternalBorders) {
620
+ td.addClass('wcdv_pivot_colval_boundary');
621
+ }
622
+ }
623
+
624
+ // Create the cell that contains row-based operations.
625
+
626
+ if (self.hasOperations('row')) {
627
+ td = document.createElement('td');
628
+ td.classList.add('wcdv_group_col_spacer');
629
+ td.classList.add('wcdv_pivot_colval_boundary');
630
+ td.classList.add('wcdv_nowrap');
631
+ td.classList.add('wcdv_row_operations');
632
+
633
+ _.each(self.defn.operations.row, function (op, index) {
634
+ var opBtn = makeOperationButton('row', op, index);
635
+ if (op.disableWhen && op.disableWhen(row)) {
636
+ opBtn.disabled = true;
637
+ }
638
+ if (op.hideWhen && op.hideWhen(row)) {
639
+ opBtn.style.display = 'none';
640
+ }
641
+ td.appendChild(opBtn);
642
+ });
643
+
644
+ tr.appendChild(td);
645
+ }
646
+
647
+ // Create the data cells.
648
+
649
+ _.each(columns, function (field, colIndex) {
650
+ var fcc = self.colConfig.get(field) || {};
651
+ var cell = row.rowData[field];
652
+
653
+ var td = document.createElement('td');
654
+ var value = format(fcc, typeInfo.get(field), cell);
655
+
656
+ setTableCell(td, value, {
657
+ field: field,
658
+ colConfig: self.colConfig,
659
+ typeInfo: typeInfo,
660
+ operations: getProp(self.defn, 'operations', 'cell', field)
661
+ });
662
+
663
+ // Buttons within cells share a common 'onClick' handler, e.g. all "show full value" buttons
664
+ // have the same callback. In that handler, we need to be able to figure out what field we
665
+ // were called for. So if we're going to render buttons within the data cell, we need to
666
+ // attach the field name so it can be used by the handler.
667
+ //
668
+ // There are two such situations right now:
669
+ //
670
+ // 1. When `maxHeight` is set on the field (the "show full value" button).
671
+ // 2. When there are operations on the field.
672
+
673
+ if (fcc.maxHeight != null || self.hasOperations('cell', field)) {
674
+ td.setAttribute('data-wcdv-field', field);
675
+ }
676
+
677
+ self.setCss(jQuery(td), field);
678
+ self.setAlignment(td, fcc, typeInfo.get(field));
679
+
680
+ if (self.opts.drawInternalBorders) {
681
+ td.classList.add('wcdv_pivot_colval_boundary');
682
+ }
683
+
684
+ tr.appendChild(td);
685
+ });
686
+
687
+ if (self.opts.addCols) {
688
+ _.each(self.opts.addCols, function (addColSpec) {
689
+ var value = addColSpec.value(row.rowData, row.rowNum);
690
+ var td = document.createElement('td');
691
+
692
+ if (!(value instanceof jQuery || value instanceof Element)) {
693
+ value = format(null, null, value);
694
+ }
695
+
696
+ setTableCell(td, value);
697
+
698
+ if (self.opts.drawInternalBorders) {
699
+ td.classList.add('wcdv_pivot_colval_boundary');
700
+ }
701
+
702
+ tr.appendChild(td);
703
+ });
704
+ }
705
+
706
+ // Create button used as the "handle" for dragging/dropping rows.
707
+
708
+ if (self.features.rowReorder) {
709
+ jQuery('<td>').append(self.makeRowReorderBtn()).appendTo(tr);
710
+ }
711
+
712
+ self.ui.tr[row.rowNum] = jQuery(tr);
713
+ self.ui.tbody.append(tr);
714
+
715
+ // When using TableTool with a pinned column, the pinned column is a clone on the left hand
716
+ // side. TableTool does not monitor the original tbody to see if new elements are added, so we
717
+ // need to add new data to the pinned column clone as well.
718
+
719
+ if (usingTableTool) {
720
+ self.ui.tbody.parents('div.ttsticky').find('table > tbody').append(tr);
721
+ }
722
+ };
723
+
724
+ var renderShowMore = function (rowNum) {
725
+ var tr;
726
+ var showMoreId = gensym();
727
+
728
+ tr = document.createElement('tr');
729
+ tr.classList.add('wcdvgrid_more');
730
+ tr.setAttribute('data-show-more-id', showMoreId);
731
+
732
+ var colSpan = columns.length
733
+ + (self.features.rowSelect ? 1 : 0)
734
+ + (self.hasOperations('row') ? 1 : 0)
735
+ + (getPropDef(0, self.opts, 'addCols', 'length'))
736
+ + (self.features.rowReorder ? 1 : 0);
737
+
738
+ var showMore = function () {
739
+ // When using pinned columns, TableTool will make a clone of the "show more rows" <TR> which
740
+ // we otherwise have no knowledge of. So we must track it using a data attribute instead, so
741
+ // we can remove both the original and the clone.
742
+
743
+ jQuery('tr[data-show-more-id="' + showMoreId + '"]').remove();
744
+ render(rowNum + 1, limitConfig.chunkSize, nextChunk);
745
+ };
746
+
747
+ var td = jQuery('<td>', {
748
+ colspan: colSpan
749
+ })
750
+ .on('click', showMore)
751
+ .append(icon('circle-chevron-down'))
752
+ .append(jQuery('<span>Showing rows '
753
+ + '1–'
754
+ + (rowNum + 1)
755
+ + ' of '
756
+ + data.data.length
757
+ + '.</span>')
758
+ .css({
759
+ 'padding-left': '0.5em',
760
+ }))
761
+ .append(jQuery('<span>Click to load ' + limitConfig.chunkSize + ' more rows.</span>')
762
+ .css({
763
+ 'padding-left': '0.5em',
764
+ 'padding-right': '0.5em'
765
+ }))
766
+ .append(icon('circle-chevron-down'));
767
+
768
+ self.moreVisibleHandler = onVisibilityChange(self.scrollEventElement, td, function(isVisible) {
769
+ if (isVisible && getProp(self.defn, 'table', 'limit', 'autoShowMore')) {
770
+ self.logDebug(self.makeLogTag() + ' "Show More Rows" button scrolled into view', self.toString());
771
+ showMore();
772
+ }
773
+ });
774
+
775
+ tr.appendChild(td.get(0));
776
+ self.ui.tbody.append(tr);
777
+
778
+ // When using TableTool with a pinned column, the pinned column is a clone on the left hand
779
+ // side. TableTool does not monitor the original tbody to see if new elements are added, so we
780
+ // need to add new data to the pinned column clone as well.
781
+
782
+ if (usingTableTool) {
783
+ self.ui.tbody.parents('div.ttsticky').find('table > tbody').append(tr);
784
+ }
785
+ };
786
+
787
+ var render = function (startIndex, howMany, nextChunk) {
788
+ var atLimit = false;
789
+
790
+ if (startIndex == null) {
791
+ startIndex = 0;
792
+ }
793
+
794
+ if (howMany == null) {
795
+ howMany = data.data.length;
796
+ }
797
+
798
+ self.logDebug(self.makeLogTag() + ' Rendering rows '
799
+ + startIndex
800
+ + ' - '
801
+ + Math.min(useLimit && startIndex === 0 ? limitConfig.threshold - 1 : Number.POSITIVE_INFINITY
802
+ , startIndex + howMany - 1
803
+ , data.data.length - 1)
804
+ + ' '
805
+ + (data.data.length - 1 <= startIndex + howMany - 1
806
+ ? '[END]'
807
+ : ('/ ' + data.data.length - 1)), self.toString());
808
+
809
+ for (var rowNum = startIndex; rowNum < data.data.length && rowNum < startIndex + howMany && !atLimit; rowNum += 1) {
810
+ renderDataRow(data.data[rowNum], rowNum);
811
+
812
+ if (!self.features.incremental
813
+ && useLimit
814
+ && limitConfig.method === 'more'
815
+ && rowNum !== data.data.length - 1 // [0]
816
+ && ((startIndex === 0 && rowNum === limitConfig.threshold - 1) // [1]
817
+ || (startIndex > 0 && rowNum === startIndex + limitConfig.chunkSize - 1))) { // [2]
818
+
819
+ // Condition [0]: We haven't reached the end of the data.
820
+ // Condition [1]: We've reached the initial threshold for showing the more button.
821
+ // Condition [2]: We're showing additional rows because they clicked the more button.
822
+
823
+ renderShowMore(rowNum);
824
+ atLimit = true;
825
+ }
826
+ }
827
+
828
+ if (atLimit) {
829
+ self.fire('limited');
830
+ }
831
+ else {
832
+ self.fire('unlimited');
833
+ }
834
+
835
+ self._updateSelectionGui();
836
+
837
+ if (self.features.floatingHeader) {
838
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
839
+ case 'tabletool':
840
+ window.TableTool.update();
841
+ break;
842
+ }
843
+ }
844
+
845
+ if (rowNum === data.data.length) {
846
+ // All rows have been produced, so we're done!
847
+
848
+ delete self.moreVisibleHandler;
849
+
850
+ //self.ui.tbl.css({'table-layout': 'auto'}); // XXX - Does nothing?!
851
+
852
+ if (typeof cont === 'function') {
853
+ return cont();
854
+ }
855
+
856
+ // Nothing to do next, but we're done here.
857
+
858
+ return;
859
+ }
860
+ else if (typeof nextChunk === 'function') {
861
+ return nextChunk(startIndex, howMany);
862
+ }
863
+
864
+ if (typeof cont === 'function') {
865
+ return cont();
866
+ }
867
+
868
+ // Nothing to do next, but we're done here.
869
+
870
+ return;
871
+ };
872
+
873
+ var nextChunk;
874
+
875
+ if (self.features.incremental) {
876
+ var incrementalConfig = self.defn.table.incremental;
877
+ if (incrementalConfig.method === 'setTimeout') {
878
+ nextChunk = function (startIndex, howMany) {
879
+ window.setTimeout(function () {
880
+ render(startIndex + howMany, howMany, nextChunk);
881
+ }, incrementalConfig.delay);
882
+ };
883
+
884
+ // Kick off the initial render starting at index 0.
885
+
886
+ window.setTimeout(function () {
887
+ render(0, incrementalConfig.chunkSize, nextChunk);
888
+ }, incrementalConfig.delay);
889
+ }
890
+ else if (incrementalConfig.method === 'requestAnimationFrame') {
891
+ nextChunk = function (startIndex, howMany) {
892
+ window.requestAnimationFrame(function () {
893
+ render(startIndex + howMany, howMany, nextChunk);
894
+ });
895
+ };
896
+
897
+ // Kick off the initial render starting at index 0.
898
+
899
+ window.requestAnimationFrame(function () {
900
+ render(0, incrementalConfig.chunkSize, nextChunk);
901
+ });
902
+ }
903
+ else {
904
+ throw new Error('Invalid value for `table.incremental.method` (' + incrementalConfig.method + ') - must be either "setTimeout" or "requestAnimationFrame"');
905
+ }
906
+ }
907
+ else {
908
+ render();
909
+ }
910
+
911
+ //self.ui.tbl.css({'table-layout': 'fixed'}); // XXX - Does nothing?!
912
+ };
913
+
914
+ // #_paginationGetTotalPages {{{2
915
+
916
+ /**
917
+ * Return the total number of pages given the current data and rows-per-page setting.
918
+ *
919
+ * @return {number}
920
+ */
921
+
922
+ GridTablePlain.prototype._paginationGetTotalPages = function () {
923
+ var self = this;
924
+ var rows = self.ui.tbody.children('tr[data-row-num]');
925
+ return Math.max(1, Math.ceil(rows.length / self._paginationRowsPerPage));
926
+ };
927
+
928
+ // #_paginationApply {{{2
929
+
930
+ /**
931
+ * Show rows belonging to the current page and hide all others. This is the core of the
932
+ * pagination feature: because every row is already in the DOM, switching pages is just toggling
933
+ * display on TR elements.
934
+ */
935
+
936
+ GridTablePlain.prototype._paginationApply = function () {
937
+ var self = this;
938
+ var perPage = self._paginationRowsPerPage;
939
+ var page = self._paginationPage;
940
+ var startIdx = page * perPage;
941
+ var endIdx = startIdx + perPage;
942
+ var usingTableTool = self.features.floatingHeader && getProp(self.defn, 'table', 'floatingHeader', 'method') === 'tabletool' && window.TableTool != null;
943
+
944
+ self.ui.tbody.children('tr[data-row-num]').each(function (idx) {
945
+ if (idx >= startIdx && idx < endIdx) {
946
+ this.style.display = '';
947
+ }
948
+ else {
949
+ this.style.display = 'none';
950
+ }
951
+ });
952
+
953
+ if (usingTableTool) {
954
+ window.TableTool.update();
955
+ }
956
+ };
957
+
958
+ // #_paginationGoToPage {{{2
959
+
960
+ /**
961
+ * Navigate to a specific page and update the pagination controls.
962
+ *
963
+ * @param {number} page Zero-based page index.
964
+ */
965
+
966
+ GridTablePlain.prototype._paginationGoToPage = function (page) {
967
+ var self = this;
968
+ var totalPages = self._paginationGetTotalPages();
969
+
970
+ if (page < 0) {
971
+ page = 0;
972
+ }
973
+ else if (page >= totalPages) {
974
+ page = totalPages - 1;
975
+ }
976
+
977
+ self._paginationPage = page;
978
+ self._paginationApply();
979
+ self._paginationDrawControls();
980
+ };
981
+
982
+ // #_paginationDrawControls {{{2
983
+
984
+ /**
985
+ * Draw (or redraw) the pagination navigation bar below the table.
986
+ *
987
+ * Layout: [first] ... [cur-2] [cur-1] [cur] [cur+1] [cur+2] ... [last]
988
+ */
989
+
990
+ GridTablePlain.prototype._paginationDrawControls = function () {
991
+ var self = this;
992
+ var totalPages = self._paginationGetTotalPages();
993
+ var current = self._paginationPage;
994
+
995
+ // Remove the existing controls if present.
996
+ if (self.ui.paginationControls) {
997
+ self.ui.paginationControls.remove();
998
+ }
999
+
1000
+ if (totalPages <= 1) {
1001
+ // Only one page — no need for controls.
1002
+ self.ui.paginationControls = null;
1003
+ return;
1004
+ }
1005
+
1006
+ var nav = jQuery('<nav>', {
1007
+ 'class': 'wcdv_pagination',
1008
+ 'aria-label': trans('GRID.PAGINATION.ARIA_LABEL')
1009
+ });
1010
+
1011
+ var makeBtn = function (label, pageIdx, isCurrent) {
1012
+ var btn = jQuery('<button>', {
1013
+ 'type': 'button',
1014
+ 'class': 'wcdv_pagination_btn' + (isCurrent ? ' wcdv_pagination_current' : ''),
1015
+ 'aria-label': trans('GRID.PAGINATION.GO_TO_PAGE', pageIdx + 1),
1016
+ 'aria-current': isCurrent ? 'page' : undefined
1017
+ }).text(label);
1018
+
1019
+ if (!isCurrent) {
1020
+ btn.on('click', function () {
1021
+ self._paginationGoToPage(pageIdx);
1022
+ });
1023
+ }
1024
+ else {
1025
+ btn.attr('disabled', true);
1026
+ }
1027
+
1028
+ return btn;
1029
+ };
1030
+
1031
+ var makeEllipsis = function () {
1032
+ return jQuery('<span>', { 'class': 'wcdv_pagination_ellipsis', 'aria-hidden': 'true' }).text('\u2026');
1033
+ };
1034
+
1035
+ // Determine the range of page buttons to show around the current page.
1036
+ var rangeStart = Math.max(0, current - 2);
1037
+ var rangeEnd = Math.min(totalPages - 1, current + 2);
1038
+
1039
+ // [first]
1040
+ if (rangeStart > 0) {
1041
+ nav.append(makeBtn('1', 0, current === 0));
1042
+ }
1043
+
1044
+ // ... before range
1045
+ if (rangeStart > 1) {
1046
+ nav.append(makeEllipsis());
1047
+ }
1048
+
1049
+ // Page buttons in range
1050
+ for (var i = rangeStart; i <= rangeEnd; i += 1) {
1051
+ nav.append(makeBtn(String(i + 1), i, i === current));
1052
+ }
1053
+
1054
+ // ... after range
1055
+ if (rangeEnd < totalPages - 2) {
1056
+ nav.append(makeEllipsis());
1057
+ }
1058
+
1059
+ // [last]
1060
+ if (rangeEnd < totalPages - 1) {
1061
+ nav.append(makeBtn(String(totalPages), totalPages - 1, current === totalPages - 1));
1062
+ }
1063
+
1064
+ self.ui.paginationControls = nav;
1065
+
1066
+ // Insert after the grid table container so pagination stays outside the scrollable area.
1067
+ self.grid.ui.grid.after(nav);
1068
+ };
1069
+
1070
+ // #drawFooter {{{2
1071
+
1072
+ GridTablePlain.prototype.drawFooter = function (columns, data, typeInfo) {
1073
+ var self = this;
1074
+
1075
+ var makeSelectAll = function (tr) {
1076
+ self.ui.checkAll_tfoot = jQuery('<input>', { 'name': 'checkAll', 'type': 'checkbox' })
1077
+ .on('change', function (evt) {
1078
+ self.checkAll(evt);
1079
+ });
1080
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).append(self.ui.checkAll_tfoot).appendTo(tr);
1081
+ };
1082
+
1083
+ var makeAggregateRow = function () {
1084
+ // Circumventing the correct logic here because TableTool requires an empty footer in order to
1085
+ // implement horizontal scrolling; if you omit the footer (with a TR and all appropriate TD's in
1086
+ // it) then you can't scroll horizontally.
1087
+ if (false && getProp(self.defn, 'table', 'footer') == null) {
1088
+ return;
1089
+ }
1090
+
1091
+ var tr = jQuery('<tr>');
1092
+
1093
+ // Add the "select all" checkbox when row selection is enabled.
1094
+
1095
+ if (self.features.rowSelect) {
1096
+ makeSelectAll(tr);
1097
+ }
1098
+
1099
+ // If there are row operations, make a column in the footer to take up that space.
1100
+ //
1101
+ // | [ ] | op op op | col1 | col2 | ... |
1102
+ // +-----+----------+------+------+-----+
1103
+ // | | <here> | |
1104
+
1105
+ if (self.hasOperations('row')) {
1106
+ tr.append(jQuery('<td>'));
1107
+ }
1108
+
1109
+ // Create the columns for the data fields, which contain aggregate function results over those
1110
+ // fields.
1111
+
1112
+ var didFooterCell = false;
1113
+
1114
+ tr.append(_.map(columns, function (field, colIndex) {
1115
+ var fcc = self.colConfig.get(field) || {};
1116
+ var colTypeInfo = typeInfo.get(field);
1117
+ var td = jQuery('<td>');
1118
+ var footerConfig = getProp(self.defn, 'table', 'footer', field);
1119
+ var agg;
1120
+ var aggFun;
1121
+ var aggResult;
1122
+ var footerVal;
1123
+
1124
+ self.setCss(td, field);
1125
+ self.setAlignment(td, fcc, typeInfo.get(field));
1126
+
1127
+ if (footerConfig == null) {
1128
+ if (didFooterCell) {
1129
+ td.addClass('wcdv_divider');
1130
+ }
1131
+
1132
+ didFooterCell = false;
1133
+ }
1134
+ else {
1135
+ if (colIndex > 0) {
1136
+ td.addClass('wcdv_divider');
1137
+ }
1138
+
1139
+ didFooterCell = true;
1140
+
1141
+ // Although the footer config is an aggregate spec, there is one place we allow more
1142
+ // flexibility. If the fields aren't set, use the field for the column in which we're
1143
+ // displaying this footer. This is merely a convenience for the most common case.
1144
+
1145
+ if (footerConfig.fields == null) {
1146
+ footerConfig.fields = [field];
1147
+ }
1148
+
1149
+ self.logDebug(self.makeLogTag() + ' Creating footer using config: %O', self.toString(), field, footerConfig);
1150
+
1151
+ var aggInfo = new AggregateInfo('all', footerConfig, 0, self.colConfig, typeInfo, function (tag, fti) {
1152
+ if (fti.needsDecoding) {
1153
+ self.logDebug(self.makeLogTag() + ' Converting data: { field = "%s", type = "%s" }',
1154
+ self.toString(), field, tag, fti.field, fti.type);
1155
+
1156
+ Source.decodeAll(data.dataByRowId, fti.field, typeInfo);
1157
+ }
1158
+
1159
+ fti.deferDecoding = false;
1160
+ fti.needsDecoding = false;
1161
+ });
1162
+ aggResult = aggInfo.instance.calculate(data.data);
1163
+ var aggResult_formatted;
1164
+
1165
+ if (isElement(aggResult)) {
1166
+ footerVal = aggResult;
1167
+ }
1168
+ else {
1169
+ if (aggInfo.instance.inheritFormatting) {
1170
+ aggResult_formatted = format(aggInfo.colConfig[0], aggInfo.typeInfo[0], aggResult, {
1171
+ overrideType: aggInfo.instance.getType()
1172
+ });
1173
+ }
1174
+ else {
1175
+ aggResult_formatted = format(null, null, aggResult, {
1176
+ overrideType: aggInfo.instance.getType(),
1177
+ decode: false
1178
+ });
1179
+ }
1180
+
1181
+ if (aggInfo.debug) {
1182
+ self.logDebug(self.makeLogTag() + ' Aggregate result: %s',
1183
+ self.toString(), field, JSON.stringify(aggResult));
1184
+ }
1185
+
1186
+ switch (typeof footerConfig.format) {
1187
+ case 'function':
1188
+ footerVal = footerConfig.format(aggResult_formatted);
1189
+ break;
1190
+ case 'string':
1191
+ footerVal = sprintf.sprintf(footerConfig.format, aggResult_formatted);
1192
+ break;
1193
+ default:
1194
+ throw new Error('Footer config for field "' + field + '": `format` must be a function or a string');
1195
+ }
1196
+ }
1197
+
1198
+ if (isElement(footerVal)) {
1199
+ td.append(footerVal);
1200
+ }
1201
+ else {
1202
+ td.text(footerVal);
1203
+ }
1204
+ }
1205
+
1206
+ return td;
1207
+ }));
1208
+
1209
+ // ...
1210
+
1211
+ if (self.features.rowReorder) {
1212
+ tr.append(jQuery('<td>').text('Options'));
1213
+ }
1214
+
1215
+ // Finish the row that contains the aggregate functions.
1216
+
1217
+ self.ui.tfoot.append(tr);
1218
+ };
1219
+
1220
+ /*
1221
+ * Create a row in the footer for an external footer that we've absorbed into the grid.
1222
+ */
1223
+
1224
+ var makeExternalFooterRow = function () {
1225
+ if (self.opts.footer == null || !self.opts.stealGridFooter) {
1226
+ return;
1227
+ }
1228
+
1229
+ var tr = jQuery('<tr>');
1230
+
1231
+ if (!isVisible(self.opts.footer)) {
1232
+ tr.hide();
1233
+ }
1234
+
1235
+ if (self.features.rowSelect) {
1236
+ // Circumventing the correct logic here because TableTool requires an empty footer in order to
1237
+ // implement horizontal scrolling; if you omit the footer (with a TR and all appropriate TD's
1238
+ // in it) then you can't scroll horizontally.
1239
+ if (true || getProp(self.defn, 'table', 'footer')) {
1240
+ // There is an aggregate row, so it contains the "select all" checkbox.
1241
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).appendTo(tr);
1242
+ }
1243
+ else {
1244
+ // There is no aggregate row, so make the "select all" checkbox here.
1245
+ makeSelectAll(tr);
1246
+ }
1247
+ }
1248
+
1249
+ // If there are row operations, make a column in the footer to take up that space.
1250
+ //
1251
+ // | [ ] | op op op | col1 | col2 | ... |
1252
+ // +-----+----------+------+------+-----+
1253
+ // | | <here> | |
1254
+
1255
+ if (self.hasOperations('row')) {
1256
+ tr.append(jQuery('<td>'));
1257
+ }
1258
+
1259
+ // If there are row operations, make a column in the footer to take up that space.
1260
+ //
1261
+ // | [ ] | op op op | col1 | col2 | ... |
1262
+ // +-----+----------+------+------+------+
1263
+ // | | | <here> ----------> |
1264
+
1265
+ tr.append(jQuery('<td>', {'colspan': columns.length}).append(self.opts.footer));
1266
+
1267
+ if (self.features.rowReorder) {
1268
+ tr.append(jQuery('<td>'));
1269
+ }
1270
+
1271
+ self.ui.tfoot.append(tr);
1272
+ };
1273
+
1274
+ makeAggregateRow();
1275
+ makeExternalFooterRow();
1276
+ };
1277
+
1278
+ // #makeRowReorderBtn {{{2
1279
+
1280
+ GridTablePlain.prototype.makeRowReorderBtn = function () {
1281
+ var self = this;
1282
+
1283
+ return jQuery('<button type="button" class="drag-handle fa">')
1284
+ .html(icon('move-vertical',null,'Drag or press up/down arrows to move'));
1285
+ };
1286
+
1287
+ // #updateFeatures {{{2
1288
+
1289
+ /**
1290
+ * Change the features of this grid table, then redraw the grid table.
1291
+ *
1292
+ * @param {Object} f
1293
+ * The new features to apply. Any features not indicated will maintain their current settings.
1294
+ *
1295
+ * @method
1296
+ */
1297
+
1298
+ GridTablePlain.prototype.updateFeatures = function (f) {
1299
+ var self = this;
1300
+
1301
+ _.each(f, function (v, k) {
1302
+ self.features[k] = v;
1303
+ });
1304
+
1305
+ self.draw(self.root);
1306
+ };
1307
+
1308
+ // #addWorkHandler {{{2
1309
+
1310
+ GridTablePlain.prototype.addWorkHandler = function () {
1311
+ var self = this;
1312
+
1313
+ self.view.on(ComputedView.events.workEnd, function (info, ops) {
1314
+ if (self._destroyed) { return; }
1315
+ self.logDebug(self.makeLogTag() + ' ComputedView has finished doing work',
1316
+ self.toString());
1317
+
1318
+ if (ops.group || ops.pivot) {
1319
+ self.logDebug(self.makeLogTag() + ' Unable to render this data: %O',
1320
+ self.toString(), ops);
1321
+ self.fire('unableToRender', null, ops);
1322
+ return;
1323
+ }
1324
+
1325
+ self.logDebug(self.makeLogTag() + ' Redrawing because the view has done work',
1326
+ self.toString());
1327
+ self.draw(self.root);
1328
+ }, { who: self });
1329
+ };
1330
+
1331
+ //GridTablePlain.prototype.addWorkHandler = function () {
1332
+ // var self = this;
1333
+ //
1334
+ // // Sets up callbacks responsible for correctly redrawing the grid when the view has done work
1335
+ // // (e.g. sorting or filtering) that will change what is displayed. This is only needed when
1336
+ // // limiting output because otherwise, sort and filter callbacks don't need to redraw the whole
1337
+ // // grid, and they are taken care of by the 'sort' and 'filter' events on a row-by-row basis.
1338
+ //
1339
+ // self.view.on(ComputedView.events.workEnd, function (info, ops) {
1340
+ // self.logDebug(self.makeLogTag('handler(workEnd)') + ' ComputedView has finished doing work');
1341
+ //
1342
+ // if (ops.group || ops.pivot) {
1343
+ //
1344
+ // // If the data is grouped or pivotted, we can't render it. Emit the "unable to render" event
1345
+ // // so that our Grid instance can replace us with a GridTableGroup or GridTablePivot instance
1346
+ // // which can render the data.
1347
+ //
1348
+ // self.fire(GridTable.events.unableToRender);
1349
+ // return;
1350
+ // }
1351
+ //
1352
+ // if (self.needsRedraw) {
1353
+ // self.logDebug(self.makeLogTag('handler(workEnd)') + ' Redrawing because the view has done work');
1354
+ //
1355
+ // self.needsRedraw = false;
1356
+ //
1357
+ // return self.view.getData(function (data) {
1358
+ // return self.view.getTypeInfo(function (typeInfo) {
1359
+ // self.timing.start(['Grid Table', 'Redraw triggered by view']);
1360
+ //
1361
+ // // Determine what columns will be in the table. This comes from the user, or from the
1362
+ // // data itself. We may then add columns for extra features (like row selection or
1363
+ // // reordering).
1364
+ //
1365
+ // var columns = determineColumns(self.defn, data, typeInfo);
1366
+ //
1367
+ // // Draw the body.
1368
+ //
1369
+ // self.drawBody(data, typeInfo, columns, function () {
1370
+ // self.timing.stop(['Grid Table', 'Redraw triggered by view']);
1371
+ //
1372
+ // // Potentially the columns resized as a result of sorting, filtering, or adding new data.
1373
+ // self.fire(GridTable.events.columnResize);
1374
+ // });
1375
+ // });
1376
+ // });
1377
+ // }
1378
+ // else {
1379
+ // // Potentially the columns resized as a result of sorting, filtering, or adding new data.
1380
+ // self.fire(GridTable.events.columnResize);
1381
+ // }
1382
+ // }, { who: self });
1383
+ //};
1384
+
1385
+ // #addDataToCsv {{{2
1386
+
1387
+ /**
1388
+ * Add all data to the CSV file. Because plain tables frequently don't show all the data, it's not
1389
+ * enough to perform the CSV generation inside the `render()` method like we do with other GridTable
1390
+ * implementations.
1391
+ *
1392
+ * @param {object} data
1393
+ */
1394
+
1395
+ GridTablePlain.prototype.addDataToCsv = function (data) {
1396
+ var self = this;
1397
+ var columns = determineColumns(self.colConfig, data, self.typeInfo);
1398
+
1399
+ self.logDebug(self.makeLogTag() + ' Started generating CSV file', self.toString());
1400
+ self.fire('generateCsvProgress', null, 0);
1401
+
1402
+ self.csv.start();
1403
+ self.csv.addRow();
1404
+ _.each(columns, function (field, colIndex) {
1405
+ var fcc = self.colConfig.get(field) || {};
1406
+ self.csv.addCol(fcc.displayText || field);
1407
+ });
1408
+
1409
+ var howMany = data.data.length / 10;
1410
+
1411
+ var f = function (startIndex) {
1412
+ var endIndex = Math.min(data.data.length, startIndex + howMany);
1413
+ for (var i = startIndex; i < endIndex; i += 1) {
1414
+ var row = data.data[i];
1415
+
1416
+ self.csv.addRow();
1417
+ _.each(columns, function (field, colIndex) {
1418
+ var fcc = self.colConfig.get(field) || {};
1419
+ var cell = row.rowData[field];
1420
+ var value = format(fcc, self.typeInfo.get(field), cell);
1421
+
1422
+ if (value instanceof Element) {
1423
+ self.csv.addCol(jQuery(value).text());
1424
+ }
1425
+ else if (value instanceof jQuery) {
1426
+ self.csv.addCol(value.text());
1427
+ }
1428
+ else if (fcc.allowHtml && self.typeInfo.get(field).type === 'string' && value.charAt(0) === '<') {
1429
+ self.csv.addCol(jQuery(value).text());
1430
+ }
1431
+ else {
1432
+ self.csv.addCol(value);
1433
+ }
1434
+ });
1435
+ }
1436
+
1437
+ if (i === data.data.length) {
1438
+ self.csv.finish(function () {
1439
+ self.logDebug(self.makeLogTag() + ' Finished generating CSV file', self.toString());
1440
+ self.csvLock.unlock();
1441
+ self.fire('generateCsvProgress', null, 100);
1442
+ self.fire('csvReady');
1443
+ });
1444
+ }
1445
+ else {
1446
+ self.fire('generateCsvProgress', null, Math.floor((i / data.data.length) * 100));
1447
+ setTimeout(function () {
1448
+ return f(i);
1449
+ }, 100);
1450
+ }
1451
+ };
1452
+
1453
+ return f(0);
1454
+ };
1455
+
1456
+ // #_updateSelectionGui {{{2
1457
+
1458
+ /**
1459
+ * Update the checkboxes in the grid table to match what the current selection is.
1460
+ */
1461
+
1462
+ GridTablePlain.prototype._updateSelectionGui = function () {
1463
+ var self = this;
1464
+
1465
+ // True if there are no rows to select.
1466
+ var isDisabled = self.data.data.length === 0;
1467
+
1468
+ // True if all rows are selected.
1469
+ var isAllChecked = !isDisabled && self.selection.length === self.data.data.length;
1470
+
1471
+ // True if some rows are selected, but not all of them.
1472
+ var isIndeterminate = !isDisabled && !isAllChecked && self.selection.length > 0;
1473
+
1474
+ var updateCheckboxState = function (elt) {
1475
+ elt.prop('disabled', isDisabled);
1476
+ elt.prop('checked', isAllChecked);
1477
+ elt.prop('indeterminate', isIndeterminate);
1478
+ };
1479
+
1480
+ // First, deselect all rows (remove "selected" class and uncheck the box).
1481
+
1482
+ self.root.find('tbody td.wcdv_selected_row').removeClass('wcdv_selected_row');
1483
+ self.root.find('tbody td:first-child input[type="checkbox"]').prop('checked', false);
1484
+
1485
+ // Next, find all the TR elements which correspond to selected rows.
1486
+
1487
+ var trs = self.root.find('tbody tr').filter(function (_idx, elt) {
1488
+ return self.selection.indexOf(+(jQuery(elt).attr('data-row-num'))) >= 0;
1489
+ });
1490
+
1491
+ // Set the "check all" input in the header.
1492
+
1493
+ if (self.ui.checkAll_thead) {
1494
+ updateCheckboxState(self.ui.checkAll_thead);
1495
+ updateCheckboxState(self.ui.checkAll_thead.parents('div.tabletool').find('input[name="checkAll"]'));
1496
+ }
1497
+
1498
+ // Set the "check all" input in the footer.
1499
+
1500
+ if (self.ui.checkAll_tfoot) {
1501
+ updateCheckboxState(self.ui.checkAll_tfoot);
1502
+ updateCheckboxState(self.ui.checkAll_tfoot.parents('div.tabletool').find('input[name="checkAll"]'));
1503
+ }
1504
+
1505
+ // Finally, select appropriate rows (add "selected" class and check the box).
1506
+
1507
+ trs.children('td').addClass('wcdv_selected_row');
1508
+ trs.find('td:first-child input[type="checkbox"]').prop('checked', true);
1509
+ };
1510
+
1511
+ // #checkAll {{{2
1512
+
1513
+ /**
1514
+ * Event handler for using the "check all" checkbox.
1515
+ *
1516
+ * @param {Event} evt
1517
+ * The event generated by the browser when the checkbox is changed.
1518
+ */
1519
+
1520
+ GridTablePlain.prototype.checkAll = function (evt) {
1521
+ var self = this;
1522
+
1523
+ // Synchronize with floating header clone.
1524
+ jQuery(evt.target).parents('div.tabletool').find('input[name="checkAll"]').prop('checked', evt.target.checked);
1525
+
1526
+ // Either select or unselect all rows.
1527
+ if (evt.target.checked) {
1528
+ self.select();
1529
+ }
1530
+ else {
1531
+ self.unselect();
1532
+ }
1533
+ };
1534
+
1535
+ // #_addRowReorderHandler {{{2
1536
+
1537
+ GridTablePlain.prototype._addRowReorderHandler = function () {
1538
+ var self = this;
1539
+
1540
+ // configureRowReordering(self.ui.tbody, _.bind(self.view.source.swapRows, self.view.source));
1541
+ };
1542
+
1543
+ // #_addRowSelectHandler {{{2
1544
+
1545
+ /**
1546
+ * Add an event handler for the row select checkboxes. The event is bound on `self.ui.tbody` and
1547
+ * looks for checkbox inputs inside TD elements with class `wcdv_group_col_spacer` to actually handle
1548
+ * the events. The handler calls `self.select(ROW_NUM)` or `self.unselect(ROW_NUM)` when the
1549
+ * checkbox is changed.
1550
+ */
1551
+
1552
+ GridTablePlain.prototype._addRowSelectHandler = function () {
1553
+ var self = this;
1554
+
1555
+ self.ui.tbody.on('change', '.wcdv_group_col_spacer > input[type="checkbox"]', function () {
1556
+ if (this.checked) {
1557
+ self.select(+(jQuery(this).attr('data-row-num')));
1558
+ }
1559
+ else {
1560
+ self.unselect(+(jQuery(this).attr('data-row-num')));
1561
+ }
1562
+ });
1563
+ };
1564
+
1565
+ GridTablePlain.prototype.clear = function () {
1566
+ var self = this;
1567
+
1568
+ if (self.ui != null && self.ui.slider != null) {
1569
+ self.ui.slider.destroy();
1570
+ }
1571
+
1572
+ if (self.ui != null && self.ui.paginationControls != null) {
1573
+ self.ui.paginationControls.remove();
1574
+ self.ui.paginationControls = null;
1575
+ }
1576
+
1577
+ jQuery(document).off('keydown.active-row-' + self._focusEventId);
1578
+ jQuery(document).off('keydown.omnifilter-' + self._focusEventId);
1579
+ removeFocusHandler(self._focusEventId);
1580
+
1581
+ self.super['GridTable'].clear();
1582
+ };
1583
+
1584
+ // Registry {{{1
1585
+
1586
+ GridRenderer.registry.set('table_plain', GridTablePlain);
1587
+
1588
+ // Exports {{{1
1589
+
1590
+ export {
1591
+ GridTablePlain
1592
+ };