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,1404 @@
1
+ // Imports {{{1
2
+
3
+ import _ from 'underscore';
4
+ import sprintf from 'sprintf-js';
5
+ import jQuery from 'jquery';
6
+
7
+ import {
8
+ determineColumns,
9
+ icon,
10
+ format,
11
+ getProp,
12
+ isElement,
13
+ isVisible,
14
+ makeSubclass,
15
+ mergeSort2,
16
+ mixinLogging,
17
+ } from '../../../util/misc.js';
18
+
19
+ import {GridRenderer} from '../../../grid_renderer.js';
20
+ import {AggregateInfo, ComputedView, GROUP_FUNCTION_REGISTRY} from 'datavis-ace';
21
+
22
+ import GridTable from '../table.js';
23
+
24
+ // GridTableGroupDetail {{{1
25
+ // Constructor {{{2
26
+
27
+ /**
28
+ * @class
29
+ * @extends GridTable
30
+ */
31
+
32
+ var GridTableGroupDetail = makeSubclass('GridTableGroupDetail', GridTable, function (grid, defn, view, features, opts, timing, id) {
33
+ var self = this;
34
+
35
+ self.super['GridTable'].ctor.apply(self, arguments);
36
+
37
+ self.features.sort = false;
38
+ self.features.columnResize = false;
39
+ self.features.columnReorder = false;
40
+
41
+ self.logDebug(self.makeLogTag() + ' DataVis // %s // Constructing grid table; features = %O', self.toString(), features);
42
+ });
43
+
44
+ mixinLogging(GridTableGroupDetail);
45
+
46
+ // #canRender {{{2
47
+
48
+ /**
49
+ * Responds whether or not this grid table can render the type of data requested.
50
+ *
51
+ * @param {string} what
52
+ * The kind of data the caller wants us to show. Must be one of: plain, group, or pivot.
53
+ *
54
+ * @return {boolean}
55
+ * True if this grid table can render that kind of data, false if it can't.
56
+ */
57
+
58
+ GridTableGroupDetail.prototype.canRender = function (what) {
59
+ return ['group'].indexOf(what) >= 0;
60
+ };
61
+
62
+ // #drawHeader {{{2
63
+
64
+ GridTableGroupDetail.prototype.drawHeader = function (columns, data, typeInfo, opts) {
65
+ var self = this,
66
+ headingTr,
67
+ headingSpan,
68
+ headingTh,
69
+ headingThContainer,
70
+ headingThControls,
71
+ headingThCss = {
72
+ 'white-space': 'nowrap'
73
+ },
74
+ filterThCss = {
75
+ 'white-space': 'nowrap',
76
+ 'padding-top': 4,
77
+ 'vertical-align': 'top'
78
+ };
79
+
80
+ _.each(data.groupFields, function (field, fieldIdx) {
81
+ var fcc = self.colConfig.get(field) || {};
82
+
83
+ headingTr = jQuery('<tr>');
84
+
85
+ if (self.features.rowSelect) {
86
+ if (fieldIdx === 0) {
87
+ self.ui.checkAll_thead = jQuery('<input>', {
88
+ 'name': 'checkAll',
89
+ 'type': 'checkbox',
90
+ 'class': 'wcdv_select_group',
91
+ 'data-group-id': '0'
92
+ })
93
+ .on('change', function (evt) {
94
+ self.checkAll(evt);
95
+ });
96
+
97
+ headingTh = jQuery('<th>', { scope: 'col' })
98
+ .addClass('wcdv_group_col_spacer')
99
+ .append(self.ui.checkAll_thead)
100
+ .appendTo(headingTr);
101
+ }
102
+ else {
103
+ jQuery('<th>')
104
+ .addClass('wcdv_group_col_spacer')
105
+ .appendTo(headingTr);
106
+ }
107
+ }
108
+
109
+ // Add spacers for the previous group fields.
110
+
111
+ for (var i = 0; i < fieldIdx + 1; i += 1) {
112
+ jQuery('<th>')
113
+ .addClass('wcdv_group_col_spacer')
114
+ .appendTo(headingTr)
115
+ ;
116
+ }
117
+
118
+ // headingTh <TH>
119
+ // headingThContainer <DIV>
120
+ // headingSpan <SPAN>
121
+ // headingThControls <DIV>
122
+
123
+ headingSpan = jQuery('<span>')
124
+ .attr({
125
+ 'data-wcdv-field': field,
126
+ 'data-wcdv-draggable-origin': 'GRID_TABLE_HEADER'
127
+ })
128
+ .addClass('wcdv_heading_title')
129
+ .text(fcc.displayText || field)
130
+ ._makeDraggableField()
131
+ ;
132
+
133
+ headingThControls = jQuery('<div>');
134
+
135
+ headingThContainer = jQuery('<div>')
136
+ .addClass('wcdv_heading_container')
137
+ .append(headingSpan, headingThControls);
138
+
139
+ headingTh = jQuery('<th>', { scope: 'col' })
140
+ .attr('colspan', columns.length - fieldIdx)
141
+ .css(headingThCss)
142
+ .append(headingThContainer)
143
+ ;
144
+
145
+ self._addSortingToHeader(data, 'vertical', {groupFieldIndex: fieldIdx}, headingThControls.get(0));
146
+
147
+ self.setCss(headingTh, field);
148
+
149
+ self.ui.thMap[field] = headingTh;
150
+
151
+ headingTr.append(headingTh);
152
+ self.ui.thead.append(headingTr);
153
+ });
154
+
155
+ headingTr = jQuery('<tr>');
156
+
157
+ // Add spacers for all the group fields.
158
+
159
+ if (self.features.rowSelect) {
160
+ jQuery('<th>')
161
+ .addClass('wcdv_group_col_spacer')
162
+ .appendTo(headingTr);
163
+ }
164
+
165
+ for (var i = 0; i < data.groupFields.length + 1; i += 1) {
166
+ jQuery('<th>')
167
+ .addClass('wcdv_group_col_spacer')
168
+ .appendTo(headingTr)
169
+ ;
170
+ }
171
+
172
+ // Make headers for all the normal (non-grouped) columns.
173
+
174
+ _.each(columns, function (field, colIndex) {
175
+ var fcc = self.colConfig.get(field) || {};
176
+
177
+ if (data.groupFields.indexOf(field) >= 0) {
178
+ return;
179
+ }
180
+
181
+ headingSpan = jQuery('<span>')
182
+ .attr({
183
+ 'data-wcdv-field': field,
184
+ 'data-wcdv-draggable-origin': 'GRID_TABLE_HEADER'
185
+ })
186
+ .addClass('wcdv_heading_title')
187
+ .text(fcc.displayText || field)
188
+ ._makeDraggableField()
189
+ ;
190
+
191
+ headingThControls = jQuery('<div>');
192
+
193
+ headingThContainer = jQuery('<div>')
194
+ .addClass('wcdv_heading_container')
195
+ .append(headingSpan, headingThControls);
196
+
197
+ headingTh = jQuery('<th>', { scope: 'col' })
198
+ .css(headingThCss)
199
+ .append(headingThContainer);
200
+
201
+ if (colIndex > 0) {
202
+ headingTh.addClass('wcdv_pivot_colval_boundary');
203
+ }
204
+
205
+ self._addSortingToHeader(data, 'vertical', {field: field}, headingThControls.get(0));
206
+
207
+ self.setCss(headingTh, field);
208
+ self.setAlignment(headingTh, fcc, typeInfo.get(field));
209
+
210
+ self.ui.thMap[field] = headingTh;
211
+ headingTr.append(headingTh);
212
+ });
213
+
214
+ self.ui.thead.append(headingTr);
215
+ };
216
+
217
+ // #drawBody {{{2
218
+
219
+ GridTableGroupDetail.prototype.drawBody = function (data, typeInfo, columns, cont, opts) {
220
+ var self = this;
221
+ // self.ui.tbl.append(self.ui.tbody);
222
+
223
+ // TYPES OF CHECKBOXES:
224
+ //
225
+ // .wcdv_select_row
226
+ // * data-row-num = What the rowNum for this data row is.
227
+ // * [tr] data-wcdv-rowValIndex = What rowVal this row is in.
228
+ //
229
+ // .wcdv_select_group
230
+
231
+ if (!data.isGroup) {
232
+ if (typeof cont === 'function') {
233
+ return cont();
234
+ }
235
+ else {
236
+ return;
237
+ }
238
+ }
239
+
240
+ if (self.opts.generateCsv) {
241
+ self.addDataToCsv(data);
242
+ }
243
+
244
+ // percolateUp() {{{3
245
+
246
+ function percolateUp(node /* groupInfo elt */) {
247
+ var disabled = false;
248
+ var checked = false;
249
+ var indeterminate = false;
250
+
251
+ // When a node has no children ...
252
+ //
253
+ // - it contains data rows in the UI
254
+ // - its height in the metadata tree is the # of group fields
255
+ // - it represents a complete rowval
256
+ //
257
+ // ... the number of selected rows is meant to be determined by the caller.
258
+
259
+ if (node.metadata.children != null) {
260
+ node.numSelected = 0;
261
+ _.each(node.metadata.children, function (child) {
262
+ node.numSelected += self.groupInfo[child.id].numSelected;
263
+ });
264
+ }
265
+
266
+ if (node.metadata.numRows === 0) {
267
+ disabled = true;
268
+ checked = false;
269
+ }
270
+ else {
271
+ if (node.numSelected === 0) {
272
+ checked = false;
273
+ }
274
+ else if (node.numSelected === node.metadata.numRows) {
275
+ checked = true;
276
+ }
277
+ else {
278
+ indeterminate = true;
279
+ }
280
+ }
281
+
282
+ node.checkbox.prop('disabled', disabled);
283
+ node.checkbox.prop('checked', checked);
284
+ node.checkbox.prop('indeterminate', indeterminate);
285
+
286
+ if (node.metadata.parent) {
287
+ percolateUp(self.groupInfo[node.metadata.parent.id]);
288
+ }
289
+ }
290
+
291
+ // percolateDown() {{{3
292
+
293
+ function percolateDown(node /* groupInfo elt */, isChecked) {
294
+ node.checkbox.prop('disabled', false);
295
+ node.checkbox.prop('checked', isChecked);
296
+ node.checkbox.prop('indeterminate', false);
297
+
298
+ node.numSelected = isChecked ? node.metadata.numRows : 0;
299
+
300
+ if (node.metadata.children == null) {
301
+ self.ui.tbody
302
+ .find('tr[data-wcdv-in-group=' + node.metadata.id + ']')
303
+ .find('input[type="checkbox"].wcdv_select_row')
304
+ .prop('checked', isChecked);
305
+ _.each(data.data[node.metadata.rowValIndex], function (row) {
306
+ if (isChecked) {
307
+ self.select(row.rowNum);
308
+ }
309
+ else {
310
+ self.unselect(row.rowNum);
311
+ }
312
+ });
313
+ }
314
+ else {
315
+ _.each(node.metadata.children, function (child) {
316
+ percolateDown(self.groupInfo[child.id], isChecked);
317
+ });
318
+ }
319
+ }
320
+
321
+ // }}}3
322
+
323
+ /*
324
+ self.ui.tbody.on('change', 'input[type="checkbox"].wcdv_select_row', function () {
325
+ var elt = jQuery(this);
326
+ var tr = elt.closest('tr');
327
+ var isChecked = elt.prop('checked');
328
+ var rowNum = +tr.attr('data-row-num');
329
+ var rowValIndex = +tr.attr('data-wcdv-rowValIndex');
330
+ var rowValMetadata = data.groupMetadata.lookup.byRowValIndex[rowValIndex];
331
+
332
+ self.logDebug(self.makeLogTag() + ' DataVis // ' + 'GRID TABLE // GROUP - DETAIL // SELECT',
333
+ 'Selecting data row: rowNum = %d, rowValIndex = %d, parentGroupId = %s, parentGroupInfo = %O',
334
+ rowNum, rowValIndex, rowValMetadata.id, self.groupInfo[rowValMetadata.id]);
335
+
336
+ self.groupInfo[rowValMetadata.id].numSelected += isChecked ? 1 : -1;
337
+
338
+ percolateUp(self.groupInfo[rowValMetadata.id]);
339
+ });
340
+
341
+ self.ui.tbody.on('change', 'input[type="checkbox"].wcdv_select_group', function () {
342
+ var elt = jQuery(this);
343
+ var tr = elt.closest('tr');
344
+ var isChecked = elt.prop('checked');
345
+ var groupMetadataId = +tr.attr('data-wcdv-toggles-group');
346
+
347
+ percolateDown(self.groupInfo[groupMetadataId], isChecked);
348
+ percolateUp(self.groupInfo[groupMetadataId]);
349
+ });
350
+ */
351
+
352
+ var isRendered = {}; // isRendered[metadataId] => boolean
353
+ var lastRenderedTr = {}; // lastRenderedTr[metadataId] => jQuery <TR>
354
+
355
+ // groupInfo {{{3
356
+
357
+ // groupInfo[id] -> {
358
+ // metadata
359
+ // numSelected
360
+ // checkbox
361
+ // }
362
+
363
+ self.groupInfo = (function () {
364
+ var mapping = {};
365
+
366
+ function recur(node) {
367
+ mapping[node.id] = {
368
+ metadata: node,
369
+ numSelected: 0
370
+ };
371
+ if (node.children != null) {
372
+ _.each(node.children, recur);
373
+ }
374
+ }
375
+
376
+ recur(data.groupMetadata);
377
+ mapping[0].checkbox = self.ui.checkAll_thead;
378
+ return mapping;
379
+ })();
380
+
381
+ // toggleGroup() {{{3
382
+
383
+ /*
384
+ * Toggle a sub-group open/closed. This is meant to be used as a jQuery event handler, e.g. for a
385
+ * click event.
386
+ */
387
+
388
+ function toggleGroup() {
389
+
390
+ /*
391
+ * Toggle the visibility of the subgroup.
392
+ *
393
+ * - metadataId: number
394
+ * What group we are expanding/collapsing.
395
+ *
396
+ * - show: boolean
397
+ * If true, show the rows in the group; otherwise hide them.
398
+ *
399
+ * - tr: jQuery (TR)
400
+ * The table row for the subgroup header.
401
+ */
402
+
403
+ function toggle(metadataId, show, tr) {
404
+ // Within the group metadata, the rowValIndex is only defined for things which are leaves in
405
+ // the grouping tree and therefore complete a rowVal.
406
+
407
+ var rowValIndex = self.data.groupMetadata.lookup.byId[metadataId].rowValIndex;
408
+
409
+ self.logDebug(self.makeLogTag() + ' show = %s, id = %s, rowValIndex = %s',
410
+ self.toString(), show, metadataId, rowValIndex);
411
+
412
+ // Check if we're expanding a leaf, thus fully expanding an entire group, and see if we need
413
+ // to render table rows for all the records in that group.
414
+
415
+ if (show && !isRendered[metadataId]) {
416
+ self.logDebug(self.makeLogTag() + ' Rendering: group metadata ID = %s',
417
+ self.toString(), metadataId);
418
+ render(metadataId, 0, tr);
419
+ }
420
+
421
+ // Set the visibility for all affected table rows. These can be for children of the current
422
+ // node in the tree (i.e. when expanding the current node does not complete a group), or for
423
+ // records in a fully expanded group: we don't distinguish between these two when it comes to
424
+ // showing/hiding as the attributes used on the elements are the same.
425
+
426
+ self.ui.tbody
427
+ .find('tr')
428
+ .filter(function (i, elt) {
429
+ return jQuery(elt).attr('data-wcdv-in-group') === '' + metadataId;
430
+ })
431
+ .each(function (i, elt) {
432
+ elt = jQuery(elt);
433
+ if (elt.attr('data-wcdv-toggles-group')) {
434
+ toggle(+elt.attr('data-wcdv-toggles-group'), show && elt.attr('data-wcdv-expanded') === '1', elt);
435
+ }
436
+ if (show) {
437
+ elt.show();
438
+ }
439
+ else {
440
+ elt.hide();
441
+ }
442
+ })
443
+ ;
444
+
445
+ if (self.ui.tbl.floatThead) {
446
+ self.ui.tbl.floatThead('reflow');
447
+ }
448
+ }
449
+
450
+ var elt = jQuery(this);
451
+ var tr = elt.closest('tr');
452
+
453
+ var op = tr.attr('data-wcdv-expanded') === '0' ? 'show' : 'hide';
454
+
455
+ if (op === 'show') {
456
+ tr.find('.spinner').show();
457
+ }
458
+ window.setTimeout(function () {
459
+ toggle(+tr.attr('data-wcdv-toggles-group'), op === 'show', tr);
460
+ if (op === 'show') {
461
+ tr.find('.spinner').hide();
462
+ }
463
+ tr.attr('data-wcdv-expanded', op === 'show' ? '1' : '0');
464
+ elt.attr('data-wcdv-expanded', op === 'show' ? '1' : '0');
465
+ elt.html(icon(op === 'show' ? 'square-minus' : 'square-plus'));
466
+ });
467
+ }
468
+
469
+ // render() {{{3
470
+
471
+ /**
472
+ * @param {number} [metadataId=0]
473
+ * @param {number} [startIndex=0]
474
+ * @param {jQuery} [afterElement]
475
+ */
476
+
477
+ function render(metadataId, startIndex, afterElement, showAll) {
478
+ if (metadataId != null && typeof metadataId !== 'number') {
479
+ throw new Error('Call Error: `metadataId` must be null or a number');
480
+ }
481
+ if (startIndex != null && typeof startIndex !== 'number') {
482
+ throw new Error('Call Error: `startIndex` must be null or a number');
483
+ }
484
+ if (afterElement != null && !(afterElement instanceof jQuery)) {
485
+ throw new Error('Call Error: `afterElement` must be null or an instance of jQuery');
486
+ }
487
+
488
+ if (metadataId == null) metadataId = 0;
489
+ if (startIndex == null) startIndex = 0;
490
+
491
+ if (startIndex > 0 && afterElement == null)
492
+ throw new Error('Call Error: `afterElement` required when `startIndex` > 0');
493
+
494
+ var metadataNode = data.groupMetadata.lookup.byId[metadataId];
495
+
496
+ if (metadataNode == null)
497
+ throw new Error('No group metadata for specified ID: ' + metadataId);
498
+
499
+ var limitConfig = self.defn.table.limit;
500
+
501
+ var showMoreTr,lastInsertedTr;
502
+
503
+ if (afterElement != null && startIndex > 0) {
504
+ showMoreTr = afterElement.nextAll('tr.wcdvgrid_more[data-wcdv-in-group="' + metadataId + '"]');
505
+ afterElement = showMoreTr.prev();
506
+ showMoreTr.remove();
507
+ }
508
+
509
+ var isExpanded = self.defn.table.whenGroup.showExpandedGroups ? '1' : '0';
510
+
511
+ if (metadataNode.children) {
512
+ // We're rendering sub-groups.
513
+
514
+ var i, j;
515
+ var childMetadataNode;
516
+ var childTr;
517
+ var checkbox;
518
+ var expandBtn;
519
+ var infoText, infoTextSpan;
520
+ var fcc;
521
+ var t, v;
522
+ var rowValElt, rowValEltSpan, rowValEltTh;
523
+ var showMoreTd;
524
+ var colSpan;
525
+
526
+ var trans = {
527
+ 'group:singular': 'group',
528
+ 'group:plural': 'groups',
529
+ 'row:singular': 'row',
530
+ 'row:plural': 'rows'
531
+ };
532
+
533
+ var childRowValElts = mergeSort2(_.pluck(metadataNode.children, 'rowValElt'));
534
+ var childRowValEltsLen = childRowValElts.length;
535
+
536
+ var howMany = !self.features.limit || showAll ? childRowValEltsLen
537
+ : startIndex === 0 ? limitConfig.threshold
538
+ : limitConfig.chunkSize;
539
+
540
+ for (i = startIndex; i < childRowValEltsLen && i < startIndex + howMany; i += 1) {
541
+ childMetadataNode = metadataNode.children[childRowValElts[i]];
542
+
543
+ childTr = jQuery('<tr>')
544
+ .attr('data-wcdv-in-group', metadataNode.id)
545
+ .attr('data-wcdv-toggles-group', childMetadataNode.id)
546
+ .attr('data-wcdv-expanded', isExpanded)
547
+ ;
548
+
549
+ // Insert spacer columns for previous group fields.
550
+
551
+ for (j = 0; j < childMetadataNode.groupFieldIndex; j += 1) {
552
+ jQuery('<th>', {'class': 'wcdv_group_col_spacer'})
553
+ .appendTo(childTr);
554
+ }
555
+
556
+ var disabled = childMetadataNode.children == null && childMetadataNode.rows.length === 0;
557
+
558
+ expandBtn = jQuery('<button>', {
559
+ 'type': 'button',
560
+ 'class': 'wcdv_icon_button wcdv_expand_button',
561
+ 'data-wcdv-expanded': isExpanded,
562
+ 'disabled': disabled
563
+ }).html(icon(isExpanded === '1' ? (disabled ? 'square':'square-minus' ) : (disabled ? 'square' : 'square-plus')));
564
+
565
+ jQuery('<th>', {'class': 'wcdv_group_col_spacer'})
566
+ .append(expandBtn)
567
+ .appendTo(childTr);
568
+
569
+ // Create the check box which selects the row.
570
+
571
+ if (self.features.rowSelect) {
572
+ checkbox = jQuery('<input>', {
573
+ 'type': 'checkbox',
574
+ 'class': 'wcdv_select_group',
575
+ 'data-group-id': childMetadataNode.id,
576
+ });
577
+ self.groupInfo[childMetadataNode.id].checkbox = checkbox;
578
+ jQuery('<th>', {'class': 'wcdv_group_col_spacer'})
579
+ .append(checkbox)
580
+ .appendTo(childTr);
581
+ }
582
+
583
+ fcc = self.colConfig.get(childMetadataNode.groupField) || {};
584
+ t = self.typeInfo.get(childMetadataNode.groupField);
585
+ v = childMetadataNode.rowValCell || childMetadataNode.rowValElt;
586
+
587
+ if (childMetadataNode.groupSpec.fun != null) {
588
+ t = {
589
+ type: GROUP_FUNCTION_REGISTRY.get(childMetadataNode.groupSpec.fun).resultType
590
+ };
591
+ v = childMetadataNode.rowValElt;
592
+ }
593
+
594
+ rowValElt = format(fcc, t, v);
595
+ rowValEltSpan = jQuery('<span>');
596
+
597
+ if (rowValElt instanceof Element || rowValElt instanceof jQuery) {
598
+ rowValEltSpan.append(rowValElt);
599
+ }
600
+ else if (fcc.allowHtml) {
601
+ rowValEltSpan.html(rowValElt);
602
+ }
603
+ else {
604
+ rowValEltSpan.text(rowValElt);
605
+ }
606
+
607
+ infoText = '(';
608
+ if (childMetadataNode.children != null) {
609
+ infoText += childMetadataNode.numChildren + ' ';
610
+ infoText += (childMetadataNode.numChildren === 1 ? trans['group:singular'] : trans['group:plural']) + ', ';
611
+ }
612
+ infoText += childMetadataNode.numRows + ' ';
613
+ infoText += childMetadataNode.numRows === 1 ? trans['row:singular'] : trans['row:plural'];
614
+ infoText += ')';
615
+
616
+ infoTextSpan = jQuery('<span>').css({'margin-left': '0.5em'}).text(infoText);
617
+
618
+ var spinnerDiv = jQuery('<div>', {'class': 'spinner'})
619
+ .append(jQuery('<div>', {'class': 'bounce1'}))
620
+ .append(jQuery('<div>', {'class': 'bounce2'}))
621
+ .append(jQuery('<div>', {'class': 'bounce3'}))
622
+ .hide();
623
+
624
+ jQuery('<th>', {
625
+ 'class': 'wcdv_group_value',
626
+ 'scope': 'row',
627
+ 'data-wcdv-field': childMetadataNode.groupField,
628
+ 'colspan': columns.length - childMetadataNode.groupFieldIndex
629
+ })
630
+ .append(rowValEltSpan)
631
+ .append(infoTextSpan)
632
+ .append(spinnerDiv)
633
+ .appendTo(childTr);
634
+
635
+ if (afterElement != null) {
636
+ afterElement.after(childTr);
637
+ }
638
+ else {
639
+ self.ui.tbody.append(childTr);
640
+ }
641
+
642
+ var rowRenderCb = getProp(self.opts, 'events', 'rowRender');
643
+ if (typeof rowRenderCb === 'function') {
644
+ rowRenderCb(childTr, {
645
+ isGroup: true,
646
+ groupMode: 'detail',
647
+ groupField: childMetadataNode.groupField,
648
+ rowValElt: childMetadataNode.rowValCell.value,
649
+ groupMetadata: childMetadataNode
650
+ });
651
+ }
652
+ if(self.defn.table.whenGroup.showExpandedGroups){
653
+ afterElement = render(childMetadataNode.id, 0, childTr);
654
+
655
+ } else {
656
+ afterElement = childTr;
657
+ }
658
+ }
659
+
660
+ isRendered[metadataNode.id] = true;
661
+
662
+ if (i < childRowValEltsLen) {
663
+ // Not all children were rendered.
664
+
665
+ lastRenderedTr[metadataNode.id] = childTr;
666
+ for (var p = metadataNode.parent; p != null; p = p.parent) {
667
+ lastRenderedTr[p.id] = childTr;
668
+ }
669
+
670
+ showMoreTr = jQuery('<tr>', {'class': 'wcdvgrid_more', 'data-wcdv-in-group': metadataNode.id});
671
+
672
+ // Insert spacer columns for previous group fields.
673
+
674
+ for (j = 0; j < childMetadataNode.groupFieldIndex; j += 1) {
675
+ jQuery('<th>', {'class': 'wcdv_group_col_spacer'})
676
+ .appendTo(showMoreTr);
677
+ }
678
+
679
+ colSpan = columns.length
680
+ + 1 // for the "expand" button column
681
+ + (self.features.rowSelect ? 1 : 0)
682
+ + (self.features.rowReorder ? 1 : 0)
683
+ - (metadataNode.groupFieldIndex || 0);
684
+
685
+ spinnerDiv = jQuery('<div>', {'class': 'spinner'})
686
+ .append(jQuery('<div>', {'class': 'bounce1'}))
687
+ .append(jQuery('<div>', {'class': 'bounce2'}))
688
+ .append(jQuery('<div>', {'class': 'bounce3'}))
689
+ .hide();
690
+
691
+ showMoreTd = jQuery('<td>', {
692
+ 'class': 'wcdv_show_more',
693
+ 'data-wcdv-in-group': metadataNode.id,
694
+ 'data-wcdv-show-more-start': i,
695
+ 'colspan': colSpan
696
+ })
697
+ .append(icon('circle-chevron-down'))
698
+ .append(jQuery('<span>Showing rows 1–' + i + ' of ' + childRowValEltsLen + '.</span>')
699
+ .css({'padding-left': '0.5em'}))
700
+ .append(jQuery('<button type="button">Load ' + limitConfig.chunkSize + ' more rows.</button>')
701
+ .css({'margin-left': '0.5em'}))
702
+ .append(jQuery('<button type="button" class="wcdv_show_all">Load all rows.</button>')
703
+ .css({'margin-left': '0.5em'})
704
+ )
705
+ .append(spinnerDiv)
706
+ .appendTo(showMoreTr);
707
+
708
+ childTr.after(showMoreTr);
709
+ }
710
+ lastInsertedTr = afterElement;
711
+ }
712
+ else if (metadataNode.rows) {
713
+ // We're rendering data rows.
714
+
715
+ var isSelected;
716
+ var checkbox;
717
+ var row;
718
+ var rowTr;
719
+ var showMoreTd;
720
+ var colSpan;
721
+
722
+ var howMany = (!self.features.limit || showAll) ? metadataNode.rows.length - startIndex
723
+ : startIndex === 0 ? limitConfig.threshold
724
+ : limitConfig.chunkSize;
725
+
726
+ for (i = startIndex; i < metadataNode.rows.length && i < startIndex + howMany; i += 1) {
727
+ row = metadataNode.rows[i];
728
+
729
+ rowTr = jQuery('<tr>', {
730
+ 'id': self.defn.table.id + '_' + i,
731
+ 'data-row-num': row.rowNum,
732
+ 'data-wcdv-in-group': metadataNode.id,
733
+ 'data-wcdv-rowValIndex': metadataNode.rowValIndex
734
+ });
735
+
736
+ // Insert some space to "indent" the data.
737
+ // TODO When does one of these work differently from the other?
738
+
739
+ //jQuery('<td>', {'colspan': data.groupFields.length + 1}).appendTo(rowTr);
740
+ for (var spacerIndex = 0; spacerIndex < data.groupFields.length + 1; spacerIndex += 1) {
741
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).appendTo(rowTr);
742
+ }
743
+
744
+ // Create the check box which selects the row.
745
+
746
+ if (self.features.rowSelect) {
747
+ isSelected = self.isSelected(row.rowNum);
748
+ checkbox = jQuery('<input>', {
749
+ 'type': 'checkbox',
750
+ 'data-row-num': row.rowNum,
751
+ 'class': 'wcdv_select_row',
752
+ 'checked': isSelected
753
+ });
754
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).append(checkbox).appendTo(rowTr);
755
+ }
756
+
757
+ // Create the data cells.
758
+
759
+ _.each(columns, function (field, colIndex) {
760
+ if (data.groupFields.indexOf(field) >= 0) {
761
+ return;
762
+ }
763
+
764
+ var fcc = self.colConfig.get(field) || {};
765
+ var cell = row.rowData[field];
766
+
767
+ var td = jQuery('<td>', {'data-wcdv-field': field});
768
+ if (colIndex > 0) {
769
+ td.addClass('wcdv_pivot_colval_boundary');
770
+ }
771
+ var value = format(fcc, typeInfo.get(field), cell);
772
+
773
+ if (value instanceof Element || value instanceof jQuery) {
774
+ td.append(value);
775
+ }
776
+ else if (fcc.allowHtml && typeInfo.get(field).type === 'string') {
777
+ td.html(value);
778
+ }
779
+ else if (value === '') {
780
+ td.html('&nbsp;');
781
+ }
782
+ else {
783
+ td.text(value);
784
+ }
785
+
786
+ self.setCss(td, field);
787
+ self.setAlignment(td, fcc, typeInfo.get(field));
788
+
789
+ rowTr.append(td);
790
+ });
791
+
792
+ if (self.features.rowSelect && isSelected) {
793
+ rowTr.children('td').addClass('wcdv_selected_row');
794
+ }
795
+
796
+ self.ui.tr[i] = rowTr;
797
+ afterElement.after(rowTr);
798
+ afterElement = rowTr;
799
+
800
+ var rowRenderCb = getProp(self.opts, 'events', 'rowRender');
801
+ if (typeof rowRenderCb === 'function') {
802
+ rowRenderCb(rowTr, {
803
+ isGroup: true,
804
+ groupMode: 'details',
805
+ rowData: row.rowData,
806
+ rowNum: row.rowNum
807
+ });
808
+ }
809
+ }
810
+
811
+ isRendered[metadataNode.id] = true;
812
+
813
+ if (i < metadataNode.rows.length) {
814
+ // Not all children were rendered.
815
+
816
+ lastRenderedTr[metadataNode.id] = rowTr;
817
+ for (var p = metadataNode.parent; p != null; p = p.parent) {
818
+ lastRenderedTr[p.id] = rowTr;
819
+ }
820
+
821
+ showMoreTr = jQuery('<tr>', {'class': 'wcdvgrid_more', 'data-wcdv-in-group': metadataNode.id});
822
+
823
+ // Insert spacer columns for previous group fields.
824
+
825
+ for (j = 0; j < metadataNode.groupFieldIndex + 1; j += 1) {
826
+ jQuery('<th>', {'class': 'wcdv_group_col_spacer'})
827
+ .appendTo(showMoreTr);
828
+ }
829
+
830
+ colSpan = columns.length
831
+ + 1 // for the "expand" button column
832
+ + (self.features.rowSelect ? 1 : 0)
833
+ + (self.features.rowReorder ? 1 : 0)
834
+ - (metadataNode.groupFieldIndex + 1);
835
+
836
+ spinnerDiv = jQuery('<div>', {'class': 'spinner'})
837
+ .append(jQuery('<div>', {'class': 'bounce1'}))
838
+ .append(jQuery('<div>', {'class': 'bounce2'}))
839
+ .append(jQuery('<div>', {'class': 'bounce3'}))
840
+ .hide();
841
+
842
+ showMoreTd = jQuery('<td>', {
843
+ 'class': 'wcdv_show_more',
844
+ 'data-wcdv-in-group': metadataNode.id,
845
+ 'data-wcdv-show-more-start': i,
846
+ 'colspan': colSpan
847
+ })
848
+ .append(icon('circle-chevron-down'))
849
+ .append(jQuery('<span>Showing rows 1–' + i + ' of ' + metadataNode.rows.length + '.</span>')
850
+ .css({'padding-left': '0.5em'}))
851
+ .append(jQuery('<button type="button">Load ' + limitConfig.chunkSize + ' more rows.</button>')
852
+ .css({'margin-left': '0.5em'}))
853
+ .append(jQuery('<button type="button" class="wcdv_show_all">Load all rows.</button>')
854
+ .css({'margin-left': '0.5em'})
855
+ )
856
+ .append(spinnerDiv)
857
+ .appendTo(showMoreTr);
858
+
859
+ rowTr.after(showMoreTr);
860
+ }
861
+ lastInsertedTr = rowTr;
862
+ }
863
+
864
+ self._updateSelectionGui();
865
+
866
+ if (self.features.floatingHeader) {
867
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
868
+ case 'tabletool':
869
+ window.TableTool.update();
870
+ break;
871
+ }
872
+ }
873
+ return lastInsertedTr;
874
+ }
875
+
876
+ // showMore() {{{3
877
+
878
+ function showMore(showAll) {
879
+ var elt = jQuery(this).closest('td');
880
+ var metadataId = +(elt.attr('data-wcdv-in-group'));
881
+ var startIndex = +(elt.attr('data-wcdv-show-more-start'));
882
+ var afterElement = lastRenderedTr[metadataId];
883
+
884
+ afterElement.nextAll('tr.wcdvgrid_more[data-wcdv-in-group="' + metadataId + '"]').find('.spinner').show();
885
+
886
+ window.setTimeout(function () {
887
+ render(metadataId, startIndex, afterElement, showAll);
888
+ // No need to hide the spinner because the "show more" row should be gone.
889
+ });
890
+ }
891
+
892
+ // }}}3
893
+
894
+ render();
895
+ self.ui.tbody.on('click', 'button.wcdv_expand_button', toggleGroup);
896
+ self.ui.tbody.on('click', 'td.wcdv_show_more button.wcdv_show_all', function (evt) {
897
+ evt.stopPropagation();
898
+ showMore.call(this, true);
899
+ });
900
+ self.ui.tbody.on('click', 'td.wcdv_show_more', function (evt) {
901
+ showMore.call(this, false);
902
+ });
903
+
904
+ self._updateSelectionGui();
905
+
906
+ if (self.features.floatingHeader) {
907
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
908
+ case 'tabletool':
909
+ window.TableTool.update();
910
+ break;
911
+ }
912
+ }
913
+
914
+ if (typeof cont === 'function') {
915
+ return cont();
916
+ }
917
+ };
918
+
919
+ // #drawFooter {{{2
920
+
921
+ GridTableGroupDetail.prototype.drawFooter = function (columns, data, typeInfo) {
922
+ var self = this;
923
+
924
+ var makeSelectAll = function (tr) {
925
+ self.ui.checkAll_tfoot = jQuery('<input>', {
926
+ 'name': 'checkAll',
927
+ 'type': 'checkbox',
928
+ 'class': 'wcdv_select_group',
929
+ 'data-group-id': '0'
930
+ })
931
+ .on('change', function (evt) {
932
+ self.checkAll(evt);
933
+ });
934
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).append(self.ui.checkAll_tfoot).appendTo(tr);
935
+ };
936
+
937
+ var makeAggregateRow = function () {
938
+ // Circumventing the correct logic here because TableTool requires an empty footer in order to
939
+ // implement horizontal scrolling; if you omit the footer (with a TR and all appropriate TD's in
940
+ // it) then you can't scroll horizontally.
941
+ if (false && getProp(self.defn, 'table', 'footer') == null) {
942
+ return;
943
+ }
944
+
945
+ var tr = jQuery('<tr>');
946
+
947
+ // Add the "select all" checkbox when row selection is enabled.
948
+
949
+ if (self.features.rowSelect) {
950
+ makeSelectAll(tr);
951
+ }
952
+
953
+ for (var spacerIndex = 0; spacerIndex < data.groupFields.length + 1; spacerIndex += 1) {
954
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).appendTo(tr);
955
+ }
956
+
957
+ // Create the columns for the data fields, which contain aggregate function results over those
958
+ // fields.
959
+
960
+ var didFooterCell = false;
961
+
962
+ tr.append(_.map(columns, function (field, colIndex) {
963
+ if (data.groupFields.indexOf(field) >= 0) {
964
+ return;
965
+ }
966
+
967
+ var fcc = self.colConfig.get(field) || {};
968
+ var colTypeInfo = typeInfo.get(field);
969
+ var td = jQuery('<td>');
970
+ var footerConfig = getProp(self.defn, 'table', 'footer', field);
971
+ var agg;
972
+ var aggFun;
973
+ var aggResult;
974
+ var footerVal;
975
+
976
+ self.setCss(td, field);
977
+ self.setAlignment(td, fcc, typeInfo.get(field));
978
+
979
+ if (footerConfig == null) {
980
+ if (didFooterCell) {
981
+ td.addClass('wcdv_divider');
982
+ }
983
+
984
+ didFooterCell = false;
985
+ }
986
+ else {
987
+ if (colIndex > 0) {
988
+ td.addClass('wcdv_divider');
989
+ }
990
+
991
+ didFooterCell = true;
992
+
993
+ // Although the footer config is an aggregate spec, there is one place we allow more
994
+ // flexibility. If the fields aren't set, use the field for the column in which we're
995
+ // displaying this footer. This is merely a convenience for the most common case.
996
+
997
+ if (footerConfig.fields == null) {
998
+ footerConfig.fields = [field];
999
+ }
1000
+
1001
+ self.logDebug(self.makeLogTag() + ' Creating footer using config: %O',
1002
+ self.toString(), field, footerConfig);
1003
+
1004
+ var aggInfo = new AggregateInfo('all', footerConfig, 0, self.colConfig, typeInfo, function (tag, fti) {
1005
+ if (fti.needsDecoding) {
1006
+ self.logDebug(self.makeLogTag() + ' Converting data: { field = "%s", type = "%s" }',
1007
+ self.toString(), field, tag, fti.field, fti.type);
1008
+
1009
+ Source.decodeAll(data.dataByRowId, fti.field);
1010
+ }
1011
+
1012
+ fti.deferDecoding = false;
1013
+ fti.needsDecoding = false;
1014
+ });
1015
+ aggResult = aggInfo.instance.calculate(data.groupMetadata.rows);
1016
+ var aggResult_formatted;
1017
+
1018
+ if (isElement(aggResult)) {
1019
+ footerVal = aggResult;
1020
+ }
1021
+ else {
1022
+ if (aggInfo.instance.inheritFormatting) {
1023
+ aggResult_formatted = format(aggInfo.colConfig[0], aggInfo.typeInfo[0], aggResult, {
1024
+ overrideType: aggInfo.instance.getType()
1025
+ });
1026
+ }
1027
+ else {
1028
+ aggResult_formatted = format(null, null, aggResult, {
1029
+ overrideType: aggInfo.instance.getType(),
1030
+ decode: false
1031
+ });
1032
+ }
1033
+
1034
+ if (aggInfo.debug) {
1035
+ self.logDebug(self.makeLogTag() + ' Aggregate result: %s',
1036
+ self.toString(), field, JSON.stringify(aggResult));
1037
+ }
1038
+
1039
+ switch (typeof footerConfig.format) {
1040
+ case 'function':
1041
+ footerVal = footerConfig.format(aggResult_formatted);
1042
+ break;
1043
+ case 'string':
1044
+ footerVal = sprintf.sprintf(footerConfig.format, aggResult_formatted);
1045
+ break;
1046
+ default:
1047
+ throw new Error('Footer config for field "' + field + '": `format` must be a function or a string');
1048
+ }
1049
+ }
1050
+
1051
+ if (footerVal instanceof Element || footerVal instanceof jQuery) {
1052
+ td.append(footerVal);
1053
+ }
1054
+ else {
1055
+ td.text(footerVal);
1056
+ }
1057
+ }
1058
+
1059
+ return td;
1060
+ }));
1061
+
1062
+ // Finish the row that contains the aggregate functions.
1063
+
1064
+ self.ui.tfoot.append(tr);
1065
+ };
1066
+
1067
+ var makeExternalFooterRow = function () {
1068
+ // Create a new footer row for an external footer that we've absorbed into the grid.
1069
+
1070
+ if (self.opts.footer == null || !self.opts.stealGridFooter) {
1071
+ return;
1072
+ }
1073
+
1074
+ var tr = jQuery('<tr>');
1075
+
1076
+ if (!isVisible(self.opts.footer)) {
1077
+ tr.hide();
1078
+ }
1079
+
1080
+ if (self.features.rowSelect) {
1081
+ // Circumventing the correct logic here because TableTool requires an empty footer in order to
1082
+ // implement horizontal scrolling; if you omit the footer (with a TR and all appropriate TD's
1083
+ // in it) then you can't scroll horizontally.
1084
+ if (true || getProp(self.defn, 'table', 'footer')) {
1085
+ // There is an aggregate row, so it contains the "select all" checkbox.
1086
+ jQuery('<td>', {'class': 'wcdv_group_col_spacer'}).appendTo(tr);
1087
+ }
1088
+ else {
1089
+ // There is no aggregate row, so make the "select all" checkbox here.
1090
+ makeSelectAll(tr);
1091
+ }
1092
+ }
1093
+ // colspan = (spacers: # groupFields + 1) + (columns: # fields - # groupFields) = (# fields) + 1
1094
+ jQuery('<td>', {'colspan': columns.length + 1}).append(self.opts.footer).appendTo(tr);
1095
+ self.ui.tfoot.append(tr);
1096
+ };
1097
+
1098
+ makeAggregateRow();
1099
+ makeExternalFooterRow();
1100
+ };
1101
+
1102
+ // #addWorkHandler {{{2
1103
+
1104
+ GridTableGroupDetail.prototype.addWorkHandler = function () {
1105
+ var self = this;
1106
+
1107
+ self.view.on(ComputedView.events.workEnd, function (info, ops) {
1108
+ if (self._destroyed) { return; }
1109
+ self.logDebug(self.makeLogTag('handler(workEnd)') + ' ComputedView has finished doing work');
1110
+
1111
+ if (!ops.group || ops.pivot) {
1112
+ self.fire('unableToRender', null, ops);
1113
+ return;
1114
+ }
1115
+
1116
+ self.logDebug(self.makeLogTag('handler(workEnd)') + ' Redrawing because the view has done work');
1117
+ self.draw(self.root);
1118
+ }, { who: self });
1119
+ };
1120
+
1121
+ // #_addRowSelectHandler {{{2
1122
+
1123
+ /**
1124
+ * Add an event handler for the row select checkboxes. The event is bound on `self.ui.tbody` and
1125
+ * looks for checkbox inputs inside TD elements with class `wcdv_group_col_spacer` to actually handle
1126
+ * the events. The handler calls `self.select(ROW_NUM)` or `self.unselect(ROW_NUM)` when the
1127
+ * checkbox is changed.
1128
+ */
1129
+
1130
+ GridTableGroupDetail.prototype._addRowSelectHandler = function () {
1131
+ var self = this;
1132
+
1133
+ self.ui.tbody.on('change', 'input[type="checkbox"].wcdv_select_row', function () {
1134
+ var elt = jQuery(this);
1135
+ var rowNum = +elt.attr('data-row-num');
1136
+ var isChecked = elt.prop('checked');
1137
+
1138
+ if (isChecked) {
1139
+ self.select(rowNum);
1140
+ }
1141
+ else {
1142
+ self.unselect(rowNum);
1143
+ }
1144
+ });
1145
+
1146
+ self.ui.tbody.on('change', 'input[type="checkbox"].wcdv_select_group', function () {
1147
+ var elt = jQuery(this);
1148
+ var isChecked = elt.prop('checked');
1149
+ var groupMetadataId = +elt.attr('data-group-id');
1150
+ var rowNums = [];
1151
+
1152
+ // Find all rows that are a descendant of the selected group.
1153
+
1154
+ function recur(node) {
1155
+ if (node.children == null) {
1156
+ rowNums = rowNums.concat(_.pluck(self.data.data[node.rowValIndex], 'rowNum'));
1157
+ }
1158
+ else {
1159
+ _.each(node.children, recur);
1160
+ }
1161
+ }
1162
+
1163
+ recur(self.data.groupMetadata.lookup.byId[groupMetadataId]);
1164
+
1165
+ if (isChecked) {
1166
+ self.select(rowNums);
1167
+ }
1168
+ else {
1169
+ self.unselect(rowNums);
1170
+ }
1171
+ });
1172
+ };
1173
+
1174
+ // #_updateSelectionGui {{{2
1175
+
1176
+ /**
1177
+ * Update the checkboxes in the grid table to match what the current selection is.
1178
+ */
1179
+
1180
+ GridTableGroupDetail.prototype._updateSelectionGui = function () {
1181
+ var self = this;
1182
+
1183
+ // First, deselect all rows (remove "selected" class and uncheck the box).
1184
+
1185
+ self.root.find('tbody td.wcdv_selected_row').removeClass('wcdv_selected_row');
1186
+ self.root.find('tbody input[type="checkbox"].wcdv_select_row').prop('checked', false);
1187
+ self.root.find('tbody input[type="checkbox"].wcdv_select_group').prop('checked', false);
1188
+
1189
+ // Next, find all the TR elements which correspond to selected rows.
1190
+
1191
+ var trs = self.root.find('tbody tr').filter(function (_idx, elt) {
1192
+ return self.selection.indexOf(+(jQuery(elt).attr('data-row-num'))) >= 0;
1193
+ });
1194
+
1195
+ // Select appropriate rows (add "selected" class and check the box).
1196
+
1197
+ trs.children('td').addClass('wcdv_selected_row');
1198
+ trs.find('input[type="checkbox"].wcdv_select_row').prop('checked', true);
1199
+
1200
+ // ===============================================================================================
1201
+ //
1202
+ // DETERMINE GROUPING (HIERARCHICAL, PARENT) CHECKBOX STATES
1203
+ //
1204
+ // ===============================================================================================
1205
+
1206
+ // Initialize the structure with no rows selected in any leaf.
1207
+
1208
+ var numSelected = {};
1209
+
1210
+ _.each(_.keys(self.data.groupMetadata.lookup.byId), function (id) {
1211
+ numSelected[id] = 0;
1212
+ });
1213
+
1214
+ // Determine how many are selected in each leaf of the tree.
1215
+
1216
+ for (var i = 0; i < self.selection.length; i += 1) {
1217
+ var s = self.selection[i];
1218
+ var id = getProp(self.data, 'groupMetadata', 'lookup', 'byRowNum', s, 'id');
1219
+
1220
+ if (id == null) {
1221
+ // This can happen when the selected row has been filtered out, so there's no group metadata
1222
+ // entry for that row number.
1223
+
1224
+ continue;
1225
+ }
1226
+
1227
+ if (numSelected[id] == null) {
1228
+ numSelected[id] = 0;
1229
+ }
1230
+
1231
+ numSelected[id] += 1;
1232
+ }
1233
+
1234
+ // Determine how many are selected at all non-leaf nodes.
1235
+
1236
+ (function () {
1237
+ function postorder(node) {
1238
+ if (node.children != null) {
1239
+ numSelected[node.id] = 0;
1240
+ _.each(node.children, function (c) {
1241
+ postorder(c);
1242
+ numSelected[node.id] += numSelected[c.id];
1243
+ });
1244
+ }
1245
+ }
1246
+
1247
+ postorder(self.data.groupMetadata);
1248
+ })();
1249
+
1250
+ _.each(numSelected, function (count, id) {
1251
+ var numRows = self.data.groupMetadata.lookup.byId[id].numRows;
1252
+ var checkbox = self.root.find('input[type="checkbox"][data-group-id="' + id + '"].wcdv_select_group');
1253
+
1254
+ if (checkbox.length === 0) {
1255
+ // This can happen when the rows for the sub-groups haven't been rendered yet.
1256
+
1257
+ return;
1258
+ }
1259
+
1260
+ if (numRows === 0) {
1261
+ checkbox.prop({
1262
+ disabled: true,
1263
+ indeterminate: false,
1264
+ checked: false,
1265
+ });
1266
+ }
1267
+ else if (count === 0) {
1268
+ checkbox.prop({
1269
+ disabled: false,
1270
+ indeterminate: false,
1271
+ checked: false,
1272
+ });
1273
+ }
1274
+ else if (numRows === count) {
1275
+ checkbox.prop({
1276
+ disabled: false,
1277
+ indeterminate: false,
1278
+ checked: true,
1279
+ });
1280
+ }
1281
+ else {
1282
+ checkbox.prop({
1283
+ disabled: false,
1284
+ indeterminate: true,
1285
+ checked: false,
1286
+ });
1287
+ }
1288
+ });
1289
+ };
1290
+
1291
+ // #checkAll {{{2
1292
+
1293
+ /**
1294
+ * Event handler for using the "check all" checkbox.
1295
+ *
1296
+ * @param {Event} evt
1297
+ * The event generated by the browser when the checkbox is changed.
1298
+ */
1299
+
1300
+ GridTableGroupDetail.prototype.checkAll = function (evt) {
1301
+ var self = this;
1302
+
1303
+ // Synchronize with floating header clone.
1304
+ jQuery(evt.target).parents('div.tabletool').find('input[name="checkAll"]').prop('checked', evt.target.checked);
1305
+
1306
+ // Either select or unselect all rows.
1307
+ if (evt.target.checked) {
1308
+ self.select();
1309
+ }
1310
+ else {
1311
+ self.unselect();
1312
+ }
1313
+ };
1314
+
1315
+ // #addDataToCsv {{{2
1316
+
1317
+ /**
1318
+ * Add all data to the CSV file. Because plain tables frequently don't show all the data, it's not
1319
+ * enough to perform the CSV generation inside the `render()` method like we do with other GridTable
1320
+ * implementations.
1321
+ *
1322
+ * @param {object} data
1323
+ */
1324
+
1325
+ GridTableGroupDetail.prototype.addDataToCsv = function (data) {
1326
+ var self = this;
1327
+ var columns = determineColumns(self.colConfig, data, self.typeInfo);
1328
+
1329
+ self.logDebug(self.makeLogTag() + ' Started generating CSV file', self.toString());
1330
+ self.fire('generateCsvProgress', null, 0);
1331
+
1332
+ self.csv.start();
1333
+ self.csv.addRow();
1334
+
1335
+ _.each(data.groupFields, function (fieldName) {
1336
+ var fcc = self.colConfig.get(fieldName) || {};
1337
+ self.csv.addCol(fcc.displayText || fieldName);
1338
+ });
1339
+ _.each(_.difference(columns, data.groupFields), function (fieldName) {
1340
+ var fcc = self.colConfig.get(fieldName) || {};
1341
+ self.csv.addCol(fcc.displayText || fieldName);
1342
+ });
1343
+
1344
+ function recur(depth, metadataNode) {
1345
+ if (metadataNode.children != null) {
1346
+ _.each(_.keys(metadataNode.children).sort(), function (childName) {
1347
+ self.csv.addRow();
1348
+ for (var j = 0; j < depth; j += 1) {
1349
+ self.csv.addCol();
1350
+ }
1351
+ self.csv.addCol(childName);
1352
+ for (var j = depth + 1; j < columns.length; j += 1) {
1353
+ self.csv.addCol();
1354
+ }
1355
+ recur(depth + 1, metadataNode.children[childName]);
1356
+ });
1357
+ }
1358
+ else {
1359
+ _.each(metadataNode.rows, function (row) {
1360
+ self.csv.addRow();
1361
+ for (var j = 0; j < depth; j += 1) {
1362
+ self.csv.addCol();
1363
+ }
1364
+ _.each(_.difference(columns, data.groupFields), function (field, colIndex) {
1365
+ var fcc = self.colConfig.get(field) || {};
1366
+ var cell = row.rowData[field];
1367
+ var value = format(fcc, self.typeInfo.get(field), cell);
1368
+
1369
+ if (value instanceof Element) {
1370
+ self.csv.addCol(jQuery(value).text());
1371
+ }
1372
+ else if (value instanceof jQuery) {
1373
+ self.csv.addCol(value.text());
1374
+ }
1375
+ else if (fcc.allowHtml && self.typeInfo.get(field).type === 'string' && value.charAt(0) === '<') {
1376
+ self.csv.addCol(jQuery(value).text());
1377
+ }
1378
+ else {
1379
+ self.csv.addCol(value);
1380
+ }
1381
+ });
1382
+ });
1383
+ }
1384
+ }
1385
+
1386
+ recur(0, data.groupMetadata);
1387
+
1388
+ self.csv.finish(function () {
1389
+ self.logDebug(self.makeLogTag() + ' Finished generating CSV file', self.toString());
1390
+ self.csvLock.unlock();
1391
+ self.fire('generateCsvProgress', null, 100);
1392
+ self.fire('csvReady');
1393
+ });
1394
+ };
1395
+
1396
+ // Registry {{{1
1397
+
1398
+ GridRenderer.registry.set('table_group_detail', GridTableGroupDetail);
1399
+
1400
+ // Exports {{{1
1401
+
1402
+ export {
1403
+ GridTableGroupDetail
1404
+ };