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,2510 @@
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
+ createLucideSvg,
10
+ deepCopy,
11
+ determineColumns,
12
+ icon,
13
+ format,
14
+ gensym,
15
+ getElement,
16
+ getProp,
17
+ getPropDef,
18
+ isElement,
19
+ makeSubclass,
20
+ mixinEventHandling,
21
+ mixinLogging,
22
+ objFromArray,
23
+ setTableCell,
24
+ setElement,
25
+ } from '../../util/misc.js';
26
+
27
+ import { Lock, ComputedView, GROUP_FUNCTION_REGISTRY, TableExport, Csv } from 'datavis-ace';
28
+ import {GridRenderer} from '../../grid_renderer.js';
29
+ import PopupMenu from '../../ui/popup_menu.js';
30
+ import { PopupWindow } from '../../ui/popup_window.js';
31
+
32
+ // GridTable {{{1
33
+ // JSDoc Types {{{2
34
+
35
+ /**
36
+ * @typedef {function} GridTable~RowRenderCb
37
+ * A callback that gets executed when a row is rendered in the table.
38
+ *
39
+ * @param {jQuery} tr
40
+ * The row we've just finished rendering.
41
+ *
42
+ * @param {object} opts
43
+ * Additional information for the callback.
44
+ *
45
+ * @param {boolean} opts.isGroup
46
+ * True if we're in group output.
47
+ *
48
+ * @param {boolean} opts.groupMode
49
+ * The group output mode, either "summary" or "detail."
50
+ *
51
+ * @param {string} opts.groupField
52
+ * In group output, detail mode, when rendering a group (i.e. non-leaf node): the name of the field
53
+ * that is currently being rendered. Example: When grouping by [State, County] this property can
54
+ * either by "State" or "County" depending on what part of the tree is being rendered.
55
+ *
56
+ * @param {string} opts.rowValElt
57
+ * In group output, detail mode, when rendering a group (i.e. non-leaf node): the shared value of
58
+ * the field given by `opts.groupField` for all rows in the grouping currently being rendered.
59
+ * Following the previous example, it could be "New Mexico" or "Donut County."
60
+ *
61
+ * @param {metadataNode} opts.groupMetadata
62
+ * In group output, detail mode, when rendering a group (i.e. non-leaf node): additional metadata
63
+ * from the grouping process. Can be used to find the number of children, for example.
64
+ *
65
+ * @param {Array.<object>} rowData
66
+ * In group output, detail mode, when rendering a row (i.e. leaf node): the data that has been
67
+ * rendered.
68
+ *
69
+ * @param {number} rowNum
70
+ * In group output, detail mode, when rendering a row (i.e. leaf node): the unique row identifier.
71
+ */
72
+
73
+ /**
74
+ * @typedef {function} GridTable~AddCols_Value_Plain
75
+ *
76
+ * @param {Array.<object>} rowData
77
+ * The data of the row that has been rendered.
78
+ *
79
+ * @param {number} rowNum
80
+ * The unique ID of thw row that was rendered.
81
+ */
82
+
83
+ /**
84
+ * @typedef {function} GridTable~AddCols_Value_Pivot
85
+ *
86
+ * @param {object} data
87
+ * @param {number} groupNum
88
+ */
89
+
90
+ /**
91
+ * @typedef GridTable~AddCols
92
+ *
93
+ * @property {string} name
94
+ * The name of the column to add, which appears in the table header.
95
+ *
96
+ * @property {GridTable~AddCols_Value_Plain|GridTable~AddCols_Value_Pivot} value
97
+ * A function that is called to determine what gets put into the table cell.
98
+ */
99
+
100
+ /**
101
+ * @typedef GridTable~CtorOpts
102
+ *
103
+ * @property {boolean} [drawInternalBorders=true]
104
+ * If true, draw borders between the cells in the table.
105
+ *
106
+ * @property {boolean} [zebraStriping=true]
107
+ * If true, use subtle alternating background colors in the table rows.
108
+ *
109
+ * @property {boolean} [generateCsv=true]
110
+ * If true, allow the generation of a CSV file from the table contents.
111
+ *
112
+ * @property {boolean} [stealGridFooter=true]
113
+ * If true, absorb the element specified by `footer` into the table footer.
114
+ *
115
+ * @property {object} [addClass]
116
+ * Additional classes to add when generating the table.
117
+ *
118
+ * @property {string} [addClass.table]
119
+ * Classes to add on the table element itself.
120
+ *
121
+ * @property {Array.<GridTable~AddCols>} [addCols]
122
+ * Columns to add to the table. These are always computed as rows are rendered, and they are not
123
+ * backed by the ComputedView so they can't be sorted or filtered. This option is best used as a way of
124
+ * adding some UI to the table row.
125
+ *
126
+ * @property {object} [events]
127
+ * Callbacks to bind on various events.
128
+ *
129
+ * @property {GridTable~RowRenderCb} [events.rowRender]
130
+ * A callback to invoke when a row is rendered.
131
+ *
132
+ * @property {jQuery} [footer]
133
+ * **Internal** An element to put into the table footer.
134
+ *
135
+ * @property {boolean} [fixedHeight]
136
+ * **Internal** If true, configure the table to scroll within the parent element.
137
+ */
138
+
139
+ // Constructor {{{2
140
+
141
+ /**
142
+ * @class
143
+ * @extends GridRenderer
144
+ *
145
+ * An abstract base class for all grid tables (which are responsible for building the DOM elements
146
+ * to represent the data in a tabular format). Concrete subclasses must implement the following
147
+ * methods:
148
+ *
149
+ * - `drawHeader(columns, data, typeInfo, opts)`
150
+ * - `drawBody(data, typeInfo, columns, cont, opts)`
151
+ * - `addWorkHandler()`
152
+ * - `canRender()`
153
+ *
154
+ * @property {number} UNIQUE_ID
155
+ * A unique number for this grid table, used to generate namespaces for event handlers.
156
+ *
157
+ * @property {string} id
158
+ *
159
+ * @property {Grid} grid
160
+ *
161
+ * @property {object} defn
162
+ *
163
+ * @property {ComputedView} view
164
+ *
165
+ * @property {object} features
166
+ *
167
+ * @property {GridTable~CtorOpts} opts
168
+ * Additional options for the renderer.
169
+ *
170
+ * @property {Timing} timing
171
+ *
172
+ * @property {Array.<number>} selection
173
+ * An array of the row IDs of selected rows. The row ID here refers to that used by the source, so
174
+ * the selection maps directly back to the underlying source data.
175
+ *
176
+ * @property {boolean} needsRedraw
177
+ * If true, then the view has done something that requires us to be redrawn.
178
+ *
179
+ * @property {OrdMap} colConfig
180
+ */
181
+
182
+ var GridTable = makeSubclass('GridTable', GridRenderer, function () {
183
+ var self = this;
184
+
185
+ self.super['GridRenderer'].ctor.apply(self, arguments);
186
+
187
+ self.selection = [];
188
+ self.needsRedraw = false;
189
+ self.popupMenus = [];
190
+ self.csvLock = new Lock('GridTable/csv');
191
+ self.autoResizeColsLock = new Lock('GridTable/autoResizeCols');
192
+ self.focus = {
193
+ rvi: [],
194
+ cvi: []
195
+ };
196
+
197
+ _.defaults(self.opts, {
198
+ drawInternalBorders: true,
199
+ zebraStriping: true,
200
+ generateCsv: true,
201
+ stealGridFooter: true
202
+ });
203
+ });
204
+
205
+ mixinLogging(GridTable);
206
+
207
+ // Events {{{2
208
+
209
+ /**
210
+ * Fired when columns have been resized automatically. No longer used.
211
+ *
212
+ * @event GridTable#columnResize
213
+ */
214
+
215
+ /**
216
+ * Fired when the current GridRenderer subclass instance is unable to render the data from the view,
217
+ * potentially because the view performed an operation (e.g. pivot) that this renderer is not able
218
+ * to show the result of.
219
+ *
220
+ * @event GridTable#unableToRender
221
+ *
222
+ * @param {ComputedView~OperationsPerformed} ops
223
+ * The operations performed by the view.
224
+ */
225
+
226
+ /**
227
+ * Fired when the output has been limited according to the renderer's limit configuration.
228
+ *
229
+ * @event GridTable#limited
230
+ */
231
+
232
+ /**
233
+ * Fired when all output is being shown, even though the grid is configured to limit output. Most
234
+ * likely, this is due to the number of rows not reaching the threshold configured for limiting.
235
+ *
236
+ * @event GridTable#unlimited
237
+ */
238
+
239
+ /**
240
+ * Fired when asynchronous CSV generation is finished.
241
+ *
242
+ * @event GridTable#csvReady
243
+ */
244
+
245
+ /**
246
+ * Fired periodically while generating the CSV file to indicate progress. Before rendering starts,
247
+ * it will be fired with a `progress` value of 0. After rendering is done, it will be fired with a
248
+ * `progress` value of 100.
249
+ *
250
+ * @event GridTable#generateCsvProgress
251
+ *
252
+ * @param {number} progress
253
+ * The progress on a scale from 0 to 100.
254
+ */
255
+
256
+ /**
257
+ * Fired when rendering has started.
258
+ *
259
+ * @event GridTable#renderBegin
260
+ */
261
+
262
+ /**
263
+ * Fired when rendering has finished.
264
+ *
265
+ * @event GridTable#renderEnd
266
+ */
267
+
268
+ mixinEventHandling(GridTable, [
269
+ 'columnResize' // A column is resized.
270
+ , 'unableToRender' // A grid table can't render the data in the view it's bound to.
271
+ , 'limited' // The grid table isn't rendering all possible rows.
272
+ , 'unlimited' // The grid table is rendering all possible rows.
273
+ , 'csvReady' // CSV data has been generated.
274
+ , 'generateCsvProgress' // CSV generation progress.
275
+ , 'renderBegin'
276
+ , 'renderEnd'
277
+ , 'selectionChange'
278
+ ]);
279
+
280
+ // #_validateFeatures {{{2
281
+
282
+ GridTable.prototype._validateFeatures = function () {
283
+ var self = this;
284
+
285
+ if (self.features.block && !jQuery.blockUI) {
286
+ self.logError(self.makeLogTag() + ' GRID TABLE // CONFIG',
287
+ 'Feature "block" requires BlockUI library, which is not present');
288
+ self.features.block = false;
289
+ }
290
+
291
+ if (self.features.limit) {
292
+ self._validateLimit();
293
+
294
+ self.scrollEvents = ['DOMContentLoaded', 'load', 'resize', 'scroll'].map(function (x) {
295
+ return x + '.wcdv_gt_' + self.UNIQUE_ID;
296
+ }).join(' ');
297
+ }
298
+
299
+ if (self.features.floatingHeader) {
300
+ self._validateFloatTableHeader();
301
+ }
302
+ };
303
+
304
+ // #_validateLimit {{{2
305
+
306
+ /**
307
+ * Make sure the limit configuration is good. If there's anything wrong, the limit feature is
308
+ * disabled automatically.
309
+ */
310
+
311
+ GridTable.prototype._validateLimit = function () {
312
+ var self = this;
313
+
314
+ if (self.features.limit) {
315
+ if (self.defn.table.limit.threshold === undefined) {
316
+ self.logDebug(self.makeLogTag('validation') + ' Disabling limit feature because no limit threshold was provided');
317
+ self.features.limit = false;
318
+ }
319
+ }
320
+ };
321
+
322
+ // #_validateFloatTableHeader {{{2
323
+
324
+ GridTable.prototype._validateFloatTableHeader = function () {
325
+ var self = this;
326
+
327
+ if (!self.features.floatingHeader) {
328
+ return;
329
+ }
330
+
331
+ var config = getPropDef({}, self.defn, 'table', 'floatingHeader');
332
+
333
+ if (config.method != null) {
334
+
335
+ // The user requested a specific method for doing the floating header, make sure that the
336
+ // library required is actually available.
337
+
338
+ switch (config.method) {
339
+ case 'floatThead':
340
+ if (jQuery.prototype.floatThead == null) {
341
+ self.logError(self.makeLogTag('validation') + ' Requested floating header method "floatThead" is not available');
342
+ self.features.floatingHeader = false;
343
+ }
344
+ break;
345
+ case 'fixedHeaderTable':
346
+ if (jQuery.prototype.fixedHeaderTable == null) {
347
+ self.logError(self.makeLogTag('validation') + ' Requested floating header method "fixedHeaderTable" is not available');
348
+ self.features.floatingHeader = false;
349
+ }
350
+ break;
351
+ case 'tabletool':
352
+ if (window.TableTool == null) {
353
+ self.logError(self.makeLogTag('validation') + ' Requested floating header method "tabletool" is not available');
354
+ self.features.floatingHeader = false;
355
+ }
356
+ break;
357
+ case 'css':
358
+ // TODO Check for browser support.
359
+ break;
360
+ default:
361
+ self.logError(self.makeLogTag('validation') + ' Unrecognized floating header method: ' + config.method);
362
+ self.features.floatingHeader = false;
363
+ }
364
+ }
365
+ else {
366
+
367
+ // The user didn't request a specific method for doing the floating header, so let's look at
368
+ // what libraries are available and pick based on that.
369
+
370
+ if (jQuery.prototype.floatThead) {
371
+ config.method = 'floatThead';
372
+ }
373
+ else if (jQuery.prototype.fixedHeaderTable) {
374
+ config.method = 'fixedHeaderTable';
375
+ }
376
+ else if (window.TableTool) {
377
+ config.method = 'tabletool';
378
+ }
379
+ else {
380
+ config.method = 'css';
381
+ }
382
+ // else {
383
+ // self.features.floatingHeader = false;
384
+ // }
385
+ }
386
+
387
+ self.defn.table.floatingHeader = config;
388
+ };
389
+
390
+ // #toString {{{2
391
+
392
+ GridTable.prototype.toString = function () {
393
+ var self = this;
394
+ return 'GridTable(' + self.UNIQUE_ID + ')';
395
+ };
396
+
397
+ // #setCss {{{2
398
+
399
+ GridTable.prototype.setCss = function (elt, field) {
400
+ var self = this;
401
+ var fcc = self.colConfig.get(field);
402
+
403
+ if (fcc == null) {
404
+ return;
405
+ }
406
+
407
+ var css = [
408
+ { configName: 'width' , cssName: 'width' },
409
+ { configName: 'minWidth' , cssName: 'min-width' },
410
+ { configName: 'maxWidth' , cssName: 'max-width' },
411
+ { configName: 'cellAlignment', cssName: 'text-align' }
412
+ ];
413
+
414
+ for (var i = 0; i < css.length; i += 1) {
415
+ if (fcc[css[i].configName] !== undefined) {
416
+ elt.css(css[i].cssName, fcc[css[i].configName]);
417
+ }
418
+ }
419
+ };
420
+
421
+ // #setAlignment {{{2
422
+
423
+ /**
424
+ * Set the alignment on a table cell.
425
+ *
426
+ * @param {HTMLElement} elt
427
+ * The element to set alignment on.
428
+ *
429
+ * @param {Grid~FieldColConfig} [fcc]
430
+ * Column configuration for the field that this cell is based on.
431
+ *
432
+ * @param {Grid~FieldTypeInfo} [fti]
433
+ * Type information for the field that this cell is based on.
434
+ *
435
+ * @param {string} [overrideType]
436
+ * Override the type of the field, used when an aggregate function produces a result with a
437
+ * different type than the source field (e.g. distinctValues of a date produces a string, not a
438
+ * date, so `overrideType` should be "string").
439
+ *
440
+ * @param {string} [fallback]
441
+ * Fallback default alignment when no alignment is determined by DataVis.
442
+ */
443
+
444
+ GridTable.prototype.setAlignment = function (elt, fcc, fti, overrideType, fallback) {
445
+ fcc = fcc || {};
446
+ fti = fti || {};
447
+
448
+ if (elt instanceof jQuery) {
449
+ elt = elt.get(0);
450
+ }
451
+
452
+ if (!(elt instanceof Element)) {
453
+ throw new Error('Call Error: `elt` must be an instance of Element');
454
+ }
455
+
456
+ var type = overrideType || fti.type;
457
+ var alignment = fcc.cellAlignment || fallback;
458
+
459
+ if (alignment == null && (type === 'number' || type === 'currency')) {
460
+ alignment = 'right';
461
+ }
462
+
463
+ switch (alignment) {
464
+ case 'left':
465
+ elt.classList.add('wcdvgrid_textLeft');
466
+ break;
467
+ case 'right':
468
+ elt.classList.add('wcdvgrid_textRight');
469
+ break;
470
+ case 'center':
471
+ elt.classList.add('wcdvgrid_textCenter');
472
+ break;
473
+ case 'justify':
474
+ elt.classList.add('wcdvgrid_textJustify');
475
+ break;
476
+ default:
477
+ // We don't have a class for every possible value, so just set the style rule on the element in
478
+ // those cases. This should be extremely rare, given what we've covered above.
479
+ elt.style.setProperty('text-align', alignment);
480
+ }
481
+ };
482
+
483
+ // #_addColumnResizeHandle {{{2
484
+
485
+ /**
486
+ * Adds a resize handle to a column header that allows the user to drag to resize the column.
487
+ *
488
+ * @param {jQuery} headingTh
489
+ * The TH element to add the resize handle to.
490
+ *
491
+ * @param {string} field
492
+ * The field name for this column.
493
+ *
494
+ * @param {number} colIndex
495
+ * The column index.
496
+ */
497
+
498
+ GridTable.prototype._addColumnResizeHandle = function (headingTh, field, colIndex) {
499
+ var self = this;
500
+
501
+ var resizeHandle = document.createElement('div');
502
+ resizeHandle.className = 'wcdv_column_resize_handle';
503
+ resizeHandle.setAttribute('title', trans('GRID.TABLE.RESIZE_COLUMN'));
504
+ resizeHandle.setAttribute('aria-label', trans('GRID.TABLE.RESIZE_COLUMN'));
505
+
506
+ var startX = 0;
507
+ var startWidth = 0;
508
+ var isResizing = false;
509
+ var resizeIndicator = null;
510
+ var targetWidth = 0;
511
+
512
+ var onMouseDown = function (e) {
513
+ e.preventDefault();
514
+ e.stopPropagation();
515
+
516
+ isResizing = true;
517
+ startX = e.pageX;
518
+ startWidth = headingTh.outerWidth();
519
+
520
+ // Create a dotted line indicator
521
+ resizeIndicator = document.createElement('div');
522
+ resizeIndicator.className = 'wcdv_column_resize_indicator';
523
+ resizeIndicator.style.position = 'absolute';
524
+ resizeIndicator.style.top = '0';
525
+ resizeIndicator.style.bottom = '0';
526
+ resizeIndicator.style.width = '2px';
527
+ resizeIndicator.style.borderLeft = '2px dotted #666';
528
+ resizeIndicator.style.pointerEvents = 'none';
529
+ resizeIndicator.style.zIndex = '1000';
530
+
531
+ // Position the indicator at the current column edge
532
+ var tableOffset = self.ui.tbl.offset();
533
+ var thOffset = headingTh.offset();
534
+ var initialLeft = thOffset.left - tableOffset.left + startWidth;
535
+ resizeIndicator.style.left = initialLeft + 'px';
536
+
537
+ self.ui.tbl.css('position', 'relative');
538
+ self.ui.tbl.get(0).appendChild(resizeIndicator);
539
+
540
+ // Add a class to the table to indicate we're resizing
541
+ self.ui.tbl.addClass('wcdv_resizing');
542
+
543
+ jQuery(document).on('mousemove.wcdv_col_resize', onMouseMove);
544
+ jQuery(document).on('mouseup.wcdv_col_resize', onMouseUp);
545
+ };
546
+
547
+ var onMouseMove = function (e) {
548
+ if (!isResizing) return;
549
+
550
+ var diff = e.pageX - startX;
551
+ targetWidth = Math.max(50, startWidth + diff); // Minimum width of 50px
552
+
553
+ // Update the position of the dotted line indicator
554
+ if (resizeIndicator) {
555
+ var tableOffset = self.ui.tbl.offset();
556
+ var thOffset = headingTh.offset();
557
+ var newLeft = thOffset.left - tableOffset.left + targetWidth;
558
+ resizeIndicator.style.left = newLeft + 'px';
559
+ }
560
+ };
561
+
562
+ var onMouseUp = function (e) {
563
+ if (!isResizing) return;
564
+
565
+ isResizing = false;
566
+ self.ui.tbl.removeClass('wcdv_resizing');
567
+
568
+ // Remove the indicator
569
+ if (resizeIndicator && resizeIndicator.parentNode) {
570
+ resizeIndicator.parentNode.removeChild(resizeIndicator);
571
+ resizeIndicator = null;
572
+ }
573
+
574
+ jQuery(document).off('mousemove.wcdv_col_resize');
575
+ jQuery(document).off('mouseup.wcdv_col_resize');
576
+
577
+ // Not honestly sure why, but the targetWidth is always 9 pixels bigger than where the dotted
578
+ // line is ending up, so if we don't put this in, the column actually ends up 9 pixels wider
579
+ // than where the indicator was. Not a good experience.
580
+
581
+ targetWidth -= 9;
582
+
583
+ headingTh.css('width', targetWidth + 'px');
584
+ headingTh.css('min-width', targetWidth + 'px');
585
+
586
+ var fcc = self.colConfig.get(field);
587
+ if (fcc != null) {
588
+ fcc.width = targetWidth;
589
+ }
590
+
591
+ self.fire('columnResize', { field: field, width: targetWidth });
592
+
593
+ if (self.grid && typeof self.grid.setColConfig === 'function') {
594
+ self.grid.setColConfig(self.colConfig, {
595
+ from: 'ui',
596
+ savePrefs: true,
597
+ dontSendEventTo: [self]
598
+ });
599
+ }
600
+ };
601
+
602
+ jQuery(resizeHandle).on('mousedown.wcdv_col_resize', onMouseDown);
603
+ headingTh.get(0).appendChild(resizeHandle);
604
+ headingTh.addClass('wcdv_resizable_column');
605
+ };
606
+
607
+ // #_addColumnReorderHandler {{{2
608
+
609
+ /**
610
+ * Makes a column header draggable using jQuery UI to allow reordering columns via drag-and-drop.
611
+ * Also makes it droppable to accept other column headers. This implementation is compatible with
612
+ * the jQuery UI droppable group control panel for dragging headers to group/pivot controls.
613
+ *
614
+ * @param {jQuery} headingTh
615
+ * The TH element to make draggable and droppable.
616
+ *
617
+ * @param {string} field
618
+ * The field name for this column.
619
+ *
620
+ * @param {number} colIndex
621
+ * The column index.
622
+ *
623
+ * @param {Array.<string>} columns
624
+ * The array of column field names.
625
+ */
626
+
627
+ GridTable.prototype._addColumnReorderHandler = function (headingTh, field, colIndex, columns) {
628
+ var self = this;
629
+ var dragHandle = headingTh.find('.wcdv_heading_title');
630
+
631
+ if (dragHandle.length === 0) {
632
+ dragHandle = headingTh;
633
+ }
634
+
635
+ dragHandle.css('cursor', 'grab');
636
+ headingTh.addClass('wcdv_reorderable_column');
637
+
638
+ // Create the drop indicator line if it doesn't exist yet
639
+ if (!self.ui.columnDropIndicator) {
640
+ self.ui.columnDropIndicator = jQuery('<div>', {
641
+ 'class': 'wcdv_column_drop_indicator'
642
+ });
643
+ self.root.append(self.ui.columnDropIndicator);
644
+ }
645
+
646
+ // Make the heading draggable using jQuery UI to be compatible with droppable group/pivot controls
647
+ dragHandle.draggable({
648
+ classes: {
649
+ 'ui-draggable-handle': 'wcdv_drag_handle'
650
+ },
651
+ distance: 8, // FIXME Deprecated [1.12]: replacement will be in 1.13
652
+ helper: 'clone',
653
+ appendTo: document.body,
654
+ revert: true,
655
+ revertDuration: 0,
656
+ start: function (event, ui) {
657
+ headingTh.addClass('wcdv_column_dragging');
658
+ self._dragSourceField = field;
659
+ self._dragSourceIndex = colIndex;
660
+ // ui.helper.css({
661
+ // 'z-index': 1000
662
+ // });
663
+ },
664
+ stop: function (event, ui) {
665
+ headingTh.removeClass('wcdv_column_dragging');
666
+ jQuery('.wcdv_column_drop_target').removeClass('wcdv_column_drop_target');
667
+ self.ui.columnDropIndicator.hide();
668
+ delete self._dragSourceField;
669
+ delete self._dragSourceIndex;
670
+ }
671
+ });
672
+
673
+ // Make the heading droppable to accept other column headers
674
+ headingTh.droppable({
675
+ accept: '.wcdv_heading_title, .wcdv_reorderable_column',
676
+ tolerance: 'pointer',
677
+ over: function (event, ui) {
678
+ if (self._dragSourceField && self._dragSourceField !== field) {
679
+ headingTh.addClass('wcdv_column_drop_target');
680
+ // Calculate the position for the drop indicator
681
+ var thOffset = headingTh.offset();
682
+ var rootOffset = self.root.offset();
683
+ var tableHeight = self.ui.tbl.outerHeight();
684
+ var thWidth = headingTh.outerWidth();
685
+
686
+ // Imagine columns: A B C. When dragging field "A" from over "C" to over "B", the events
687
+ // fire in the order of columns, so moving backwards fires B's <over> before C's <out>,
688
+ // meaning the indicator is moved and immediately hidden. This slight delay ensures the
689
+ // indicator is always hidden by "C" before being moved and shown by "B".
690
+
691
+ window.setTimeout(function () {
692
+ // Position the indicator at the right edge of the target column
693
+ self.ui.columnDropIndicator.css({
694
+ left: (thOffset.left - rootOffset.left + thWidth) + 8 + 'px',
695
+ top: thOffset.top + 'px',
696
+ height: tableHeight + 'px'
697
+ }).show();
698
+ });
699
+ }
700
+ },
701
+ out: function (event, ui) {
702
+ headingTh.removeClass('wcdv_column_drop_target');
703
+ self.ui.columnDropIndicator.hide();
704
+ },
705
+ drop: function (event, ui) {
706
+ headingTh.removeClass('wcdv_column_drop_target');
707
+ self.ui.columnDropIndicator.hide();
708
+
709
+ var sourceField = self._dragSourceField;
710
+ var sourceIndex = self._dragSourceIndex;
711
+
712
+ if (!sourceField || sourceField === field) {
713
+ return;
714
+ }
715
+
716
+ // Find the target index
717
+ var targetIndex = colIndex;
718
+
719
+ // Reorder the columns in colConfig
720
+ self._reorderColumn(sourceField, sourceIndex, targetIndex);
721
+ }
722
+ });
723
+ };
724
+
725
+ // #_reorderColumn {{{2
726
+
727
+ /**
728
+ * Reorders a column from one position to another.
729
+ *
730
+ * @param {string} sourceField
731
+ * The field being moved.
732
+ *
733
+ * @param {number} fromIndex
734
+ * The original index of the column.
735
+ *
736
+ * @param {number} toIndex
737
+ * The target index for the column.
738
+ */
739
+
740
+ GridTable.prototype._reorderColumn = function (sourceField, fromIndex, toIndex) {
741
+ var self = this;
742
+
743
+ // Get all keys in order
744
+ var keys = self.colConfig.keys();
745
+
746
+ // Remove the source field from its current position
747
+ var actualFromIndex = keys.indexOf(sourceField);
748
+ if (actualFromIndex === -1) {
749
+ return;
750
+ }
751
+
752
+ keys.splice(actualFromIndex, 1);
753
+
754
+ // Insert at the new position
755
+ // Adjust target index if needed (if we removed an item before the target)
756
+ var adjustedToIndex = toIndex;
757
+ if (actualFromIndex < toIndex) {
758
+ adjustedToIndex -= 1;
759
+ }
760
+
761
+ keys.splice(adjustedToIndex, 0, sourceField);
762
+
763
+ // Rebuild colConfig with new order
764
+ var OrdMap = self.colConfig.constructor;
765
+ var newColConfig = new OrdMap();
766
+
767
+ keys.forEach(function (key) {
768
+ newColConfig.set(key, self.colConfig.get(key));
769
+ });
770
+
771
+ self.grid.setColConfig(newColConfig, {
772
+ from: 'ui',
773
+ savePrefs: true
774
+ });
775
+ };
776
+
777
+ // #_addSortingToHeader {{{2
778
+
779
+ /**
780
+ * Attaches a sort icon to the given table header element, which (1) indicates the current sort, and
781
+ * (2) when clicked brings up a menu to allow sorting by that header.
782
+ *
783
+ * @param {any} data
784
+ *
785
+ * @param {string} orientation
786
+ * Indicates whether the sorting is `horizontal` (i.e. sorting reorders columns) or `vertical` (i.e.
787
+ * sorting reorders rows).
788
+ *
789
+ * @param {ComputedView~SortSpec} spec
790
+ * The sort spec.
791
+ *
792
+ * @param {Element} th
793
+ * Where to place the sort icon.
794
+ *
795
+ * @param {Array.<ComputedView~AggInfo>} agg
796
+ * Aggregate functions which we can sort by their results.
797
+ */
798
+
799
+ GridTable.prototype._addSortingToHeader = function (data, orientation, spec, container, agg) {
800
+ var self = this;
801
+
802
+ if (!self.features.sort) {
803
+ return;
804
+ }
805
+
806
+ if (['horizontal', 'vertical'].indexOf(orientation) < 0) {
807
+ throw new Error('Call Error: `orientation` must be "horizontal" or "vertical"');
808
+ }
809
+ if (!(container instanceof Element)) {
810
+ throw new Error('Call Error: `container` must be an Element');
811
+ }
812
+
813
+ var sortIcon_orientationClass = 'wcdv_sort_icon_' + orientation;
814
+
815
+ /**
816
+ * @param {Element} span
817
+ * The sort indicator span to replace.
818
+ *
819
+ * @param {string} [dir]
820
+ * What direction we're sorting by, ascending or descending.
821
+ */
822
+
823
+ var replaceSortIndicator = function (span, dir) {
824
+ if (!(span instanceof Element)) {
825
+ throw new Error('Call Error: `span` must be an Element');
826
+ }
827
+ if (dir != null) {
828
+ if (!_.isString(dir)) {
829
+ throw new Error('Call Error: `dir` must be null or a string');
830
+ }
831
+ else if (dir.toUpperCase() !== 'ASC' && dir.toUpperCase() !== 'DESC') {
832
+ throw new Error('Call Error: `dir` must be either "ASC" or "DESC"');
833
+ }
834
+ }
835
+
836
+ var th = container.closest('th');
837
+
838
+ th.classList.remove('wcdv_sort_column_active');
839
+ th.classList.remove('wcdv_bg-primary');
840
+
841
+ // Replace the icon content based on the sort direction.
842
+ var iconName = 'arrow-up-down';
843
+ if (dir != null) {
844
+ th.classList.add('wcdv_sort_column_active');
845
+ th.classList.add('wcdv_bg-primary');
846
+ iconName = dir.toUpperCase() === 'ASC' ? 'arrow-up' : 'arrow-down';
847
+ }
848
+
849
+ var newIcon = createLucideSvg(iconName);
850
+ if (newIcon) {
851
+ newIcon.classList.add('wcdv_icon');
852
+ newIcon.setAttribute('data-icon', iconName);
853
+ while (span.firstChild) {
854
+ span.removeChild(span.firstChild);
855
+ }
856
+ span.appendChild(newIcon);
857
+ }
858
+ };
859
+
860
+ /**
861
+ * Set the sorting for the view to the current orientation/spec, on the specified aggregate number
862
+ * and in the specified direction.
863
+ *
864
+ * @param {string} dir
865
+ *
866
+ * @param {number} [aggNum]
867
+ * If missing, no aggregate number is added to the sort spec. Used when sorting directly by the
868
+ * field (e.g. in plain output) or by the group field index (e.g. in group detail output).
869
+ */
870
+
871
+ var setSort = function (dir, aggNum) {
872
+ if (!_.isString(dir)) {
873
+ throw new Error('Call Error: `dir` must be a string');
874
+ }
875
+ else if (dir.toUpperCase() !== 'ASC' && dir.toUpperCase() !== 'DESC') {
876
+ throw new Error('Call Error: `dir` must be either "ASC" or "DESC"');
877
+ }
878
+
879
+ if (aggNum != null && !_.isNumber(aggNum)) {
880
+ throw new Error('Call Error: `aggNum` must be a number');
881
+ }
882
+
883
+ jQuery('button.wcdv_icon_button' + sortIcon_orientationClass + '.wcdv_sort_icon').each(function (i, elt) {
884
+ replaceSortIndicator(elt);
885
+ });
886
+
887
+ jQuery('button.wcdv_icon_button.' + sortIcon_class).each(function (i, elt) {
888
+ replaceSortIndicator(elt, dir);
889
+ });
890
+
891
+ spec.aggNum = aggNum;
892
+ spec.dir = dir;
893
+
894
+ var sortSpec = self.view.getSort() || {};
895
+ sortSpec[orientation] = deepCopy(spec);
896
+ self.view.setSort(sortSpec, self.makeProgress('Sort'));
897
+ };
898
+
899
+ var sortIcon_class = gensym();
900
+
901
+ // Create the sort icon container with an initial neutral "sortable" icon.
902
+ var sortIcon_btn = document.createElement('button');
903
+ sortIcon_btn.classList.add('wcdv_icon_button');
904
+ sortIcon_btn.classList.add(sortIcon_class);
905
+ sortIcon_btn.classList.add(sortIcon_orientationClass);
906
+ sortIcon_btn.classList.add('wcdv_sort_icon');
907
+ if (orientation === 'horizontal') {
908
+ sortIcon_btn.classList.add('wcdv_icon_rotate_270');
909
+ }
910
+ var initialIcon = createLucideSvg('arrow-up-down');
911
+ if (initialIcon) {
912
+ initialIcon.classList.add('wcdv_icon');
913
+ initialIcon.setAttribute('data-icon', 'arrow-up-down');
914
+ sortIcon_btn.appendChild(initialIcon);
915
+ }
916
+
917
+ var sortIcon_menu = new PopupMenu();
918
+
919
+ if (spec.field != null || spec.groupFieldIndex != null || spec.pivotFieldIndex != null) {
920
+
921
+ // We're sorting by a field. This can occur in these situations:
922
+ //
923
+ // 1. Sorting plain output by any column.
924
+ // 2. Sorting group/pivot output by a field that we've grouped by.
925
+ // 3. Sorting pivot output by a field that we've pivotted by.
926
+
927
+ var name = spec.field != null
928
+ ? spec.field
929
+ : spec.groupFieldIndex != null
930
+ ? data.groupFields[spec.groupFieldIndex]
931
+ : spec.pivotFieldIndex != null
932
+ ? data.pivotFields[spec.pivotFieldIndex]
933
+ : 'Unknown'
934
+ ;
935
+
936
+ sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.ASCENDING', name), 'arrow-up-narrow-wide', function () {
937
+ setSort('asc');
938
+ });
939
+ sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.DESCENDING', name), 'arrow-down-wide-narrow', function () {
940
+ setSort('desc');
941
+ });
942
+ sortIcon_menu.addSeparator();
943
+ }
944
+ else {
945
+
946
+ // We're sorting by the result of an aggregate function.
947
+
948
+ _.each(agg, function (aggInfo, aggNum) {
949
+ if (spec.aggType != null && spec.aggNum !== aggNum) {
950
+ return;
951
+ }
952
+
953
+ //var aggType = aggInfo.instance.getType();
954
+ sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.ASCENDING', aggInfo.instance.getFullName()), 'arrow-up-narrow-wide', (function (n) {
955
+ return function () { setSort('asc', n); };
956
+ })(aggNum));
957
+ sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.DESCENDING', aggInfo.instance.getFullName()), 'arrow-down-wide-narrow', (function (n) {
958
+ return function () { setSort('desc', n); };
959
+ })(aggNum));
960
+ sortIcon_menu.addSeparator();
961
+ });
962
+ }
963
+
964
+ // Include an option to reset the sort. This is just as much to fluff up the all-too-common
965
+ // two-entry menu as anything else.
966
+
967
+ sortIcon_menu.addItem(trans('GRID.TABLE.SORT_MENU.RESET_SORT'), 'ban', function () {
968
+ self.view.clearSort();
969
+ });
970
+
971
+ sortIcon_btn.addEventListener('click', function () {
972
+ sortIcon_menu.open(sortIcon_btn);
973
+ });
974
+
975
+ self.popupMenus.push(sortIcon_menu);
976
+
977
+ container.appendChild(sortIcon_btn);
978
+
979
+ // Now check the existing sort specification in the view to see if any of the sort icons that we
980
+ // just created should be lit up.
981
+
982
+ var sortSpec_copy = deepCopy(self.view.getSort());
983
+ var spec_copy = deepCopy(spec);
984
+
985
+ if (sortSpec_copy[orientation]) {
986
+ var currentDir = sortSpec_copy[orientation].dir;
987
+
988
+ // Delete things that would be in the view's spec that aren't in the spec we were provided by
989
+ // the caller (because they're independent of the user interface reflecting the sort). This way
990
+ // we can just do an object-object comparison to see if what we just made corresponds to the
991
+ // sort that is already set in the view. Crucially, for grid tables that redraw when the view
992
+ // is updated, this is the only way you're ever going to see what the sort is.
993
+
994
+ delete sortSpec_copy[orientation].dir;
995
+
996
+ // Note that `aggNum` is an important part of the spec when sorting group or pivot aggregates
997
+ // (i.e. total rows/columns) because they have their own row/column, and aren't thrown together
998
+ // like cell aggregates are.
999
+
1000
+ if (spec.aggType == null) {
1001
+ delete sortSpec_copy[orientation].aggNum;
1002
+ delete spec_copy.aggNum;
1003
+ }
1004
+
1005
+ self.logDebug(self.makeLogTag() + ' orientation = %s ; spec = %O ; current = %O ; dir = %s',
1006
+ self.toString(), orientation, spec_copy, sortSpec_copy[orientation], currentDir);
1007
+
1008
+ if (_.isEqual(sortSpec_copy[orientation], spec_copy)) {
1009
+ replaceSortIndicator(sortIcon_btn, currentDir);
1010
+ }
1011
+ }
1012
+ };
1013
+
1014
+ // #_addFilterToHeader {{{2
1015
+
1016
+ GridTable.prototype._addFilterToHeader = function (container, field, displayText) {
1017
+ var self = this;
1018
+
1019
+ if (self.grid.filterControl == null) {
1020
+ return;
1021
+ }
1022
+
1023
+ jQuery('<button>', {
1024
+ 'data-tooltip': trans('GRID.TABLE.ADD_FILTER_HELP', field)
1025
+ })
1026
+ .addClass('wcdv_icon_button')
1027
+ .css({'color': '#FFF'})
1028
+ .append(icon('filter'))
1029
+ .on('click', function () {
1030
+ self.grid.filterControl.addField(field, displayText, {
1031
+ openControls: true
1032
+ });
1033
+ })
1034
+ .appendTo(container);
1035
+ };
1036
+
1037
+ // #_addDrillDownHandler {{{2
1038
+
1039
+ GridTable.prototype._addDrillDownHandler = function (tbl, data) {
1040
+ var self = this;
1041
+
1042
+ tbl.on('mousedown', function (evt) {
1043
+ if (evt.detail > 1) {
1044
+ evt.preventDefault();
1045
+ }
1046
+ });
1047
+ tbl.on('dblclick', 'td.wcdv_drill_down', function () {
1048
+ if (window.getSelection) {
1049
+ window.getSelection().removeAllRanges();
1050
+ }
1051
+ else if (document.selection) {
1052
+ document.selection.empty();
1053
+ }
1054
+
1055
+ var elt = jQuery(this);
1056
+ var filter = deepCopy(self.view.getFilter());
1057
+ var rowValIndex = elt.dvAttr('rvi');
1058
+ var colValIndex = elt.dvAttr('cvi');
1059
+
1060
+ if (rowValIndex != null) {
1061
+ _.each(data.rowVals[rowValIndex], function (x, i) {
1062
+ var gs = data.groupSpec[i];
1063
+ filter[data.groupFields[i]] = gs.fun != null
1064
+ ? GROUP_FUNCTION_REGISTRY.get(gs.fun).valueToFilter(x)
1065
+ : { '$eq': x };
1066
+ });
1067
+ }
1068
+
1069
+ if (colValIndex != null) {
1070
+ _.each(data.colVals[colValIndex], function (x, i) {
1071
+ var ps = data.pivotSpec[i];
1072
+ filter[data.pivotFields[i]] = ps.fun != null
1073
+ ? GROUP_FUNCTION_REGISTRY.get(ps.fun).valueToFilter(x)
1074
+ : { '$eq': x };
1075
+ });
1076
+ }
1077
+
1078
+ self.logDebug(self.makeLogTag() + ' Creating new perspective: filter = %O', self.toString(), filter);
1079
+
1080
+ window.setTimeout(function () {
1081
+ self.view.prefs.addPerspective(null, 'Drill Down', { view: { filter: filter } }, { isTemporary: true }, null, { onDuplicate: 'replace' });
1082
+ });
1083
+ });
1084
+ };
1085
+
1086
+ // #_addDrillDownClass {{{2
1087
+
1088
+ GridTable.prototype._addDrillDownClass = function (elt) {
1089
+ elt.classList.add('wcdv_drill_down');
1090
+ };
1091
+
1092
+ GridTable.prototype._updateFocus = function (tbl) {
1093
+ var self = this;
1094
+
1095
+ tbl.find('td').removeClass('wcdv_focus');
1096
+
1097
+ _.each(self.focus.rvi, function (rvi) {
1098
+ tbl.find('td[data-wcdv-rvi=' + rvi + ']').addClass('wcdv_focus');
1099
+ });
1100
+
1101
+ _.each(self.focus.cvi, function (cvi) {
1102
+ tbl.find('td[data-wcdv-cvi=' + cvi + ']').addClass('wcdv_focus');
1103
+ });
1104
+ };
1105
+
1106
+ // #_addFocusHandler {{{2
1107
+
1108
+ GridTable.prototype._addFocusHandler = function (tbl, data) {
1109
+ var self = this;
1110
+
1111
+ tbl._onSingleClick('tr[data-wcdv-rvi] > th', function () {
1112
+ var rvi = jQuery(this).parent('tr').attr('data-wcdv-rvi');
1113
+
1114
+ if (rvi == null || rvi === '') {
1115
+ return;
1116
+ }
1117
+
1118
+ var fi = self.focus.rvi.indexOf(rvi);
1119
+
1120
+ if (fi < 0) {
1121
+ // Adding a new focus for this rowval.
1122
+ self.focus.rvi.push(rvi);
1123
+ }
1124
+ else {
1125
+ // Remove the focus for this rowval.
1126
+ self.focus.rvi.splice(fi, 1);
1127
+ }
1128
+
1129
+ self._updateFocus(tbl);
1130
+ }, ['shift']);
1131
+
1132
+ tbl._onSingleClick('th[data-wcdv-cvi]', function () {
1133
+ var cvi = jQuery(this).attr('data-wcdv-cvi');
1134
+
1135
+ if (cvi == null || cvi === '') {
1136
+ return;
1137
+ }
1138
+
1139
+ var fi = self.focus.cvi.indexOf(cvi);
1140
+
1141
+ if (fi < 0) {
1142
+ // Adding a new focus for this rowval.
1143
+ self.focus.cvi.push(cvi);
1144
+ }
1145
+ else {
1146
+ // Remove the focus for this rowval.
1147
+ self.focus.cvi.splice(fi, 1);
1148
+ }
1149
+
1150
+ self._updateFocus(tbl);
1151
+ }, ['shift']);
1152
+ };
1153
+
1154
+ // #addSortHandler {{{2
1155
+
1156
+ GridTable.prototype.addSortHandler = function () {
1157
+ var self = this;
1158
+
1159
+ // Register the event handler for when a sort occurs in the view. The way this works is that
1160
+ // the view will invoke the callback for each row in order. We just append them to the table
1161
+ // body in that same order, and boom: all the rows are sorted.
1162
+ //
1163
+ // However, we DON'T want to do this if we're limiting the output because we're currently only
1164
+ // showing part of the data. So, when we sort, we need to completely redraw the window (e.g.
1165
+ // rows 21-40) that we're showing.
1166
+ //
1167
+ // FIXME - This will cause problems with multiple grids (some supporting sorting, some not)
1168
+ // using the same view.
1169
+
1170
+ self.view.off('sort');
1171
+
1172
+ if (self.features.sort) {
1173
+ // if (self.features.limit) {
1174
+ self.view.on('sortEnd', function () {
1175
+ self.logDebug(self.makeLogTag() + ' Marking table to be redrawn', self.toString());
1176
+ self.needsRedraw = true;
1177
+ }, { who: self });
1178
+ // }
1179
+ // else {
1180
+ // self.view.on('sort', function (rowNum, position) {
1181
+ // var elt = jQuery(document.getElementById(self.defn.table.id + '_' + rowNum));
1182
+ //
1183
+ // // Add one to the position (which is 0-based) to match the 1-based row number in CSS.
1184
+ //
1185
+ // elt.removeClass('even odd');
1186
+ // elt.addClass((position + 1) % 2 === 0 ? 'even' : 'odd');
1187
+ // self.ui.tbody.append(elt);
1188
+ //
1189
+ // self.csv.setOrder(rowNum, position);
1190
+ // }, { who: self });
1191
+ // }
1192
+ }
1193
+ };
1194
+
1195
+ // #addFilterHandler {{{2
1196
+
1197
+ GridTable.prototype.addFilterHandler = function () {
1198
+ var self = this;
1199
+
1200
+ // Register the event handler for when a filter occurs in the view. The way this works is that
1201
+ // the view will invoke the callback for each row and indicate if it should be shown or hidden.
1202
+ //
1203
+ // However, we DON'T want to do this if we're limiting the output because we're currently only
1204
+ // showing part of the data. So, when we filter, we need to completely redraw the window (e.g.
1205
+ // rows 21-40) that we're showing.
1206
+ //
1207
+ // We also can't use this approach when we're using preferences, because those can cause the data
1208
+ // to be filtered down before our grid actually creates all the rows. (The prefs are applied
1209
+ // before the grid table is created.) At that point, showing or hiding rows is irrelevant because
1210
+ // the grid table doesn't event know what the unfiltered ones are, it's only ever seen the data
1211
+ // with filters applied.
1212
+
1213
+ self.view.off('filter');
1214
+
1215
+ // if (self.features.limit || self.view.opts.saveViewConfig) {
1216
+ self.view.on(ComputedView.events.filterEnd, function () {
1217
+ self.logDebug(self.makeLogTag('handler(filterEnd)') + ' Marking table to be redrawn');
1218
+ self.needsRedraw = true;
1219
+ }, { who: self });
1220
+ // }
1221
+ // else {
1222
+ // var even = false; // Rows are 1-based to match our CSS zebra-striping.
1223
+ //
1224
+ // self.view.on(ComputedView.events.filter, function (rowNum, hide) {
1225
+ // if (isNothing(self.ui.tr[rowNum])) {
1226
+ // self.logDebug(self.makeLogTag('filter') + ' We were told to ' + (hide ? 'hide' ] 'show') + ' row ' + rowNum + ', but it doesn\'t exist');
1227
+ // return;
1228
+ // }
1229
+ //
1230
+ // self.ui.tr[rowNum].removeClass('even odd');
1231
+ // if (hide) {
1232
+ // self.ui.tr[rowNum].hide();
1233
+ // }
1234
+ // else {
1235
+ // self.ui.tr[rowNum].show();
1236
+ // self.ui.tr[rowNum].addClass(even ? 'even' : 'odd');
1237
+ // even = !even;
1238
+ // }
1239
+ //
1240
+ // self.csv.updateVisibility(rowNum, hide);
1241
+ // }, { who: self });
1242
+ // }
1243
+ };
1244
+
1245
+ // #_addRowReorderHandler {{{2
1246
+
1247
+ GridTable.prototype._addRowReorderHandler = function () {
1248
+ var self = this;
1249
+
1250
+ self.ui.tbody._makeSortableTable(_.bind(self.view.source.swapRows, self.view.source));
1251
+ };
1252
+
1253
+ // #_addRowSelectHandler {{{2
1254
+
1255
+ /**
1256
+ * Add an event handler for the row select checkboxes. The event is bound on `self.ui.tbody` and
1257
+ * looks for checkbox inputs inside TD elements with class `wcdv-row-select-col` to actually handle
1258
+ * the events. The handler calls `self.select(ROW_NUM)` or `self.unselect(ROW_NUM)` when the
1259
+ * checkbox is changed.
1260
+ */
1261
+
1262
+ GridTable.prototype._addRowSelectHandler = function () {
1263
+ var self = this;
1264
+
1265
+ self.ui.tbody.on('change', 'td.wcdv-row-select-col > input[type="checkbox"]', function () {
1266
+ if (this.checked) {
1267
+ self.select(+(jQuery(this).attr('data-row-num')));
1268
+ }
1269
+ else {
1270
+ self.unselect(+(jQuery(this).attr('data-row-num')));
1271
+ }
1272
+ });
1273
+ };
1274
+
1275
+ // #_getAggInfo {{{2
1276
+
1277
+ GridTable.prototype._getAggInfo = function (data) {
1278
+ var ai = objFromArray(['cell', 'group', 'pivot', 'all'], [[]]);
1279
+ ai = _.mapObject(ai, function (val, key) {
1280
+ return _.filter(
1281
+ getPropDef([], data, 'agg', 'info', key),
1282
+ function (aggInfo) {
1283
+ return !aggInfo.isHidden;
1284
+ }
1285
+ );
1286
+ });
1287
+ return ai;
1288
+ };
1289
+
1290
+ // #_getDisplayFormat {{{2
1291
+
1292
+ GridTable.prototype._getDisplayFormat = function () {
1293
+ var self = this;
1294
+ var df = objFromArray(['cell', 'group', 'pivot', 'all'], [[]]);
1295
+ df = _.mapObject(df, function (val, key) {
1296
+ return getPropDef([], self.opts, 'displayFormat', key);
1297
+ });
1298
+ return df;
1299
+ };
1300
+
1301
+ // #_setupFullValueWin {{{2
1302
+
1303
+ /**
1304
+ * Setup the behavior to show the full value of a cell when it's been truncated due to having the
1305
+ * `maxHeight` property set in the column config.
1306
+ *
1307
+ * For plain output, you need to set:
1308
+ *
1309
+ * - `data-row-num` on the TR
1310
+ * - `data-wcdv-field` on the TD
1311
+ *
1312
+ * For group & pivot output, you need to set:
1313
+ *
1314
+ * - `data-wcdv-rvi` on the TR (for group & cell aggregates)
1315
+ * - `data-wcdv-cvi` on the TD (for pivot & cell aggregates)
1316
+ * - `data-wcdv-agg-scope` on the TD
1317
+ * - `data-wcdv-agg-num` on the TD
1318
+ */
1319
+
1320
+ GridTable.prototype._setupFullValueWin = function (data) {
1321
+ var self = this;
1322
+
1323
+ // Create a window that will show the full value of a cell whose display has been truncated by
1324
+ // setting the `maxHeight` property in the column configuration.
1325
+
1326
+ var fullValueWinDiv = document.createElement('div');
1327
+
1328
+ var fullValueWin = new PopupWindow({
1329
+ title: 'Full Value',
1330
+ width: 800,
1331
+ maxHeight: 600
1332
+ });
1333
+
1334
+ fullValueWin.setContent(fullValueWinDiv);
1335
+
1336
+ // When the "show full value" button is clicked, use the attached data attributes to determine the
1337
+ // value that will be shown in the window.
1338
+
1339
+ self.ui.tbody.on('click', 'button.wcdv_show_full_value', function (evt) {
1340
+ evt.stopPropagation();
1341
+
1342
+ var btn = jQuery(this);
1343
+ var td = btn.parents('td');
1344
+ var tr = td.parents('tr');
1345
+
1346
+ var field
1347
+ , rowNum
1348
+ , rvi
1349
+ , cvi
1350
+ , aggScope
1351
+ , aggNum
1352
+ , aggInfo
1353
+ , aggResult
1354
+ , val;
1355
+
1356
+ if (data.isPlain) {
1357
+ field = td.attr('data-wcdv-field');
1358
+ rowNum = +tr.attr('data-row-num');
1359
+ val = getProp(data, 'data', rowNum, 'rowData', field, 'cachedRender');
1360
+ setElement(fullValueWinDiv, val);
1361
+ }
1362
+ else if (data.isGroup || data.isPivot) {
1363
+ aggScope = td.attr('data-wcdv-agg-scope');
1364
+ aggNum = +td.attr('data-wcdv-agg-num');
1365
+
1366
+ switch (aggScope) {
1367
+ case 'cell':
1368
+ rvi = +tr.dvAttr('rvi');
1369
+ cvi = +td.dvAttr('cvi');
1370
+ aggResult = data.agg.results[aggScope][aggNum][rvi][cvi];
1371
+ break;
1372
+ case 'group':
1373
+ rvi = +tr.dvAttr('rvi');
1374
+ aggResult = data.agg.results[aggScope][aggNum][rvi];
1375
+ break;
1376
+ case 'pivot':
1377
+ cvi = +td.dvAttr('cvi');
1378
+ aggResult = data.agg.results[aggScope][aggNum][cvi];
1379
+ break;
1380
+ case 'all':
1381
+ aggResult = data.agg.results[aggScope][aggNum];
1382
+ break;
1383
+ }
1384
+
1385
+ aggInfo = data.agg.info[aggScope][aggNum];
1386
+ field = getProp(aggInfo, 'fields', 0);
1387
+
1388
+ if (isElement(aggResult)) {
1389
+ setElement(fullValueWinDiv, aggResult);
1390
+ }
1391
+ else {
1392
+ if (aggInfo.instance.inheritFormatting) {
1393
+ val = format(aggInfo.colConfig[0], aggInfo.typeInfo[0], aggResult, {
1394
+ overrideType: aggInfo.instance.getType()
1395
+ });
1396
+ setElement(fullValueWinDiv, val, {
1397
+ field: aggInfo.fields[0],
1398
+ colConfig: aggInfo.colConfig[0],
1399
+ typeInfo: aggInfo.typeInfo[0]
1400
+ });
1401
+ }
1402
+ else {
1403
+ val = format(null, null, aggResult, {
1404
+ overrideType: aggInfo.instance.getType(),
1405
+ decode: false
1406
+ });
1407
+ setElement(fullValueWinDiv, val);
1408
+ }
1409
+ }
1410
+ }
1411
+
1412
+ fullValueWin.setContent(fullValueWinDiv);
1413
+ fullValueWin.open();
1414
+ });
1415
+ };
1416
+
1417
+ // #draw {{{2
1418
+
1419
+ GridTable.prototype.draw = function (root, opts, cont) {
1420
+ var self = this
1421
+ , args = Array.prototype.slice.call(arguments);
1422
+
1423
+ if (self.opts.generateCsv) {
1424
+ if (self.csvLock.isLocked()) {
1425
+ return self.csvLock.onUnlock(function () {
1426
+ self.logDebug(self.makeLogTag() + ' Retrying table draw due to CSV lock: %O %O', self.toString(), root, opts);
1427
+ self.draw.apply(self, args);
1428
+ });
1429
+ }
1430
+ else {
1431
+ self.logDebug(self.makeLogTag() + ' Creating new CSV buffer', self.toString());
1432
+ self.csvLock.lock();
1433
+ self.csv = new Csv();
1434
+ }
1435
+ }
1436
+ else {
1437
+ self.csv = new TableExport();
1438
+ }
1439
+
1440
+ return self.super['GridRenderer'].draw(root, opts, function (ok, data, typeInfo, andThen) {
1441
+ if (!ok) {
1442
+ return cont();
1443
+ }
1444
+
1445
+ self.timing.start(['Grid Table', 'Draw']);
1446
+
1447
+ // Configuration for floating header feature.
1448
+
1449
+ if (!self.features.floatingHeader || self.defn.table.floatingHeader.method !== 'tabletool') {
1450
+ root.css({ 'overflow-x': 'auto' });
1451
+ }
1452
+
1453
+ // Configuration for limit feature.
1454
+
1455
+ if (self.features.limit && self.defn.table.limit.method === 'more') {
1456
+ self.scrollEventElement = self.opts.fixedHeight ? self.root : window;
1457
+ jQuery(self.scrollEventElement).on(self.scrollEvents, function () {
1458
+ if (typeof self.moreVisibleHandler === 'function') {
1459
+ self.moreVisibleHandler();
1460
+ }
1461
+ });
1462
+ }
1463
+
1464
+ // All operations buttons share the same 'onClick' callback.
1465
+
1466
+ if (self.features.operations) {
1467
+ jQuery(self.root).on('click.wcdv_operation', 'button.wcdv_operation', function () {
1468
+ var btn = this;
1469
+ var opType = btn.getAttribute('data-operation-type');
1470
+ var opIndex = btn.getAttribute('data-operation-index');
1471
+ var sel, cellElt, rowElt, rowNum, field, op;
1472
+
1473
+ switch (opType) {
1474
+ case 'row':
1475
+ rowElt = jQuery(btn).parents('tr');
1476
+ rowNum = +(rowElt.attr('data-row-num'));
1477
+ op = self.defn.operations.row[opIndex];
1478
+ op.callback({
1479
+ rowId: rowNum,
1480
+ rowElt: rowElt,
1481
+ row: self.data.dataByRowId[rowNum],
1482
+ opBtn: jQuery(btn)
1483
+ });
1484
+ break;
1485
+ case 'cell':
1486
+ cellElt = jQuery(btn).parents('td');
1487
+ field = jQuery(btn).parents('td').attr('data-wcdv-field');
1488
+ rowElt = jQuery(btn).parents('tr');
1489
+ rowNum = +(jQuery(btn).parents('tr').attr('data-row-num'));
1490
+ op = self.defn.operations.cell[field][opIndex];
1491
+ op.callback({
1492
+ rowId: rowNum,
1493
+ rowElt: rowElt,
1494
+ row: self.data.dataByRowId[rowNum],
1495
+ cellElt: cellElt,
1496
+ cell: self.data.dataByRowId[rowNum][field].value,
1497
+ opBtn: jQuery(btn)
1498
+ });
1499
+ break;
1500
+ }
1501
+ });
1502
+ }
1503
+
1504
+ var tr;
1505
+ var srcIndex = 0;
1506
+
1507
+ self.ui = {
1508
+ tbl: jQuery('<table>'),
1509
+ thead: jQuery('<thead>'),
1510
+ tbody: jQuery('<tbody>'),
1511
+ tfoot: jQuery('<tfoot>'),
1512
+ thMap: {},
1513
+ tr: {},
1514
+ progress: jQuery('<div>'),
1515
+ columnDropIndicator: null
1516
+ };
1517
+
1518
+ self._addDrillDownHandler(self.ui.tbl, data);
1519
+ self._addFocusHandler(self.ui.tbl, data);
1520
+
1521
+ if (self.features.block) {
1522
+ var blockConfig = {
1523
+ overlayCSS: {
1524
+ opacity: 0.9,
1525
+ backgroundColor: '#FFF'
1526
+ }
1527
+ };
1528
+
1529
+ if (self.features.progress && getProp(self.defn, 'table', 'progress', 'method') === 'jQueryUI') {
1530
+ blockConfig.message = jQuery('<div>')
1531
+ .append(jQuery('<h1>').text('Working...'))
1532
+ .append(self.ui.progress);
1533
+ }
1534
+ }
1535
+
1536
+ self.view.on(ComputedView.events.workBegin, function () {
1537
+ if (self.features.block) {
1538
+ self.logDebug(self.makeLogTag() + ' Blocking table body', self.toString());
1539
+ if (getProp(self.defn, 'table', 'block', 'wholePage')) {
1540
+ jQuery.blockUI(blockConfig);
1541
+ }
1542
+ else {
1543
+ self.ui.tbl.block(blockConfig);
1544
+ }
1545
+ }
1546
+ if (self.features.floatingHeader) {
1547
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
1548
+ case 'tabletool':
1549
+ window.TableTool.update();
1550
+ break;
1551
+ }
1552
+ }
1553
+ }, { who: self });
1554
+
1555
+ self.view.on(ComputedView.events.workEnd, function () {
1556
+ if (self.features.block) {
1557
+ self.logDebug(self.makeLogTag() + ' Unblocking table body', self.toString());
1558
+ if (getProp(self.defn, 'table', 'block', 'wholePage')) {
1559
+ jQuery.unblockUI();
1560
+ }
1561
+ else {
1562
+ self.ui.tbl.unblock();
1563
+ }
1564
+ }
1565
+ if (self.features.floatingHeader) {
1566
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
1567
+ case 'tabletool':
1568
+ window.TableTool.update();
1569
+ break;
1570
+ }
1571
+ }
1572
+ }, { who: self });
1573
+
1574
+ /*
1575
+ * Determine what columns will be in the table. This comes from the user, or from the data
1576
+ * itself. We may then add columns for extra features (like row selection or reordering).
1577
+ */
1578
+
1579
+ var columns = determineColumns(self.colConfig, data, typeInfo);
1580
+
1581
+ self.drawHeader(columns, data, typeInfo, opts);
1582
+
1583
+ if (self.features.footer) {
1584
+ self.drawFooter(columns, data, typeInfo);
1585
+ }
1586
+
1587
+ self.addSortHandler();
1588
+
1589
+ if (self.features.rowSelect) {
1590
+ if (typeof self._addRowSelectHandler !== 'function') {
1591
+ self.logWarning(self.makeLogTag() + ' Requested feature "rowSelect" is not available: `_addRowSelectHandler` method does not exist');
1592
+ }
1593
+ else {
1594
+ self._addRowSelectHandler();
1595
+ }
1596
+ }
1597
+
1598
+ if (self.features.rowReorder) {
1599
+ self._addRowReorderHandler();
1600
+ }
1601
+
1602
+ if (self.opts.zebraStriping) {
1603
+ self.ui.tbl.addClass('zebra');
1604
+ }
1605
+
1606
+ if (getProp(self.opts, 'addClass', 'table')) {
1607
+ self.ui.tbl.addClass(getProp(self.opts, 'addClass', 'table'));
1608
+ }
1609
+
1610
+ self.ui.tbl.append(self.ui.thead);
1611
+
1612
+ if (self.features.incremental && !getProp(self.defn, 'table', 'incremental', 'appendBodyLast')) {
1613
+ self.ui.tbl.append(self.ui.tbody);
1614
+
1615
+ if (self.features.footer) {
1616
+ self.ui.tbl.append(self.ui.tfoot);
1617
+ }
1618
+ }
1619
+
1620
+ // IMPORTANT: We use appendChild() here instead of jQuery's append() because the latter will
1621
+ // re-run any <script> elements in the footer, which we don't want.
1622
+
1623
+ self.root.get(0).appendChild(self.ui.tbl.get(0));
1624
+
1625
+ self.drawBody(data, typeInfo, columns, function () {
1626
+ if (!self.features.incremental || getProp(self.defn, 'table', 'incremental', 'appendBodyLast')) {
1627
+ self.ui.tbl.append(self.ui.tbody);
1628
+
1629
+ if (self.features.footer) {
1630
+ self.ui.tbl.append(self.ui.tfoot);
1631
+ }
1632
+ }
1633
+
1634
+ // Activate TableTool using this attribute, if the user asked for it.
1635
+
1636
+ if (self.features.floatingHeader) {
1637
+ self.logDebug(self.makeLogTag() + ' Enabling floating header using method "%s"',
1638
+ self.toString(), getProp(self.defn, 'table', 'floatingHeader', 'method'));
1639
+ switch (getProp(self.defn, 'table', 'floatingHeader', 'method')) {
1640
+ case 'floatThead':
1641
+ var floatTheadConfig = {
1642
+ zIndex: 1
1643
+ };
1644
+
1645
+ if (self.opts.fixedHeight) {
1646
+ floatTheadConfig.position = 'fixed';
1647
+ floatTheadConfig.scrollContainer = true;
1648
+ }
1649
+ else {
1650
+ floatTheadConfig.responsiveContainer = function () {
1651
+ return self.root;
1652
+ };
1653
+ }
1654
+
1655
+ self.grid.on('showControls', function () {
1656
+ self.ui.tbl.floatThead('reflow');
1657
+ }, { who: self });
1658
+ self.grid.on('hideControls', function () {
1659
+ self.ui.tbl.floatThead('reflow');
1660
+ }, { who: self });
1661
+ self.grid.filterControl.on(['fieldAdded', 'fieldRemoved'], function () {
1662
+ self.ui.tbl.floatThead('reflow');
1663
+ }, { who: self });
1664
+ self.grid.aggregateControl.on(['fieldAdded', 'fieldRemoved'], function () {
1665
+ self.ui.tbl.floatThead('reflow');
1666
+ }, { who: self });
1667
+
1668
+ self.ui.tbl.floatThead(floatTheadConfig);
1669
+ break;
1670
+ case 'tabletool':
1671
+ if (self.opts.fixedHeight) {
1672
+ self.ui.tbl.attr('data-tttype', 'fixed');
1673
+ self.ui.tbl.attr('data-ttheight', self.grid.rootHeight);
1674
+ }
1675
+ else {
1676
+ self.ui.tbl.attr('data-tttype', 'sticky');
1677
+ }
1678
+ if (data.isPlain) {
1679
+ var pinnedColumns = 0;
1680
+ _.each(columns, function (field) {
1681
+ var fcc = self.colConfig.get(field);
1682
+ if (fcc != null && fcc.isPinned) {
1683
+ pinnedColumns += 1;
1684
+ }
1685
+ });
1686
+ if (pinnedColumns > 0) {
1687
+ // Figure out if there's a column for the row selection checkbox.
1688
+ if (self.features.rowSelect) {
1689
+ pinnedColumns += 1;
1690
+ }
1691
+ // Figure out if there's a column for row-based operations.
1692
+ if (self.hasOperations('row')) {
1693
+ pinnedColumns += 1;
1694
+ }
1695
+ self.ui.tbl.attr('data-tttype', 'sidescroll');
1696
+ self.ui.tbl.attr('data-ttsidecells', pinnedColumns);
1697
+ }
1698
+ }
1699
+ else if ((data.isGroup || data.isPivot) && getProp(self.defn, 'table', 'whenGroup', 'pinRowvals')) {
1700
+ self.ui.tbl.attr('data-tttype', 'sidescroll');
1701
+ self.ui.tbl.attr('data-ttsidecells', data.groupFields.length);
1702
+ }
1703
+ if (window.TableTool != null) {
1704
+ self.on('columnResize', function () {
1705
+ // Without this, resizing the columns updates the width in the body of the table but
1706
+ // leaves the headers in their original size.
1707
+
1708
+ window.TableTool.update();
1709
+ });
1710
+ }
1711
+ break;
1712
+ case 'css':
1713
+ self.ui.thead.addClass('sticky');
1714
+ self.ui.tfoot.addClass('sticky');
1715
+ break;
1716
+ }
1717
+ }
1718
+
1719
+ // This isn't fast or reliable but it is one way to get rid of excess "show full value" buttons
1720
+ // if the cell doesn't actually get cut off. It's fine for small numbers of cells, but once you
1721
+ // get over like 1000 cells it's going to take a while. Plus, it technically needs to be rerun
1722
+ // whenever the table size changes. I just want to leave it here in case I need it later.
1723
+
1724
+ // jQuery(self.ui.tbody).find('div.wcdv_maxheight_wrapper').each(function (i, elt) {
1725
+ // var s = window.getComputedStyle(elt);
1726
+ // var height = s.height.slice(0, -2);
1727
+ // var maxHeight = s.maxHeight.slice(0, -2);
1728
+ // if (+height < +maxHeight) {
1729
+ // jQuery(elt).children('button.wcdv_show_full_value').hide();
1730
+ // }
1731
+ // });
1732
+
1733
+ if (self.features.columnResize) {
1734
+ self.ui.tbl.addClass('wcdv-resizable-cols');
1735
+ self.makeResponsive();
1736
+ }
1737
+ self.addWorkHandler();
1738
+
1739
+ self.timing.stop(['Grid Table', 'Draw']);
1740
+ andThen(cont);
1741
+ }, opts);
1742
+ });
1743
+ };
1744
+
1745
+ // #drawHeader_aggregates {{{2
1746
+
1747
+ /**
1748
+ * Add TH elements for all the aggregates to the specified TR.
1749
+ *
1750
+ * @param {Object} data
1751
+ *
1752
+ * @param {Element} tr
1753
+ * Where to put the TH elements.
1754
+ */
1755
+
1756
+ GridTable.prototype.drawHeader_aggregates = function (data, tr, displayOrderIndex, displayOrderMax) {
1757
+ var self = this;
1758
+ var ai = self._getAggInfo(data);
1759
+
1760
+ _.each(ai.group, function (aggInfo, aggIndex) {
1761
+ var aggNum = aggInfo.aggNum,
1762
+ text = aggInfo.instance.getFullName(),
1763
+ span = jQuery('<span>')
1764
+ .addClass('wcdv_heading_title')
1765
+ .text(text),
1766
+ headingThControls = jQuery('<div>'),
1767
+ headingThContainer = jQuery('<div>')
1768
+ .addClass('wcdv_heading_container')
1769
+ .append(span, headingThControls),
1770
+ th = jQuery('<th>', { scope: 'col' })
1771
+ .append(headingThContainer)
1772
+ .appendTo(tr);
1773
+
1774
+ if (self.opts.drawInternalBorders || data.agg.info.group.length > 1) {
1775
+ if (displayOrderIndex > 0 && aggIndex === 0) {
1776
+ th.addClass('wcdv_bld'); // border-left: double
1777
+ }
1778
+ if (displayOrderIndex < displayOrderMax - 1 && aggIndex === ai.group.length - 1) {
1779
+ th.addClass('wcdv_brd'); // border-right: double
1780
+ }
1781
+ if (aggIndex > 0) {
1782
+ th.addClass('wcdv_pivot_colval_boundary');
1783
+ }
1784
+ }
1785
+ self.csv.addCol(text);
1786
+ self._addSortingToHeader(data, 'vertical', {aggType: 'group', aggNum: aggNum}, headingThControls.get(0), ai.group);
1787
+ self.setAlignment(th, aggInfo.colConfig[0], aggInfo.typeInfo[0], aggInfo.instance.getType());
1788
+ });
1789
+ };
1790
+
1791
+ // #drawHeader_addCols {{{2
1792
+
1793
+ /**
1794
+ * Add user-defined columns to the header.
1795
+ */
1796
+
1797
+ GridTable.prototype.drawHeader_addCols = function (tr, typeInfo, opts) {
1798
+ var self = this;
1799
+ var span, th;
1800
+
1801
+ if (self.opts.addCols) {
1802
+ _.each(self.opts.addCols, function (addCol) {
1803
+ span = jQuery('<span>')
1804
+ .text(addCol.name);
1805
+
1806
+ th = jQuery('<th>', { scope: 'col' })
1807
+ .append(span)
1808
+ .appendTo(tr);
1809
+
1810
+ self.csv.addCol(addCol.name);
1811
+
1812
+ // When the added column is an aggregate function over some field, we can use that information
1813
+ // to look up the colConfig and typeInfo of the field to determine the alignment. For example
1814
+ // if the aggregate is Max(Age) we can look up Age and find it's a number and therefore should
1815
+ // be right-aligned.
1816
+ //
1817
+ // TODO Implement this for the aggregate type as well, as aggregates like Sum() only produce
1818
+ // numbers which should be right-aligned.
1819
+
1820
+ if (getProp(opts, 'pivotConfig', 'aggField')) {
1821
+ self.setAlignment(th, self.colConfig.get(opts.pivotConfig.aggField), typeInfo.get(opts.pivotConfig.aggField));
1822
+ }
1823
+ });
1824
+ }
1825
+ };
1826
+
1827
+ // #drawBody_rowVals {{{2
1828
+
1829
+ /**
1830
+ * Draw the rowvals from a single group. For example, if grouping by "State" and "County", group
1831
+ * number 0 might be the rowval `["Alabama", "Autauga"]` — and that's what this function would put
1832
+ * out as TH elements.
1833
+ *
1834
+ * @param {object} data
1835
+ *
1836
+ * @param {Element} tr
1837
+ * The row to attach the TH elements to.
1838
+ *
1839
+ * @param {number} rowValIndex
1840
+ * What group number you want to print out.
1841
+ */
1842
+
1843
+ GridTable.prototype.drawBody_rowVals = function (data, tr, rowValIndex) {
1844
+ var self = this;
1845
+
1846
+ if (!(tr instanceof Element)) {
1847
+ throw new Error('Call Error: `tr` must be an instance of Element');
1848
+ }
1849
+
1850
+ if (typeof rowValIndex !== 'number') {
1851
+ throw new Error('Call Error: `rowValIndex` must be a number');
1852
+ }
1853
+
1854
+ // Create the cells that show the values of the grouped columns.
1855
+ //
1856
+ // EXAMPLE
1857
+ // -------
1858
+ //
1859
+ // groupFields = ["First Name", "Last Name"]
1860
+ // rowVals = [["Luke", "Skywalker"], ...]
1861
+ //
1862
+ // <tr>
1863
+ // <th>Luke</th>
1864
+ // <th>Skywalker</th>
1865
+ // ... row[col] | col ∉ groupFields ...
1866
+ // </tr>
1867
+
1868
+ var leafMetadataNode = data.groupMetadata.lookup.byRowValIndex[rowValIndex];
1869
+ var metadataNode = leafMetadataNode;
1870
+ var th = [];
1871
+ var i;
1872
+
1873
+ // Iterate through the group fields from last to first, navigating through the group metadata tree
1874
+ // from leaf (last group field) to root (first group field). Along the way, construct the <TH>
1875
+ // elements for the rowval elements in reverse order.
1876
+
1877
+ for (i = data.groupFields.length - 1; i >= 0; i -= 1) {
1878
+ var groupField = data.groupFields[i];
1879
+ var groupSpec = data.groupSpec[i];
1880
+ var fcc = self.colConfig.get(groupField) || {};
1881
+ var t = self.typeInfo.get(groupField);
1882
+ var v = metadataNode.rowValCell || metadataNode.rowValElt;
1883
+
1884
+ if (groupSpec.fun != null) {
1885
+ t = {
1886
+ type: GROUP_FUNCTION_REGISTRY.get(groupSpec.fun).resultType
1887
+ };
1888
+ v = metadataNode.rowValElt;
1889
+ }
1890
+
1891
+ // The rowValCell is a representative cell that matches the rowValElt. If there is more than
1892
+ // one rowVal containing the same rowValElt, the rowValCell is shared between them all. It's
1893
+ // the same representative cell. Because it's shared, we need to enable `saferCaching` so any
1894
+ // Element produced by a `render` function on the cell doesn't get reused and moved around on
1895
+ // the page. A good example of this issue can be seen in the allowHtml tests, on the link3 and
1896
+ // link4 fields which use a `render` function to create an <A> element.
1897
+ //
1898
+ // After more difficulty was discovered, `saferCaching` was turned on by default. This will
1899
+ // have some performance impacts, but until a different way is found to implement this, it's
1900
+ // necessary.
1901
+
1902
+ v = format(fcc, t, v);
1903
+
1904
+ // TH (th[i])
1905
+ // DIV (headingThContainer)
1906
+ // SPAN (headingThValue)
1907
+ // DIV (headingThControls)
1908
+
1909
+ var headingThValue = document.createElement('span');
1910
+ headingThValue.classList.add('wcdv_heading_title');
1911
+
1912
+ var headingThControls = document.createElement('div');
1913
+
1914
+ var headingThContainer = document.createElement('div');
1915
+ headingThContainer.classList.add('wcdv_heading_container');
1916
+ headingThContainer.appendChild(headingThValue);
1917
+ headingThContainer.appendChild(headingThControls);
1918
+
1919
+ th[i] = document.createElement('th');
1920
+ th[i].setAttribute('scope', 'row');
1921
+ th[i].appendChild(headingThContainer);
1922
+
1923
+ if (v instanceof jQuery) {
1924
+ v = v.get(0);
1925
+ }
1926
+
1927
+ if (v instanceof Element) {
1928
+ headingThValue.appendChild(v);
1929
+ }
1930
+ else if (fcc.allowHtml) {
1931
+ headingThValue.innerHTML = v;
1932
+ }
1933
+ else {
1934
+ headingThValue.innerText = v;
1935
+ }
1936
+
1937
+ self.csv.addCol(headingThValue.innerText, {
1938
+ prepend: true
1939
+ });
1940
+
1941
+ if (data.isPivot && i === data.groupFields.length - 1) {
1942
+ self._addSortingToHeader(data, 'horizontal', {rowVal: data.rowVals[rowValIndex], aggNum: 0}, headingThControls, getPropDef([], data, 'agg', 'info', 'cell'));
1943
+ }
1944
+
1945
+ metadataNode = metadataNode.parent;
1946
+ }
1947
+
1948
+ for (i = 0; i < data.groupFields.length; i += 1) {
1949
+ tr.appendChild(th[i]);
1950
+ }
1951
+ };
1952
+
1953
+ // #drawBody_groupAggregates {{{2
1954
+
1955
+ /**
1956
+ * Render the group aggregate results in a row.
1957
+ *
1958
+ * @param {any} data
1959
+ *
1960
+ * @param {Element} tr
1961
+ * Row to which we add the group aggregate results.
1962
+ *
1963
+ * @param {number} groupNum
1964
+ * Group number (a.k.a. the rowVal index) to render the aggregate results for.
1965
+ *
1966
+ * @param {number} displayOrderIndex
1967
+ * What position we're rendering the group aggregate results in. When greater than zero, draw a
1968
+ * left border.
1969
+ *
1970
+ * @param {number} displayOrderMax
1971
+ * The max number of positions for rendering data. When this isn't the last thing rendered, draw a
1972
+ * right border.
1973
+ */
1974
+
1975
+ GridTable.prototype.drawBody_groupAggregates = function (data, tr, groupNum, displayOrderIndex, displayOrderMax) {
1976
+ var self = this;
1977
+ var ai = self._getAggInfo(data);
1978
+
1979
+ // Go through all the group aggregates and create columns for each one in the specified row.
1980
+
1981
+ _.each(ai.group, function (aggInfo, aggGroupIndex) {
1982
+ var aggNum = aggInfo.aggNum;
1983
+ var aggType = aggInfo.instance.getType();
1984
+ var aggResult = data.agg.results.group[aggNum][groupNum];
1985
+ var text;
1986
+
1987
+ var td = document.createElement('td');
1988
+ td.setAttribute('data-wcdv-rvi', groupNum);
1989
+ td.setAttribute('data-wcdv-agg-scope', 'group');
1990
+ td.setAttribute('data-wcdv-agg-num', aggNum);
1991
+
1992
+ if (aggResult instanceof jQuery) {
1993
+ aggResult = aggResult.get(0);
1994
+ }
1995
+
1996
+ if (aggResult instanceof Element) {
1997
+ td.appendChild(aggResult);
1998
+ self.csv.addCol(getElement(aggResult).innerText);
1999
+ }
2000
+ else {
2001
+ if (aggInfo.instance.inheritFormatting) {
2002
+ text = format(aggInfo.colConfig[0], aggInfo.typeInfo[0], aggResult, {
2003
+ overrideType: aggType
2004
+ });
2005
+ setTableCell(td, text, {
2006
+ field: aggInfo.fields[0],
2007
+ colConfig: aggInfo.colConfig[0],
2008
+ typeInfo: aggInfo.typeInfo[0]
2009
+ });
2010
+ td.setAttribute('data-wcdv-field', aggInfo.fields[0]);
2011
+ }
2012
+ else {
2013
+ text = format(null, null, aggResult, {
2014
+ overrideType: aggType,
2015
+ decode: false
2016
+ });
2017
+ setTableCell(td, text);
2018
+ }
2019
+ self.csv.addCol(td.innerText);
2020
+ }
2021
+
2022
+ // Allow drilldown, but only when there's no group function set. This limitation is currently
2023
+ // in place because we lack the ability to set filters that match all group functions' results.
2024
+ // For example, day of week, because we can't filter to show "only Mondays."
2025
+
2026
+ if (_.every(data.groupSpec, function (gs) {
2027
+ return gs.fun == null || GROUP_FUNCTION_REGISTRY.get(gs.fun).canFilter;
2028
+ })) {
2029
+ self._addDrillDownClass(td);
2030
+ }
2031
+
2032
+ // Decide how we should draw borders based on the display order index & max.
2033
+
2034
+ if (self.opts.drawInternalBorders || data.agg.info.group.length > 1) {
2035
+ if (displayOrderIndex > 0 && aggGroupIndex === 0) {
2036
+ td.classList.add('wcdv_bld'); // border-left: double
2037
+ }
2038
+ if (displayOrderIndex < displayOrderMax - 1 && aggGroupIndex === ai.group.length - 1) {
2039
+ td.classList.add('wcdv_brd'); // border-right: double
2040
+ }
2041
+ if (aggGroupIndex > 0) {
2042
+ td.classList.add('wcdv_pivot_colval_boundary');
2043
+ }
2044
+ }
2045
+
2046
+ self.setAlignment(td, aggInfo.colConfig[0], aggInfo.typeInfo[0], aggInfo.instance.getType());
2047
+ tr.appendChild(td);
2048
+ });
2049
+ };
2050
+
2051
+ // #clear {{{2
2052
+
2053
+ /**
2054
+ * Remove the table from page.
2055
+ */
2056
+
2057
+ GridTable.prototype.clear = function () {
2058
+ var self = this;
2059
+
2060
+ self.logDebug(self.makeLogTag() + ' Removing %d popup menus', self.toString(), self.popupMenus.length);
2061
+
2062
+ _.each(self.popupMenus, function (menu) {
2063
+ menu.destroy();
2064
+ });
2065
+
2066
+ self.popupMenus = [];
2067
+
2068
+ if (self.features.limit && self.defn.table.limit.method === 'more') {
2069
+ jQuery(self.scrollEventElement).off(self.scrollEvents);
2070
+ }
2071
+
2072
+ if (self.features.operations) {
2073
+ jQuery(self.root).off('click.wcdv_operation', 'button.wcdv_operation');
2074
+ }
2075
+
2076
+ // Remove the event handler from clicking on the "show full value" buttons.
2077
+
2078
+ if (getProp(self, 'ui', 'tbody') != null) {
2079
+ self.ui.tbody.off('click', 'button.wcdv_show_full_value');
2080
+ }
2081
+
2082
+ self.view.off('*', self, {silent: true});
2083
+
2084
+ if (self.resizeObserver != null) {
2085
+ self.resizeObserver.disconnect();
2086
+ self.resizeObserver = null;
2087
+ }
2088
+
2089
+ if (self.opts.footer != null && self.opts.stealGridFooter) {
2090
+ self.grid.ui.content.get(0).appendChild(self.opts.footer.get(0));
2091
+ }
2092
+
2093
+ self.root.children().remove();
2094
+ };
2095
+
2096
+ // #makeProgress {{{2
2097
+
2098
+ GridTable.prototype.makeProgress = function (thing) {
2099
+ var self = this;
2100
+
2101
+ if (!self.features.progress) {
2102
+ return;
2103
+ }
2104
+
2105
+ if (getProp(self.defn, 'table', 'progress', 'method') === 'NProgress') {
2106
+ return {
2107
+ begin: function () {
2108
+ self.logDebug(self.makeLogTag() + ' Begin', self.toString(), thing);
2109
+ if (window.NProgress !== undefined) {
2110
+ window.NProgress.start();
2111
+ }
2112
+ },
2113
+ update: function (amount, estTotal) {
2114
+ self.logDebug(self.makeLogTag() + ' %s', self.toString(), thing, sprintf.sprintf('Update: %d / %d = %.0f%%', amount, estTotal, (amount / estTotal) * 100));
2115
+ if (window.NProgress !== undefined) {
2116
+ window.NProgress.set(amount / estTotal);
2117
+ }
2118
+ },
2119
+ end: function () {
2120
+ self.logDebug(self.makeLogTag() + ' End', self.toString(), thing);
2121
+ if (window.NProgress !== undefined) {
2122
+ window.NProgress.done();
2123
+ jQuery('.nprogress-custom-parent').removeClass('nprogress-custom-parent');
2124
+ }
2125
+ }
2126
+ };
2127
+ }
2128
+ else if (getProp(self.defn, 'table', 'progress', 'method') === 'jQueryUI') {
2129
+ return {
2130
+ begin: function () {
2131
+ self.logDebug(self.makeLogTag() + ' Begin', self.toString(), thing);
2132
+ self.ui.progress.progressbar({
2133
+ 'classes': {
2134
+ 'ui-progressbar': 'wcdvgrid_progressbar',
2135
+ 'ui-progressbar-value': 'wcdvgrid_progressbar'
2136
+ }
2137
+ });
2138
+ },
2139
+ update: function (amount, estTotal) {
2140
+ self.logDebug(self.makeLogTag() + ' %s', self.toString(), thing, sprintf.sprintf('Update: %d / %d = %.0f%%', amount, estTotal, (amount / estTotal) * 100));
2141
+ self.ui.progress.progressbar('value', (amount / estTotal) * 100);
2142
+ },
2143
+ end: function () {
2144
+ self.logDebug(self.makeLogTag() + ' End', self.toString(), thing);
2145
+ self.ui.progress.progressbar('destroy');
2146
+ }
2147
+ };
2148
+ }
2149
+ };
2150
+
2151
+ // #getCsv {{{2
2152
+
2153
+ GridTable.prototype.getCsv = function () {
2154
+ var self = this;
2155
+
2156
+ return self.csv.toString();
2157
+ };
2158
+
2159
+ // #getSelection {{{2
2160
+
2161
+ /**
2162
+ * Get the currently selected rows.
2163
+ *
2164
+ * @return {object}
2165
+ * Information on what rows are selected. Contains the following properties:
2166
+ *
2167
+ * - `rowIds` — An array of the unique IDs of the selected rows. Probably not that useful to you,
2168
+ * but it's available.
2169
+ *
2170
+ * - `rows` — An array of the data represented by each row. Each row is an object, each key in the
2171
+ * object is a field in the source data. Values are references to the actual data used by the
2172
+ * grid, so don't mess with their internal structures.
2173
+ *
2174
+ * The ordering of the results is not guaranteed to have any relationship to the order of the rows
2175
+ * from the source, or the order in which they were checked.
2176
+ */
2177
+
2178
+ GridTable.prototype.getSelection = function () {
2179
+ var self = this;
2180
+
2181
+ return {
2182
+ rowIds: self.selection,
2183
+ rows: _.map(self.selection, function (rowId) {
2184
+ return self.data.dataByRowId[rowId];
2185
+ })
2186
+ };
2187
+ };
2188
+
2189
+ // #setSelection {{{2
2190
+
2191
+ /**
2192
+ * Set the currently selected rows. This is different from {@link GridTable#select} and {@link
2193
+ * GridTable#unselect} because this straight-up sets the selection (the other methods add to and
2194
+ * remove from the selection).
2195
+ *
2196
+ * @param {number[]} [what]
2197
+ * Set the selection to the specified row IDs, or select nothing if not specified.
2198
+ */
2199
+
2200
+ GridTable.prototype.setSelection = function (what) {
2201
+ var self = this;
2202
+ var data = self.data.data;
2203
+
2204
+ if (!self.features.rowSelect) {
2205
+ return;
2206
+ }
2207
+
2208
+ if (self.data.isGroup) {
2209
+ data = _.flatten(data);
2210
+ }
2211
+ else if (self.data.isPivot) {
2212
+ self.logError(self.makeLogTag() + ' Selection is not supported for pivotted data, because there is no way to see or change the selection in the user interface');
2213
+ return;
2214
+ }
2215
+
2216
+ if (what == null) {
2217
+ self.selection = [];
2218
+ }
2219
+ else if (_.isArray(what)) {
2220
+ self.selection = what;
2221
+ }
2222
+ else {
2223
+ self.logError(self.makeLogTag() + ' GridTable#setSelection(): parameter `what` must be null/undef or an array');
2224
+ return false;
2225
+ }
2226
+
2227
+ // Try to reflect these changes in the user interface.
2228
+
2229
+ if (typeof self._updateSelectionGui === 'function') {
2230
+ self._updateSelectionGui();
2231
+ }
2232
+
2233
+ self.fire('selectionChange', null, self.getSelection().rows);
2234
+ };
2235
+
2236
+ // #select {{{2
2237
+
2238
+ /**
2239
+ * Adds to the current selection.
2240
+ *
2241
+ * To add all rows where the field "Model" is Civic, Fit, or Accord:
2242
+ *
2243
+ * ```
2244
+ * grid.select((row) => { ['Civic', 'Fit', 'Accord'].indexOf(row['Model'].value) >= 0 });
2245
+ * ```
2246
+ *
2247
+ * @param {number|number[]|function} [what]
2248
+ * Behaves as follows:
2249
+ *
2250
+ * - When not specified, adds all rows to the selection.
2251
+ * - When a number or array of numbers, adds all those row IDs to the selection.
2252
+ * - When a function, adds all rows that pass that filter to the selection.
2253
+ */
2254
+
2255
+ GridTable.prototype.select = function (what) {
2256
+ var self = this;
2257
+ var data = self.data.data;
2258
+
2259
+ if (self.data.isGroup) {
2260
+ data = _.flatten(data);
2261
+ }
2262
+ else if (self.data.isPivot) {
2263
+ self.logError(self.makeLogTag() + ' Selection is not supported for pivotted data, because there is no way to see or change the selection in the user interface');
2264
+ return;
2265
+ }
2266
+
2267
+ if (what == null) {
2268
+ // Select all.
2269
+ self.selection = _.pluck(data, 'rowNum');
2270
+ }
2271
+ else if (_.isArray(what)) {
2272
+ // Add elements to the selection.
2273
+ self.selection = _.union(self.selection, what);
2274
+ }
2275
+ else if (typeof what === 'function') {
2276
+ // Add passing rows to the selection.
2277
+ var passing = _.filter(data, function (d) {
2278
+ return what(d.rowData);
2279
+ });
2280
+ self.selection = _.union(self.selection, _.pluck(passing, 'rowNum'));
2281
+ }
2282
+ else if (!_.contains(self.selection, what)) {
2283
+ // Add item to ths selection.
2284
+ self.selection.push(what);
2285
+ }
2286
+
2287
+ // Try to reflect these changes in the user interface.
2288
+
2289
+ if (typeof self._updateSelectionGui === 'function') {
2290
+ self._updateSelectionGui();
2291
+ }
2292
+
2293
+ self.fire('selectionChange', null, self.getSelection().rows);
2294
+ };
2295
+
2296
+ // #unselect {{{2
2297
+
2298
+ /**
2299
+ * Removes from the current selection.
2300
+ *
2301
+ * To remove all rows where the field "Make" is Honda:
2302
+ *
2303
+ * ```
2304
+ * grid.unselect((row) => { row['Make'].value === 'Honda' });
2305
+ * ```
2306
+ *
2307
+ * @param {number|number[]|function} [what]
2308
+ * Behaves as follows:
2309
+ *
2310
+ * - When not specified, removes all rows from the selection.
2311
+ * - When a number or array of numbers, removes all those row IDs from the selection.
2312
+ * - When a function, removes all rows that pass that filter from the selection.
2313
+ */
2314
+
2315
+ GridTable.prototype.unselect = function (what) {
2316
+ var self = this;
2317
+ var data = self.data.data;
2318
+
2319
+ if (self.data.isGroup) {
2320
+ data = _.flatten(data);
2321
+ }
2322
+ else if (self.data.isPivot) {
2323
+ self.logError(self.makeLogTag() + ' Selection is not supported for pivotted data, because there is no way to see or change the selection in the user interface');
2324
+ return;
2325
+ }
2326
+
2327
+ if (what == null) {
2328
+ // Unselect all.
2329
+ self.selection = [];
2330
+ }
2331
+ else if (_.isArray(what)) {
2332
+ // Remove elements from the selection.
2333
+ self.selection = _.difference(self.selection, what);
2334
+ }
2335
+ else if (typeof what === 'function') {
2336
+ // Remove passing elements from the selection.
2337
+ self.selection = _.reject(self.selection, function (x) {
2338
+ return what(self.data.dataByRowId[x]);
2339
+ });
2340
+ }
2341
+ else {
2342
+ // Remove item from the selection.
2343
+ self.selection = _.without(self.selection, what);
2344
+ }
2345
+
2346
+ // Try to reflect these changes in the user interface.
2347
+
2348
+ if (typeof self._updateSelectionGui === 'function') {
2349
+ self._updateSelectionGui();
2350
+ }
2351
+
2352
+ self.fire('selectionChange', null, self.getSelection().rows);
2353
+ };
2354
+
2355
+ // #isSelected {{{2
2356
+
2357
+ /**
2358
+ * Tells if a row is selected.
2359
+ *
2360
+ * @param {number} what
2361
+ * Row ID to check.
2362
+ *
2363
+ * @return {boolean}
2364
+ * True if the row is selected, false if it isn't.
2365
+ */
2366
+
2367
+ GridTable.prototype.isSelected = function (what) {
2368
+ var self = this;
2369
+
2370
+ return self.selection.indexOf(what) >= 0;
2371
+ };
2372
+
2373
+ // #_updateSelectionGui {{{2
2374
+
2375
+ GridTable.prototype._updateSelectionGui = function () {
2376
+ var self = this;
2377
+ self.logError(self.makeLogTag() + ' GridTable#_updateSelectionGui(): Must be implemented by subclass');
2378
+ };
2379
+
2380
+ GridTable.prototype.autoResizeColumns = function () {
2381
+ var self = this;
2382
+
2383
+ if (self.autoResizeColsLock.isLocked()) {
2384
+ return;
2385
+ }
2386
+
2387
+ self.autoResizeColsLock.lock();
2388
+ if (getProp(self.features, 'floatingHeader') &&
2389
+ getProp(self.defn, 'table', 'floatingHeader', 'method') === 'tabletool' &&
2390
+ window.TableTool != null) {
2391
+ window.TableTool.disable();
2392
+ }
2393
+ self.logDebug(self.makeLogTag('Auto Resize Cols') + ' Fitting column widths...');
2394
+
2395
+ // Cache the header cells to avoid repeated DOM queries
2396
+ var headerCells = self.ui.thead.children('tr:nth(0)').children('th');
2397
+ var widthsToSet = [];
2398
+ var i, len, th, thElt, fieldName, fcc;
2399
+
2400
+ // Clone the entire table for measurement with table-layout: auto
2401
+ // This avoids changing the main table's layout which would force a reflow of all visible cells
2402
+ var measureTable = self.ui.tbl.get(0).cloneNode(true);
2403
+
2404
+ measureTable.style.position = 'absolute';
2405
+ measureTable.style.visibility = 'hidden';
2406
+ measureTable.style.tableLayout = 'auto';
2407
+ measureTable.style.width = 'auto';
2408
+
2409
+ // Clear all explicit column widths in the clone
2410
+ var measureHeaders = measureTable.tHead.rows[0].cells;
2411
+ for (i = 0, len = measureHeaders.length; i < len; i++) {
2412
+ measureHeaders[i].style.width = 'auto';
2413
+ }
2414
+
2415
+ // Add to DOM (causes one reflow of the hidden clone)
2416
+ self.root.get(0).appendChild(measureTable);
2417
+
2418
+ // Measure all column widths (batched read)
2419
+ for (i = 0, len = headerCells.length; i < len; i++) {
2420
+ thElt = headerCells[i];
2421
+ th = jQuery(thElt);
2422
+
2423
+ // Check if this column has an explicit width set
2424
+ fieldName = th.find('span.wcdv_heading_title').attr('data-wcdv-field');
2425
+
2426
+ if (fieldName != null) {
2427
+ fcc = self.colConfig.get(fieldName);
2428
+
2429
+ if (fcc != null && fcc.width != null) {
2430
+ self.logDebug(self.makeLogTag('Auto Resize Columns') + ' Width of "' + fieldName + '" already set to ' + fcc.width + 'px');
2431
+ widthsToSet.push(null); // Skip this column
2432
+ continue;
2433
+ }
2434
+ }
2435
+
2436
+ // Measure the width from the cloned table
2437
+ var measuredWidth = measureHeaders[i].getBoundingClientRect().width;
2438
+ widthsToSet.push(measuredWidth);
2439
+ }
2440
+
2441
+ // Remove measurement table
2442
+ self.root.get(0).removeChild(measureTable);
2443
+
2444
+ // Apply all width changes (batched write)
2445
+ for (i = 0, len = headerCells.length; i < len; i++) {
2446
+ if (widthsToSet[i] != null) {
2447
+ headerCells[i].style.width = widthsToSet[i] + 'px';
2448
+ }
2449
+ }
2450
+
2451
+ if (getProp(self.features, 'floatingHeader') &&
2452
+ getProp(self.defn, 'table', 'floatingHeader', 'method') === 'tabletool' &&
2453
+ window.TableTool != null) {
2454
+ window.TableTool.enable();
2455
+ }
2456
+
2457
+ // Give TableTool a chance to redraw itself before we go allowing ResizeObserver events again.
2458
+ window.setTimeout(function () {
2459
+ self.autoResizeColsLock.unlock();
2460
+ });
2461
+ };
2462
+
2463
+ // #makeResponsive {{{2
2464
+
2465
+ /**
2466
+ * Set up a ResizeObserver to automatically adjust column widths when the table is resized.
2467
+ * When the table size changes, this method:
2468
+ * 1. Creates a temporary hidden table with table-layout: auto
2469
+ * 2. Measures the optimal widths for each column header
2470
+ * 3. Applies those widths to the actual table headers
2471
+ *
2472
+ * The callback is debounced to prevent infinite loops from self-triggered resizes.
2473
+ */
2474
+
2475
+ GridTable.prototype.makeResponsive = function () {
2476
+ var self = this;
2477
+
2478
+ if (window.ResizeObserver == null) {
2479
+ self.logWarning(self.makeLogTag() + ' ResizeObserver is not supported; table will not be responsive.');
2480
+ return;
2481
+ }
2482
+
2483
+ if (self.ui == null || self.ui.tbl == null) {
2484
+ self.logWarning(self.makeLogTag() + ' Table not initialized; cannot make responsive.');
2485
+ return;
2486
+ }
2487
+
2488
+ var timer;
2489
+
2490
+ self.resizeObserver = new ResizeObserver(function () {
2491
+ if (self.autoResizeColsLock.isLocked()) {
2492
+ return;
2493
+ }
2494
+
2495
+ if (timer != null) {
2496
+ // Stop the previous event handler, resetting the 100ms wait time.
2497
+ clearTimeout(timer);
2498
+ }
2499
+
2500
+ // Wait 100ms and then deal with the resize event.
2501
+ timer = setTimeout(function () {
2502
+ timer = null;
2503
+ self.autoResizeColumns();
2504
+ }, 100);
2505
+ });
2506
+
2507
+ self.resizeObserver.observe(self.ui.tbl.get(0));
2508
+ };
2509
+
2510
+ export default GridTable;