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,1073 @@
1
+ import _ from 'underscore';
2
+ import moment from 'moment';
3
+
4
+ import jQuery from 'jquery';
5
+
6
+ import {
7
+ deepDefaults,
8
+ gensym,
9
+ getPropDef,
10
+ makeSubclass,
11
+ mixinEventHandling,
12
+ mixinLogging,
13
+ } from './util/misc.js';
14
+
15
+ import GridFilter from './ui/grid_filter.js';
16
+ import DateFilter from './ui/filters/date.js';
17
+
18
+ // Filters {{{1
19
+ // StringTextboxGridFilter {{{2
20
+
21
+ /**
22
+ * Represents a filter on a single string.
23
+ *
24
+ * @class
25
+ * @extends GridFilter
26
+ */
27
+
28
+ var StringTextboxGridFilter = makeSubclass('StringTextboxGridFilter', GridFilter, function () {
29
+ var self = this;
30
+
31
+ self.super['GridFilter'].ctor.apply(self, arguments);
32
+
33
+ self.input = jQuery('<input type="text">');
34
+ self.input.on('change', function (evt) {
35
+ self.gridFilterSet.update(false);
36
+ });
37
+
38
+ self.operatorDrop = self.makeOperatorDrop(/*['$eq', '$ne']*/);
39
+
40
+ /*
41
+ self.strictChkbox = jQuery('<input>', {id: gensym(), type: 'checkbox'})
42
+ .on('change', function () {
43
+ self.gridFilterSet.update();
44
+ });
45
+ */
46
+
47
+ self.div
48
+ .append(self.operatorDrop)
49
+ .append(self.input);
50
+
51
+ if (self.removeBtn) {
52
+ self.div.append(self.removeBtn);
53
+ }
54
+ });
55
+
56
+ // StringDropdownGridFilterChosen {{{2
57
+
58
+ /**
59
+ * Represents a filter for multiple strings.
60
+ *
61
+ * @class
62
+ * @extends GridFilter
63
+ */
64
+
65
+ var StringDropdownGridFilterChosen = makeSubclass('StringDropdownGridFilterChosen', GridFilter, function () {
66
+ var self = this;
67
+
68
+ self.super['GridFilter'].ctor.apply(self, arguments);
69
+
70
+ self.limit = 1;
71
+ self.input = jQuery('<select>').attr({
72
+ 'multiple': true
73
+ });
74
+ self.input.on('change', function (evt) {
75
+ self.gridFilterSet.update(false);
76
+ });
77
+
78
+ self.div
79
+ .append(self.input);
80
+
81
+ if (self.removeBtn) {
82
+ self.div.append(self.removeBtn);
83
+ }
84
+
85
+ self.afterAdd = function (target) {
86
+ self.gridFilterSet.view.getUniqueVals(function (uniqueVals) {
87
+ _.each(getPropDef([], uniqueVals, self.field, 'values'), function (val) {
88
+ jQuery('<option>').attr({
89
+ 'value': val
90
+ }).text(val).appendTo(self.input);
91
+ });
92
+ self.input.chosen({'width': self.div.innerWidth() - self.removeBtn.outerWidth()});
93
+ self.chosen = self.input.next('div.chosen-container');
94
+ });
95
+ };
96
+
97
+ if (self.gridFilterSet.gridTable) {
98
+ self.gridFilterSet.gridTable.on('columnResize', function () {
99
+ var targetWidth = self.opts.sizingElement.innerWidth() - self.removeBtn.outerWidth() - 14;
100
+ self.logDebug(self.makeLogTag() + ' Adjusting Chosen widget width to ' + targetWidth + 'px to match column width');
101
+ self.chosen.innerWidth(targetWidth);
102
+ });
103
+ }
104
+ });
105
+
106
+ // #getOperator {{{3
107
+
108
+ StringDropdownGridFilterChosen.prototype.getOperator = function () {
109
+ return '$in';
110
+ };
111
+
112
+ // #getValue {{{3
113
+
114
+ StringDropdownGridFilterChosen.prototype.getValue = function () {
115
+ var self = this
116
+ , val = self.super['GridFilter'].getValue(self);
117
+
118
+ return val === null ? undefined : val;
119
+ };
120
+
121
+ // StringDropdownGridFilterSumo {{{2
122
+
123
+ /**
124
+ * Represents a filter for multiple strings.
125
+ *
126
+ * @class
127
+ * @extends GridFilter
128
+ */
129
+
130
+ var StringDropdownGridFilterSumo = makeSubclass('StringDropdownGridFilterSumo', GridFilter, function () {
131
+ var self = this;
132
+
133
+ self.super['GridFilter'].ctor.apply(self, arguments);
134
+
135
+ self.limit = 1;
136
+ self.minDropdownWidth = 200;
137
+ self.input = jQuery('<select>').attr({
138
+ 'multiple': true
139
+ })
140
+ .on('change', function (evt) {
141
+ if (self.pleaseDontFireChangeEvent) {
142
+ delete self.pleaseDontFireChangeEvent;
143
+ return;
144
+ }
145
+ self.gridFilterSet.update(false);
146
+ });
147
+
148
+ self.operatorDrop = self.makeOperatorDrop(['$in', '$nin', '$exists', '$notexists']);
149
+
150
+ self.div.append(self.operatorDrop);
151
+ self.div.append(self.input);
152
+
153
+ if (self.removeBtn) {
154
+ self.div.append(self.removeBtn);
155
+ }
156
+
157
+ self.afterAdd = function (target) {
158
+ self.gridFilterSet.view.getUniqueVals(function (uniqueVals) {
159
+ _.each(getPropDef([], uniqueVals, self.field, 'values'), function (val) {
160
+ var elt = jQuery('<option>').attr({
161
+ 'value': val
162
+ });
163
+ var fcc = self.gridFilterSet.view.colConfig.get(self.field) || {};
164
+ elt[fcc.allowHtml ? 'html' : 'text'](val === '' ? '[blank]' : val);
165
+ elt.appendTo(self.input);
166
+ });
167
+ self.input.SumoSelect({
168
+ triggerChangeCombined: true,
169
+ selectAll: true,
170
+ search: true,
171
+ okCancelInMulti: true,
172
+ isClickAwayOk: true
173
+ });
174
+ self.optWrapper = self.input.closest('div.SumoSelect').find('div.optWrapper');
175
+ /*
176
+ optWrapper.resizable({
177
+ helper: 'ui-resizable-helper'
178
+ });
179
+ */
180
+ //self.adjustInputWidth();
181
+ });
182
+ };
183
+ });
184
+
185
+ // #adjustInputWidth {{{3
186
+
187
+ StringDropdownGridFilterSumo.prototype.adjustInputWidth = function (opts) {
188
+ var self = this;
189
+
190
+ if (opts === undefined) {
191
+ opts = {};
192
+ }
193
+
194
+ opts.input = self.input.closest('div.SumoSelect');
195
+ opts.callback = function (width) {
196
+ self.optWrapper.outerWidth(Math.max(width, self.minDropdownWidth));
197
+ };
198
+
199
+ self.super['GridFilter'].adjustInputWidth(opts);
200
+ };
201
+
202
+ // #getValue {{{3
203
+
204
+ StringDropdownGridFilterSumo.prototype.getValue = function () {
205
+ var self = this
206
+ , val = self.super['GridFilter'].getValue();
207
+
208
+ return val === null ? undefined : val;
209
+ };
210
+
211
+ // #setValue {{{3
212
+
213
+ StringDropdownGridFilterSumo.prototype.setValue = function (val) {
214
+ var self = this;
215
+
216
+ if (!_.isArray(val)) {
217
+ val = [val];
218
+ }
219
+ else {
220
+ val = _.flatten(val);
221
+ }
222
+
223
+ _.each(val, function (v) {
224
+ if (typeof v === 'string') {
225
+ self.pleaseDontFireChangeEvent = true;
226
+ self.input.get(0).sumo.selectItem(v);
227
+ }
228
+ });
229
+ };
230
+
231
+ // #setOperator {{{3
232
+
233
+ StringDropdownGridFilterSumo.prototype.setOperator = function (op) {
234
+ var self = this;
235
+
236
+ if (op === '$eq') {
237
+ op = '$in';
238
+ }
239
+
240
+ return self.super['GridFilter'].setOperator(op);
241
+ };
242
+
243
+ // #showInput {{{3
244
+
245
+ StringDropdownGridFilterSumo.prototype.showInput = function (op) {
246
+ var self = this;
247
+
248
+ self.input.closest('div.SumoSelect').show();
249
+ };
250
+
251
+ // #hideInput {{{3
252
+
253
+ StringDropdownGridFilterSumo.prototype.hideInput = function (op) {
254
+ var self = this;
255
+
256
+ self.input.closest('div.SumoSelect').hide();
257
+ };
258
+
259
+ // NumberTextboxGridFilter {{{2
260
+
261
+ var NumberTextboxGridFilter = makeSubclass('NumberTextboxGridFilter', GridFilter, function () {
262
+ var self = this;
263
+
264
+ self.super['GridFilter'].ctor.apply(self, arguments);
265
+
266
+ self.input = jQuery('<input>', {
267
+ 'type': 'text'
268
+ });
269
+ self.input.attr('size', '10');
270
+ self.input.on('change', function (evt) {
271
+ self.gridFilterSet.update(false);
272
+ });
273
+
274
+ self.operatorDrop = self.makeOperatorDrop(['$eq', '$ne', '$lt', '$lte', '$gt', '$gte', '$exists', '$notexists']);
275
+
276
+ self.div.append(self.operatorDrop);
277
+ self.div.append(self.input);
278
+
279
+ if (self.removeBtn) {
280
+ self.div.append(self.removeBtn);
281
+ }
282
+ });
283
+
284
+ // NumberCheckboxGridFilter {{{2
285
+
286
+ var NumberCheckboxGridFilter = makeSubclass('NumberCheckboxGridFilter', GridFilter, function () {
287
+ var self = this;
288
+
289
+ self.super['GridFilter'].ctor.apply(self, arguments);
290
+
291
+ self.input = jQuery('<input>', {'id': gensym(), 'type': 'checkbox'});
292
+ self.input.on('change', function () {
293
+ self.gridFilterSet.update(false);
294
+ });
295
+
296
+ self.div
297
+ .append(jQuery('<label>')
298
+ .append(self.input)
299
+ .append(' Filter'));
300
+
301
+ if (self.removeBtn) {
302
+ self.div.append(self.removeBtn);
303
+ }
304
+
305
+ //self.applyImmediately = true;
306
+ self.limit = 1;
307
+ });
308
+
309
+ // #getValue {{{3
310
+
311
+ NumberCheckboxGridFilter.prototype.getValue = function () {
312
+ return this.input[0].checked ? 1 : 0;
313
+ };
314
+
315
+ // #getOperator {{{3
316
+
317
+ NumberCheckboxGridFilter.prototype.getOperator = function () {
318
+ return '$eq';
319
+ };
320
+
321
+ // NumberTriBoolGridFilter {{{2
322
+
323
+ var NumberTriBoolGridFilter = makeSubclass('NumberTriBoolGridFilter', GridFilter, function () {
324
+ var self = this;
325
+
326
+ self.super['GridFilter'].ctor.apply(self, arguments);
327
+
328
+ self.inputName = gensym();
329
+
330
+ var trueRadio = jQuery('<input>', {'type': 'radio', 'name': self.inputName, 'value': 'true'});
331
+ var falseRadio = jQuery('<input>', {'type': 'radio', 'name': self.inputName, 'value': 'false'});
332
+ var bothRadio = jQuery('<input>', {'type': 'radio', 'name': self.inputName, 'value': 'both'});
333
+
334
+ self.inputs = jQuery([trueRadio.get(0), falseRadio.get(0), bothRadio.get(0)]);
335
+
336
+ self.inputs.css('margin-right', '0.4em');
337
+
338
+ self.inputs.each(function (i, elt) {
339
+ elt = jQuery(elt);
340
+
341
+ elt.on('change', function (evt) {
342
+ self.gridFilterSet.update(false);
343
+ });
344
+ });
345
+
346
+ self.div
347
+ .append(jQuery('<label>')
348
+ .append(trueRadio)
349
+ .append('True'))
350
+ .append(jQuery('<label>')
351
+ .css('padding-left', '0.8em')
352
+ .append(falseRadio)
353
+ .append('False'))
354
+ .append(jQuery('<label>')
355
+ .css('padding-left', '0.8em')
356
+ .append(bothRadio)
357
+ .append('Both'))
358
+ ;
359
+
360
+ if (self.removeBtn) {
361
+ self.div.append(self.removeBtn);
362
+ }
363
+
364
+ //self.applyImmediately = true;
365
+ self.limit = 1;
366
+ });
367
+
368
+ // #getValue {{{3
369
+
370
+ NumberTriBoolGridFilter.prototype.getValue = function () {
371
+ var self = this;
372
+
373
+ var val = self.inputs.filter(':checked').val();
374
+
375
+ switch (val) {
376
+ case 'true':
377
+ return 1;
378
+ case 'false':
379
+ return 0;
380
+ case 'both':
381
+ return undefined;
382
+ default:
383
+ throw new Error('Impossible');
384
+ }
385
+ };
386
+
387
+ // #setValue {{{3
388
+
389
+ NumberTriBoolGridFilter.prototype.setValue = function (val) {
390
+ var self = this;
391
+
392
+ var internalVal;
393
+
394
+ switch (val) {
395
+ case 0:
396
+ internalVal = 'false';
397
+ break;
398
+ case 1:
399
+ internalVal = 'true';
400
+ break;
401
+ default:
402
+ internalVal = 'both';
403
+ }
404
+
405
+ self.inputs.filter('[value="' + internalVal + '"]').prop('checked', true);
406
+ };
407
+
408
+ // #getOperator {{{3
409
+
410
+ NumberTriBoolGridFilter.prototype.getOperator = function () {
411
+ return '$eq';
412
+ };
413
+
414
+ // DateSingleGridFilter {{{2
415
+
416
+ /**
417
+ * Represents a filter for a single date.
418
+ *
419
+ * @class
420
+ * @extends GridFilter
421
+ */
422
+
423
+ var DateSingleGridFilter = makeSubclass('DateSingleGridFilter', GridFilter, function () {
424
+ var self = this;
425
+
426
+ self.super['GridFilter'].ctor.apply(self, arguments);
427
+
428
+ self.input = jQuery('<input>').attr({
429
+ 'type': 'text',
430
+ 'placeholder': 'Select date...'
431
+ });
432
+
433
+ self.input.flatpickr({
434
+ 'altInput': false,
435
+ 'allowInput': true,
436
+ 'onChange': function (selectedDates, dateStr, instance) {
437
+ console.log(selectedDates, dateStr);
438
+ //self.gridFilterSet.update();
439
+ }
440
+ });
441
+
442
+ self.div
443
+ .append(self.input);
444
+
445
+ if (self.removeBtn) {
446
+ self.div.append(self.removeBtn);
447
+ }
448
+ });
449
+
450
+ // DateRangeGridFilter {{{2
451
+
452
+ /**
453
+ * Represents a filter for a range of dates.
454
+ *
455
+ * @class
456
+ * @extends GridFilter
457
+ */
458
+
459
+ var DateRangeGridFilter = makeSubclass('DateRangeGridFilter', GridFilter, function () {
460
+ var self = this;
461
+
462
+ self.super['GridFilter'].ctor.apply(self, arguments);
463
+
464
+ self.limit = 1;
465
+
466
+ self.input = jQuery('<input>').attr({
467
+ 'type': 'text',
468
+ 'placeholder': 'Click here; pick start/end dates.',
469
+ 'size': 28
470
+ });
471
+
472
+ self.widget = self.input.flatpickr({
473
+ 'altInput': false,
474
+ 'allowInput': true,
475
+ 'mode': 'range',
476
+ 'onChange': function (selectedDates, dateStr, instance) {
477
+ self.selectedDates = selectedDates;
478
+ self.gridFilterSet.update(false);
479
+ }
480
+ });
481
+
482
+ self.div
483
+ .append(self.input);
484
+
485
+ if (self.removeBtn) {
486
+ self.div.append(self.removeBtn);
487
+ }
488
+ });
489
+
490
+ // #getValue {{{3
491
+
492
+ /**
493
+ * Get the value(s) for this date range filter. After you bring up the calendar, when you select
494
+ * the start date, the "onChange" event handler is run. When you select the end date, the event is
495
+ * fired again. So, we use #isRange() to decide if you've only selected one date, or if you've just
496
+ * picked the second. When it's a range, we need to produce an object, instead of a simple value.
497
+ *
498
+ * @returns {GridFilter~Value|GridFilter~RangeValue} The value that should be used for filtering all
499
+ * the data in the grid.
500
+ */
501
+
502
+ DateRangeGridFilter.prototype.getValue = function () {
503
+ var self = this
504
+ , result;
505
+
506
+ if (self.selectedDates == null) {
507
+ return undefined;
508
+ }
509
+
510
+ if (self.isRange()) {
511
+ result = {
512
+ 'start': moment(self.selectedDates[0]),
513
+ 'end': moment(self.selectedDates[1]).hour(23).minute(59).second(59)
514
+ };
515
+
516
+ if (self.typeInfo.internalType === 'string') {
517
+ if (self.typeInfo.type === 'date') {
518
+ result = _.mapObject(result, function (m) {
519
+ return m.format('YYYY-MM-DD');
520
+ });
521
+ }
522
+ else if (self.typeInfo.type === 'datetime') {
523
+ result = _.mapObject(result, function (m) {
524
+ return m.format('YYYY-MM-DD HH:mm:ss');
525
+ });
526
+ }
527
+ }
528
+ }
529
+ else {
530
+ result = moment(self.selectedDates[0]);
531
+
532
+ if (self.typeInfo.internalType === 'string') {
533
+ if (self.typeInfo.type === 'date') {
534
+ result = result.format('YYYY-MM-DD');
535
+ }
536
+ else if (self.typeInfo.type === 'datetime') {
537
+ result = result.format('YYYY-MM-DD HH:mm:ss');
538
+ }
539
+ }
540
+ }
541
+
542
+ return result;
543
+ };
544
+
545
+ // #setValue {{{3
546
+
547
+ DateRangeGridFilter.prototype.setValue = function (val) {
548
+ var self = this;
549
+
550
+ self.selectedDates = val;
551
+ self.widget.setDate(val);
552
+ };
553
+
554
+ // #getOperator {{{3
555
+
556
+ DateRangeGridFilter.prototype.getOperator = function () {
557
+ var self = this;
558
+
559
+ if (self.isRange()) {
560
+ return ['$gte', '$lte'];
561
+ }
562
+
563
+ return '$gte';
564
+ };
565
+
566
+ // #isRange {{{3
567
+
568
+ DateRangeGridFilter.prototype.isRange = function () {
569
+ var self = this;
570
+
571
+ return self.selectedDates != null && self.selectedDates.length > 1;
572
+ };
573
+
574
+ // BooleanCheckboxGridFilter {{{2
575
+
576
+ var BooleanCheckboxGridFilter = makeSubclass('BooleanCheckboxGridFilter', GridFilter, function (field, gridFilter) {
577
+ });
578
+
579
+ // #getValue {{{3
580
+
581
+ BooleanCheckboxGridFilter.prototype.getValue = function () {
582
+ return this.input.val();
583
+ };
584
+
585
+ // #getOperator {{{3
586
+
587
+ BooleanCheckboxGridFilter.prototype.getOperator = function () {
588
+ return '$eq';
589
+ };
590
+
591
+ // #getOperator {{{3
592
+
593
+ BooleanCheckboxGridFilter.prototype.getId = function () {
594
+ return this.input.attr('id');
595
+ };
596
+
597
+ // Widget Map {{{2
598
+
599
+ // Type -> Filter Widget -> Constructor
600
+
601
+ GridFilter.widgets = {
602
+ 'string': {
603
+ 'textbox': StringTextboxGridFilter,
604
+ 'dropdown': StringDropdownGridFilterSumo,
605
+ },
606
+ 'number': {
607
+ 'textbox': NumberTextboxGridFilter,
608
+ 'checkbox': NumberCheckboxGridFilter,
609
+ 'tribool': NumberTriBoolGridFilter,
610
+ },
611
+ 'currency': {
612
+ 'textbox': NumberTextboxGridFilter,
613
+ },
614
+ 'date': {
615
+ 'single': DateSingleGridFilter,
616
+ 'range': DateRangeGridFilter,
617
+ 'neon': DateFilter,
618
+ },
619
+ 'datetime': {
620
+ 'single': DateSingleGridFilter,
621
+ 'range': DateRangeGridFilter,
622
+ 'neon': DateFilter,
623
+ }
624
+ };
625
+
626
+ GridFilter.defaultWidgets = {
627
+ 'string': 'dropdown',
628
+ 'number': 'textbox',
629
+ 'currency': 'textbox',
630
+ 'date': 'neon',
631
+ 'datetime': 'neon'
632
+ };
633
+
634
+ // GridFilterSet {{{1
635
+ // Constructor {{{2
636
+
637
+ /**
638
+ * Create a new collection of filters.
639
+ *
640
+ * @param {View} view
641
+ * The view that we will be updating the filter for.
642
+ *
643
+ * @param {Prefs} prefs
644
+ *
645
+ *
646
+ * @param {GridTable} gridTable A reference to the table that this filter set is displayed on. This
647
+ * is used only to make sure that the widgets shown in the columns are resized correctly when the
648
+ * table's columns change width.
649
+ *
650
+ * @param {object} progress An object describing how to show a progress dialog when the view is
651
+ * updated.
652
+ *
653
+ * @class
654
+ * @property {View} view
655
+ * The view that we will be updating the filter for.
656
+ *
657
+ * @property {Prefs} prefs
658
+ *
659
+ * @property {object} progress An object describing how to show a progress dialog when the view is
660
+ * updated.
661
+ *
662
+ * @property {Element} thead
663
+ *
664
+ * @property {Object} filters Stores the filters that are within this set, with different properties
665
+ * to facilitate different lookup methods.
666
+ *
667
+ * @property {Array} filters.all An array of all the filters.
668
+ *
669
+ * @property {Object} filters.byId An object indexing all the filters by its internal ID.
670
+ *
671
+ * @property {Object.<Array>} filters.byCol An object indexing all the filters by the column that
672
+ * they're filtering.
673
+ *
674
+ * @property {boolean} delayUpdate If true, calls to the update() method do nothing. This is used
675
+ * internally when loading preferences to avoid updating for every single filter.
676
+ */
677
+
678
+ var GridFilterSet = makeSubclass('GridFilterSet', Object, function (view, prefs, gridTable, progress, opts) {
679
+ var self = this;
680
+
681
+ self.view = view;
682
+ self.prefs = prefs;
683
+ self.gridTable = gridTable;
684
+ self.progress = progress;
685
+ self.opts = deepDefaults(opts, {
686
+ sendEvent: true,
687
+ dontSendEventTo: [],
688
+ updateData: true
689
+ });
690
+
691
+ self.filters = {
692
+ all: [],
693
+ byId: {},
694
+ byCol: {}
695
+ };
696
+
697
+ self.delayUpdate = false;
698
+ });
699
+
700
+ mixinLogging(GridFilterSet);
701
+
702
+ // Events {{{2
703
+
704
+ /**
705
+ * Fired when a filter has been added.
706
+ *
707
+ * @event GridFilterSet#filterAdded
708
+ */
709
+
710
+ /**
711
+ * Fired when a filter has been removed.
712
+ *
713
+ * @event GridFilterSet#filterRemoved
714
+ */
715
+
716
+ mixinEventHandling(GridFilterSet, [
717
+ 'filterAdded'
718
+ , 'filterRemoved'
719
+ , 'widgetResizedHoriz'
720
+ , 'widgetResizedVert'
721
+ ]);
722
+
723
+ // #add {{{2
724
+
725
+ /**
726
+ * Add a new filter to this set. This creates the user interface elements and places them in the
727
+ * appropriate place in the grid.
728
+ *
729
+ * @param {string} field Name of the column to filter on.
730
+ *
731
+ * @param {Element} target Where to place the filter widget.
732
+ *
733
+ * @param {string} [filterType] The developer's requested filter type. If missing, we use the first
734
+ * one from the "allowed" list. If present, and not in the allowed list, you'll get an error.
735
+ *
736
+ * @param {Element} filterBtn The "add filter" button from the column header. Needed so we can hide
737
+ * it, if we've reached the maximum number of filters allowed on the column.
738
+ */
739
+
740
+ GridFilterSet.prototype.add = function (field, target, opts) {
741
+ var self = this
742
+ , filter;
743
+
744
+ opts = opts || {};
745
+ filter = self.build(field, target, opts);
746
+
747
+ if (filter == null) {
748
+ return null;
749
+ }
750
+
751
+ // Make sure that requisite data structures are there.
752
+
753
+ if (self.filters.byCol[field] === undefined) {
754
+ self.filters.byCol[field] = [];
755
+ }
756
+
757
+ // Add the filter to all of our data structures.
758
+
759
+ self.filters.all.push(filter);
760
+ self.filters.byCol[field].push(filter);
761
+ self.filters.byId[filter.getId()] = filter;
762
+
763
+ // Add the filter to the user interface.
764
+
765
+ target.append(filter.div);
766
+
767
+ if (typeof filter.afterAdd === 'function') {
768
+ filter.afterAdd(target);
769
+ }
770
+
771
+ filter.adjustInputWidth();
772
+
773
+ // Hide the "add filter" button if we've reached the limit of the number of filters we're allowed
774
+ // to have for this column.
775
+
776
+ if (opts.filterBtn && self.filters.byCol[field].length === filter.limit) {
777
+ opts.filterBtn.hide();
778
+ }
779
+
780
+ self.fire(GridFilterSet.events.filterAdded);
781
+
782
+ // Check to see if this filter should take effect as soon as it is created.
783
+
784
+ if (filter.applyImmediately) {
785
+ self.update();
786
+ }
787
+
788
+ return filter;
789
+ };
790
+
791
+ // #build {{{2
792
+
793
+ /**
794
+ * Create a new GridFilter instance.
795
+ *
796
+ * @param {string} field
797
+ * Name of the field to apply the filter to. Passed to the View.
798
+ *
799
+ * @param {string} filterType
800
+ * What type of widget to use for the filter (e.g. dropdown, text box, checkbox).
801
+ *
802
+ * @param {Element} filterBtn
803
+ * Button to add a new filter item. Might be shown/hidden depending on how many items are allowed
804
+ * (e.g. a multi-select dropdown only allows one "item" as that's all you need).
805
+ *
806
+ * @param {Element} target
807
+ * Where the filter should be placed.
808
+ *
809
+ * @param {function} onRemove
810
+ * Function to call when the filter is removed.
811
+ *
812
+ * @param {Element} [sizingElement]
813
+ * If present, the element to use to calculate the width of the filter widget. When absent, the
814
+ * div which is placed within the `target` is used.
815
+ */
816
+
817
+ GridFilterSet.prototype.build = function (field, target, opts) {//filterType, filterBtn, target, onRemove, sizingElement, noRemoveBtn) {
818
+ var self = this;
819
+
820
+ // We use a data source to get the type information, so if the grid was built without a data
821
+ // source, this isn't going to work.
822
+ //
823
+ // FIXME Don't rely on the cache, do it right.
824
+
825
+ var fti = self.view.typeInfo.get(field);
826
+
827
+ if (fti == null) {
828
+ return null;
829
+ }
830
+
831
+ var colType = fti.type;
832
+
833
+ // Make sure that we are able to get the column type.
834
+
835
+ if (colType == null) {
836
+ self.logError(self.makeLogTag() + ' Unable to determine type of column "' + field + '"');
837
+ return null;
838
+ }
839
+
840
+ // Make sure that we know what kinds of filters are allowed for the column type.
841
+
842
+ if (GridFilter.widgets[colType] === undefined) {
843
+ self.logError(self.makeLogTag() + ' Unknown type "' + colType + '" for column "' + field + '"');
844
+ return null;
845
+ }
846
+
847
+ // When the user didn't request a filter type, just use the first one in the allowed list.
848
+ // Otherwise, make sure that the filter type they asked for makes sense for the column type.
849
+
850
+ var filterType = opts.filterType || GridFilter.defaultWidgets[colType];
851
+ var ctor = GridFilter.widgets[colType][filterType];
852
+
853
+ if (ctor === undefined) {
854
+ throw new Error('Invalid filter type "' + filterType + '" for type "' + colType + '" of column "' + field + '"');
855
+ }
856
+
857
+ self.logDebug(self.makeLogTag() + ' Creating new widget: column type = "' + colType + '" ; filter type = "' + filterType + '"');
858
+
859
+ return new ctor(field, self, fti, opts);
860
+ };
861
+
862
+ // #remove {{{2
863
+
864
+ /**
865
+ * Remove a filter.
866
+ *
867
+ * @param {string} id
868
+ * The unique ID of the filter to remove.
869
+ *
870
+ * @param {Element} filterBtn
871
+ * Button to add a new filter item. Might be shown/hidden depending on how many items are allowed
872
+ * (e.g. a multi-select dropdown only allows one "item" as that's all you need).
873
+ */
874
+
875
+ GridFilterSet.prototype.remove = function (id, filterBtn, noEvent) {
876
+ var self = this
877
+ , filter = self.filters.byId[id];
878
+
879
+ // Make sure that a filter with that ID exists.
880
+
881
+ if (filter === undefined) {
882
+ self.logWarning(self.makeLogTag() + ' Attempted to remove filter with ID "' + id + '" from the grid, but it doesn\'t exist');
883
+ return;
884
+ }
885
+
886
+ var sameId = function (elt) { return elt.getId() === id; };
887
+ var allIndex = _.findIndex(self.filters.all, sameId);
888
+ var colIndex = _.findIndex(self.filters.byCol[filter.field], sameId);
889
+
890
+ delete self.filters.byId[id];
891
+ self.filters.all.splice(allIndex, 1);
892
+ self.filters.byCol[filter.field].splice(colIndex, 1);
893
+
894
+ filter.remove();
895
+
896
+ // Show the "add filter" button if we're below the limit of the number of filters we're allowed to
897
+ // have for this column.
898
+
899
+ if (filterBtn && self.filters.byCol[filter.field].length < filter.limit) {
900
+ filterBtn.show();
901
+ }
902
+
903
+ if (!noEvent) {
904
+ self.fire(GridFilterSet.events.filterRemoved);
905
+ }
906
+ };
907
+
908
+ // #removeField {{{2
909
+
910
+ GridFilterSet.prototype.removeField = function (fieldName, filterBtn) {
911
+ var self = this;
912
+
913
+ _.each(self.filters.byCol[fieldName], function (filter) {
914
+ self.remove(filter.getId(), filterBtn, true);
915
+ });
916
+
917
+ self.fire(GridFilterSet.events.filterRemoved);
918
+ };
919
+
920
+ // #reset {{{2
921
+
922
+ /**
923
+ * Clear all filters.
924
+ */
925
+
926
+ GridFilterSet.prototype.reset = function (opts) {
927
+ var self = this;
928
+
929
+ opts = opts || {};
930
+ _.defaults(opts, {
931
+ updateView: true
932
+ });
933
+
934
+ self.delayUpdate = true;
935
+
936
+ // Remove every filter from the user interface.
937
+
938
+ _.each(self.filters.all, function (filter) {
939
+ filter.remove();
940
+ });
941
+
942
+ // Reset our internal data structures.
943
+
944
+ self.filters = {
945
+ all: [],
946
+ byId: {},
947
+ byCol: {}
948
+ };
949
+
950
+ if (opts.updateView) {
951
+ self.view.clearFilter();
952
+ }
953
+
954
+ self.delayUpdate = false;
955
+ };
956
+
957
+ // #update {{{2
958
+
959
+ /**
960
+ * Set the filters on the View based on what the user has entered into the user interface.
961
+ */
962
+
963
+ GridFilterSet.prototype.update = function () {
964
+ var self = this
965
+ , spec = {};
966
+
967
+ // Check for the "don't actually update" property, set when we're loading prefs to prevent any
968
+ // `applyImmediately` filters from causing unnecessary updates until we're done.
969
+
970
+ if (self.delayUpdate) {
971
+ return;
972
+ }
973
+
974
+ if (self.filters.all.length === 0) {
975
+ self.view.setFilter(null);
976
+ return;
977
+ }
978
+
979
+ _.each(self.filters.byCol, function (filterList, field) {
980
+ _.each(filterList, function (filter) {
981
+ var operator = filter.getOperator();
982
+ var value = filter.getValue();
983
+
984
+ if (value === undefined && ['$exists', '$notexists'].indexOf(operator) < 0) {
985
+ return;
986
+ }
987
+
988
+ if (spec[field] === undefined) {
989
+ spec[field] = {};
990
+ }
991
+
992
+ if (operator === '$exists') {
993
+ spec[field]['$exists'] = true;
994
+ }
995
+ else if (operator === '$notexists') {
996
+ spec[field]['$notexists'] = true;
997
+ }
998
+ else if (operator === '$bet') {
999
+ spec[field]['$gte'] = value[0];
1000
+ spec[field]['$lte'] = value[1];
1001
+ }
1002
+ else if (spec[field][operator] === undefined) {
1003
+ spec[field][operator] = value;
1004
+ }
1005
+ else if (_.isArray(spec[field][operator])) {
1006
+ spec[field][operator].push(value);
1007
+ }
1008
+ else if (['$eq', '$ne', '$contains'].indexOf(operator) >= 0) {
1009
+ spec[field][operator] = [spec[field][operator], value];
1010
+ }
1011
+ else {
1012
+ spec[field][operator] = value;
1013
+ }
1014
+ });
1015
+ });
1016
+
1017
+ self.logDebug(self.makeLogTag() + ' Updating with ' + self.filters.all.length + ' filters: ', spec);
1018
+
1019
+ self.view.setFilter(spec, self.progress, self.opts);
1020
+ };
1021
+
1022
+ // #set {{{2
1023
+
1024
+ GridFilterSet.prototype.set = function (field, fieldSpec, opts) {
1025
+ var self = this;
1026
+ opts = deepDefaults(opts, {
1027
+ updateView: true
1028
+ });
1029
+
1030
+ if (typeof fieldSpec !== 'object') {
1031
+ fieldSpec = { '$eq': fieldSpec };
1032
+ }
1033
+
1034
+ var filters = self.filters.byCol[field];
1035
+
1036
+ if (filters == null || filters.length == null || filters.length === 0) {
1037
+ // ERROR: No filter exists for that field.
1038
+ return;
1039
+ }
1040
+
1041
+ var widget = filters[0];
1042
+
1043
+ if (!opts.updateView) {
1044
+ self.delayUpdate = true;
1045
+ }
1046
+
1047
+ if (widget instanceof DateRangeGridFilter && '$lte' in fieldSpec && '$gte' in fieldSpec) {
1048
+ widget.setValue([fieldSpec['$gte'], fieldSpec['$lte']]);
1049
+ }
1050
+ else if (widget instanceof DateFilter && '$lte' in fieldSpec && '$gte' in fieldSpec) {
1051
+ widget.setOperator('$bet');
1052
+ widget.setValue(fieldSpec['$gte'], fieldSpec['$lte']);
1053
+ }
1054
+ else {
1055
+ _.each(fieldSpec, function (val, op) {
1056
+ self.logDebug(self.makeLogTag() + ' Setting filter: { field = %s ; operator = %s ; value = %s }',
1057
+ field, op, typeof val === 'object' ? JSON.stringify(val) : val);
1058
+
1059
+ widget.setOperator(op);
1060
+ widget.setValue(val);
1061
+ });
1062
+ }
1063
+
1064
+ if (!opts.updateView) {
1065
+ self.delayUpdate = false;
1066
+ }
1067
+ };
1068
+
1069
+ // Exports {{{1
1070
+
1071
+ export {
1072
+ GridFilterSet,
1073
+ };