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,1957 @@
1
+ import _ from 'underscore';
2
+
3
+ import jQuery from 'jquery';
4
+
5
+ import { trans } from './trans.js';
6
+ import {
7
+ deepCopy,
8
+ deepDefaults,
9
+ determineColumns,
10
+ icon,
11
+ gensym,
12
+ getProp,
13
+ makeSubclass,
14
+ mapLimit,
15
+ mixinEventHandling,
16
+ mixinLogging,
17
+ objFromArray,
18
+ } from './util/misc.js';
19
+
20
+ import './util/jquery.js';
21
+ import {AGGREGATE_REGISTRY, ComputedView, GROUP_FUNCTION_REGISTRY, types} from 'datavis-ace';
22
+ import {Grid} from './grid.js';
23
+ import {GridFilterSet} from './grid_filter.js';
24
+ import {GroupFunWin} from './group_fun_win.js';
25
+ import {PopupWindow} from './ui/popup_window.js';
26
+
27
+ /*
28
+ * Grid controls are the rounded boxes that appear between the toolbar and the grid. They allow
29
+ * dynamic configuration of the view to which the grid is bound.
30
+ *
31
+ * - Filters
32
+ * - Group Fields
33
+ * - Pivot Fields
34
+ * - Aggregates
35
+ *
36
+ * Each control is basically a list of things that have been added to it, e.g. for grouping, it's a
37
+ * list of fields to group by. Internally, the control is an instance of a subclass of GridControl,
38
+ * and the items are corresponding instances of a subclass of GridControlField. The name "Field"
39
+ * here is historical, before aggregates were specified this way, all controls managed fields from
40
+ * the source data.
41
+ */
42
+
43
+ var GRID_CONTROL_FIELD_POOL = {};
44
+
45
+ // GridControlField {{{1
46
+
47
+ // Constructor {{{2
48
+
49
+ /**
50
+ * Create a new GridControlField instance.
51
+ *
52
+ * @param {GridControl} control
53
+ *
54
+ * @param {string} field
55
+ *
56
+ * @param {string} displayText
57
+ *
58
+ * @param {object} colConfig
59
+ *
60
+ * @class
61
+ *
62
+ * Represents an individual field added to a control. In an older iteration, this literally
63
+ * corresponded to a field in the data (e.g. because the control was a filter, group, or pivot).
64
+ * Now that aggregate functions are also managed through a GridControl subclass, the "field" name is
65
+ * no longer strictly accurate.
66
+ *
67
+ * @property {GridControl} control
68
+ *
69
+ * @property {string|object} spec
70
+ * If a string, simply the field to add. If an object, should contain a `field` property along with
71
+ * anything else that this instance needs to carry.
72
+ *
73
+ * @property {string} displayText
74
+ *
75
+ * @property {object} colConfig
76
+ *
77
+ * @property {object} [opts]
78
+ *
79
+ * @property {object} ui
80
+ * Refers to all user interface constructs that we might need to use later.
81
+ *
82
+ * @property {Element} ui.root
83
+ * The DIV that completely contains the control field.
84
+ *
85
+ * @property {Element} ui.removeButton
86
+ * A button that is used to remove the control field.
87
+ */
88
+
89
+ var GridControlField = (function () {
90
+ var CONTROL_FIELD_ID = 0;
91
+ return makeSubclass('GridControlField', Object, function (control, spec, displayText, colConfig, opts) {
92
+ var self = this;
93
+
94
+ self.control = control;
95
+
96
+ if (typeof spec === 'string') {
97
+ self.field = {
98
+ field: spec
99
+ };
100
+ }
101
+ else {
102
+ self.field = deepCopy(spec);
103
+ }
104
+ self.displayText = displayText;
105
+ self.colConfig = colConfig;
106
+ self.opts = opts;
107
+
108
+ self.fti = self.control.typeInfo.get(self.field.field);
109
+
110
+ self.ui = {};
111
+ self.id = CONTROL_FIELD_ID++;
112
+ });
113
+ })();
114
+
115
+ mixinLogging(GridControlField);
116
+
117
+ // #draw {{{2
118
+
119
+ /**
120
+ * Renders the control field into a DIV.
121
+ *
122
+ * @returns {Element}
123
+ * A newly created DIV that contains everything needed by the control field.
124
+ */
125
+
126
+ GridControlField.prototype.draw = function () {
127
+ var self = this;
128
+ var label = self.displayText || (self.colConfig && self.colConfig.displayText) || self.field.field;
129
+
130
+ self.ui.removeButton = jQuery('<button>', {'type': 'button'})
131
+ .append(icon('square-minus'))
132
+ .attr('title', trans('GRID_CONTROL.FIELD.REMOVE'))
133
+ .addClass('wcdv_icon_button wcdv_remove wcdv_text-primary')
134
+ .on('click', function () {
135
+ self.control.removeField(self);
136
+ })
137
+ ;
138
+
139
+ self.ui.fieldLabel = jQuery('<span>', {
140
+ 'class': 'wcdv_field_name',
141
+ 'title': label
142
+ })
143
+ .text(label);
144
+
145
+ self.ui.root = jQuery('<div>', { 'class': 'wcdv_field' })
146
+ .append(self.ui.removeButton)
147
+ .append(self.ui.fieldLabel)
148
+ ;
149
+
150
+ self._addErrorIndicator(self.ui.root, 'wcdv_aggregate_control_error');
151
+
152
+ return self.ui.root;
153
+ };
154
+
155
+ // #getElement {{{2
156
+
157
+ /**
158
+ * Gets the DIV that contains the UI for this control field.
159
+ *
160
+ * @returns {Element}
161
+ * The DIV that this control field was rendered into.
162
+ */
163
+
164
+ GridControlField.prototype.getElement = function () {
165
+ var self = this;
166
+
167
+ return self.ui.root;
168
+ };
169
+
170
+ // #destroy {{{2
171
+
172
+ /**
173
+ * Called when the control field is removed; should be used to clean up resources like DOM nodes and
174
+ * event handlers.
175
+ */
176
+
177
+ GridControlField.prototype.destroy = function () {
178
+ // DO NOTHING
179
+ };
180
+
181
+ // #showError {{{2
182
+
183
+ GridControlField.prototype.showError = function (errMsg) {
184
+ var self = this;
185
+
186
+ self.logDebug(self.makeLogTag() + ' GRID // CONTROL', errMsg);
187
+
188
+ if (self.ui.error) {
189
+ self.ui.error.attr('data-tooltip', errMsg);
190
+ self.ui.error.show();
191
+ }
192
+ else {
193
+ self.logError(self.makeLogTag() + ' Call Error: Attempted to call `showError()` on a ControlField subclass instance that does not provide a way of indicating errors in the user interface.');
194
+ }
195
+ };
196
+
197
+ // #_addErrorIndicator {{{2
198
+
199
+ GridControlField.prototype._addErrorIndicator = function (parent, cls) {
200
+ var self = this;
201
+
202
+ self.ui.error = icon('triangle-alert', cls)
203
+ .hide()
204
+ .appendTo(parent);
205
+ };
206
+
207
+ // #getSpec {{{2
208
+
209
+ GridControlField.prototype.getSpec = function () {
210
+ var self = this;
211
+
212
+ return {
213
+ field: self.field.field
214
+ };
215
+ };
216
+
217
+ // FunGridControlField {{{1
218
+
219
+ // Constructor {{{2
220
+
221
+ /**
222
+ * @class
223
+ * @extends GridControlField
224
+ */
225
+
226
+ var FunGridControlField = makeSubclass('FunGridControlField', GridControlField);
227
+
228
+ // #draw {{{2
229
+
230
+ FunGridControlField.prototype.draw = function () {
231
+ var self = this;
232
+
233
+ self.super['GridControlField'].draw();
234
+
235
+ // Let's find out what group functions there are that work on the type of the field that we
236
+ // represent, e.g. if we are a date, find out what group functions work on dates.
237
+
238
+ var applicableGroupFuns = GROUP_FUNCTION_REGISTRY.filter(function (gf) {
239
+ if (self.fti == null) {
240
+ return false;
241
+ }
242
+ return gf.allowedTypes.indexOf(self.fti.type) >= 0;
243
+ });
244
+
245
+ if (applicableGroupFuns.size() > 0) {
246
+ // When there are some group functions for the type of this field, we need to create a window to
247
+ // choose between them, plus a button to show the window.
248
+
249
+ self.ui.groupFunWin = new GroupFunWin(trans('GRID.GROUP_FUN.DIALOG.TITLE', self.field.field), applicableGroupFuns);
250
+
251
+ self.ui.groupFunWinBtn = jQuery('<button>', {
252
+ 'type': 'button',
253
+ 'data-wcdv-role': 'set-group-fun',
254
+ title: trans('GRID_CONTROL.FIELD.SHOW_FUNCTIONS')
255
+ })
256
+ .addClass('wcdv_icon_button wcdv_button_left wcdv_text-primary')
257
+ .on('click', function () {
258
+ self.showFunWin();
259
+ })
260
+ .append(icon('zap'))
261
+ .appendTo(self.ui.root)
262
+ ;
263
+
264
+ if (self.field.fun != null) {
265
+ var gf = GROUP_FUNCTION_REGISTRY.get(self.field.fun);
266
+ self.ui.fieldLabel.text(self.field.field + ' (' + gf.getTransName() + ')');
267
+ self.ui.fieldLabel.attr('title', self.field.field + ' (' + gf.getTransName() + ')');
268
+ }
269
+ self.ui.fieldLabel.after(self.ui.groupFunWinBtn);
270
+ }
271
+
272
+ return self.ui.root;
273
+ };
274
+
275
+ // #getSpec {{{2
276
+
277
+ FunGridControlField.prototype.getSpec = function () {
278
+ var self = this;
279
+
280
+ return {
281
+ field: self.field.field,
282
+ fun: self.field.fun
283
+ };
284
+ };
285
+
286
+ // #showFunWin {{{2
287
+
288
+ FunGridControlField.prototype.showFunWin = function () {
289
+ var self = this;
290
+
291
+ self.ui.groupFunWin.show(self.field.fun || 'none', function (groupFunName) {
292
+ if (groupFunName != null) {
293
+ if (groupFunName === 'none') {
294
+ self.field.fun = null;
295
+ self.ui.fieldLabel.text(self.field.field);
296
+ }
297
+ else {
298
+ self.field.fun = groupFunName;
299
+ var gf = GROUP_FUNCTION_REGISTRY.get(self.field.fun);
300
+ self.ui.fieldLabel.text(self.field.field + ' (' + gf.getTransName() + ')');
301
+ }
302
+ self.control.updateView();
303
+ }
304
+ else if (self.field.fun === undefined) {
305
+ self.field.fun = null;
306
+ self.control.updateView();
307
+ }
308
+ });
309
+ };
310
+
311
+ // GroupControlField {{{1
312
+
313
+ // Constructor {{{2
314
+
315
+ /**
316
+ * @class
317
+ * @extends FunGridControlField
318
+ */
319
+
320
+ var GroupControlField = makeSubclass('GroupControlField', FunGridControlField);
321
+
322
+ // PivotControlField {{{1
323
+
324
+ // Constructor {{{2
325
+
326
+ /**
327
+ * @class
328
+ * @extends FunGridControlField
329
+ */
330
+
331
+ var PivotControlField = makeSubclass('PivotControlField', FunGridControlField);
332
+
333
+ // FilterControlField {{{1
334
+ // Constructor {{{2
335
+
336
+ /**
337
+ * @class
338
+ * @extends GridControlField
339
+ */
340
+
341
+ var FilterControlField = makeSubclass('FilterControlField', GridControlField);
342
+
343
+ // #draw {{{2
344
+
345
+ FilterControlField.prototype.draw = function () {
346
+ var self = this;
347
+
348
+ self.super['GridControlField'].draw();
349
+ self.ui.filterContainer = jQuery('<div>')
350
+ .addClass('wcdv_filter_control_filter_container')
351
+ .appendTo(self.ui.root);
352
+ self.control.gfs.add(self.field.field, self.ui.filterContainer, {
353
+ filterType: self.colConfig && self.colConfig.filter
354
+ });
355
+
356
+ return self.ui.root;
357
+ };
358
+ // AggregateControlField {{{1
359
+ // Constructor {{{2
360
+
361
+ /**
362
+ * @class
363
+ * @extends GridControlField
364
+ *
365
+ * @property {object} [opts]
366
+ *
367
+ * @property {string[]} [opts.fields]
368
+ * List of the fields used by the aggregate function.
369
+ *
370
+ * @property {object} [aggFunOpts]
371
+ * Options passed to the aggregate function.
372
+ */
373
+
374
+ var AggregateControlField = makeSubclass('AggregateControlField', GridControlField, function () {
375
+ var self = this;
376
+
377
+ self.super['GridControlField'].ctor.apply(self, arguments);
378
+ self.fieldDropdowns = [];
379
+ self.shouldGraph = false;
380
+ });
381
+
382
+ // #draw {{{2
383
+
384
+ AggregateControlField.prototype.draw = function () {
385
+ var self = this;
386
+
387
+ self.super['GridControlField'].draw();
388
+
389
+ self._addErrorIndicator(self.ui.root, 'wcdv_aggregate_control_error');
390
+
391
+ var aggDefn = AGGREGATE_REGISTRY.get(self.field.field);
392
+
393
+ var fieldList = jQuery('<ul>', {
394
+ 'class': 'wcdv_aggregate_control_fieldlist'
395
+ }).appendTo(self.ui.root);
396
+
397
+ for (var i = 0; i < aggDefn.prototype.fieldCount; i += 1) {
398
+ var li = jQuery('<li>').addClass('wcdv_aggregate_field').appendTo(fieldList);
399
+ if (getProp(aggDefn.prototype, 'fieldInfo', i, 'transLabel')) {
400
+ var label = jQuery('<label>').text(trans(aggDefn.prototype.fieldInfo[i].transLabel) + ':').appendTo(li);
401
+ }
402
+ var select = jQuery('<select>')
403
+ .on('change', function (evt) {
404
+ select.children('option[data-wcdv-bad-field]').filter(function (eltIndex, elt) {
405
+ return jQuery(elt).attr('value') !== select.val();
406
+ }).remove();
407
+ self.control.updateView();
408
+ })
409
+ .appendTo(li);
410
+ self.fieldDropdowns.push(select);
411
+ }
412
+
413
+ _.each(determineColumns(self.control.colConfig, null, self.control.typeInfo), function (fieldName) {
414
+ var text = getProp(self.control.colConfig.get(fieldName), 'displayText') || fieldName;
415
+ _.each(self.fieldDropdowns, function (dropdown, i) {
416
+ jQuery('<option>', { 'value': fieldName }).text(text).appendTo(dropdown);
417
+ });
418
+ });
419
+
420
+ // For each field dropdown, set its value to whatever we received. This has the effect of making
421
+ // the user interface match the internal aggregate configuration.
422
+
423
+ _.each(self.fieldDropdowns, function (dropdown, i) {
424
+ if (getProp(self.opts, 'fields', i)) {
425
+ var matchingOption = dropdown.children('option').filter(function (eltIndex, elt) {
426
+ return jQuery(elt).attr('value') === self.opts.fields[i];
427
+ });
428
+
429
+ // When the field in the configuration isn't in the dropdown (i.e. it's not in colConfig) then
430
+ // we need to make an entry for it. This happens when the aggregate spec from prefs refers to
431
+ // a field that no longer exists in the data.
432
+
433
+ if (matchingOption.length === 0) {
434
+ jQuery('<option>', {
435
+ 'value': self.opts.fields[i],
436
+ 'data-wcdv-bad-field': 'yup'
437
+ })
438
+ // FIXME: i18n
439
+ .text(self.opts.fields[i] + ' — Invalid')
440
+ .appendTo(dropdown);
441
+ }
442
+
443
+ dropdown.val(self.opts.fields[i]);
444
+ }
445
+ });
446
+
447
+ if (aggDefn.prototype.options != null) {
448
+ jQuery('<button>', {
449
+ 'type': 'button',
450
+ title: trans('GRID_CONTROL.AGGREGATE.EDIT_OPTIONS')
451
+ })
452
+ .addClass('wcdv_icon_button wcdv_button_left wcdv_text-primary')
453
+ .on('click', function () {
454
+ self.ui.optionsDialog.open();
455
+ })
456
+ .append(icon('square-pen'))
457
+ .appendTo(self.ui.root)
458
+ ;
459
+ self._makeOptionsDialog(aggDefn);
460
+ }
461
+
462
+ // if (self.control.view.hasClientKind('graph')) {
463
+ // self.ui.graphBtn = jQuery('<button>', {
464
+ // 'type': 'button'
465
+ // })
466
+ // .addClass('wcdv_icon_button wcdv_text-primary')
467
+ // .on('click', function () {
468
+ // // TODO Think of a better way to do this. I feel like the coupling here is too high.
469
+ // self.control.clearGraphFlag();
470
+ // self.shouldGraph = true;
471
+ // self.control.updateView();
472
+ // })
473
+ // .append(icon('bar-chart-2'))
474
+ // .appendTo(self.ui.root)
475
+ // ;
476
+ // }
477
+
478
+ self.ui.isHiddenCheckbox = jQuery('<input>', {
479
+ 'type': 'checkbox'
480
+ })
481
+ .prop('checked', getProp(self.opts, 'isHidden'))
482
+ .on('change', function () {
483
+ self.control.updateView();
484
+ })
485
+ .appendTo(self.ui.root)
486
+ ._makeIconCheckbox({
487
+ on: {
488
+ icon: 'eye-off',
489
+ classes: ['wcdv_text-primary']
490
+ },
491
+ off: {
492
+ icon: 'eye',
493
+ classes: ['wcdv_text-primary']
494
+ }
495
+ })
496
+ ;
497
+
498
+ return self.ui.root;
499
+ };
500
+
501
+ // #_makeOptionsDialog {{{2
502
+
503
+ AggregateControlField.prototype._makeOptionsDialog = function (aggDefn) {
504
+ var self = this;
505
+
506
+ var contentDiv = jQuery('<div>');
507
+
508
+ var table = jQuery('<table>').appendTo(contentDiv);
509
+ var opts = {};
510
+
511
+ _.each(aggDefn.prototype.options, function (optConfig, optName) {
512
+ optConfig = deepDefaults(optConfig, {
513
+ type: 'string',
514
+ widget: 'text',
515
+ displayText: optName
516
+ });
517
+ var id = gensym();
518
+ var input = jQuery('<input>', {
519
+ 'type': 'text',
520
+ 'id': id
521
+ });
522
+ opts[optName] = input;
523
+ var label = jQuery('<label>', {
524
+ 'for': id
525
+ }).text(optConfig.displayText);
526
+ jQuery('<tr>')
527
+ .append(jQuery('<td>').append(label))
528
+ .append(jQuery('<td>').append(input))
529
+ .appendTo(table);
530
+ });
531
+
532
+ self.ui.optionsDialog = new PopupWindow({
533
+ title: trans('GRID_CONTROL.AGGREGATE.OPTIONS_DIALOG.TITLE', aggDefn.prototype.getTransName()),
534
+ content: contentDiv,
535
+ buttons: [{
536
+ icon: 'check',
537
+ label: trans('DIALOG.OK'),
538
+ callback: function () {
539
+ self.aggFunOpts = opts;
540
+ self.control.updateView();
541
+ self.ui.optionsDialog.close();
542
+ }
543
+ }, {
544
+ icon: 'ban',
545
+ label: trans('DIALOG.CANCEL'),
546
+ callback: function () {
547
+ self.ui.optionsDialog.close();
548
+ }
549
+ }]
550
+ });
551
+ };
552
+
553
+ // #destroy {{{2
554
+
555
+ AggregateControlField.prototype.destroy = function () {
556
+ var self = this;
557
+
558
+ if (self.ui.optionsDialog != null) {
559
+ self.ui.optionsDialog.destroy();
560
+ }
561
+
562
+ self.super['GridControlField'].destroy();
563
+ };
564
+
565
+ // #getInfo {{{2
566
+
567
+ AggregateControlField.prototype.getInfo = function () {
568
+ var self = this;
569
+
570
+ return {
571
+ fun: self.field.field,
572
+ name: null,
573
+ fields: _.map(self.fieldDropdowns, function (dropdown) {
574
+ return dropdown.val();
575
+ }),
576
+ isHidden: self.ui.isHiddenCheckbox._isChecked(),
577
+ shouldGraph: self.shouldGraph,
578
+ opts: _.mapObject(self.aggFunOpts, function (input, optName) {
579
+ return input.val();
580
+ }),
581
+ debug: true
582
+ };
583
+ };
584
+
585
+ // GridControl {{{1
586
+
587
+ // Constructor {{{2
588
+
589
+ /**
590
+ * Creates a new GridControl instance.
591
+ *
592
+ * @param {Grid} grid
593
+ * @param {OrdMap.<Grid~ColConfig>} colConfig
594
+ * @param {ComputedView} view
595
+ * @param {object} features
596
+ * @param {Timing} timing
597
+ *
598
+ * @class
599
+ *
600
+ * An abstract class that represents some kind of interface that the user can operate over the
601
+ * available fields.
602
+ *
603
+ * Subclasses should implement the following functions:
604
+ *
605
+ * - `draw(TARGET)`
606
+ * Called to create all required user interface components.
607
+ *
608
+ * - `updateView()`
609
+ * Use `self.fields` to set whatever properties are needed on the view.
610
+ *
611
+ * @property {Grid} grid
612
+ * @property {ComputedView} view
613
+ * @property {object} features
614
+ * @property {Timing} timing
615
+ * @property {OrdMap.<Grid~ColConfig>} colConfig
616
+ *
617
+ * @property {Array.<string>} fields
618
+ * List of all the fields selected by the user.
619
+ *
620
+ * @property {Array.<ControlField>} controlFields
621
+ * List of all the control fields currently in the UI.
622
+ *
623
+ * @property {Object.<string, Array.<ControlField>>} controlFieldsByField
624
+ * Object for looking up control fields by name.
625
+ *
626
+ * @property {Object.<string, ControlField>} controlFieldsById
627
+ * Object for looking up control fields by ID.
628
+ *
629
+ * @property {object} ui
630
+ * Object containing different user interface components.
631
+ *
632
+ * @property {jQuery} ui.dropdown
633
+ * The SELECT element containing the available fields.
634
+ *
635
+ * @property {boolean} [prototype.isHorizontal=false]
636
+ * If true, display the list horizontally rather than vertically.
637
+ *
638
+ * @property {boolean} [prototype.isReorderable=true]
639
+ * If true, display an arrow for reordering the items in the list (when `isHorizontal=false`).
640
+ *
641
+ * @property {boolean} [prototype.showColumns=true]
642
+ * If true, display a dropdown with field names to choose from.
643
+ *
644
+ * @property {boolean} [prototype.disableUsedItems=false]
645
+ * If true, items that are added will be disabled in the columns dropdown.
646
+ *
647
+ * @property {boolean} [prototype.useColConfig=true]
648
+ * If true, pass colConfig for the item to the appropriate `Field` subclass.
649
+ *
650
+ * @property {boolean} [prototype.updateCanHide=true]
651
+ * If true, automatically update colConfig to show (and prohibit hiding of) the column being added.
652
+ */
653
+
654
+ var GridControl = makeSubclass('GridControl', Object, function (grid, colConfig, view, features, timing) {
655
+ var self = this;
656
+
657
+ if (!(grid instanceof Grid)) {
658
+ throw new Error('Call Error: `grid` must be an instance of MIE.WC_DataVis.Grid');
659
+ }
660
+
661
+ self.grid = grid;
662
+ self.colConfig = colConfig;
663
+ self.view = view;
664
+ self.features = features;
665
+ self.timing = timing;
666
+ self.fields = [];
667
+ self.controlFields = [];
668
+ self.controlFieldsByField = {};
669
+ self.controlFieldsById = {};
670
+
671
+ self.ui = {};
672
+
673
+ self.grid.on('colConfigUpdate', function (colConfig) {
674
+ self.colConfig = colConfig;
675
+ });
676
+ }, {
677
+ isHorizontal: false,
678
+ isReorderable: true,
679
+ showColumns: true,
680
+ disableUsedItems: false,
681
+ useColConfig: true,
682
+ updateCanHide: true
683
+ });
684
+
685
+ mixinLogging(GridControl);
686
+
687
+ // Events {{{2
688
+
689
+ /**
690
+ * Fired when a field has been added to the control.
691
+ *
692
+ * @event GridControl#fieldAdded
693
+ *
694
+ * @param {string} fieldAdded
695
+ * The field that was added.
696
+ *
697
+ * @param {Array.<string>} allFields
698
+ * All fields in the control, after the addition.
699
+ */
700
+
701
+ /**
702
+ * Fired when a field has been removed from the control.
703
+ *
704
+ * @event GridControl#fieldRemoved
705
+ *
706
+ * @param {string} fieldRemoved
707
+ * The field that was removed.
708
+ *
709
+ * @param {Array.<string>} allFields
710
+ * All fields in the control, after the removal.
711
+ */
712
+
713
+ /**
714
+ * Fired when the control has been cleared (reset).
715
+ *
716
+ * @event GridControl#cleared
717
+ */
718
+
719
+ mixinEventHandling(GridControl, [
720
+ 'fieldAdded'
721
+ , 'fieldRemoved'
722
+ , 'cleared'
723
+ ]);
724
+
725
+ // #makeClearButton {{{2
726
+
727
+ /**
728
+ * Make a button that calls the `clear` method when clicked.
729
+ *
730
+ * @param {jQuery} target
731
+ * Where to append the button.
732
+ *
733
+ * @returns {jQuery}
734
+ * The button created.
735
+ */
736
+
737
+ GridControl.prototype.makeClearButton = function (target) {
738
+ var self = this;
739
+
740
+ return jQuery('<button>')
741
+ .addClass('wcdv_icon_button wcdv_text-primary wcdv_control_clear_button')
742
+ .append(icon('ban'))
743
+ .hide()
744
+ .on('click', function () {
745
+ jQuery(this).hide();
746
+ self.clear();
747
+ })
748
+ .appendTo(target);
749
+ };
750
+
751
+ // #addField {{{2
752
+
753
+ /**
754
+ * Add a field to this control. Automatically updates the view afterwards.
755
+ *
756
+ * @param {string} field
757
+ * Name of the field to add.
758
+ *
759
+ * @param {string} displayText
760
+ *
761
+ * @param {object} opts
762
+ *
763
+ * @param {object} controlFieldOpts
764
+ *
765
+ * @param {function} next
766
+ */
767
+
768
+ GridControl.prototype.addField = function (field, displayText, opts, controlFieldOpts, next) {
769
+ var self = this
770
+ , args = Array.prototype.slice.call(arguments)
771
+ , fieldName;
772
+
773
+ opts = deepDefaults(opts, {
774
+ updateView: true,
775
+ silent: false,
776
+ openControls: false
777
+ });
778
+
779
+ if (field == null || field === '') {
780
+ return typeof next === 'function' ? next(false) : undefined;
781
+ }
782
+
783
+ fieldName = typeof field === 'string' ? field : field.field;
784
+
785
+ if (fieldName == null || fieldName === '') {
786
+ return typeof next === 'function' ? next(false) : undefined;
787
+ }
788
+
789
+ // Make sure we have access to typeinfo before continuing. The typeinfo is used for:
790
+ //
791
+ // 1. Making sure aggregates are only applied to certain fields.
792
+ // 2. Showing group/pivot functions for applicable fields only.
793
+
794
+ if (self.typeInfo == null) {
795
+ return self.view.getTypeInfo(function (ok, typeInfo) {
796
+ if (!ok) {
797
+ return typeof next === 'function' ? next(false) : undefined;
798
+ }
799
+ self.typeInfo = typeInfo;
800
+ return GridControl.prototype.addField.apply(self, args);
801
+ });
802
+ }
803
+
804
+ if (opts.openControls) {
805
+ self.grid.showControls();
806
+ }
807
+
808
+ if (self.disableUsedItems && self.fields.indexOf(fieldName) >= 0) {
809
+ return typeof next === 'function' ? next(false) : undefined;
810
+ }
811
+
812
+ // Check to see if we are supposed to update the 'canHide' property of the column config. Since
813
+ // we're adding the field, we mark it so that the field can't be hidden.
814
+
815
+ if (self.updateCanHide && self.colConfig != null && self.colConfig.isSet(fieldName)) {
816
+ self.colConfig.get(fieldName).isHidden = false;
817
+ self.colConfig.get(fieldName).canHide = false;
818
+ }
819
+
820
+ var cf = new self.controlFieldCtor(self, field, displayText, self.useColConfig ? self.colConfig.get(fieldName) : null, controlFieldOpts);
821
+
822
+ self.controlFields.push(cf);
823
+ self.controlFieldsById[cf.id] = cf;
824
+ GRID_CONTROL_FIELD_POOL[cf.id] = cf;
825
+
826
+ if (self.controlFieldsByField[fieldName] == null) {
827
+ self.controlFieldsByField[fieldName] = [];
828
+ }
829
+ self.controlFieldsByField[fieldName].push(cf);
830
+
831
+ self.ui.clearBtn.show();
832
+
833
+ var li = jQuery('<li>')
834
+ .attr({
835
+ 'data-wcdv-field': fieldName,
836
+ 'data-wcdv-control-field-id': cf.id,
837
+ 'data-wcdv-draggable-origin': 'GRID_CONTROL_FIELD'
838
+ });
839
+
840
+ if (self.isHorizontal) {
841
+ li.append(icon('arrow-right'));
842
+ }
843
+
844
+ li.append(cf.draw());
845
+ li.appendTo(self.ui.fields); // Add it to the DOM.
846
+
847
+ if (self.disableUsedItems) {
848
+ self.ui.dropdown.find('option').filter(function () {
849
+ return jQuery(this).val() === fieldName;
850
+ }).prop('disabled', true);
851
+ }
852
+
853
+ self.ui.dropdown.val('');
854
+ self.fields.push(fieldName); // Add it to the fields array.
855
+
856
+ if (typeof self.updateView === 'function' && opts.updateView) {
857
+ self.updateView();
858
+ }
859
+
860
+ if (!opts.silent) {
861
+ self.fire('fieldAdded', null, fieldName, self.fields);
862
+ }
863
+
864
+ return typeof next === 'function' ? next(true, cf) : undefined;
865
+ };
866
+
867
+ // #removeField {{{2
868
+
869
+ /**
870
+ * Remove a field from this control. Automatically updates the view afterwards.
871
+ *
872
+ * @param {ControlField} cf
873
+ * The field to remove.
874
+ */
875
+
876
+ GridControl.prototype.removeField = function (cf) {
877
+ var self = this
878
+ , fieldName = cf.field.field;
879
+
880
+ // Check to see if we are supposed to update the 'canHide' property of the column config. Since
881
+ // we're removing the field, we mark it so that the field can be hidden.
882
+
883
+ if (self.updateCanHide && self.colConfig != null && self.colConfig.isSet(fieldName)) {
884
+ self.colConfig.get(fieldName).canHide = true;
885
+ }
886
+
887
+ // Remove it from the UI.
888
+
889
+ cf.destroy();
890
+ cf.getElement().parent('li').remove();
891
+
892
+ // Remove it from the internal data structures.
893
+
894
+ self.controlFields = _.without(self.controlFields, cf);
895
+ self.controlFieldsByField[fieldName] = _.without(self.controlFieldsByField[fieldName], cf);
896
+
897
+ delete self.controlFieldsById[cf.id];
898
+ delete GRID_CONTROL_FIELD_POOL[cf.id];
899
+
900
+ // Re-enable the option in the dropdown, if necessary.
901
+
902
+ self.fields.splice(self.fields.indexOf(fieldName), 1);
903
+
904
+ if (self.disableUsedItems) {
905
+ self.ui.dropdown.find('option').filter(function () {
906
+ return jQuery(this).val() === fieldName;
907
+ }).prop('disabled', false);
908
+ }
909
+
910
+ // Hide the "clear" button if there's nothing to clear.
911
+
912
+ if (self.controlFields.length === 0) {
913
+ self.ui.clearBtn.hide();
914
+ }
915
+
916
+ self.updateView();
917
+ self.fire(GridControl.events.fieldRemoved, null, fieldName, self.fields);
918
+ };
919
+
920
+ // #clear {{{2
921
+
922
+ /**
923
+ * Removes all fields from the control. Automatically updates the view afterwards.
924
+ */
925
+
926
+ GridControl.prototype.clear = function (opts) {
927
+ var self = this;
928
+
929
+ opts = opts || {};
930
+ _.defaults(opts, {
931
+ updateView: true
932
+ });
933
+
934
+ // Check to see if we are supposed to update the 'canHide' property of the column config. Since
935
+ // we're removing all fields, we mark it so that they can all be hidden.
936
+
937
+ if (self.updateCanHide && self.colConfig != null) {
938
+ self.colConfig.each(function (cc) {
939
+ cc.canHide = true;
940
+ });
941
+ }
942
+
943
+ self.fields = [];
944
+ self.controlFields = [];
945
+ self.controlFieldsById = {};
946
+ self.controlFieldsByField = {};
947
+ self.ui.fields.children().remove();
948
+ self.ui.dropdown.find('option:disabled').filter(function () {
949
+ return jQuery(this).val() !== '';
950
+ }).prop('disabled', false);
951
+ self.ui.clearBtn.hide();
952
+
953
+ if (opts.updateView) {
954
+ self.updateView();
955
+ }
956
+
957
+ self.fire(GridControl.events.cleared);
958
+ };
959
+
960
+ // #destroy {{{2
961
+
962
+ GridControl.prototype.destroy = function () {
963
+ var self = this;
964
+
965
+ self.logDebug(self.makeLogTag() + ' Good-bye, cruel world!');
966
+
967
+ self.view.off('*', self);
968
+ self.grid.off('*', self);
969
+ self.ui.root.remove();
970
+ };
971
+
972
+ // #addViewConfigChangeHandler {{{2
973
+
974
+ /**
975
+ * Registers an event handler on the view to update the UI when the view is changed (typically by
976
+ * loading preferences, but also possibly by another grid connected to the same view).
977
+ *
978
+ * @param {string} event
979
+ * Name of the event to register on in the view.
980
+ *
981
+ * @param {function} sync
982
+ * Event handler for the specified event.
983
+ */
984
+
985
+ GridControl.prototype.addViewConfigChangeHandler = function (event, sync) {
986
+ var self = this;
987
+
988
+ var clearDropdown = function () {
989
+ self.ui.dropdown.children().remove();
990
+ jQuery('<option>', {
991
+ 'value': '',
992
+ 'disabled': true,
993
+ 'selected': true
994
+ })
995
+ .text(trans('GRID_CONTROL.SELECT_FIELD'))
996
+ .appendTo(self.ui.dropdown);
997
+ };
998
+
999
+ // There are two main things that we sync:
1000
+ //
1001
+ // 1. The dropdown that shows all the fields. (Not used by aggregate control.) This is done when
1002
+ // the column configuration is updated. Interactive column configuration can change the names
1003
+ // shown for the fields in the dropdown.
1004
+ //
1005
+ // 2. The list of elements applied in the control; for group & pivot these are the fields with
1006
+ // arrows connecting them; for filter it's the list of filters; for aggregate it's the list of
1007
+ // aggregate functions. It's up to the caller (i.e. the subclass) to provide a function that
1008
+ // does this synchronization.
1009
+
1010
+ var sync_colConfig = function (colConfig) {
1011
+ self.logDebug(self.makeLogTag() + ' Synchronizing column configuration with grid', self.grid.toString(), self.controlType.toUpperCase());
1012
+ self.colConfig = colConfig;
1013
+ if (self.showColumns) {
1014
+ clearDropdown();
1015
+ colConfig.each(function (fcc) {
1016
+ jQuery('<option>', { 'value': fcc.field }).text(fcc.displayText || fcc.field).appendTo(self.ui.dropdown);
1017
+ });
1018
+ }
1019
+ };
1020
+
1021
+ var sync_view = function () {
1022
+ self.logDebug(self.makeLogTag() + ' Synchronizing user interface with view', self.grid.toString(), self.controlType.toUpperCase());
1023
+ sync();
1024
+ };
1025
+
1026
+ // To fully sync, you need column configuration and type info. Obviously you need column config
1027
+ // because that says what all the available fields' names are. Type info is only needed right now
1028
+ // for the filter control, to determine what type of control to show (e.g. the widget used for
1029
+ // numbers is different from that used for dates).
1030
+ //
1031
+ // We need to do things in that order: sync #1 (column config) first, then #2 (view). The reason
1032
+ // is that synchronizing #2 may cause us to modify the dropdown, i.e. to disable a field that must
1033
+ // already exist due to synchronizing #1.
1034
+ //
1035
+ // BUT we don't know that any of this code will necessarily execute *before* the column config
1036
+ // and/or type info has been determined. This code may run before either of those are known, or
1037
+ // it may be afterwards (because column config could come directly from the JS instantiating the
1038
+ // grid, from prefs, or from the source itself). So we need to always take that info account ---
1039
+ // if the column config is already known, use it; otherwise register an event handler to capture
1040
+ // it when it's decided. Similarly with type info.
1041
+
1042
+ if (self.grid.colConfig != null) {
1043
+ sync_colConfig(self.grid.colConfig);
1044
+ self.grid.on('colConfigUpdate', sync_colConfig);
1045
+ if (self.view.typeInfo != null) {
1046
+ sync_view();
1047
+ self.view.on(event, sync_view, { who: self });
1048
+ }
1049
+ else {
1050
+ self.view.on('getTypeInfo', function () {
1051
+ sync_view();
1052
+ self.view.on(event, sync_view, { who: self });
1053
+ }, { limit: 1 });
1054
+ }
1055
+ }
1056
+ else {
1057
+ // This setup of event handlers forces us to receive one `colConfigUpdate` event before we allow
1058
+ // any `*Set` events to come through. This is important because the `*Set` events will cause us
1059
+ // to disable elements in the dropdown, so we need to have populated it first.
1060
+
1061
+ self.grid.on('colConfigUpdate', function (colConfig) {
1062
+ sync_colConfig(colConfig);
1063
+ self.grid.on('colConfigUpdate', sync_colConfig);
1064
+ if (self.view.typeInfo != null) {
1065
+ sync_view();
1066
+ self.view.on(event, sync_view, { who: self });
1067
+ }
1068
+ else {
1069
+ self.view.on('getTypeInfo', function (ok) {
1070
+ sync_view();
1071
+ self.view.on(event, sync_view, { who: self });
1072
+ }, { limit: 1 });
1073
+ }
1074
+ }, { limit: 1 });
1075
+ }
1076
+ };
1077
+
1078
+ // #getListElement {{{2
1079
+
1080
+ GridControl.prototype.getListElement = function () {
1081
+ var self = this;
1082
+
1083
+ return self.ui.fields;
1084
+ };
1085
+
1086
+ // #draw {{{2
1087
+
1088
+ /**
1089
+ * Render this grid control and attach it to the specified parent element.
1090
+ *
1091
+ * @abstract
1092
+ *
1093
+ * @param {jQuery} parent
1094
+ * Element to append this grid control to.
1095
+ */
1096
+
1097
+ GridControl.prototype.draw = function (parent) {
1098
+ throw new Error('ABSTRACT');
1099
+ };
1100
+
1101
+ // #updateView {{{2
1102
+
1103
+ /**
1104
+ * Update the view with the configuration entered using this grid control.
1105
+ *
1106
+ * @abstract
1107
+ */
1108
+
1109
+ GridControl.prototype.updateView = function () {
1110
+ throw new Error('ABSTRACT');
1111
+ };
1112
+
1113
+ // GroupControl {{{1
1114
+
1115
+ // Constructor {{{2
1116
+
1117
+ /**
1118
+ * Part of the user interface which governs the fields that are part of the group, including
1119
+ * filtering.
1120
+ *
1121
+ * @class
1122
+ * @extends GridControl
1123
+ */
1124
+
1125
+ var GroupControl = makeSubclass('GroupControl', GridControl, function () {
1126
+ var self = this;
1127
+
1128
+ self.super['GridControl'].ctor.apply(self, arguments);
1129
+
1130
+ self.view.on(ComputedView.events.invalidGroupField, function (field) {
1131
+ _.each(self.controlFieldsByField[field], function (cf) {
1132
+ cf.showError('This field does not exist in the data.');
1133
+ });
1134
+ });
1135
+ }, {
1136
+ controlFieldCtor: GroupControlField,
1137
+ controlType: 'Group'
1138
+ });
1139
+
1140
+ // #draw {{{2
1141
+
1142
+ /**
1143
+ * Create a DIV element that can be placed within the Grid instance to hold the user interface for
1144
+ * the GroupControl. The caller must add the result to the DOM somewhere.
1145
+ *
1146
+ * @returns {jQuery} The DIV element that holds the entire UI.
1147
+ */
1148
+
1149
+ GroupControl.prototype.draw = function (parent) {
1150
+ var self = this;
1151
+
1152
+ parent.droppable({
1153
+ classes: {
1154
+ 'ui-droppable-hover': 'wcdv_drop_target_hover'
1155
+ },
1156
+ drop: function (evt, ui) {
1157
+ // Turn this off for the sake of efficiency.
1158
+ //ui.draggable.draggable('option', 'refreshPositions', false);
1159
+
1160
+ // The problem is, this event gets triggered both (1) when dropping a field from the grid
1161
+ // table's header, and (2) when shuffling fields between the group & pivot controls. In the
1162
+ // case of (1) we need to make an <LI>. But in the case of (2), we don't need to modify the
1163
+ // DOM in any way, jQuery UI sortable does that for us. To tell the difference, we use the
1164
+ // `wcdv-draggable-origin` data attribute, which tells where the draggable came from.
1165
+
1166
+ if (ui.draggable.attr('data-wcdv-draggable-origin') === 'GRID_TABLE_HEADER') {
1167
+ var field = ui.draggable.attr('data-wcdv-field');
1168
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'), {
1169
+ autoShowFunWin: true
1170
+ });
1171
+ }
1172
+ }
1173
+ });
1174
+
1175
+ self.ui.root = jQuery('<div>').appendTo(parent);
1176
+ self.ui.title = jQuery('<div>')
1177
+ .addClass('wcdv_control_title_bar')
1178
+ .appendTo(self.ui.root);
1179
+ jQuery('<span>', { 'class': 'wcdv_control_title' })
1180
+ .text(trans('GRID_CONTROL.GROUP.TITLE'))
1181
+ .appendTo(self.ui.title);
1182
+ self.ui.clearBtn = self.makeClearButton(self.ui.title);
1183
+ self.ui.fields = jQuery('<ul>', {
1184
+ id: gensym(),
1185
+ 'class': self.isHorizontal ? 'wcdv_control_horizontal' : 'wcdv_control_vertical'
1186
+ }).appendTo(self.ui.root);
1187
+
1188
+ var dropdownContainer = jQuery('<div>').appendTo(self.ui.root);
1189
+ self.ui.dropdown = jQuery('<select>', { 'class': 'wcdv_control_addField' }).appendTo(dropdownContainer);
1190
+ self.ui.dropdown.on('change', function () {
1191
+ self.addField(self.ui.dropdown.val(), self.ui.dropdown.find('option:selected').text(), {
1192
+ autoShowFunWin: true
1193
+ });
1194
+ });
1195
+
1196
+ self.addViewConfigChangeHandler('groupSet', function () {
1197
+ var spec = self.view.getGroup();
1198
+ var fields = (!self.view.source.origin.isLimited && spec && spec.fieldNames) || [];
1199
+ self.clear({ updateView: false });
1200
+ self.logDebug(self.makeLogTag() + ' View set group fields to: %s', self.grid.toString(), JSON.stringify(fields));
1201
+ _.each(fields, function (field) {
1202
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'), { updateView: false });
1203
+ });
1204
+ });
1205
+
1206
+ return self.ui.root;
1207
+ };
1208
+
1209
+ // #updateView {{{2
1210
+
1211
+ GroupControl.prototype.updateView = function () {
1212
+ var self = this;
1213
+ var fieldNames = _.map(self.controlFields, function (cf) {
1214
+ return cf.getSpec();
1215
+ });
1216
+
1217
+ if (fieldNames.length > 0) {
1218
+ self.view.setGroup({fieldNames: fieldNames}, {
1219
+ dontSendEventTo: self
1220
+ });
1221
+ }
1222
+ else {
1223
+ self.view.clearGroup();
1224
+ }
1225
+ };
1226
+
1227
+ // #toString {{{2
1228
+
1229
+ GroupControl.prototype.toString = function () {
1230
+ var self = this;
1231
+
1232
+ return self.grid.id + ', Group';
1233
+ };
1234
+
1235
+ // #sortableSync {{{2
1236
+
1237
+ GroupControl.prototype.sortableSync = function () {
1238
+ var self = this;
1239
+
1240
+ var controlFieldIds = self.ui.fields.children('li').map(function (index, elt) {
1241
+ return jQuery(elt).attr('data-wcdv-control-field-id');
1242
+ }).get();
1243
+
1244
+ self.controlFields = [];
1245
+ _.each(controlFieldIds, function (id) {
1246
+ self.controlFields.push(GRID_CONTROL_FIELD_POOL[id]);
1247
+ });
1248
+
1249
+ if (self.controlFields.length > 0) {
1250
+ self.ui.clearBtn.show();
1251
+ }
1252
+ else {
1253
+ self.ui.clearBtn.hide();
1254
+ }
1255
+
1256
+ return self.updateView();
1257
+ };
1258
+
1259
+ // #addField {{{2
1260
+
1261
+ GroupControl.prototype.addField = function (field, displayText, opts) {
1262
+ var self = this;
1263
+
1264
+ // Make sure we have typeInfo. We need that so we can detect when the user is dragging a field
1265
+ // with a type that doesn't permit grouping (e.g. JSON).
1266
+
1267
+ if (self.typeInfo == null) {
1268
+ return self.view.getTypeInfo(function (ok, typeInfo) {
1269
+ if (!ok) {
1270
+ self.logError(self.makeLogTag() + ' Failed to retrieve typeInfo');
1271
+ return;
1272
+ }
1273
+ self.typeInfo = typeInfo;
1274
+ return self.addField(field, displayText, opts);
1275
+ });
1276
+ }
1277
+
1278
+ var fieldName = typeof field === 'string' ? field : field.field;
1279
+ var fti = self.typeInfo.get(fieldName);
1280
+ if (fti == null) {
1281
+ self.logError(self.makeLogTag() + ' Field not in typeInfo: %s', fieldName);
1282
+ self.ui.dropdown.val('');
1283
+ return;
1284
+ }
1285
+
1286
+ var tc = types.registry.get(fti.type);
1287
+ if (tc == null) {
1288
+ self.logError(self.makeLogTag() + ' Field "%s" type "%s" not in registry', fieldName, fti.type);
1289
+ self.ui.dropdown.val('');
1290
+ return;
1291
+ }
1292
+
1293
+ if (!tc.supports.group) {
1294
+ self.logError(self.makeLogTag() + ' Field "%s" type "%s" does not support grouping', fieldName, fti.type);
1295
+ self.ui.dropdown.val('');
1296
+ return;
1297
+ }
1298
+
1299
+ opts = deepDefaults(opts, {
1300
+ autoShowFunWin: false,
1301
+ updateView: true
1302
+ });
1303
+ var updateView = opts.updateView;
1304
+ opts.updateView = false;
1305
+
1306
+ self.super['GridControl'].addField(field, displayText, opts, null, function (ok, cf) {
1307
+ if (!ok) {
1308
+ return;
1309
+ }
1310
+ if (opts.autoShowFunWin && cf.fti != null && ['date', 'datetime'].indexOf(cf.fti.type) >= 0 && cf.field.fun === undefined) {
1311
+ cf.showFunWin();
1312
+ }
1313
+ else if (updateView) {
1314
+ self.updateView();
1315
+ }
1316
+ });
1317
+ };
1318
+
1319
+ // PivotControl {{{1
1320
+
1321
+ // Constructor {{{2
1322
+
1323
+ /**
1324
+ * Part of the user interface which governs: (1) the fields that are part of the pivot, including
1325
+ * filtering; (2) the aggregate function [and potentially its arguments] that produces the values in
1326
+ * the pivot table.
1327
+ *
1328
+ * @class
1329
+ * @extends GridControl
1330
+ *
1331
+ * @property {GridControl} super
1332
+ * Proxy to call prototype ("superclass") methods even if we override them.
1333
+ *
1334
+ * @property {string[]} fields
1335
+ * Names of the fields
1336
+ */
1337
+
1338
+ var PivotControl = makeSubclass('PivotControl', GridControl, function () {
1339
+ var self = this;
1340
+
1341
+ self.super['GridControl'].ctor.apply(self, arguments);
1342
+
1343
+ self.view.on(ComputedView.events.invalidPivotField, function (field) {
1344
+ _.each(self.controlFieldsByField[field], function (cf) {
1345
+ cf.showError('This field does not exist in the data.');
1346
+ });
1347
+ });
1348
+ }, {
1349
+ controlFieldCtor: PivotControlField,
1350
+ controlType: 'Pivot'
1351
+ });
1352
+
1353
+ // #draw {{{2
1354
+
1355
+ /**
1356
+ * Create a DIV element that can be placed within the Grid instance to hold the user interface for
1357
+ * the PivotControl. The caller must add the result to the DOM somewhere.
1358
+ *
1359
+ * @returns {jQuery} The DIV element that holds the entire UI.
1360
+ */
1361
+
1362
+ PivotControl.prototype.draw = function (parent) {
1363
+ var self = this;
1364
+
1365
+ parent.droppable({
1366
+ classes: {
1367
+ 'ui-droppable-hover': 'wcdv_drop_target_hover'
1368
+ },
1369
+ drop: function (evt, ui) {
1370
+ // Turn this off for the sake of efficiency.
1371
+ //ui.draggable.draggable('option', 'refreshPositions', false);
1372
+
1373
+ // The problem is, this event gets triggered both (1) when dropping a field from the grid
1374
+ // table's header, and (2) when shuffling fields between the group & pivot controls. In the
1375
+ // case of (1) we need to make an <LI>. But in the case of (2), we don't need to modify the
1376
+ // DOM in any way, jQuery UI sortable does that for us. To tell the difference, we use the
1377
+ // `wcdv-draggable-origin` data attribute, which tells where the draggable came from.
1378
+
1379
+ if (ui.draggable.attr('data-wcdv-draggable-origin') === 'GRID_TABLE_HEADER') {
1380
+ var field = ui.draggable.attr('data-wcdv-field');
1381
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'), {
1382
+ autoShowFunWin: true
1383
+ });
1384
+ }
1385
+ }
1386
+ });
1387
+
1388
+ self.ui.root = jQuery('<div>').appendTo(parent);
1389
+ self.ui.title = jQuery('<div>')
1390
+ .addClass('wcdv_control_title_bar')
1391
+ .appendTo(self.ui.root);
1392
+ jQuery('<span>')
1393
+ .addClass('wcdv_control_title')
1394
+ .text(trans('GRID_CONTROL.PIVOT.TITLE'))
1395
+ .appendTo(self.ui.title);
1396
+ self.ui.clearBtn = self.makeClearButton(self.ui.title);
1397
+ self.ui.fields = jQuery('<ul>', {
1398
+ id: gensym(),
1399
+ 'class': self.isHorizontal ? 'wcdv_control_horizontal' : 'wcdv_control_vertical'
1400
+ }).appendTo(self.ui.root);
1401
+
1402
+ var dropdownContainer = jQuery('<div>').appendTo(self.ui.root);
1403
+ self.ui.dropdown = jQuery('<select>', { 'class': 'wcdv_control_addField' }).appendTo(dropdownContainer);
1404
+ self.ui.dropdown.on('change', function () {
1405
+ self.addField(self.ui.dropdown.val(), self.ui.dropdown.find('option:selected').text(), {
1406
+ autoShowFunWin: true
1407
+ });
1408
+ });
1409
+
1410
+ self.addViewConfigChangeHandler('pivotSet', function (spec) {
1411
+ spec = self.view.getPivot();
1412
+ var fields = (!self.view.source.origin.isLimited && spec && spec.fieldNames) || [];
1413
+ self.clear({ updateView: false });
1414
+ self.logDebug(self.makeLogTag() + ' View set pivot fields to: %s', self.grid.toString(), JSON.stringify(fields));
1415
+ _.each(fields, function (field) {
1416
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'), { updateView: false });
1417
+ });
1418
+ });
1419
+
1420
+ return self.ui.root;
1421
+ };
1422
+
1423
+ // #updateView {{{2
1424
+
1425
+ /**
1426
+ * Set the pivot configuration on the ComputedView. The pivot configuration consists of:
1427
+ *
1428
+ * - Fields that are part of the pivot.
1429
+ */
1430
+
1431
+ PivotControl.prototype.updateView = function () {
1432
+ var self = this;
1433
+ var fieldNames = _.map(self.controlFields, function (cf) {
1434
+ return cf.getSpec();
1435
+ });
1436
+
1437
+ if (fieldNames.length > 0) {
1438
+ self.view.setPivot({fieldNames: fieldNames}, {
1439
+ dontSendEventTo: self
1440
+ });
1441
+ }
1442
+ else {
1443
+ self.view.clearPivot();
1444
+ }
1445
+ };
1446
+
1447
+ // #toString {{{2
1448
+
1449
+ PivotControl.prototype.toString = function () {
1450
+ var self = this;
1451
+
1452
+ return self.grid.id + ', Pivot';
1453
+ };
1454
+
1455
+ // #sortableSync {{{2
1456
+
1457
+ PivotControl.prototype.sortableSync = function () {
1458
+ var self = this;
1459
+
1460
+ var controlFieldIds = self.ui.fields.children('li').map(function (index, elt) {
1461
+ return jQuery(elt).attr('data-wcdv-control-field-id');
1462
+ }).get();
1463
+
1464
+ self.controlFields = [];
1465
+ _.each(controlFieldIds, function (id) {
1466
+ self.controlFields.push(GRID_CONTROL_FIELD_POOL[id]);
1467
+ });
1468
+
1469
+ return self.updateView();
1470
+ };
1471
+
1472
+ // #addField {{{2
1473
+
1474
+ PivotControl.prototype.addField = function (field, displayText, opts) {
1475
+ var self = this;
1476
+
1477
+ // Make sure we have typeInfo. We need that so we can detect when the user is dragging a field
1478
+ // with a type that doesn't permit grouping (e.g. JSON).
1479
+
1480
+ if (self.typeInfo == null) {
1481
+ return self.view.getTypeInfo(function (ok, typeInfo) {
1482
+ if (!ok) {
1483
+ self.logError(self.makeLogTag() + ' Failed to retrieve typeInfo');
1484
+ return;
1485
+ }
1486
+ self.typeInfo = typeInfo;
1487
+ return self.addField(field, displayText, opts);
1488
+ });
1489
+ }
1490
+
1491
+ var fieldName = typeof field === 'string' ? field : field.field;
1492
+ var fti = self.typeInfo.get(fieldName);
1493
+ if (fti == null) {
1494
+ self.logError(self.makeLogTag() + ' Field not in typeInfo: %s', fieldName);
1495
+ self.ui.dropdown.val('');
1496
+ return;
1497
+ }
1498
+
1499
+ var tc = types.registry.get(fti.type);
1500
+ if (tc == null) {
1501
+ self.logError(self.makeLogTag() + ' Field "%s" type "%s" not in registry', fieldName, fti.type);
1502
+ self.ui.dropdown.val('');
1503
+ return;
1504
+ }
1505
+
1506
+ if (!tc.supports.group) {
1507
+ self.logError(self.makeLogTag() + ' Field "%s" type "%s" does not support grouping', fieldName, fti.type);
1508
+ self.ui.dropdown.val('');
1509
+ return;
1510
+ }
1511
+
1512
+ opts = deepDefaults(opts, {
1513
+ autoShowFunWin: false,
1514
+ updateView: true
1515
+ });
1516
+ var updateView = opts.updateView;
1517
+ opts.updateView = false;
1518
+
1519
+ self.super['GridControl'].addField(field, displayText, opts, null, function (ok, cf) {
1520
+ if (!ok) {
1521
+ return;
1522
+ }
1523
+ if (opts.autoShowFunWin && cf.fti != null && ['date', 'datetime'].indexOf(cf.fti.type) >= 0 && cf.field.fun === undefined) {
1524
+ cf.showFunWin();
1525
+ }
1526
+ else if (updateView) {
1527
+ self.updateView();
1528
+ }
1529
+ });
1530
+ };
1531
+
1532
+ // AggregateControl {{{1
1533
+
1534
+ // Constructor {{{2
1535
+
1536
+ /**
1537
+ * Part of the user interface which governs the aggregate function (and potentially its arguments)
1538
+ * that produces the values in (1) group summary columns, (2) pivot cells.
1539
+ *
1540
+ * @class
1541
+ * @extends GridControl
1542
+ *
1543
+ * @property {string[]} fields
1544
+ * Names of the fields
1545
+ */
1546
+
1547
+ var AggregateControl = makeSubclass('AggregateControl', GridControl, function () {
1548
+ var self = this;
1549
+
1550
+ self.super['GridControl'].ctor.apply(self, arguments);
1551
+
1552
+ self.view.on(ComputedView.events.invalidAggregate, function (aggNum, errMsg) {
1553
+ self.controlFields[aggNum].showError(errMsg);
1554
+ });
1555
+ }, {
1556
+ disableUsedItems: false,
1557
+ showColumns: false,
1558
+ updateCanHide: false,
1559
+ controlFieldCtor: AggregateControlField,
1560
+ controlType: 'Aggregate'
1561
+ });
1562
+
1563
+ // #draw {{{2
1564
+
1565
+ /**
1566
+ * Create a DIV element that can be placed within the Grid instance to hold the user interface for
1567
+ * the AggregateControl. The caller must add the result to the DOM somewhere.
1568
+ *
1569
+ * @returns {jQuery} The DIV element that holds the entire UI.
1570
+ */
1571
+
1572
+ AggregateControl.prototype.draw = function (parent) {
1573
+ var self = this;
1574
+
1575
+ self.ui.root = jQuery('<div>').appendTo(parent);
1576
+
1577
+ self.ui.title = jQuery('<div>')
1578
+ .addClass('wcdv_control_title_bar')
1579
+ .appendTo(self.ui.root);
1580
+ jQuery('<span>')
1581
+ .addClass('wcdv_control_title')
1582
+ .text(trans('GRID_CONTROL.AGGREGATE.TITLE'))
1583
+ .appendTo(self.ui.title);
1584
+ self.ui.clearBtn = self.makeClearButton(self.ui.title);
1585
+ self.ui.fields = jQuery('<ul>', {
1586
+ id: gensym(),
1587
+ 'class': self.isHorizontal ? 'wcdv_control_horizontal' : 'wcdv_control_vertical'
1588
+ }).appendTo(self.ui.root);
1589
+ var dropdownContainer = jQuery('<div>').appendTo(self.ui.root);
1590
+ self.ui.dropdown = jQuery('<select>', { 'class': 'wcdv_control_addField' }).appendTo(dropdownContainer);
1591
+ self.ui.dropdown.on('change', function () {
1592
+ self.addField(self.ui.dropdown.val(), self.ui.dropdown.find('option:selected').text());
1593
+ });
1594
+
1595
+ jQuery('<option>', { 'value': '', 'disabled': true, 'selected': true })
1596
+ .text(trans('GRID_CONTROL.SELECT_AGGREGATE'))
1597
+ .appendTo(self.ui.dropdown);
1598
+
1599
+ AGGREGATE_REGISTRY.each(function (aggFunDefn, aggFunShortName) {
1600
+ jQuery('<option>', { 'value': aggFunShortName }).text(aggFunDefn.prototype.getTransName()).appendTo(self.ui.dropdown);
1601
+ });
1602
+ /*
1603
+ self.ui.fun = jQuery('<div>').css({'margin-top': '7px'}).appendTo(self.ui.root);
1604
+ jQuery('<label>').text('Function:').appendTo(self.ui.fun);
1605
+ self.ui.funDropdown = jQuery('<select>')
1606
+ .appendTo(self.ui.fun)
1607
+ .on('change', function () {
1608
+ self.triggerAggChange();
1609
+ })
1610
+ ;
1611
+
1612
+ // Create a dropdown containing all the aggregate functions that are allowed to be used for
1613
+ // calculating pivot cells. Right now that's everything that needs no external parameters aside
1614
+ // from the field.
1615
+
1616
+ AGGREGATE_REGISTRY.each(function (aggClass, aggFunName) {
1617
+ if (aggClass.prototype.enabled && aggClass.prototype.enabled) {
1618
+ jQuery('<option>', {
1619
+ value: aggFunName
1620
+ })
1621
+ .text(aggClass.prototype.name || aggFunName)
1622
+ .appendTo(self.ui.funDropdown);
1623
+ }
1624
+ });
1625
+
1626
+ // When we receive type information, use that to populate the "fields" dropdown.
1627
+ //
1628
+ // TODO This needs to be expanded to the possibility of having multiple fields.
1629
+
1630
+ self.view.on('getTypeInfo', function (typeInfo) {
1631
+ self.typeInfo = typeInfo;
1632
+ self.updateFieldDropdowns();
1633
+ }, { limit: 1 });
1634
+
1635
+ var syncAgg = function (spec) {
1636
+ var agg;
1637
+ if (getProp(spec, 'cell', 0, 'fun')) {
1638
+ self.ui.funDropdown.val(spec.cell[0].fun);
1639
+ agg = AGGREGATE_REGISTRY.get(spec.cell[0].fun);
1640
+ if (agg.prototype.fieldCount >= self.ui.fields.length) {
1641
+ self.addFieldDropdowns(agg);
1642
+ }
1643
+ self.showHideFields(agg);
1644
+ }
1645
+ if (getProp(spec, 'cell', 0, 'fields')) {
1646
+ _.each(spec.cell[0].fields, function (f, i) {
1647
+ self.ui.fields[i].dropdown.val(f);
1648
+ });
1649
+ }
1650
+
1651
+ debug.info('GRID // AGGREGATE CONTROL',
1652
+ 'ComputedView set aggregate to: ' + JSON.stringify(spec));
1653
+ };
1654
+
1655
+ self.view.on(ComputedView.events.aggregateSet, function (spec) {
1656
+ syncAgg(spec)
1657
+ }, { who: self });
1658
+ */
1659
+
1660
+ self.addViewConfigChangeHandler('aggregateSet', function () {
1661
+ var spec = self.view.getAggregate();
1662
+ self.clear({ updateView: false });
1663
+ if (spec != null) {
1664
+ self.logDebug(self.makeLogTag() + ' View set aggregate to: %s', self.grid.toString(), JSON.stringify(spec.all));
1665
+
1666
+ _.each(spec.all, function (agg) {
1667
+ self.addField(agg.fun, AGGREGATE_REGISTRY.get(agg.fun).prototype.getTransName(), { updateView: false }, {
1668
+ fields: agg.fields,
1669
+ isHidden: agg.isHidden
1670
+ });
1671
+ });
1672
+ }
1673
+ });
1674
+ return self.ui.root;
1675
+ };
1676
+
1677
+ // #updateView {{{2
1678
+
1679
+ AggregateControl.prototype.updateView = function () {
1680
+ var self = this;
1681
+ var info = _.map(self.controlFields, function (cf) {
1682
+ return cf.getInfo();
1683
+ });
1684
+ self.ui.root.find('.wcdv_aggregate_control_error').hide();
1685
+ self.view.setAggregate(objFromArray(['group', 'pivot', 'cell', 'all'], [info]), {
1686
+ dontSendEventTo: self
1687
+ });
1688
+ };
1689
+
1690
+ // #clearGraphFlag {{{2
1691
+
1692
+ AggregateControl.prototype.clearGraphFlag = function () {
1693
+ var self = this;
1694
+
1695
+ _.each(self.controlFields, function (cf) {
1696
+ cf.shouldGraph = false;
1697
+ });
1698
+ };
1699
+
1700
+ // #triggerAggChange (PROTOTYPE) {{{2
1701
+
1702
+ /**
1703
+ * Perform necessary actions when the aggregate function is changed.
1704
+ *
1705
+ * - Update the UI to show/hide field argument.
1706
+ */
1707
+
1708
+ AggregateControl.prototype.triggerAggChange = function () {
1709
+ var self = this;
1710
+ var agg = AGGREGATE_REGISTRY.get(self.ui.funDropdown.val());
1711
+
1712
+ if (agg.prototype.fieldCount > self.ui.fields.length) {
1713
+ self.addFieldDropdowns(agg);
1714
+ }
1715
+
1716
+ self.showHideFields(agg);
1717
+
1718
+ var aggSpec = objFromArray(['group', 'pivot', 'cell', 'all'], [[{
1719
+ fun: self.ui.funDropdown.val(),
1720
+ fields: agg.prototype.fieldCount > 0 && mapLimit(self.ui.fields, function (f) {
1721
+ return f.dropdown.val();
1722
+ }, agg.prototype.fieldCount)
1723
+ }]]);
1724
+ var i;
1725
+ var div;
1726
+
1727
+ self.view.setAggregate(aggSpec, {
1728
+ dontSendEventTo: self
1729
+ });
1730
+ };
1731
+
1732
+ // #showHideFields (PROTOTYPE) {{{2
1733
+
1734
+ AggregateControl.prototype.showHideFields = function (agg) {
1735
+ var self = this;
1736
+ var i;
1737
+
1738
+ for (i = 0; i < self.ui.fields.length; i += 1) {
1739
+ if (i < agg.prototype.fieldCount) {
1740
+ self.ui.fields[i].div.show();
1741
+ }
1742
+ else {
1743
+ self.ui.fields[i].div.hide();
1744
+ }
1745
+ }
1746
+ };
1747
+
1748
+ // #addFieldDropdowns (PROTOTYPE) {{{2
1749
+
1750
+ /**
1751
+ * For each field that an aggregate function requires, add a dropdown for it to the user interface.
1752
+ * This is used by some prototype code that allows changing the aggregate function dynamically. If
1753
+ * the new aggregate function needs more fields than the old one (e.g. going from "count" to "sum")
1754
+ * then this function adds the extra UI elements needed to get those fields from the user.
1755
+ */
1756
+
1757
+ AggregateControl.prototype.addFieldDropdowns = function (agg) {
1758
+ var self = this;
1759
+
1760
+ self.logDebug(self.makeLogTag() + ' Adding %s extra field dropdowns for the %s aggregate function',
1761
+ self.grid.toString(), agg.prototype.fieldCount - self.ui.fields.length, agg.prototype.name);
1762
+
1763
+ // Create the extra dropdowns that we need to get all the fields required by the aggregate
1764
+ // function selected.
1765
+
1766
+ while (self.ui.fields.length < agg.prototype.fieldCount) {
1767
+ var x = {};
1768
+ x.div = jQuery('<div>').css({'margin-top': '4px'}).appendTo(self.ui.root);
1769
+ x.label = jQuery('<label>').text(trans('GRID_CONTROL.FIELD') + ':').appendTo(x.div);
1770
+ x.dropdown = jQuery('<select>').on('change', function () { self.triggerAggChange(); }).appendTo(x.div);
1771
+ self.ui.fields.push(x);
1772
+ }
1773
+
1774
+ self.updateFieldDropdowns();
1775
+ };
1776
+
1777
+ // #updateFieldDropdowns (PROTOTYPE) {{{2
1778
+
1779
+ /**
1780
+ * Populate the field dropdowns with the list of fields that are available in the view. This is
1781
+ * used by prototype code that allows changing the aggregate function dynamically.
1782
+ */
1783
+
1784
+ AggregateControl.prototype.updateFieldDropdowns = function () {
1785
+ var self = this;
1786
+
1787
+ // Clear out the fields that are already in the dropdown (in case anything was removed, and to
1788
+ // prevent duplicates from being added).
1789
+
1790
+ _.each(self.ui.fields, function (f) {
1791
+ f.dropdown.children().remove();
1792
+ });
1793
+
1794
+ // Add <OPTION> elements for all the fields.
1795
+
1796
+ _.each(determineColumns(self.colConfig, null, self.typeInfo), function (fieldName) {
1797
+ var text = getProp(self.colConfig.get(fieldName), 'displayText') || fieldName;
1798
+ _.each(self.ui.fields, function (f) {
1799
+ jQuery('<option>', { 'value': fieldName }).text(text).appendTo(f.dropdown);
1800
+ });
1801
+ });
1802
+ };
1803
+
1804
+ // #toString {{{2
1805
+
1806
+ AggregateControl.prototype.toString = function () {
1807
+ var self = this;
1808
+
1809
+ return self.grid.id + ', Aggregate';
1810
+ };
1811
+
1812
+ // FilterControl {{{1
1813
+
1814
+ // Constructor {{{2
1815
+
1816
+ /**
1817
+ * Part of the user interface which lets users filter columns.
1818
+ *
1819
+ * @param {object} defn
1820
+ *
1821
+ * @param {ComputedView} view
1822
+ *
1823
+ * @param {Grid~Features} features
1824
+ *
1825
+ * @param {object} timing
1826
+ *
1827
+ * @class
1828
+ * @extends GridControl
1829
+ */
1830
+
1831
+ var FilterControl = makeSubclass('FilterControl', GridControl, function () {
1832
+ var self = this;
1833
+
1834
+ self.super['GridControl'].ctor.apply(self, arguments);
1835
+ self.gfs = new GridFilterSet(self.view, null, null, null, {
1836
+ dontSendEventTo: self
1837
+ });
1838
+ }, {
1839
+ isReorderable: false,
1840
+ disableUsedItems: true,
1841
+ controlFieldCtor: FilterControlField,
1842
+ controlType: 'Filter'
1843
+ });
1844
+
1845
+ // #draw {{{2
1846
+
1847
+ /**
1848
+ * Create a DIV element that can be placed within the Grid instance to hold the user interface for
1849
+ * the FilterControl. The caller must add the result to the DOM somewhere.
1850
+ *
1851
+ * @returns {jQuery} The DIV element that holds the entire UI.
1852
+ */
1853
+
1854
+ FilterControl.prototype.draw = function (parent) {
1855
+ var self = this;
1856
+
1857
+ /*
1858
+ parent.resizable({
1859
+ handles: 'e',
1860
+ minWidth: 100
1861
+ });
1862
+ */
1863
+
1864
+ parent.droppable({
1865
+ classes: {
1866
+ 'ui-droppable-hover': 'wcdv_drop_target_hover'
1867
+ },
1868
+ drop: function (evt, ui) {
1869
+ // Turn this off for the sake of efficiency.
1870
+ //ui.draggable.draggable('option', 'refreshPositions', false);
1871
+ var field = ui.draggable.attr('data-wcdv-field');
1872
+
1873
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'));
1874
+ }
1875
+ });
1876
+
1877
+ self.ui.root = jQuery('<div>').appendTo(parent);
1878
+ self.ui.title = jQuery('<div>')
1879
+ .addClass('wcdv_control_title_bar')
1880
+ .appendTo(self.ui.root);
1881
+ jQuery('<span>', { 'class': 'wcdv_control_title' })
1882
+ .text(trans('GRID_CONTROL.FILTER.TITLE'))
1883
+ .appendTo(self.ui.title);
1884
+ self.ui.clearBtn = self.makeClearButton(self.ui.title);
1885
+ self.ui.fields = jQuery('<ul>', {
1886
+ id: gensym(),
1887
+ 'class': self.isHorizontal ? 'wcdv_control_horizontal' : 'wcdv_control_vertical'
1888
+ }).appendTo(self.ui.root);
1889
+
1890
+ var dropdownContainer = jQuery('<div>').appendTo(self.ui.root);
1891
+ self.ui.dropdown = jQuery('<select>', { 'class': 'wcdv_control_addField' }).appendTo(dropdownContainer);
1892
+ self.ui.dropdown.on('change', function () {
1893
+ self.addField(self.ui.dropdown.val(), self.ui.dropdown.find('option:selected').text());
1894
+ });
1895
+
1896
+ self.addViewConfigChangeHandler('filterSet', function () {
1897
+ var spec = self.view.getFilter();
1898
+ self.logDebug(self.makeLogTag() + 'View set filter to: %s', self.grid.toString(), JSON.stringify(spec));
1899
+ self.clear({ updateView: false });
1900
+ _.each(spec, function (fieldSpec, field) {
1901
+ self.addField(field, getProp(self.colConfig.get(field), 'displayText'), { updateView: false });
1902
+ self.gfs.set(field, fieldSpec, { updateView: false });
1903
+ });
1904
+ });
1905
+
1906
+ return self.ui.root;
1907
+ };
1908
+
1909
+ // #addField {{{2
1910
+
1911
+ FilterControl.prototype.addField = function (field, displayText, opts) {
1912
+ var self = this;
1913
+
1914
+ self.super['GridControl'].addField(field, displayText || getProp(self.colConfig.get(field), 'displayText'), opts);
1915
+ };
1916
+
1917
+ // #removeField {{{2
1918
+
1919
+ FilterControl.prototype.removeField = function (cf) {
1920
+ var self = this;
1921
+
1922
+ self.gfs.removeField(cf.field.field);
1923
+ self.super['GridControl'].removeField(cf);
1924
+ };
1925
+
1926
+ // #clear {{{2
1927
+
1928
+ FilterControl.prototype.clear = function (opts) {
1929
+ var self = this;
1930
+
1931
+ self.gfs.reset(opts);
1932
+ self.super['GridControl'].clear(opts);
1933
+ };
1934
+
1935
+ // #updateView {{{2
1936
+
1937
+ FilterControl.prototype.updateView = function () {
1938
+ // NOTE This function intentionally does nothing!
1939
+ // It overrides the behavior of the superclass' method.
1940
+ };
1941
+
1942
+ // #toString {{{2
1943
+
1944
+ FilterControl.prototype.toString = function () {
1945
+ var self = this;
1946
+
1947
+ return self.grid.id + ', Filter';
1948
+ };
1949
+
1950
+ // Exports {{{1
1951
+
1952
+ export {
1953
+ FilterControl,
1954
+ GroupControl,
1955
+ PivotControl,
1956
+ AggregateControl,
1957
+ };