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
package/src/grid.js ADDED
@@ -0,0 +1,2777 @@
1
+ // Imports {{{1
2
+
3
+ import _ from 'underscore';
4
+
5
+ import jQuery from 'jquery';
6
+
7
+ import {
8
+ deepCopy,
9
+ deepDefaults,
10
+ delegate,
11
+ icon,
12
+ getProp,
13
+ getPropDef,
14
+ I,
15
+ makeSubclass,
16
+ mixinEventHandling,
17
+ mixinLogging,
18
+ mixinNameSetting,
19
+ presentDownload,
20
+ setProp,
21
+ setPropDef,
22
+ Timing,
23
+ } from './util/misc.js';
24
+ import { OrdMap, Lock, Prefs, ComputedView, MirageView, FileSource } from 'datavis-ace';
25
+ import {
26
+ AggregateControl,
27
+ FilterControl,
28
+ GroupControl,
29
+ PivotControl,
30
+ } from './grid_control.js';
31
+ import './prefs_modules.js';
32
+ import { GridRenderer } from './grid_renderer.js';
33
+ import './renderers/grid/handlebars.js';
34
+ import './renderers/grid/squirrelly.js';
35
+ import './renderers/grid/table/plain.js';
36
+ import './renderers/grid/table/group_detail.js';
37
+ import './renderers/grid/table/group_summary.js';
38
+ import './renderers/grid/table/pivot.js';
39
+ import { ColConfigWin } from './ui/windows/col_config.js';
40
+ import { DebugWin } from './ui/windows/debug.js';
41
+ import { TemplatesEditor } from './ui/templates.js';
42
+ import { PopupWindow } from './ui/popup_window.js';
43
+ import {
44
+ ComputedViewToolbar,
45
+ PlainToolbar,
46
+ GroupToolbar,
47
+ PivotToolbar,
48
+ PrefsToolbar,
49
+ RendererToolbar,
50
+ } from './ui/toolbars/grid.js';
51
+ import { OperationsPalette } from './operations_palette.js';
52
+ import { trans } from './trans.js';
53
+
54
+ // Server-Side Filter/Sort {{{1
55
+
56
+ /*
57
+ * Here's the list of filter conditions supported by jQWidgets:
58
+ *
59
+ * - NULL
60
+ * - NOT_NULL
61
+ * - EQUAL
62
+ *
63
+ * These only apply to strings:
64
+ *
65
+ * - EMPTY
66
+ * - NOT_EMPTY
67
+ * - CONTAINS
68
+ * - CONTAINS_CASE_SENSITIVE
69
+ * - DOES_NOT_CONTAIN
70
+ * - DOES_NOT_CONTAIN_CASE_SENSITIVE
71
+ * - STARTS_WITH
72
+ * - STARTS_WITH_CASE_SENSITIVE
73
+ * - ENDS_WITH
74
+ * - ENDS_WITH_CASE_SENSITIVE
75
+ * - EQUAL_CASE_SENSITIVE
76
+ *
77
+ * These only apply to numbers and dates:
78
+ *
79
+ * - NOT_EQUAL
80
+ * - LESS_THAN
81
+ * - LESS_THAN_OR_EQUAL
82
+ * - GREATER_THAN
83
+ * - GREATER_THAN_OR_EQUAL
84
+ *
85
+ * I find it weird that strings can't be NOT_EQUAL, but I'm just going by what their documentation
86
+ * says they do.
87
+ */
88
+
89
+ function makeJsonHaving(filters) {
90
+ var having = {};
91
+ var numClauses = 0;
92
+ _.each(filters, function (f) {
93
+ var h = having[f.datafield] = {};
94
+ var numItems = 0;
95
+ _.each(f.filter.getfilters(), function (filter) {
96
+ var isSupported = true;
97
+ switch (filter.condition) {
98
+ case 'EQUAL':
99
+ case 'EQUAL_CASE_SENSITIVE':
100
+ if (h['$eq']) {
101
+ h['$in'] = [h['$eq']];
102
+ delete h['$eq'];
103
+ }
104
+ if (h['$in']) {
105
+ h['$in'].push(filter.value);
106
+ }
107
+ else {
108
+ h['$eq'] = filter.value;
109
+ }
110
+ break;
111
+ case 'CONTAINS':
112
+ case 'CONTAINS_CASE_SENSITIVE':
113
+ h['$like'] = '%' + filter.value + '%';
114
+ break;
115
+ case 'EMPTY':
116
+ h['$eq'] = '';
117
+ break;
118
+ case 'NOT_EMPTY':
119
+ h['$ne'] = '';
120
+ break;
121
+ default:
122
+ self.logError(self.makeLogTag() + ' Unsupported filter condition "' + filter.condition + '" for type "' + filter.type + '"');
123
+ isSupported = false;
124
+ }
125
+ if (isSupported) {
126
+ numItems += 1;
127
+ }
128
+ });
129
+ if (numItems > 0) {
130
+ numClauses += 1;
131
+ }
132
+ else {
133
+ delete having[f.datafield];
134
+ }
135
+ });
136
+ return numClauses > 0 ? having : null;
137
+ }
138
+
139
+ /**
140
+ * Make a JSON ORDER BY object based on sort information from jQWidgets.
141
+ *
142
+ * @param {object} o A description of the sort from a jqxGrid.
143
+ *
144
+ * @return {object} A description of the sort that can be used by the system report code.
145
+ */
146
+
147
+ function makeJsonOrderBy(o) {
148
+ if (o.sortcolumn === null) {
149
+ return null;
150
+ }
151
+ return [{
152
+ column: o.sortcolumn,
153
+ direction: o.sortdirection.ascending ? 'ASC' : 'DESC'
154
+ }];
155
+ }
156
+
157
+ // Grid {{{1
158
+ // JSDoc Types {{{2
159
+
160
+ /**
161
+ * @typedef {object} Grid~Defn
162
+ *
163
+ * @property {Object} table
164
+ *
165
+ * @property {string} table.id
166
+ *
167
+ * @property {Array.<Grid~ColConfig>} [table.columns]
168
+ * Specifies the order that fields are rendered in plain output. If not provided, all fields are
169
+ * rendered in the order received from the source; fields with names starting with an underscore are
170
+ * not shown. If provided, only those fields specified are rendered, and in the order indicated.
171
+ *
172
+ * @property {Grid~Features} [table.features]
173
+ * The features that are enabled for this grid.
174
+ *
175
+ * @property {object} [table.floatingHeader]
176
+ * Configuration for the "floating header" feature.
177
+ *
178
+ * @property {string} [table.floatingHeader.method]
179
+ * What library to use to create the floating table header. Must be one of the following:
180
+ *
181
+ * - `floatThead`
182
+ * - `fixedHeaderTable`
183
+ * - `tabletool`
184
+ *
185
+ * If this is not specified, the default is based on what library is available in the page, in the
186
+ * order listed above.
187
+ *
188
+ * @property {string} [table.groupMode]
189
+ * The starting mode for group output. Must be one of the following:
190
+ *
191
+ * - `summary`
192
+ * - `detail`
193
+ *
194
+ * The perspective will override this.
195
+ *
196
+ * @property {object} [table.incremental]
197
+ * Configuration for the "incremental" feature.
198
+ *
199
+ * @property {boolean} [table.incremental.appendBodyLast=false]
200
+ *
201
+ * @property {string} [table.incremental.method="setTimeout"]
202
+ * Must be one of the following:
203
+ *
204
+ * - `setTimeout`
205
+ * - `requestAnimationFrame`
206
+ *
207
+ * @property {number} [table.incremental.delay=10]
208
+ *
209
+ * @property {number} [table.incremental.chunkSize=100]
210
+ *
211
+ * @property {object} [table.limit]
212
+ * Configuration for the "limit" feature.
213
+ *
214
+ * @property {string} [table.limit.method="more"]
215
+ * How to limit the output. Must be one of the following:
216
+ *
217
+ * - `more` — Show a row at the bottom, which when clicked, loads more rows.
218
+ *
219
+ * @property {number} [table.limit.threshold=100]
220
+ * The total number of rows must exceed this in order to trigger using the limit method. If
221
+ * omitted, then the "limit" feature is effectively disabled.
222
+ *
223
+ * @property {number} [table.limit.chunkSize=50]
224
+ * When using the "more" limit method, how many additional rows to load each time.
225
+ *
226
+ * @property {object} [table.whenPlain]
227
+ * When the data has not been grouped, this is passed as the `opts` parameter to the GridRenderer
228
+ * constructor.
229
+ *
230
+ * @property {object} [table.whenGroup]
231
+ * When the data has been grouped, but not pivotted, this is passed as the `opts` parameter to the
232
+ * GridRenderer constructor.
233
+ *
234
+ * @property {object} [table.whenPivot]
235
+ * When the data has been pivotted, this is passed as the `opts` parameter to the GridRenderer
236
+ * constructor.
237
+ *
238
+ * @property {object} [table.activeRow]
239
+ * Configure the active row feature.
240
+ *
241
+ * @property {boolean} [table.activeRow.slider=true]
242
+ * If true, automatically deploy the slider when the active row is set.
243
+ *
244
+ * @property {function} [table.activeRow.callback]
245
+ * If set, a callback to invoke when the active row is changed. If the active row is set, the
246
+ * callback receives: (1) the active row ID, (2) the active row TR element. If the active row is
247
+ * cleared, the callback receives: (1) null.
248
+ */
249
+
250
+ /**
251
+ * @typedef {object} Grid~FieldColConfig
252
+ * Represents the column configuration for a single field.
253
+ *
254
+ * @property {string} field
255
+ * We're configuring the output of this field.
256
+ *
257
+ * @property {string} [displayText]
258
+ * What to show as the name of the column; the default is to show the field name.
259
+ *
260
+ * @property {string} [format]
261
+ * If the value is a number or currency: a Numeral format string used to render the value. If the
262
+ * value is a date, datetime, or time: a Moment format string used to render the value. Otherwise,
263
+ * this option is not used. The default format strings are:
264
+ *
265
+ * - number: [none]
266
+ * - currency: `$0,0.00` (e.g. "$1,000.23")
267
+ * - date: `LL` (e.g. "September 4, 1986")
268
+ * - datetime: `LLL` (e.g. "September 4, 1986 8:30 PM")
269
+ *
270
+ * @property {string} [format_dateOnly="LL"]
271
+ * When `hideMidnight = true` this is the Moment format string used to display just the date
272
+ * component of the datetime. Note that the time component is still present in the value when it is
273
+ * formatted, so don't reference the hours/minutes/seconds from the format string.
274
+ *
275
+ * @property {boolean} [hideMidnight=false]
276
+ * If the value is a datetime, and this value is true, then the time component is not rendered when
277
+ * it's midnight (00:00:00). If the value is not a datetime, this option is not used.
278
+ *
279
+ * @property {string} [cellAlignment]
280
+ * How to align the value within the cell horizontally. Possible values:
281
+ *
282
+ * - `left`
283
+ * - `center`
284
+ * - `right`
285
+ *
286
+ * The default depends on the type of the field. Strings, dates, datetimes, and times are
287
+ * left-aligned by default. Numbers and currencies are right-aligned by default.
288
+ *
289
+ * @property {boolean} [allowHtml=false]
290
+ * If true and the type of the field is a string, the value is interpreted as HTML and the resulting
291
+ * nodes are inserted into the table result. When exporting to CSV, the value emitted will be the
292
+ * text nodes only.
293
+ *
294
+ * @property {string} [maxHeight]
295
+ * If present, sets the maximum height allowed for the cell, and puts a "fullscreen" icon button in
296
+ * the top-right which will pop open a window showing the full value. Useful for extremely long
297
+ * pieces of data that would otherwise blow up the table. Only works in plain output.
298
+ */
299
+
300
+ /**
301
+ * @typedef {OrdMap.<string, Grid~FieldColConfig>} Grid~ColConfig
302
+ * A collection of configurations across all the available fields in the grid. If a field isn't in
303
+ * this object, then it might as well not exist.
304
+ */
305
+
306
+ /**
307
+ * @typedef {object} Grid~Features
308
+ *
309
+ * @property {boolean} [footer=false] If true, then a footer is shown at the bottom of the table.
310
+ * This is automatically enabled if `defn.table.footer` is provided.
311
+ *
312
+ * @property {boolean} [sort=false] If true, the user is allowed to sort the data by clicking the
313
+ * column header.
314
+ *
315
+ * @property {boolean} [filter=false] If true, the user is allowed to filter the data by clicking
316
+ * the "add filter" button in the column header.
317
+ *
318
+ * @property {boolean} [group=false] If true, the user is allowed to group the data.
319
+ *
320
+ * @property {boolean} [pivot=false] If true, the user is allowed to pivot the data.
321
+ *
322
+ * @property {boolean} [rowSelect=false] If true, the user is allowed to select rows by using the
323
+ * checkbox in the first column.
324
+ *
325
+ * @property {boolean} [rowReorder=false] If true, the user is allowed to manually reorder the rows
326
+ * using the handle in the last column.
327
+ *
328
+ * @property {boolean} [add=false] Unused
329
+ *
330
+ * @property {boolean} [edit=false] Unused
331
+ *
332
+ * @property {boolean} [delete=false] Unused
333
+ *
334
+ * @property {boolean} [limit=false] If true, then limit the amount of rows output by some method.
335
+ *
336
+ * @property {boolean} [tabletool=false] If true, then use TableTool to create a floating header for
337
+ * the table.
338
+ *
339
+ * @property {boolean} [blockUI=false] If true, use BlockUI to prevent interaction with the table
340
+ * while the ComputedView is doing something.
341
+ *
342
+ * @property {boolean} [nprogress=false] If true, use nprogress to show the progress of sort/filter
343
+ * operations that the ComputedView is performing.
344
+ *
345
+ * @property {boolean} [incremental=false] If true, render rows in the table incrementally, which
346
+ * prevents UI freezes while doing so. However, the overall time required to finish rendering the
347
+ * table goes way up.
348
+ *
349
+ * @property {boolean} [columnResize=false] If true, allow the user to resize columns by dragging
350
+ * the column border. Column widths are persisted to the column configuration.
351
+ *
352
+ * @property {boolean} [columnReorder=false] If true, allow the user to reorder columns by dragging
353
+ * column headers to new positions. Column order is persisted to the column configuration.
354
+ *
355
+ * @property {boolean} [activeRow=false]
356
+ * If true, then clicking a row in plain output makes the row "active." An active row is highlighted
357
+ * and causes other configurable behavior to occur. By default, the slider appears on the right side
358
+ * of the page to show information about the active row.
359
+ *
360
+ * @property {boolean} [omnifilter=true]
361
+ * If true, a text input is shown above the titlebar that filters visible table rows in plain
362
+ * output. Typing into the omnifilter hides all rows whose cell values do not contain the search
363
+ * text. This is a visual-only filter and does not affect the underlying data in the view. The
364
+ * omnifilter is automatically hidden when the output is grouped or pivoted.
365
+ *
366
+ * @property {boolean} [pagination=false]
367
+ * If true, rows in plain output are divided into pages of 40 rows each. All rows are rendered
368
+ * into the DOM but only the rows belonging to the current page are visible. Navigation controls
369
+ * at the bottom of the table allow the user to switch pages. Because every row already exists
370
+ * in the DOM, page changes are near-instant (just show/hide TR elements).
371
+ */
372
+
373
+ /**
374
+ * @typedef {object} Grid~Opts
375
+ * Various options for the grid.
376
+ *
377
+ * @param {string} [name]
378
+ * The name of the grid, used for logging and debugging. If not provided, one will be generated,
379
+ * but you won't like it.
380
+ *
381
+ * @param {boolean} [opts.runImmediately=true]
382
+ * If true, then show the grid immediately.
383
+ *
384
+ * @param {boolean} [opts.showOnDataChange=true]
385
+ * Whether or not to show the grid automatically when the view reports there's new data available.
386
+ * Useful when using push-oriented data flow, causing view updates to cascade to multiple outputs.
387
+ *
388
+ * @param {number} [opts.height]
389
+ * If present, sets the height of the grid.
390
+ *
391
+ * @param {string} [opts.title]
392
+ * If present, create a title bar for the grid.
393
+ *
394
+ * @param {string} [opts.helpText]
395
+ * If present, create a help bubble with this text.
396
+ *
397
+ * @param {boolean} [opts.showToolbar=true]
398
+ * Whether or not to show the toolbar by default.
399
+ *
400
+ * @param {boolean} [opts.showControls=false]
401
+ * Whether or not to show the controls by default.
402
+ */
403
+
404
+ // Constructor {{{2
405
+
406
+ /**
407
+ * Create a new Grid and place it somewhere in the page. A Grid consists of two major parts: the
408
+ * decoration (e.g. titlebar and toolbar), and the underlying grid (e.g. jQWidgets or Tablesaw).
409
+ *
410
+ * @param {string} id The ID of a DIV (which must already exist in the page) where we will put the
411
+ * grid and its decoration. This DIV is also known as the "tag container" because it's typically
412
+ * created by the <WCGRID> layout tag.
413
+ *
414
+ * @param {Grid~Defn} defn The definition of the grid itself.
415
+ *
416
+ * @param {Grid~Opts} [opts] Configuration of the decoration of the grid.
417
+ *
418
+ * @param {function} cb A function that will be called after the grid has finished rendering, with
419
+ * the underlying output method grid object (e.g. the jqxGrid instance) being passed.
420
+ *
421
+ * @class
422
+ *
423
+ * @property {string} id The ID of the div that contains the whole tag output.
424
+ * @property {Grid~Defn} defn The definition object used to create the grid.
425
+ * @property {Grid~Opts} opts Options for the grid's container.
426
+ * @property {object} grid The underlying grid object (e.g. a jqxGrid instance).
427
+ * @property {object} ui Contains various user interface components which are tracked for convenience.
428
+ * @property {Grid~Features} features
429
+ * @property {Timing} timing
430
+ *
431
+ * @property {boolean} rootHasFixedHeight
432
+ * If true, then the root DIV element has a fixed height (e.g. "600px") and the grid must fit within
433
+ * that size. Basically, this controls the "overflow" CSS property of the grid table, and also the
434
+ * scroll handler for when a grid table automatically shows more rows.
435
+ *
436
+ * @property {boolean} _isIdle
437
+ * If true, then the grid currently has no pending operations that would require the UI to change.
438
+ *
439
+ * @property {Grid~ColConfig} colConfig
440
+ *
441
+ * @property {string} colConfigSource
442
+ * Where the column configuration came from, recognized values are: `defn`, `typeinfo`.
443
+ *
444
+ * @property {boolean} colConfigRestricted
445
+ * If true, then the available columns in column configuration are restricted and cannot be added to
446
+ * via the source or user preferences. In other words, the set of available columns is restricted
447
+ * to the subset specified via the grid definition.
448
+ *
449
+ * @borrows GridTable#getSelection
450
+ * @borrows GridTable#setSelection
451
+ * @borrows GridTable#select
452
+ * @borrows GridTable#unselect
453
+ * @borrows GridTable#isSelected
454
+ */
455
+
456
+ var Grid = makeSubclass('Grid', Object, function (defn, opts, cb) {
457
+ var self = this;
458
+
459
+ opts = deepDefaults(opts, {
460
+ runImmediately: true,
461
+ showOnDataChange: true,
462
+ showToolbar: true,
463
+ showControls: false,
464
+ });
465
+
466
+ self.setName(opts.name);
467
+
468
+ var rowCount = null; // Container span for the row counter.
469
+ var clearFilter = null; // Container span for the "clear filter" link.
470
+ var doingServerFilter = getProp(defn, 'server', 'filter') && getProp(defn, 'server', 'limit') !== -1;
471
+ var viewDropdown = null;
472
+
473
+ self._isIdle = false;
474
+
475
+ self.mode = 'plain';
476
+
477
+ self.generateCsv = false;
478
+ self.csvReady = false;
479
+ self.exportLock = new Lock('Export');
480
+ self.colConfigLock = new Lock('colConfig');
481
+
482
+ self.rootHasFixedHeight = false;
483
+ self.timing = new Timing();
484
+
485
+ self.colConfigWin = new ColConfigWin(self);
486
+ self.debugWin = new DebugWin();
487
+
488
+ self.defn = self._normalize(defn); // Definition used to retrieve data and output grid.
489
+ self.opts = opts; // Other tag options, not related to the grid.
490
+ self.grid = null; // List of all grids generated as a result.
491
+ self.ui = {}; // User interface elements.
492
+ self.selected = {}; // Information about what rows are selected.
493
+
494
+ self._validateFeatures();
495
+ self._validateId(self.defn.id);
496
+
497
+ self.logDebug(self.makeLogTag() + ' Definition: %O', defn);
498
+ self.logDebug(self.makeLogTag() + ' Options: %O', opts);
499
+
500
+ // Check the validity of the provided computed/mirage views and prefs.
501
+
502
+ if (defn.computedView != null && !(defn.computedView instanceof ComputedView)) {
503
+ throw new Error('Call Error: `defn.computedView` must be null or an instance of ComputedView');
504
+ }
505
+
506
+ if (defn.mirageView != null && !(defn.mirageView instanceof MirageView)) {
507
+ throw new Error('Call Error: `defn.mirageView` must be null or an instance of MirageView');
508
+ }
509
+
510
+ if (defn.prefs != null && !(defn.prefs instanceof Prefs)) {
511
+ throw new Error('Call Error: `defn.prefs` must be null or an instance of Prefs');
512
+ }
513
+
514
+ self.computedView = defn.computedView;
515
+ self.mirageView = defn.mirageView;
516
+ self.prefs = defn.prefs;
517
+
518
+ // Create default versions of the computed/mirage views and prefs if none were provided.
519
+
520
+ if (self.computedView == null) {
521
+ self.logDebug(self.makeLogTag() + ' No computed view specified, creating our own.');
522
+ self.computedView = new ComputedView();
523
+ }
524
+
525
+ if (self.mirageView == null) {
526
+ self.logDebug(self.makeLogTag() + ' No mirage view specified, creating our own.');
527
+ self.mirageView = new MirageView();
528
+ }
529
+
530
+ if (self.prefs == null) {
531
+ self.logDebug(self.makeLogTag() + ' No prefs specified, creating our own.');
532
+ self.prefs = new Prefs(self.id);
533
+ }
534
+
535
+ // Make sure we're all using the same prefs.
536
+
537
+ self.computedView.setPrefs(self.prefs);
538
+ self.mirageView.setPrefs(self.prefs);
539
+
540
+ self.view = self.defn.computedView || self.defn.mirageView || self.computedView;
541
+
542
+ if (self.colConfig != null) {
543
+ self.view.setColConfig(self.colConfig);
544
+ }
545
+ self.view.addClient(self, 'grid');
546
+
547
+ self.defn.grid = self;
548
+
549
+ self.TemplatesEditor = new TemplatesEditor(self, function () {
550
+ self.redraw();
551
+ });
552
+
553
+ // Set up UI elements {{{3
554
+
555
+ self.ui.root = jQuery(document.getElementById(self.id))
556
+ .addClass('wcdv_grid')
557
+ .attr('data-title', self.id + '_title');
558
+
559
+ self.ui.root.children().remove();
560
+
561
+ if (self.ui.root.height() !== 0) {
562
+ self.rootHasFixedHeight = true;
563
+ self.rootHeight = self.ui.root.height();
564
+ // When using TableTool, we can't just set the height of the whole grid and use flex to control
565
+ // the height of the table automatically. See DV-196.
566
+ // Remove the height CSS property here, so the renderer can use it for data-ttheight instead.
567
+ if (self.features.floatingHeader &&
568
+ getProp(self.defn, 'table', 'floatingHeader', 'method') === 'tabletool' &&
569
+ window.TableTool != null) {
570
+ self.ui.root.css('height', '');
571
+ }
572
+ }
573
+
574
+ if (self.view.source.origin instanceof FileSource) {
575
+ self.ui.root._onFileDrop(function (files) {
576
+ self.view.source.origin.setFiles(files);
577
+ });
578
+ }
579
+
580
+ // Titlebar {{{4
581
+
582
+ self.ui.titlebar = jQuery('<div class="wcdv_grid_titlebar">')
583
+ .attr('title', trans('GRID.TITLEBAR.SHOW_HIDE'))
584
+ .on('click', function (evt) {
585
+ evt.stopPropagation();
586
+ self.toggle();
587
+ })
588
+ .droppable({
589
+ accept: '.wcdv_drag_handle',
590
+ over: function (evt, ui) {
591
+ self.showControls();
592
+
593
+ // Need to recalculate the position of the droppable targets, because they are now
594
+ // guaranteed to be visible (they may have been hidden within the grid control before).
595
+
596
+ ui.draggable.draggable('option', 'refreshPositions', true);
597
+ }
598
+ });
599
+
600
+ self._addTitleWidgets(self.ui.titlebar, doingServerFilter, self.id);
601
+
602
+ // Omnifilter {{{4
603
+
604
+ if (self.features.omnifilter) {
605
+ self.ui.omnifilter = jQuery('<div class="wcdv_omnifilter">')
606
+ .on('click', function (evt) {
607
+ evt.stopPropagation();
608
+ })
609
+ .hide();
610
+
611
+ self._omnifilterDebounceTimer = null;
612
+
613
+ self.ui.omnifilterInput = jQuery('<input>', {
614
+ 'type': 'text',
615
+ 'class': 'wcdv_omnifilter_input',
616
+ 'placeholder': trans('GRID.OMNIFILTER.PLACEHOLDER'),
617
+ 'aria-label': trans('GRID.OMNIFILTER.ARIA_LABEL')
618
+ })
619
+ .on('input', function () {
620
+ clearTimeout(self._omnifilterDebounceTimer);
621
+ self._omnifilterDebounceTimer = setTimeout(function () {
622
+ self._applyOmnifilter();
623
+ }, 500);
624
+ })
625
+ .on('keydown', function (evt) {
626
+ if (evt.key === 'Escape') {
627
+ clearTimeout(self._omnifilterDebounceTimer);
628
+ self.ui.omnifilterInput.val('');
629
+ self._applyOmnifilter();
630
+ self.ui.omnifilter.hide();
631
+ self.ui.omnifilterToggle.removeClass('wcdv_omnifilter_active');
632
+ }
633
+ });
634
+
635
+ self.ui.omnifilterClear = jQuery('<button>', {
636
+ 'class': 'wcdv_omnifilter_clear wcdv_icon_button',
637
+ 'type': 'button',
638
+ 'aria-label': trans('GRID.OMNIFILTER.CLEAR')
639
+ })
640
+ .append(icon('x'))
641
+ .on('click', function () {
642
+ clearTimeout(self._omnifilterDebounceTimer);
643
+ self.ui.omnifilterInput.val('');
644
+ self._applyOmnifilter();
645
+ })
646
+ .hide();
647
+
648
+ self.ui.omnifilterInputWrap = jQuery('<div class="wcdv_omnifilter_input_wrap">')
649
+ .append(self.ui.omnifilterInput)
650
+ .append(self.ui.omnifilterClear);
651
+
652
+ self.ui.omnifilter
653
+ .append(self.ui.omnifilterInputWrap);
654
+
655
+ self.ui.omnifilterToggle = jQuery('<button>', {
656
+ 'type': 'button',
657
+ 'style': 'font-size: 18px',
658
+ 'class': 'wcdv_icon_button wcdv_text-primary wcdv_omnifilter_toggle',
659
+ 'aria-label': trans('GRID.OMNIFILTER.TOGGLE')
660
+ })
661
+ .append(icon('search'))
662
+ .on('click', function (evt) {
663
+ evt.stopPropagation();
664
+ if (self.ui.omnifilter.is(':visible')) {
665
+ clearTimeout(self._omnifilterDebounceTimer);
666
+ self.ui.omnifilterInput.val('');
667
+ self._applyOmnifilter();
668
+ self.ui.omnifilter.hide();
669
+ self.ui.omnifilterToggle.removeClass('wcdv_omnifilter_active');
670
+ }
671
+ else {
672
+ self.ui.omnifilterToggle.addClass('wcdv_omnifilter_active');
673
+ self.ui.omnifilter.show();
674
+ self.ui.omnifilterInput.focus();
675
+ }
676
+ });
677
+
678
+ // Insert into titlebar controls, before the debug/export/refresh buttons.
679
+ self.ui.titlebar_controls
680
+ .prepend(self.ui.omnifilterToggle)
681
+ .prepend(self.ui.omnifilter);
682
+ }
683
+
684
+ self.ui.autoLimit = jQuery('<div>', {
685
+ 'class': 'wcdv_warning_banner auto_limit_warning'
686
+ })
687
+ .text(trans('GRID.TITLEBAR.DATA_LIMITED_WARNING'))
688
+ .on('click', function () {
689
+ self.ui.autoLimit.hide();
690
+ self.view.unlimit();
691
+ self.refresh();
692
+ })
693
+ .hide();
694
+
695
+ // Toolbar {{{4
696
+
697
+ self.ui.content = jQuery('<div>', {
698
+ 'class': 'wcdv_grid_content'
699
+ });
700
+
701
+ self.ui.toolbar = jQuery('<div>')
702
+ .addClass('wcdv_grid_toolbar')
703
+ .droppable({
704
+ accept: '.wcdv_drag_handle',
705
+ over: function (evt, ui) {
706
+ self.showControls();
707
+
708
+ // Need to recalculate the position of the droppable targets, because they are now
709
+ // guaranteed to be visible (they may have been hidden within the grid control before).
710
+
711
+ ui.draggable.draggable('option', 'refreshPositions', true);
712
+ }
713
+ });
714
+
715
+ self.ui.toolbar_computedView = new ComputedViewToolbar(self);
716
+ self.ui.toolbar_computedView.attach(self.ui.toolbar);
717
+
718
+ self.ui.toolbar_plain = new PlainToolbar(self);
719
+ self.ui.toolbar_plain.attach(self.ui.toolbar);
720
+ self.ui.toolbar_plain.hide();
721
+
722
+ self.ui.toolbar_group = new GroupToolbar(self);
723
+ self.ui.toolbar_group.attach(self.ui.toolbar);
724
+ self.ui.toolbar_group.hide();
725
+
726
+ self.ui.toolbar_pivot = new PivotToolbar(self);
727
+ self.ui.toolbar_pivot.attach(self.ui.toolbar);
728
+ self.ui.toolbar_pivot.hide();
729
+
730
+ self.ui.toolbar_renderer = new RendererToolbar(self);
731
+ self.ui.toolbar_renderer.attach(self.ui.toolbar);
732
+
733
+ if (!self.opts.showToolbar) {
734
+ self.ui.toolbar.hide();
735
+ }
736
+
737
+ // Controls {{{4
738
+
739
+ self.ui.controls = jQuery('<div>', { 'class': 'wcdv_grid_control' });
740
+ self.ui.filterControl = jQuery('<div>', { 'class': 'wcdv_control_pane wcdv_filter_control' });
741
+ self.ui.groupControl = jQuery('<div>', { 'class': 'wcdv_control_pane wcdv_group_control' });
742
+ self.ui.pivotControl = jQuery('<div>', { 'class': 'wcdv_control_pane wcdv_pivot_control' });
743
+ self.ui.aggregateControl = jQuery('<div>', { 'class': 'wcdv_control_pane wcdv_aggregate_control' });
744
+ self.ui.operationsPalette = jQuery('<div>', { 'class': 'wcdv_grid_control' }).css({
745
+ display: 'block'
746
+ });
747
+
748
+ // Filter Control {{{5
749
+
750
+ self.filterControl = new FilterControl(self, self.colConfig, self.view, self.features, self.timing);
751
+ self.ui.filterControl.children().remove();
752
+ self.filterControl.draw(self.ui.filterControl);
753
+ self.ui.filterControl.show();
754
+
755
+ // Group Control {{{5
756
+
757
+ self.groupControl = new GroupControl(self, self.colConfig, self.view, self.features, self.timing);
758
+ self.groupControl.draw(self.ui.groupControl);
759
+
760
+ self.groupControl.on('fieldAdded', function (fieldAdded, fields) {
761
+ self.ui.toolbar_computedView.ui.storeMirageBtn.attr('disabled', false);
762
+ self.ui.pivotControl.show();
763
+ self.ui.aggregateControl.show();
764
+ });
765
+ self.groupControl.on('fieldRemoved', function (fieldRemoved, fields) {
766
+ if (fields.length === 0) {
767
+ self.ui.toolbar_computedView.ui.storeMirageBtn.attr('disabled', true);
768
+ self.ui.pivotControl.hide();
769
+ self.ui.aggregateControl.hide();
770
+ }
771
+ });
772
+ self.groupControl.on('cleared', function () {
773
+ self.ui.toolbar_computedView.ui.storeMirageBtn.attr('disabled', true);
774
+ self.ui.pivotControl.hide();
775
+ self.ui.aggregateControl.hide();
776
+ });
777
+
778
+ // Pivot Control {{{5
779
+
780
+ self.pivotControl = new PivotControl(self, self.colConfig, self.view, self.features, self.timing);
781
+ self.pivotControl.draw(self.ui.pivotControl);
782
+
783
+ // Group <-> Pivot (Drag & Drop) {{{5
784
+
785
+ self.groupControl.getListElement().sortable({
786
+ connectWith: '#' + self.pivotControl.getListElement().attr('id'),
787
+ placeholder: 'ui-state-highlight',
788
+ forcePlaceholderSize: true,
789
+ cursor: 'move',
790
+ start: function (evt, ui) {
791
+ ui.placeholder.css('height', ui.item.get(0).offsetHeight);
792
+ ui.helper.addClass('wcdv_sortable_helper');
793
+ },
794
+ activate: function (evt, ui) {
795
+ // Leave room for item to be added to empty list.
796
+ jQuery(this).addClass('wcdv_sortable_sender');
797
+ },
798
+ deactivate: function (evt, ui) {
799
+ jQuery(this).removeClass('wcdv_sortable_sender');
800
+ ui.item.removeClass('wcdv_sortable_helper');
801
+ },
802
+ update: function (evt, ui) {
803
+ // If dragging from group list to pivot list, and there was only one item in the group list,
804
+ // prevent the action (because this would be pivot w/o group).
805
+ if (ui.sender === null && self.groupControl.controlFields.length === 1) {
806
+ jQuery(this).sortable('cancel');
807
+ return;
808
+ }
809
+ jQuery(this).removeClass('wcdv_sortable_sender');
810
+ ui.item.removeClass('wcdv_sortable_helper');
811
+ self.groupControl.sortableSync();
812
+ },
813
+ });
814
+ self.pivotControl.getListElement().sortable({
815
+ connectWith: '#' + self.groupControl.getListElement().attr('id'),
816
+ placeholder: 'ui-state-highlight',
817
+ forcePlaceholderSize: true,
818
+ cursor: 'move',
819
+ start: function (evt, ui) {
820
+ ui.placeholder.css('height', ui.item.get(0).offsetHeight);
821
+ ui.helper.addClass('wcdv_sortable_helper');
822
+ },
823
+ activate: function (evt, ui) {
824
+ // Leave room for item to be added to empty list.
825
+ jQuery(this).addClass('wcdv_sortable_sender');
826
+ },
827
+ deactivate: function (evt, ui) {
828
+ jQuery(this).removeClass('wcdv_sortable_sender');
829
+ ui.item.removeClass('wcdv_sortable_helper');
830
+ },
831
+ update: function (evt, ui) {
832
+ jQuery(this).removeClass('wcdv_sortable_sender');
833
+ ui.item.removeClass('wcdv_sortable_helper');
834
+ self.pivotControl.sortableSync();
835
+ }
836
+ });
837
+
838
+ self.operationsPalette = new OperationsPalette(self);
839
+ self.operationsPalette.setOperations(self.defn.operations);
840
+ self.operationsPalette.draw(self.ui.operationsPalette);
841
+
842
+ // Aggregate Control {{{5
843
+
844
+ self.aggregateControl = new AggregateControl(self, self.colConfig, self.view, self.features, self.timing);
845
+ self.aggregateControl.draw(self.ui.aggregateControl);
846
+
847
+ // }}}5
848
+
849
+ if (!self.opts.showControls) {
850
+ self.ui.controls.hide();
851
+ }
852
+
853
+ // }}}4
854
+
855
+ self.ui.grid = jQuery('<div>', { 'id': defn.table.id, 'class': 'wcdv_grid_table' });
856
+
857
+ // Apply the initial row mode class
858
+ var rowMode = getPropDef('wrapped', defn, 'table', 'rowMode');
859
+ self.ui.grid.addClass('wcdv_row_mode_' + rowMode);
860
+
861
+ if (self.rootHasFixedHeight) {
862
+ // When using TableTool, we can't just set the height of the whole grid and use flex to control
863
+ // the height of the table automatically. See DV-196.
864
+ // Don't use the height: 0px trick in this situation and let TableTool manage the table height.
865
+ // FIXME Is this needed with the CSS method?
866
+ if (!self.features.floatingHeader || getProp(self.defn, 'table', 'floatingHeader', 'method') !== 'tabletool') {
867
+ // This is a trick to make 'flex: 1 1 auto' work right in Firefox, IE, Edge.
868
+ // Otherwise, the table takes up as much space as it needs and doesn't scroll.
869
+ self.ui.grid.css('height', '0px');
870
+ }
871
+ }
872
+
873
+ // The user has fixed the height of the containing grid, so we will need to have the browser put
874
+ // in some scrollbars for the overflow.
875
+
876
+ if (self.rootHasFixedHeight) {
877
+ self.ui.grid.css({ 'overflow': 'auto' });
878
+ }
879
+
880
+ if (document.getElementById(self.id + '_footer')) {
881
+ // There was a footer which was printed out by dashboard.c which we are now going to move
882
+ // inside the structure that we've been creating.
883
+
884
+ self.ui.footer = jQuery(document.getElementById(self.id + '_footer'));
885
+ }
886
+
887
+ self.ui.root
888
+ .append(self.ui.titlebar)
889
+ .append(self.ui.content
890
+ .append(self.ui.toolbar)
891
+ .append(self.ui.controls
892
+ .append(self.ui.filterControl)
893
+ .append(self.ui.groupControl)
894
+ .append(self.ui.pivotControl)
895
+ .append(self.ui.aggregateControl))
896
+ .append(self.ui.operationsPalette)
897
+ .append(self.ui.autoLimit)
898
+ .append(self.ui.grid)
899
+ .append(self.ui.footer))
900
+ ;
901
+
902
+ if (self.defn.renderer != null) {
903
+ self.clearRenderers();
904
+ self.addRenderer(0, null, {
905
+ name: self.defn.renderer,
906
+ opts: self.defn.rendererOpts
907
+ });
908
+ }
909
+ else {
910
+ self.resetRenderers();
911
+ }
912
+
913
+ self.makeResponsive();
914
+
915
+ // }}}3
916
+
917
+ var initialRender = true;
918
+
919
+ self.tableDoneCont = function (grid, srcIndex) {
920
+ self.logDebug(self.makeLogTag() + ' Finished drawing grid table!');
921
+
922
+ if (initialRender) {
923
+ initialRender = false;
924
+ }
925
+
926
+ // Invoke the callback for the Grid constructor, after the grid has been created. Sometimes
927
+ // people want to start manipulating the grid from JS right away.
928
+
929
+ if (typeof cb === 'function') {
930
+ cb();
931
+ }
932
+ };
933
+
934
+ self.view.on('fetchDataBegin', function () {
935
+ self._setSpinner('loading');
936
+ self._showSpinner();
937
+ if (self.opts.title) {
938
+ self.ui.title._addTrailing(',');
939
+ self.ui.statusSpan.show().text(trans('GRID.TITLEBAR.LOADING'));
940
+ self.ui.rowCount.hide();
941
+ }
942
+ if (self.view.source.isCancellable()) {
943
+ self.ui.cancelFetchBtn.show();
944
+ }
945
+ });
946
+ self.view.on('fetchDataEnd', function () {
947
+ self._hideSpinner();
948
+ self.ui.cancelFetchBtn.hide();
949
+ self.ui.statusSpan.show().text(trans('GRID.TITLEBAR.LOADED'));
950
+ });
951
+ self.view.source.on('fetchDataCancel', function () {
952
+ self.ui.cancelFetchBtn.hide();
953
+ if (initialRender) {
954
+ if (self.opts.title) {
955
+ self.ui.title._addTrailing(',');
956
+ self.ui.statusSpan.show().text(trans('GRID.TITLEBAR.NOT_LOADED'));
957
+ self.ui.rowCount.hide();
958
+ }
959
+ self._setSpinner('not-loaded');
960
+ self.hasRun = false;
961
+ self.hide();
962
+ }
963
+ else {
964
+ if (self.opts.title) {
965
+ self.ui.title._addTrailing(',');
966
+ self.ui.statusSpan.hide();
967
+ self.ui.rowCount.show();
968
+ }
969
+ self._hideSpinner();
970
+ }
971
+ });
972
+
973
+ self.view.on('workBegin', function () {
974
+ self._isIdle = false;
975
+ self._setSpinner('working');
976
+ self._showSpinner();
977
+ if (self.opts.title) {
978
+ self.ui.title._addTrailing(',');
979
+ self.ui.statusSpan.show().text(trans('GRID.TITLEBAR.WORKING'));
980
+ self.ui.rowCount.hide();
981
+ }
982
+ });
983
+ self.view.on('workEnd', function (info, ops) {
984
+ self._isIdle = true;
985
+ self._hideSpinner();
986
+ self.ui.title._stripTrailing(',');
987
+ self.ui.statusSpan.hide();
988
+ self.ui.rowCount.show();
989
+ self._updateRowCount(info, ops);
990
+ self.mode = info.isPlain ? 'plain' : info.isGroup ? 'group' : info.isPivot ? 'pivot' : null;
991
+ });
992
+
993
+ self.view.on('dataUpdated', function () {
994
+ if (self.opts.showOnDataChange && !self.isVisible()) {
995
+ self.show({ redraw: false });
996
+ }
997
+ self.redraw();
998
+ });
999
+
1000
+ self.view.on('getTypeInfo', function (typeInfo) {
1001
+ self.colConfigFromTypeInfo(typeInfo);
1002
+ });
1003
+
1004
+ self.prefs.prime(function () {
1005
+ // Create a way to switch back and forth between the two types of views depending on if a
1006
+ // perspective is live or not.
1007
+
1008
+ self.prefs.on('perspectiveChanged', function (id, p) {
1009
+ self.setView();
1010
+ self.redraw();
1011
+ }, {
1012
+ info: 'Changing view type to match new perspective'
1013
+ });
1014
+
1015
+ if (self.opts.runImmediately) {
1016
+ self.setView();
1017
+ self.redraw();
1018
+ }
1019
+ else {
1020
+ self.hasRun = false;
1021
+ self.hide();
1022
+ }
1023
+ });
1024
+
1025
+ /*
1026
+ * Store self object so it can be accessed from other JavaScript in the page.
1027
+ */
1028
+
1029
+ setProp(self, window, 'MIE', 'WC_DataVis', 'grids', self.id);
1030
+ });
1031
+
1032
+ // Mixins {{{2
1033
+
1034
+ mixinEventHandling(Grid, [
1035
+ 'showControls'
1036
+ , 'hideControls'
1037
+ , 'renderBegin'
1038
+ , 'renderEnd'
1039
+ , 'colConfigUpdate'
1040
+ , 'selectionChange'
1041
+ , 'rowModeChange'
1042
+ ]);
1043
+
1044
+ delegate(Grid, 'renderer', ['setSelection', 'getSelection', 'select', 'unselect', 'isSelected']);
1045
+
1046
+ mixinLogging(Grid);
1047
+ mixinNameSetting(Grid);
1048
+
1049
+ // Events JSDoc {{{3
1050
+
1051
+ /**
1052
+ * Fired when controls are shown in the grid.
1053
+ *
1054
+ * @event Grid#showControls
1055
+ */
1056
+
1057
+ /**
1058
+ * Fired when controls are hidden in the grid.
1059
+ *
1060
+ * @event Grid#hideControls
1061
+ */
1062
+
1063
+ /**
1064
+ * Fired when rendering has started.
1065
+ *
1066
+ * @event Grid#renderBegin
1067
+ */
1068
+
1069
+ /**
1070
+ * Fired when rendering has finished.
1071
+ *
1072
+ * @event Grid#renderEnd
1073
+ */
1074
+
1075
+ /**
1076
+ * Fired when column configuration has changed.
1077
+ *
1078
+ * @event Grid#colConfigUpdate
1079
+ */
1080
+
1081
+ /**
1082
+ * Fired when selection is changed.
1083
+ *
1084
+ * @event Grid#selectionChange
1085
+ *
1086
+ * @param {Array.<ComputedView~Data_Row>} selected
1087
+ * Data from rows that are selected.
1088
+ */
1089
+
1090
+ // #toString {{{2
1091
+
1092
+ Grid.prototype.toString = function () {
1093
+ var self = this;
1094
+ return 'Grid(' + self.id + ')';
1095
+ };
1096
+
1097
+ // #_validateFeatures {{{2
1098
+
1099
+ Grid.prototype._validateFeatures = function () {
1100
+ var self = this;
1101
+
1102
+ self.features = {};
1103
+
1104
+ var availableFeatures = [
1105
+ 'footer',
1106
+ 'sort',
1107
+ 'filter',
1108
+ 'group',
1109
+ 'pivot',
1110
+ 'rowSelect',
1111
+ 'rowReorder',
1112
+ 'add',
1113
+ 'edit',
1114
+ 'delete',
1115
+ 'limit',
1116
+ 'floatingHeader',
1117
+ 'block',
1118
+ 'progress',
1119
+ 'incremental',
1120
+ 'operations',
1121
+ 'columnResize',
1122
+ 'columnReorder',
1123
+ 'activeRow',
1124
+ 'omnifilter',
1125
+ 'pagination'
1126
+ ];
1127
+
1128
+ // When the user has specified the `footer` option, enable the footer feature (if it hasn't
1129
+ // already been set by the user - in other words, the user can override this automatic behavior).
1130
+
1131
+ if (getProp(self.defn, 'table', 'footer') !== undefined) {
1132
+ setPropDef(true, self.defn, 'table', 'features', 'footer');
1133
+ }
1134
+
1135
+ _.each(availableFeatures, function (feat) {
1136
+ self.features[feat] = getPropDef(false, self.defn, 'table', 'features', feat);
1137
+ });
1138
+
1139
+ self.logDebug(self.makeLogTag() + ' Features =', self.features);
1140
+ };
1141
+
1142
+ // #_validateId {{{2
1143
+
1144
+ Grid.prototype._validateId = function (id) {
1145
+ var self = this;
1146
+
1147
+ // If the ID was specified as a jQuery object, extract the ID from the element.
1148
+
1149
+ if (_.isArray(id) && id[0] instanceof jQuery) {
1150
+ id = id[0];
1151
+ }
1152
+
1153
+ if (id instanceof jQuery) {
1154
+ id = id.attr('id');
1155
+ }
1156
+
1157
+ if (typeof id !== 'string') {
1158
+ throw '<grid> "id" is not a string';
1159
+ }
1160
+
1161
+ if (document.getElementById(id) === null) {
1162
+ throw 'No element exists with given ID: ' + id;
1163
+ }
1164
+
1165
+ self.id = id;
1166
+ setProp(id + '_gridContainer', self.defn, 'table', 'id');
1167
+ };
1168
+
1169
+ // #setView {{{2
1170
+
1171
+ Grid.prototype.setView = function () {
1172
+ var self = this;
1173
+
1174
+ var p = self.prefs.currentPerspective;
1175
+
1176
+ // If the perspective is meant for live data then configure the grid to use a ComputedView.
1177
+ // Otherwise, configure the grid to use a MirageView.
1178
+
1179
+ if (p.isMirage()) {
1180
+ self.logDebug(self.makeLogTag('setView') + ' Switching to Mirage View for pre-computed data for perspective "%s"', p.name);
1181
+ self.view = self.prefs.modules['mirage'].target;
1182
+ self.view.setPerspectiveName(p.name);
1183
+ }
1184
+ else {
1185
+ self.logDebug(self.makeLogTag('setView') + ' Switching to Computed View for live data for perspective "%s"', p.name);
1186
+ self.view = self.prefs.modules['view'].target;
1187
+ }
1188
+ };
1189
+ // #_addTitleWidgets {{{2
1190
+
1191
+ /**
1192
+ * Add widgets to the header of the grid.
1193
+ *
1194
+ * @method
1195
+ * @memberof Grid
1196
+ * @private
1197
+ *
1198
+ * @param {object} header
1199
+ * @param {boolean} doingServerFilter If true, then we are filtering and sorting on the server.
1200
+ * @param {string} id
1201
+ */
1202
+
1203
+ Grid.prototype._addTitleWidgets = function (titlebar, doingServerFilter, id) {
1204
+ var self = this;
1205
+
1206
+ self.ui.spinner = jQuery('<div>', {
1207
+ 'class': 'wcdv_loading_spinner'
1208
+ })
1209
+ .appendTo(titlebar)
1210
+ ;
1211
+
1212
+ self._setSpinner(self.opts.runImmediately ? 'loading' : 'not-loaded');
1213
+
1214
+ self.ui.title = jQuery('<strong>', {'id': id + '_title', 'data-parent': id})
1215
+ .addClass('wcdv_title')
1216
+ .text(self.opts.title)
1217
+ .appendTo(titlebar);
1218
+
1219
+ var notHeader = jQuery('<span>', {'class': 'headingInfo'})
1220
+ .on('click', function (evt) {
1221
+ evt.stopPropagation();
1222
+ })
1223
+ .appendTo(titlebar);
1224
+
1225
+ notHeader.append(' ');
1226
+
1227
+ self.ui.statusSpan = jQuery('<span>').appendTo(notHeader);
1228
+ self.ui.rowCount = jQuery('<span>').appendTo(notHeader);
1229
+
1230
+ self.ui.selectionInfo = jQuery('<span>').appendTo(notHeader);
1231
+
1232
+ self.ui.clearFilter = jQuery('<span>')
1233
+ .hide()
1234
+ .append(' (')
1235
+ .append(jQuery('<span>', {'class': 'link'})
1236
+ .text(trans('GRID.TITLEBAR.CLEAR_FILTER'))
1237
+ .on('click', function (evt) {
1238
+ evt.stopPropagation();
1239
+ self.ui.clearFilter.hide();
1240
+ self.view.clearFilter({ notify: true });
1241
+ }))
1242
+ .append(')')
1243
+ .appendTo(notHeader);
1244
+
1245
+ self.ui.cancelFetchBtn = jQuery('<button>', {
1246
+ 'type': 'button',
1247
+ 'title': trans('GRID.TITLEBAR.CANCEL')
1248
+ })
1249
+ .css({'margin-left': '0.5em'})
1250
+ .text(trans('GRID.TITLEBAR.CANCEL'))
1251
+ .on('click', function (evt) {
1252
+ evt.stopPropagation();
1253
+ self.view.source.cancel();
1254
+ })
1255
+ .hide()
1256
+ .appendTo(notHeader);
1257
+
1258
+ if (typeof self.opts.helpText === 'string' && self.opts.helpText !== '') {
1259
+ notHeader.append(' ');
1260
+ jQuery('<span>', {
1261
+ 'data-tooltip': self.opts.helpText
1262
+ })
1263
+ .append(icon('circle-help').css({
1264
+ 'margin-bottom': '-4px'
1265
+ }))
1266
+ .appendTo(notHeader);
1267
+ }
1268
+
1269
+ self.ui.toolbar_prefs = new PrefsToolbar(self);
1270
+ self.ui.toolbar_prefs.attach(titlebar);
1271
+
1272
+ self.prefs.bind('grid', self, {
1273
+ toolbar: self.ui.toolbar_prefs.ui.root
1274
+ });
1275
+
1276
+ // Create container to hold all the controls in the titlebar
1277
+
1278
+ self.ui.titlebar_controls = jQuery('<div>')
1279
+ .addClass('wcdv_titlebar_controls pull-right')
1280
+ .appendTo(titlebar);
1281
+
1282
+ // Create the Debug Info button.
1283
+
1284
+ if (window.MIE && window.MIE.DEBUGGING) {
1285
+ jQuery('<button>', {
1286
+ 'type': 'button',
1287
+ 'style': 'font-size: 18px',
1288
+ 'class': 'wcdv_icon_button wcdv_text-primary'
1289
+ })
1290
+ .attr('title', trans('GRID.TITLEBAR.SHOW_DEBUG_INFO'))
1291
+ .click(function (evt) {
1292
+ evt.stopPropagation();
1293
+ self.debugWin.show(self, self.view, self.view.source);
1294
+ })
1295
+ .append(icon('bug'))
1296
+ .appendTo(self.ui.titlebar_controls);
1297
+ }
1298
+
1299
+ // Create the Export button
1300
+
1301
+ self.ui.exportBtn = jQuery('<button>', {
1302
+ 'type': 'button',
1303
+ 'style': 'font-size: 18px',
1304
+ 'class': 'wcdv_icon_button wcdv_text-primary'
1305
+ })
1306
+ .on('click', function (evt) {
1307
+ evt.stopPropagation();
1308
+ self.export();
1309
+ })
1310
+ .appendTo(self.ui.titlebar_controls)
1311
+ ;
1312
+
1313
+ self._setExportStatus('notReady');
1314
+
1315
+ // Create the Refresh button
1316
+
1317
+ self.ui.refreshBtn = jQuery('<button>', {
1318
+ 'type': 'button',
1319
+ 'style': 'font-size: 18px',
1320
+ 'class': 'wcdv_icon_button wcdv_text-primary'
1321
+ })
1322
+ .attr('title', trans('GRID.TITLEBAR.REFRESH'))
1323
+ .on('click', function (evt) {
1324
+ evt.stopPropagation();
1325
+ self.refresh();
1326
+ })
1327
+ .append(icon('refresh-cw'))
1328
+ .appendTo(self.ui.titlebar_controls)
1329
+ ;
1330
+
1331
+ var pWin = new PopupWindow({
1332
+ title: trans('GRID.PERSPECTIVE_WIN.TITLE'),
1333
+ width: 500,
1334
+ position: {
1335
+ my: 'top',
1336
+ at: 'bottom',
1337
+ of: titlebar
1338
+ }
1339
+ });
1340
+
1341
+ var pWinContentDiv = jQuery('<div>');
1342
+
1343
+ var pWinWarning = jQuery('<div>')
1344
+ .addClass('wcdv_dlg_warning_banner')
1345
+ .appendTo(pWinContentDiv);
1346
+
1347
+ var pWinTextArea = jQuery('<textarea>', {'style': 'font-family: monospace; font-size: 10pt; width: 100%', 'rows': '20', 'readonly': true})
1348
+ .appendTo(pWinContentDiv);
1349
+
1350
+ pWin.setContent(pWinContentDiv);
1351
+
1352
+ // This is the "gear" icon that shows/hides the controls below the toolbar. The controls are used
1353
+ // to set the group, pivot, aggregate, and filters. Ideally the user only has to utilize these
1354
+ // once, and then switches between perspectives to get the same effect.
1355
+
1356
+ jQuery('<button>', {
1357
+ 'type': 'button',
1358
+ 'style': 'font-size: 18px',
1359
+ 'class': 'wcdv_icon_button wcdv_text-primary'
1360
+ })
1361
+ .attr('title', trans('GRID.TITLEBAR.SHOW_HIDE_CONTROLS'))
1362
+ .click(function (evt) {
1363
+ evt.stopPropagation();
1364
+ if (evt.shiftKey) {
1365
+ if (self.prefs.currentPerspective.opts.isTemporary) {
1366
+ pWinWarning.text(trans('GRID.PERSPECTIVE_WIN.TEMP_PERSPECTIVE_WARNING'));
1367
+ pWinWarning.show();
1368
+ }
1369
+ else {
1370
+ pWinWarning.hide();
1371
+ }
1372
+ pWinTextArea.val(JSON.stringify(self.prefs.currentPerspective.config, null, 2));
1373
+ pWin.open();
1374
+ }
1375
+ else {
1376
+ self.toggleControls();
1377
+ }
1378
+ })
1379
+ .append(jQuery(icon('settings')))
1380
+ .appendTo(self.ui.titlebar_controls)
1381
+ ;
1382
+
1383
+ // Create the down-chevron button that shows/hides everything under the titlebar.
1384
+
1385
+ self.ui.showHideButton = jQuery('<button>', {
1386
+ 'type': 'button',
1387
+ 'style': 'font-size: 18px',
1388
+ 'class': 'wcdv_icon_button wcdv_text-primary showhide'
1389
+ })
1390
+ .attr('title', trans('GRID.TITLEBAR.SHOW_HIDE'))
1391
+ .click(function (evt) {
1392
+ evt.stopPropagation();
1393
+ self.toggle();
1394
+ })
1395
+ .append(jQuery(icon('chevron-down')))
1396
+ .appendTo(self.ui.titlebar_controls)
1397
+ ;
1398
+ };
1399
+
1400
+ // #clear {{{2
1401
+
1402
+ Grid.prototype.clear = function () {
1403
+ var self = this;
1404
+
1405
+ if (self.resizeObserver != null) {
1406
+ self.resizeObserver.disconnect();
1407
+ self.resizeObserver = null;
1408
+ }
1409
+
1410
+ self.ui.root.children().remove();
1411
+ };
1412
+ // #redraw {{{2
1413
+
1414
+ /**
1415
+ * Redraw the data shown in a grid. If the grid is not visible, this function does nothing (i.e.
1416
+ * you cannot use it to retrieve data for an invisible grid).
1417
+ *
1418
+ * @method
1419
+ * @memberof Grid
1420
+ *
1421
+ * @param {function} [contOk]
1422
+ * Function to call on success.
1423
+ *
1424
+ * @param {function} [contFail]
1425
+ * Function to call on failure.
1426
+ */
1427
+
1428
+ Grid.prototype.redraw = function (contOk, contFail) {
1429
+ var self = this;
1430
+
1431
+ if (contOk != null && typeof contOk !== 'function') {
1432
+ throw new Error('Call Error: `contOk` must be null or a function');
1433
+ }
1434
+ if (contFail != null && typeof contFail != 'function') {
1435
+ throw new Error('Call Error: `contFail` must be null or a function');
1436
+ }
1437
+
1438
+ contOk = contOk || I;
1439
+ contFail = contFail || I;
1440
+
1441
+ self.logDebug(self.makeLogTag() + ' Redrawing...');
1442
+
1443
+ var rendererCtor
1444
+ , rendererCtorOpts;
1445
+
1446
+ self.colConfigLock.lock('redrawing grid; prevent colConfig changes from notifying existing renderer');
1447
+
1448
+ self.view.getData(function (ok, data) {
1449
+ if (!ok) {
1450
+ return contFail();
1451
+ }
1452
+
1453
+ var mode = data.isPlain ? 'plain' : data.isGroup ? 'group' : data.isPivot ? 'pivot' : null;
1454
+ var renderer = self.findRenderer(self.ui.root.get(0).getBoundingClientRect().width, mode);
1455
+
1456
+ self.rendererName = renderer.name;
1457
+ self.rendererId = renderer.id;
1458
+
1459
+ var rendererCtor = GridRenderer.registry.get(self.rendererName);
1460
+ var rendererCtorOpts = deepCopy(renderer.opts);
1461
+
1462
+ if (self.ui.footer) {
1463
+ rendererCtorOpts.footer = self.ui.footer;
1464
+ }
1465
+
1466
+ if (self.renderer) {
1467
+ self.renderer.destroy();
1468
+ }
1469
+
1470
+ rendererCtorOpts.generateCsv = self.generateCsv;
1471
+ rendererCtorOpts.fixedHeight = self.rootHasFixedHeight;
1472
+
1473
+ self.ui.exportBtn.attr('disabled', true);
1474
+ self.renderer = new rendererCtor(self, self.defn, self.view, self.features, rendererCtorOpts, self.timing, self.id, self.colConfig);
1475
+
1476
+ // Update the toolbar sections. This needs to be done after creating the renderer because the
1477
+ // renderer validates (and possibly changes) the supported features, and that changes what parts
1478
+ // of the toolbar we show. Obviously, we shouldn't show buttons for features that the current
1479
+ // renderer doesn't implement.
1480
+
1481
+ if (data.isPlain) {
1482
+ self.ui.toolbar_plain.show();
1483
+ self.ui.toolbar_group.hide();
1484
+ self.ui.toolbar_pivot.hide();
1485
+ if (self.features.omnifilter) {
1486
+ self.ui.omnifilterToggle.show();
1487
+ }
1488
+ }
1489
+ else if (data.isGroup) {
1490
+ self.ui.toolbar_plain.hide();
1491
+ self.ui.toolbar_group.show();
1492
+ self.ui.toolbar_pivot.hide();
1493
+ if (self.features.omnifilter) {
1494
+ self.ui.omnifilterToggle.removeClass('wcdv_omnifilter_active');
1495
+ self.ui.omnifilterToggle.hide();
1496
+ self.ui.omnifilter.hide();
1497
+ }
1498
+ }
1499
+ else if (data.isPivot) {
1500
+ self.ui.toolbar_plain.hide();
1501
+ self.ui.toolbar_group.hide();
1502
+ self.ui.toolbar_pivot.show();
1503
+ if (self.features.omnifilter) {
1504
+ self.ui.omnifilterToggle.removeClass('wcdv_omnifilter_active');
1505
+ self.ui.omnifilterToggle.hide();
1506
+ self.ui.omnifilter.hide();
1507
+ }
1508
+ }
1509
+
1510
+ self.renderer.on('renderBegin', function () {
1511
+ self._isIdle = false;
1512
+ self.fire('renderBegin');
1513
+ });
1514
+ self.renderer.on('renderEnd', function () {
1515
+ self.fire('renderEnd');
1516
+ self._isIdle = true;
1517
+ });
1518
+
1519
+ self.renderer.on('unableToRender', function () {
1520
+ self._setExportStatus('notReady');
1521
+ self.redraw();
1522
+ });
1523
+
1524
+ self.renderer.on('csvReady', function () {
1525
+ if (self.exportLock.isLocked()) {
1526
+ self.exportLock.unlock();
1527
+ }
1528
+ self._setExportStatus('ready');
1529
+ });
1530
+ self.renderer.on('generateCsvProgress', function (progress) {
1531
+ if (progress === 0) {
1532
+ self.ui.exportBtn.children('svg.wcdv_icon').remove();
1533
+ self.ui.exportBtn.append(icon('loader-circle', ['wcdv_icon_pulse']));
1534
+ }
1535
+ });
1536
+
1537
+ if (self.features.limit) {
1538
+ self.renderer.on('limited', function () {
1539
+ self.ui.limit_div.show();
1540
+ });
1541
+ self.renderer.on('unlimited', function () {
1542
+ self.ui.limit_div.hide();
1543
+ });
1544
+ }
1545
+
1546
+ if (self.features.rowSelect) {
1547
+ self.renderer.on('selectionChange', function (selection) {
1548
+ if (selection.length === 0) {
1549
+ self.ui.selectionInfo.text('');
1550
+ }
1551
+ else {
1552
+ var addComma = self.ui.rowCount.text().length > 0;
1553
+ var str = addComma ? ', ' : '';
1554
+ str += trans(selection.length === 1 ? 'GRID.TITLEBAR.SELECTED_COUNT_SINGULAR' : 'GRID.TITLEBAR.SELECTED_COUNT_PLURAL', selection.length);
1555
+ self.ui.selectionInfo.text(str);
1556
+ }
1557
+ self.fire('selectionChange', null, selection);
1558
+ });
1559
+ }
1560
+
1561
+ self.renderer.draw(self.ui.grid, null, function () {
1562
+ if (self.colConfigLock.isLocked()) {
1563
+ self.colConfigLock.unlock('renderer finished drawing');
1564
+ }
1565
+ self.setSelection();
1566
+ self.ui.exportBtn.attr('disabled', false);
1567
+ if (self.features.omnifilter) {
1568
+ self._applyOmnifilter();
1569
+ }
1570
+ self.tableDoneCont();
1571
+ });
1572
+ });
1573
+ };
1574
+
1575
+ // #_applyOmnifilter {{{2
1576
+
1577
+ /**
1578
+ * Apply the omnifilter to the currently rendered table. Hides all rows in the table body that do
1579
+ * not contain the search text in any cell. This is a visual-only operation and does not affect the
1580
+ * underlying data in the view.
1581
+ *
1582
+ * @method
1583
+ * @memberof Grid
1584
+ * @private
1585
+ */
1586
+
1587
+ Grid.prototype._applyOmnifilter = function () {
1588
+ var self = this;
1589
+
1590
+ if (!self.features.omnifilter) {
1591
+ return;
1592
+ }
1593
+
1594
+ var query = (self.ui.omnifilterInput.val() || '').toLowerCase();
1595
+ var tbody = self.ui.grid.find('tbody');
1596
+
1597
+ if (!tbody.length) {
1598
+ return;
1599
+ }
1600
+
1601
+ // Show or hide the clear button based on whether there is text in the input.
1602
+
1603
+ if (query.length > 0) {
1604
+ self.ui.omnifilterClear.show();
1605
+ }
1606
+ else {
1607
+ self.ui.omnifilterClear.hide();
1608
+ }
1609
+
1610
+ // Determine which column indices contain string data by inspecting the TH elements for the
1611
+ // data-wcdv-field-type attribute output by the renderer.
1612
+
1613
+ var stringColIndices = [];
1614
+ var thead = self.ui.grid.find('thead');
1615
+
1616
+ if (thead.length) {
1617
+ var ths = thead.find('tr:first th');
1618
+
1619
+ ths.each(function (i) {
1620
+ var fieldType = this.getAttribute('data-wcdv-field-type');
1621
+
1622
+ if (fieldType === 'string') {
1623
+ stringColIndices.push(i);
1624
+ }
1625
+ });
1626
+ }
1627
+
1628
+ // Iterate through all data rows (not "show more" rows) and toggle visibility based on whether
1629
+ // any cell text in the row contains the query string.
1630
+
1631
+ var even = false;
1632
+ var hasStringCols = stringColIndices.length > 0;
1633
+
1634
+ tbody.children('tr').each(function () {
1635
+ var tr = jQuery(this);
1636
+
1637
+ // Skip non-data rows (e.g. "show more rows" button).
1638
+ if (tr.hasClass('wcdvgrid_more')) {
1639
+ return;
1640
+ }
1641
+
1642
+ if (query.length === 0) {
1643
+ tr.show();
1644
+ tr.removeClass('even odd').addClass(even ? 'even' : 'odd');
1645
+ even = !even;
1646
+ return;
1647
+ }
1648
+
1649
+ var matched = false;
1650
+ var cells = this.children;
1651
+ var cellText;
1652
+
1653
+ if (hasStringCols) {
1654
+ for (var i = 0; i < stringColIndices.length; i++) {
1655
+ var cell = cells[stringColIndices[i]];
1656
+
1657
+ if (cell != null) {
1658
+ cellText = cell.textContent || cell.innerText || '';
1659
+
1660
+ if (cellText.toLowerCase().indexOf(query) >= 0) {
1661
+ matched = true;
1662
+ break;
1663
+ }
1664
+ }
1665
+ }
1666
+ }
1667
+ else {
1668
+ // Fallback: if no string columns were identified, search the entire row.
1669
+ cellText = this.textContent || this.innerText || '';
1670
+ matched = cellText.toLowerCase().indexOf(query) >= 0;
1671
+ }
1672
+
1673
+ if (matched) {
1674
+ tr.show();
1675
+ tr.removeClass('even odd').addClass(even ? 'even' : 'odd');
1676
+ even = !even;
1677
+ }
1678
+ else {
1679
+ tr.hide();
1680
+ }
1681
+ });
1682
+ };
1683
+
1684
+ // #refresh {{{2
1685
+
1686
+ /**
1687
+ * Refreshes the data from the data view in the grid.
1688
+ *
1689
+ * @method
1690
+ * @memberof Grid
1691
+ */
1692
+
1693
+ Grid.prototype.refresh = function () {
1694
+ var self = this;
1695
+
1696
+ if (!self.isVisible()) {
1697
+ return;
1698
+ }
1699
+
1700
+ self.logDebug(self.makeLogTag() + ' Refreshing...');
1701
+
1702
+ self._isIdle = false;
1703
+ self.view.refresh();
1704
+ };
1705
+
1706
+ // #clearRenderCache {{{2
1707
+
1708
+ /**
1709
+ * Clear the cache of the render on each cell.
1710
+ */
1711
+
1712
+ Grid.prototype.clearRenderCache = function (cols) {
1713
+ var self = this;
1714
+
1715
+ if (self.renderer != null) {
1716
+ self.renderer.clearRenderCache(cols);
1717
+ }
1718
+ };
1719
+
1720
+ // #_updateRowCount {{{2
1721
+
1722
+ /**
1723
+ * Set the number of rows shown in the titlebar. You can provider the number yourself!
1724
+ *
1725
+ * @method
1726
+ * @memberof Grid
1727
+ *
1728
+ * @param {object} info
1729
+ * @param {number} info.numRows
1730
+ * @param {number} info.totalRows
1731
+ * @param {number} info.numGroups
1732
+ * @param {number} info.numPivots
1733
+ * @param {boolean} info.isPlain
1734
+ * @param {boolean} info.isGroup
1735
+ * @param {boolean} info.isPivot
1736
+ *
1737
+ * @param {object} ops
1738
+ * Describes what the view did.
1739
+ *
1740
+ * @param {boolean} ops.filter
1741
+ * If true, then the view filtered data.
1742
+ *
1743
+ * @param {boolean} ops.group
1744
+ * If true, then the view grouped data.
1745
+ *
1746
+ * @param {boolean} ops.pivot
1747
+ * If true, then the view pivotted data.
1748
+ *
1749
+ * @param {boolean} ops.sort
1750
+ * If true, then the view sorted data.
1751
+ */
1752
+
1753
+ Grid.prototype._updateRowCount = function (info, ops) {
1754
+ var self = this;
1755
+ var doingServerFilter = getProp(self.defn, 'server', 'filter') && getProp(self.defn, 'server', 'limit') !== -1;
1756
+ var text = [];
1757
+
1758
+ self.logDebug(self.makeLogTag() + ' Updating row count');
1759
+
1760
+ // When there's no titlebar, there's nothing for us to do here.
1761
+
1762
+ if (!self.opts.title) {
1763
+ return;
1764
+ }
1765
+
1766
+ self._hideSpinner();
1767
+
1768
+ if (info.numRows != null) {
1769
+ if (info.totalRows != null) {
1770
+ text.push(info.numRows + ' / ' + trans(info.totalRows === 1 ? 'GRID.TITLEBAR.RECORD_COUNT_SINGULAR' : 'GRID.TITLEBAR.RECORD_COUNT_PLURAL', info.totalRows));
1771
+ }
1772
+ else {
1773
+ text.push(trans(info.numRows === 1 ? 'GRID.TITLEBAR.RECORD_COUNT_SINGULAR' : 'GRID.TITLEBAR.RECORD_COUNT_PLURAL', info.numRows));
1774
+ }
1775
+ }
1776
+
1777
+ if (info.isGroup || info.isPivot) {
1778
+ text.push(trans(info.numGroups === 1 ? 'GRID.TITLEBAR.GROUP_COUNT_SINGULAR' : 'GRID.TITLEBAR.GROUP_COUNT_PLURAL', info.numGroups));
1779
+ }
1780
+
1781
+ self.ui.rowCount.text(text.join(', '));
1782
+
1783
+ // When we have been auto-limited, show the banner message showing as much and prevent people from
1784
+ // grouping (because we don't have all the data, grouping / pivotting is misleading).
1785
+
1786
+ if (getProp(self.view, 'source', 'origin', 'isLimited')) {
1787
+ self.ui.autoLimit.show();
1788
+ self.ui.groupControl.hide();
1789
+ self.ui.toolbar_computedView.ui.storeMirageBtn.attr('disabled', true);
1790
+ }
1791
+ else {
1792
+ self.ui.autoLimit.hide();
1793
+ self.ui.groupControl.show();
1794
+ }
1795
+
1796
+ if (self.ui.clearFilter) {
1797
+ if (info.totalRows) {
1798
+ self.ui.clearFilter.show();
1799
+ }
1800
+ else {
1801
+ self.ui.clearFilter.hide();
1802
+ }
1803
+ }
1804
+
1805
+ self.ui.title._addTrailing(',');
1806
+ };
1807
+
1808
+ // #hide {{{2
1809
+
1810
+ /**
1811
+ * Hide the grid.
1812
+ *
1813
+ * @method
1814
+ * @memberof Grid
1815
+ */
1816
+
1817
+ Grid.prototype.hide = function () {
1818
+ var self = this;
1819
+
1820
+ self.logDebug(self.makeLogTag() + ' Hiding...');
1821
+
1822
+ self.ui.content.hide({
1823
+ duration: 0,
1824
+ done: function () {
1825
+ if (self.opts.title) {
1826
+ self.ui.showHideButton.removeClass('open');
1827
+ self.ui.showHideButton.children('svg.wcdv_icon').removeClass('wcdv_icon_rotate_180');
1828
+ }
1829
+ }
1830
+ });
1831
+ };
1832
+
1833
+ // #show {{{2
1834
+
1835
+ /**
1836
+ * Make the grid visible. If the grid has not been "run" yet, it will be done now.
1837
+ *
1838
+ * @param {object} [opts]
1839
+ *
1840
+ * @param {boolean} [opts.redraw=true]
1841
+ * If true, automatically redraw the grid after it has been shown. This is almost always what you
1842
+ * want, unless you intend to manually call `redraw()` or `refresh()` immediately after showing it.
1843
+ */
1844
+
1845
+ Grid.prototype.show = function (opts) {
1846
+ var self = this;
1847
+
1848
+ opts = deepDefaults(opts, {
1849
+ redraw: true
1850
+ });
1851
+
1852
+ self.logDebug(self.makeLogTag() + ' Showing...');
1853
+
1854
+ self.ui.content.show({
1855
+ duration: 0,
1856
+ done: function () {
1857
+ if (self.opts.title) {
1858
+ self.ui.showHideButton.addClass('open');
1859
+ self.ui.showHideButton.children('svg.wcdv_icon').addClass('wcdv_icon_rotate_180');
1860
+ }
1861
+ if (!self.hasRun && opts.redraw) {
1862
+ self.hasRun = true;
1863
+ self.redraw();
1864
+ }
1865
+ }
1866
+ });
1867
+ };
1868
+
1869
+ // #toggle {{{2
1870
+
1871
+ /**
1872
+ * Toggle grid visibility.
1873
+ */
1874
+
1875
+ Grid.prototype.toggle = function () {
1876
+ var self = this;
1877
+
1878
+ if (self.ui.content.css('display') === 'none') {
1879
+ self.show();
1880
+ }
1881
+ else {
1882
+ self.hide();
1883
+ }
1884
+ };
1885
+
1886
+ // #isVisible {{{2
1887
+
1888
+ /**
1889
+ * Determine if the grid is currently visible.
1890
+ *
1891
+ * @returns {boolean}
1892
+ * True if the grid is currently visible, false if it is not.
1893
+ */
1894
+
1895
+ Grid.prototype.isVisible = function () {
1896
+ var self = this;
1897
+
1898
+ return self.ui.content.css('display') !== 'none';
1899
+ };
1900
+
1901
+ // hideControls {{{2
1902
+
1903
+ Grid.prototype.hideControls = function () {
1904
+ var self = this;
1905
+
1906
+ if (self.ui.controls._isHidden()) {
1907
+ return;
1908
+ }
1909
+
1910
+ // We need this to happen after both of the async functions (to hide the
1911
+ // controls & toolbar) happen below.
1912
+
1913
+ var l = new Lock('Hide Controls', {start: 2});
1914
+ l.onUnlock(function () {
1915
+ if (window.Tabletool) {
1916
+ window.Tabletool.update();
1917
+ }
1918
+ }, 'Update Tabletool');
1919
+
1920
+ self.ui.controls.hide({
1921
+ duration: 0,
1922
+ complete: function () {
1923
+ self.fire(Grid.events.hideControls);
1924
+ l.unlock();
1925
+ }
1926
+ });
1927
+
1928
+ self.ui.toolbar.hide({
1929
+ duration: 0,
1930
+ complete: function () {
1931
+ //self.fire(Grid.events.hideToolbar);
1932
+ l.unlock();
1933
+ }
1934
+ });
1935
+ };
1936
+
1937
+ // showControls {{{2
1938
+
1939
+ Grid.prototype.showControls = function () {
1940
+ var self = this;
1941
+
1942
+ if (!self.ui.controls._isHidden()) {
1943
+ return;
1944
+ }
1945
+
1946
+ // We need this to happen after both of the async functions (to show the
1947
+ // controls & toolbar) happen below.
1948
+
1949
+ var l = new Lock('Show Controls', {start: 2});
1950
+ l.onUnlock(function () {
1951
+ if (window.Tabletool) {
1952
+ window.Tabletool.update();
1953
+ }
1954
+ }, 'Update Tabletool');
1955
+
1956
+ self.ui.controls.show({
1957
+ duration: 0,
1958
+ complete: function () {
1959
+ self.fire(Grid.events.showControls);
1960
+ l.unlock();
1961
+ }
1962
+ });
1963
+
1964
+ self.ui.toolbar.show({
1965
+ duration: 0,
1966
+ complete: function () {
1967
+ //self.fire(Grid.events.showToolbar);
1968
+ l.unlock();
1969
+ }
1970
+ });
1971
+ };
1972
+
1973
+ // toggleControls {{{2
1974
+
1975
+ Grid.prototype.toggleControls = function () {
1976
+ var self = this;
1977
+
1978
+ if (self.ui.controls._isHidden()) {
1979
+ self.showControls();
1980
+ }
1981
+ else {
1982
+ self.hideControls();
1983
+ }
1984
+ };
1985
+
1986
+ // #_setSpinner {{{2
1987
+
1988
+ /**
1989
+ * Set the type of the spinner icon.
1990
+ *
1991
+ * @param {string} what
1992
+ * The kind of spinner icon to show. Must be one of: loading, not-loaded, working.
1993
+ */
1994
+
1995
+ Grid.prototype._setSpinner = function (what) {
1996
+ var self = this;
1997
+
1998
+ switch (what) {
1999
+ case 'loading':
2000
+ self.ui.spinner.html(icon('refresh-cw', ['wcdv_icon_spin'], trans('GRID.TITLEBAR.LOADING')));
2001
+ break;
2002
+ case 'not-loaded':
2003
+ self.ui.spinner.html(icon('ban', null, trans('GRID.TITLEBAR.NOT_LOADED')));
2004
+ break;
2005
+ case 'working':
2006
+ self.ui.spinner.html(icon('loader-circle', ['wcdv_icon_spin'], trans('GRID.TITLEBAR.WORKING')));
2007
+ break;
2008
+ }
2009
+ };
2010
+
2011
+ // #_showSpinner {{{2
2012
+
2013
+ /**
2014
+ * Show the spinner icon.
2015
+ */
2016
+
2017
+ Grid.prototype._showSpinner = function () {
2018
+ var self = this;
2019
+
2020
+ if (self.opts.title) {
2021
+ self.ui.spinner.show();
2022
+ }
2023
+ };
2024
+
2025
+ // #_hideSpinner {{{2
2026
+
2027
+ /**
2028
+ * Hide the spinner icon.
2029
+ */
2030
+
2031
+ Grid.prototype._hideSpinner = function () {
2032
+ var self = this;
2033
+
2034
+ if (self.opts.title) {
2035
+ self.ui.spinner.hide();
2036
+ }
2037
+ };
2038
+
2039
+ // #_normalize {{{2
2040
+
2041
+ /**
2042
+ * The point of "normalizing" a definition is to expand shortcut configurations. For example, lots
2043
+ * of properties can be a string (the shortcut) or an object which contains the same info plus some
2044
+ * additional configuration. This function would convert the string into the object. This way,
2045
+ * later code only has to check for the object version. It also adds a layer of backwards
2046
+ * compatibility.
2047
+ *
2048
+ * You only need to normalize a definition once; after doing so, we flag it so we won't mess with it
2049
+ * again, even though it should be possible to normalize something that's already been done.
2050
+ */
2051
+
2052
+ Grid.prototype._normalize = function (defn) {
2053
+ var self = this;
2054
+
2055
+ if (defn == null) {
2056
+ defn = {};
2057
+ }
2058
+
2059
+ if (defn.normalized) {
2060
+ return;
2061
+ }
2062
+
2063
+ defn.normalized = true;
2064
+
2065
+ deepDefaults(true, defn, {
2066
+ prefs: null,
2067
+ table: {
2068
+ groupMode: 'detail',
2069
+ rowMode: 'wrapped',
2070
+ features: {
2071
+ sort: true,
2072
+ filter: true,
2073
+ group: true,
2074
+ pivot: true,
2075
+ rowSelect: false,
2076
+ rowReorder: false,
2077
+ add: false,
2078
+ edit: false,
2079
+ delete: false,
2080
+ limit: true,
2081
+ floatingHeader: true,
2082
+ block: false,
2083
+ progress: false,
2084
+ columnResize: false,
2085
+ columnReorder: false,
2086
+ activeRow: false,
2087
+ omnifilter: true
2088
+ },
2089
+ limit: {
2090
+ appendBodyLast: false,
2091
+ method: 'more',
2092
+ threshold: 100,
2093
+ chunkSize: 50
2094
+ },
2095
+ floatingHeader: {
2096
+ method: 'tabletool'
2097
+ },
2098
+ incremental: {
2099
+ method: 'setTimeout',
2100
+ delay: 10,
2101
+ chunkSize: 100
2102
+ },
2103
+ activeRow: {
2104
+ slider: true
2105
+ }
2106
+ }
2107
+ });
2108
+
2109
+ self._normalizeColumns(defn);
2110
+
2111
+ return defn;
2112
+ };
2113
+
2114
+ // #_normalizeColumns {{{2
2115
+
2116
+ Grid.prototype._normalizeColumns = function (defn) {
2117
+ var self = this;
2118
+
2119
+ // When the developer did not provider column configuration, take it from the ComputedView via typeInfo.
2120
+ // Potentially the source could change what fields it contains (e.g. add/remove a field to/from a
2121
+ // report) and this would all still work OK, we would stay up-to-date because every time the ComputedView
2122
+ // got new typeInfo we would update our colConfig.
2123
+
2124
+ if (getProp(defn, 'table', 'columns') == null) {
2125
+ self.initColConfig = null;
2126
+ self.colConfig = null;
2127
+ return;
2128
+ }
2129
+
2130
+ var colConfig = new OrdMap();
2131
+
2132
+ for (var i = 0; i < defn.table.columns.length; i += 1) {
2133
+ var cc = defn.table.columns[i];
2134
+
2135
+ if (_.isString(cc)) {
2136
+ cc = { field: cc };
2137
+ }
2138
+
2139
+ if (typeof cc.field !== 'string') {
2140
+ self.logWarning(self.makeLogTag() + ' Column Configuration: `field` must be a string');
2141
+ continue;
2142
+ }
2143
+
2144
+ cc = deepDefaults(cc, {
2145
+ hideMidnight: false,
2146
+ format_dateOnly: 'LL',
2147
+ allowHtml: false,
2148
+ allowFormatting: false,
2149
+ canHide: true
2150
+ });
2151
+
2152
+ colConfig.set(cc.field, cc);
2153
+ }
2154
+
2155
+ self.initColConfig = colConfig.clone();
2156
+
2157
+ _.each(getPropDef([], defn, 'table', 'columnConfig'), function (cc, colName) {
2158
+
2159
+ // When you want to show a checkbox to represent the value, it only makes sense to have a
2160
+ // checkbox for the filter widget.
2161
+
2162
+ if (cc.widget === 'checkbox') {
2163
+ if (cc.filter !== undefined && cc.filter !== 'checkbox') {
2164
+ self.logWarning(self.makeLogTag() + ' Overriding configuration to use filter type "' + cc.filter + '" for checkbox widgets.');
2165
+ }
2166
+ cc.filter = 'checkbox';
2167
+ }
2168
+ });
2169
+
2170
+ self.setColConfig(colConfig, {
2171
+ from: 'defn',
2172
+ savePrefs: false
2173
+ });
2174
+ };
2175
+
2176
+ // #export {{{2
2177
+
2178
+ /**
2179
+ * Export whatever this grid is currently showing as a CSV file for the user to download.
2180
+ */
2181
+
2182
+ Grid.prototype.export = function () {
2183
+ var self = this;
2184
+
2185
+ if (self.exportLock.isLocked()) {
2186
+ return;
2187
+ }
2188
+
2189
+ if (self.csvReady) {
2190
+ var fileName = (self.opts.title || self.id) + '.csv';
2191
+ var csv = self.renderer.getCsv();
2192
+ var contentType = 'text/csv';
2193
+ var blob = new Blob([csv], {'type': contentType});
2194
+
2195
+ presentDownload(blob, fileName);
2196
+ }
2197
+ else {
2198
+ self.exportLock.lock(); // Unlocked in `csvReady` event handler.
2199
+ self.generateCsv = true;
2200
+ self.redraw();
2201
+ }
2202
+ };
2203
+
2204
+ // #_setExportStatus {{{2
2205
+
2206
+ Grid.prototype._setExportStatus = function (status) {
2207
+ var self = this;
2208
+
2209
+ switch (status) {
2210
+ case 'notReady':
2211
+ self.csvReady = false;
2212
+ self.ui.exportBtn.attr('title', trans('GRID.TITLEBAR.GENERATE_CSV'));
2213
+ self.ui.exportBtn.children('svg.wcdv_icon').remove();
2214
+ self.ui.exportBtn.append(icon('file'));
2215
+ break;
2216
+ case 'ready':
2217
+ self.csvReady = true;
2218
+ self.ui.exportBtn.attr('title', trans('GRID.TITLEBAR.DOWNLOAD_CSV'));
2219
+ self.ui.exportBtn.children('svg.wcdv_icon').remove();
2220
+ self.ui.exportBtn.append(icon('download'));
2221
+ break;
2222
+ default:
2223
+ throw new Error('Call Error: invalid status "' + status + '"');
2224
+ }
2225
+ };
2226
+
2227
+ // #setColConfig {{{2
2228
+
2229
+ /**
2230
+ * Set the column configuration.
2231
+ *
2232
+ * @param {OrdMap} colConfig
2233
+ * @param {Object} opts
2234
+ * @param {string} opts.from
2235
+ * @param {boolean} [opts.sendEvent=true]
2236
+ * @param {Array.<Object>} [opts.dontSendEventTo]
2237
+ * @param {boolean} [opts.redraw=true]
2238
+ * @param {boolean} [opts.savePrefs=true]
2239
+ */
2240
+
2241
+ Grid.prototype.setColConfig = function (colConfig, opts) {
2242
+ var self = this;
2243
+ var updated = false;
2244
+
2245
+ if (['defn', 'prefs', 'typeInfo', 'ui', 'reset', 'autoResizeCols'].indexOf(opts.from) < 0) {
2246
+ throw new Error('Call Error: `opts.from` must be one of: [defn, prefs, typeInfo, ui, reset]');
2247
+ }
2248
+
2249
+ opts = deepDefaults(opts, {
2250
+ sendEvent: true,
2251
+ dontSendEventTo: [],
2252
+ redraw: true,
2253
+ savePrefs: true
2254
+ });
2255
+
2256
+ // We use the colConfig lock so that we don't have a bunch of processes updating the colConfig
2257
+ // when we're trying to redraw the grid. If we already have a renderer, it's going to be get
2258
+ // replaced by `Grid#redraw()` so we shouldn't send an event to the renderer to have it redraw.
2259
+
2260
+ if (self.colConfigLock.isLocked() && self.renderer) {
2261
+ opts.dontSendEventTo.push(self.renderer);
2262
+ }
2263
+
2264
+ var setCurrent = function () {
2265
+ self.logDebug(self.makeLogTag('colConfig') + ' Setting from %s: %O', opts.from || '[unknown]', colConfig);
2266
+ self.colConfig = colConfig;
2267
+ self.colConfigSource = opts.from;
2268
+
2269
+ if (self.renderer != null) {
2270
+ self.renderer.colConfig = self.colConfig;
2271
+ }
2272
+
2273
+ self.logDebug(self.makeLogTag('colConfig') + ' Setting shadow from %s: %O', opts.from || '[unknown]', colConfig);
2274
+ self.shadowColConfig = colConfig.clone();
2275
+ updated = true;
2276
+ };
2277
+
2278
+ var setInitial = function () {
2279
+ self.logDebug(self.makeLogTag('colConfig') + ' Setting initial from %s: %O', opts.from || '[unknown]', colConfig);
2280
+ self.initColConfig = colConfig.clone();
2281
+ };
2282
+
2283
+ /**
2284
+ * Add elements (that are absent in `dst`) from `src` to `dst`.
2285
+ *
2286
+ * @param {OrdMap} src
2287
+ * @param {string} srcMsg
2288
+ * @param {OrdMap} dst
2289
+ * @param {string} dstMsg
2290
+ */
2291
+
2292
+ var addMissing = function (src, srcMsg, dst, dstMsg) {
2293
+ var count = dst.mergeWith(src);
2294
+ self.logDebug(self.makeLogTag('colConfig') + ' Merged %d fields from %s into %s', count, srcMsg, dstMsg);
2295
+ return count;
2296
+ };
2297
+
2298
+ /**
2299
+ * Remove elements from `dst` that are absent from `src`.
2300
+ *
2301
+ * @param {OrdMap} src
2302
+ * @param {string} srcMsg
2303
+ * @param {OrdMap} dst
2304
+ * @param {string} dstMsg
2305
+ */
2306
+
2307
+ var removeMissing = function (src, srcMsg, dst, dstMsg) {
2308
+ var absent = [];
2309
+
2310
+ dst.each(function (fcc, fieldName) {
2311
+ if (!src.isSet(fieldName)) {
2312
+ absent.push(fieldName);
2313
+ }
2314
+ });
2315
+
2316
+ if (absent.length > 0) {
2317
+ self.logDebug(self.makeLogTag('colConfig') + ' Removing %d fields from %s which are absent from %s: %O',
2318
+ absent.length, dstMsg, srcMsg, absent);
2319
+ _.each(absent, function (fieldName) {
2320
+ dst.unset(fieldName);
2321
+ });
2322
+ return true;
2323
+ }
2324
+
2325
+ return false;
2326
+ };
2327
+
2328
+ if (typeof getProp(self.defn, 'advice', 'setColConfig', 'before') === 'function') {
2329
+ self.defn.advice.setColConfig.before(colConfig, opts.from, self);
2330
+ }
2331
+
2332
+ switch (opts.from) {
2333
+ case 'defn':
2334
+ setCurrent();
2335
+ setInitial();
2336
+ self.colConfigRestricted = true;
2337
+ break;
2338
+ case 'prefs':
2339
+ if (self.colConfigRestricted) {
2340
+ self.colConfig.each(function (v, k) {
2341
+ if (colConfig.isSet(k)) {
2342
+ _.defaults(colConfig.get(k), v);
2343
+ }
2344
+ });
2345
+
2346
+ // The column configuration is restricted by defn, so remove anything from prefs that's
2347
+ // missing from defn.
2348
+
2349
+ removeMissing(self.colConfig, 'defn', colConfig, 'prefs');
2350
+
2351
+ // Add anything that's in defn but not in prefs.
2352
+
2353
+ addMissing(self.colConfig, 'defn', colConfig, 'prefs');
2354
+ }
2355
+
2356
+ setCurrent();
2357
+ break;
2358
+ case 'reset':
2359
+ case 'ui':
2360
+ case 'autoResizeCols':
2361
+ setCurrent();
2362
+ break;
2363
+ case 'typeInfo':
2364
+ // Column configuration derived from typeInfo merges with existing config (by removing config on
2365
+ // columns that don't exist in the source, and by adding defaults for columns that exist in the
2366
+ // source but aren't specified in the current config). It can also set the initial, filling in
2367
+ // when no defn is specified.
2368
+
2369
+ if (self.colConfig == null) {
2370
+ setCurrent();
2371
+ }
2372
+ else {
2373
+ self.colConfig = self.shadowColConfig.clone();
2374
+ if (self.renderer != null) {
2375
+ self.renderer.colConfig = self.colConfig;
2376
+ }
2377
+
2378
+ // Delete fields from existing colConfig which aren't in the source.
2379
+
2380
+ if (removeMissing(colConfig, 'source', self.colConfig, 'existing')) {
2381
+ updated = true;
2382
+ }
2383
+
2384
+ // Add fields from source that are missing from existing colConfig. Columns set explicitly in
2385
+ // the grid's definition are there to limit what we see, so don't try to add to them.
2386
+
2387
+ if (!self.colConfigRestricted) {
2388
+ if (addMissing(colConfig, 'source', self.colConfig, 'existing')) {
2389
+ updated = true;
2390
+ }
2391
+ }
2392
+ }
2393
+ if (self.initColConfig == null) {
2394
+ setInitial();
2395
+ }
2396
+ break;
2397
+ }
2398
+
2399
+ if (!updated) {
2400
+ return;
2401
+ }
2402
+
2403
+ if (opts.savePrefs) {
2404
+ self.prefs.save();
2405
+ }
2406
+
2407
+ if (opts.sendEvent) {
2408
+ self.fire('colConfigUpdate', {
2409
+ notTo: opts.dontSendEventTo
2410
+ }, self.colConfig, self.initColConfig, ['autoResizeCols'].indexOf(opts.from) >= 0 ? false : true);
2411
+ }
2412
+
2413
+ if (opts.redraw) {
2414
+ //self.redraw();
2415
+ }
2416
+ };
2417
+
2418
+ // #getColConfig {{{2
2419
+
2420
+ Grid.prototype.getColConfig = function (colConfig) {
2421
+ var self = this;
2422
+
2423
+ return self.colConfig;
2424
+ };
2425
+
2426
+ // #resetColConfig {{{2
2427
+
2428
+ Grid.prototype.resetColConfig = function (opts) {
2429
+ var self = this;
2430
+
2431
+ self.logDebug(self.makeLogTag('colConfig') + ' Resetting to: %O', self.initColConfig);
2432
+
2433
+ opts = deepDefaults(opts, {
2434
+ from: 'reset',
2435
+ savePrefs: false
2436
+ });
2437
+
2438
+ self.setColConfig(self.initColConfig.clone(), opts);
2439
+ };
2440
+
2441
+ // #setRowMode {{{2
2442
+
2443
+ /**
2444
+ * Set the row display mode for the grid.
2445
+ *
2446
+ * @param {string} mode
2447
+ * The row mode to use. Must be either "wrapped" (default) or "clipped".
2448
+ * - "wrapped": Cells can wrap text to multiple lines (default behavior)
2449
+ * - "clipped": Single-line cells with text truncated (similar to AG Grid)
2450
+ */
2451
+
2452
+ Grid.prototype.setRowMode = function (mode) {
2453
+ var self = this;
2454
+
2455
+ if (['wrapped', 'clipped'].indexOf(mode) < 0) {
2456
+ self.logWarn(self.makeLogTag('setRowMode') + ' Invalid row mode "' + mode + '". Using "wrapped" as default.');
2457
+ mode = 'wrapped';
2458
+ }
2459
+
2460
+ self.defn.table.rowMode = mode;
2461
+ self.logDebug(self.makeLogTag('setRowMode') + ' Setting row mode to: %s', mode);
2462
+
2463
+ // Update the CSS class on the grid table container
2464
+ if (self.ui && self.ui.grid) {
2465
+ self.ui.grid.removeClass('wcdv_row_mode_wrapped wcdv_row_mode_clipped');
2466
+ self.ui.grid.addClass('wcdv_row_mode_' + mode);
2467
+ }
2468
+
2469
+ // Fire an event for any listeners
2470
+ self.fire('rowModeChange', mode);
2471
+ };
2472
+
2473
+ // #getRowMode {{{2
2474
+
2475
+ /**
2476
+ * Get the current row display mode for the grid.
2477
+ *
2478
+ * @returns {string} The current row mode ("wrapped" or "clipped").
2479
+ */
2480
+
2481
+ Grid.prototype.getRowMode = function () {
2482
+ var self = this;
2483
+
2484
+ return getPropDef('wrapped', self.defn, 'table', 'rowMode');
2485
+ };
2486
+
2487
+ // #isIdle {{{2
2488
+
2489
+ /**
2490
+ * Ask the grid whether there are currently any pending operations that would change the UI.
2491
+ *
2492
+ * Caveats:
2493
+ *
2494
+ * - If you yield after checking this, then it's no longer guaranteed to be true; some other
2495
+ * asynchronous event could cause the grid to become active.
2496
+ *
2497
+ * - If you have `renderEnd` event handlers that yield, it is possible that those event handlers
2498
+ * will continue executing after the grid has been marked idle.
2499
+ *
2500
+ * @returns {boolean} True if the grid is currently idle, false if there are changes pending which
2501
+ * might cause the grid to be redrawn.
2502
+ */
2503
+
2504
+ Grid.prototype.isIdle = function () {
2505
+ var self = this;
2506
+
2507
+ return self._isIdle;
2508
+ };
2509
+
2510
+ // #colConfigFromTypeInfo {{{2
2511
+
2512
+ Grid.prototype.colConfigFromTypeInfo = function (typeInfo, opts) {
2513
+ var self = this;
2514
+
2515
+ opts = deepDefaults(opts, {
2516
+ from: 'typeInfo',
2517
+ savePrefs: false
2518
+ });
2519
+
2520
+ if (!(typeInfo instanceof OrdMap)) {
2521
+ throw new Error('Call Error: `typeInfo` must be an OrdMap');
2522
+ }
2523
+
2524
+ var typeInfoColConfig = new OrdMap();
2525
+
2526
+ typeInfo.each(function (fti, fieldName) {
2527
+ typeInfoColConfig.set(fieldName, {
2528
+ field: fieldName
2529
+ });
2530
+ });
2531
+
2532
+ self.logDebug(self.makeLogTag() + ' Creating colConfig from typeInfo: %O -> %O', typeInfo.asMap(), typeInfoColConfig.asMap());
2533
+
2534
+ //self.setColConfig(self.colConfig == null
2535
+ // ? typeInfoColConfig
2536
+ // : OrdMap.fromMerge([self.colConfig, typeInfoColConfig]), opts);
2537
+ self.setColConfig(typeInfoColConfig, opts);
2538
+ };
2539
+
2540
+ // #setOperations {{{2
2541
+
2542
+ Grid.prototype.setOperations = function (ops) {
2543
+ var self = this;
2544
+
2545
+ if (self.operationsPalette != null) {
2546
+ self.operationsPalette.setOperations(ops);
2547
+ }
2548
+
2549
+ self.defn.operations = ops;
2550
+
2551
+ // We need to redraw the grid because operations that affect one row at a time might change,
2552
+ // therefore the buttons in the row need to be redrawn.
2553
+
2554
+ self.redraw();
2555
+ };
2556
+
2557
+ // #makeResponsive {{{2
2558
+
2559
+ Grid.prototype.makeResponsive = function () {
2560
+ var self = this;
2561
+
2562
+ if (window.ResizeObserver == null) {
2563
+ self.logWarning(self.makeLogTag() + ' ResizeObserver is not supported; grid will not be responsive.');
2564
+ return;
2565
+ }
2566
+
2567
+ var timer;
2568
+
2569
+ // We use a timer to create a delay, so the page has to be "still" for 500ms before we'll try to
2570
+ // redraw the grid with a different renderer.
2571
+
2572
+ self.resizeObserver = new ResizeObserver(function (elts) {
2573
+ if (timer != null) {
2574
+ clearTimeout(timer);
2575
+ }
2576
+ timer = setTimeout(function () {
2577
+ timer = null;
2578
+ var renderer = self.findRenderer(elts[0].contentRect.width, self.mode);
2579
+ if (renderer.id !== self.rendererId) {
2580
+ self.logDebug(self.makeLogTag() + ' Resized to ' + elts[0].contentRect.width + '; using renderer: %O', renderer);
2581
+ self.redraw();
2582
+ }
2583
+ }, 500);
2584
+ });
2585
+
2586
+ self.resizeObserver.observe(self.ui.root.get(0));
2587
+ };
2588
+
2589
+ // #addRenderer {{{2
2590
+
2591
+ /**
2592
+ * @typedef RendererSpec
2593
+ * Either `name` or `fn` must exist.
2594
+ *
2595
+ * @prop {string} [name]
2596
+ * Name of the renderer to use; must be registered in {@see GridRenderer.registry}.
2597
+ *
2598
+ * @prop {function} [fn]
2599
+ * A nullary function that returns the name of the name of a renderer registered in
2600
+ * {@see GridRenderer.registry}.
2601
+ *
2602
+ * @prop {object} [opts]
2603
+ * Additional options to pass to the renderer contructor.
2604
+ */
2605
+
2606
+ /**
2607
+ * Adds a new renderer to the grid.
2608
+ *
2609
+ * @param {number} minWidth
2610
+ * The minimum width at which this renderer will work.
2611
+ *
2612
+ * @param {string[]} modes
2613
+ * List of modes for which this renderer will work.
2614
+ *
2615
+ * @param {RendererSpec} renderer
2616
+ * Specification of the renderer to add.
2617
+ */
2618
+
2619
+ Grid.prototype.addRenderer = (function () {
2620
+ var id = 1;
2621
+
2622
+ return function (minWidth, modes, renderer) {
2623
+ var self = this
2624
+ , i;
2625
+
2626
+ renderer.id = 'CUSTOM.' + id++;
2627
+
2628
+ for (i = 0; i < self.widthBreaks.length; i += 1) {
2629
+ if (minWidth < self.widthBreaks[i].minWidth) {
2630
+ // Insert at the appropriate place in the list.
2631
+
2632
+ self.widthBreaks.splice(i, 0, {
2633
+ minWidth: minWidth,
2634
+ modes: modes,
2635
+ renderer: renderer
2636
+ });
2637
+
2638
+ return;
2639
+ }
2640
+ }
2641
+
2642
+ // New entry has the largest minWidth in the list, put it at the end.
2643
+
2644
+ self.widthBreaks.splice(-1, 0, {
2645
+ minWidth: minWidth,
2646
+ modes: modes,
2647
+ renderer: renderer
2648
+ });
2649
+ };
2650
+ })();
2651
+
2652
+ // #clearRenderers {{{2
2653
+
2654
+ /**
2655
+ * Completely clears all grid renderers.
2656
+ */
2657
+
2658
+ Grid.prototype.clearRenderers = function () {
2659
+ var self = this;
2660
+
2661
+ self.widthBreaks = [];
2662
+ };
2663
+
2664
+ // #resetRenderers {{{2
2665
+
2666
+ /**
2667
+ * Resets the list of grid renderers to the initial state.
2668
+ */
2669
+
2670
+ Grid.prototype.resetRenderers = function () {
2671
+ var self = this;
2672
+
2673
+ self.widthBreaks = [{
2674
+ minWidth: 1024,
2675
+ modes: ['plain'],
2676
+ renderer: {
2677
+ name: 'table_plain',
2678
+ opts: getPropDef({}, self.defn, 'table', 'whenPlain')
2679
+ }
2680
+ }, {
2681
+ minWidth: 1024,
2682
+ modes: ['group'],
2683
+ renderer: {
2684
+ fn: function () {
2685
+ switch (self.defn.table.groupMode) {
2686
+ case 'summary':
2687
+ return {
2688
+ name: 'table_group_summary',
2689
+ opts: getPropDef({}, self.defn, 'table', 'whenGroup')
2690
+ };
2691
+ case 'detail':
2692
+ return {
2693
+ name: 'table_group_detail',
2694
+ opts: getPropDef({}, self.defn, 'table', 'whenGroup')
2695
+ };
2696
+ }
2697
+ }
2698
+ }
2699
+ }, {
2700
+ minWidth: 1024,
2701
+ modes: ['pivot'],
2702
+ renderer: {
2703
+ name: 'table_pivot',
2704
+ opts: getPropDef({}, self.defn, 'table', 'whenPivot')
2705
+ }
2706
+ }];
2707
+ };
2708
+
2709
+ // #findRenderer {{{2
2710
+
2711
+ /**
2712
+ * Find a renderer suitable for drawing the grid. A "suitable" renderer is one that (1) can handle
2713
+ * the data `mode`, and (2) has a `minWidth` property less than the current width. If no such
2714
+ * renderer exists, we pick one that can handle the data, at the smallest `minWidth` available. If
2715
+ * there still aren't any renderers available (e.g. if the developer cleared the list) then null is
2716
+ * returned.
2717
+ *
2718
+ * @param {number} width
2719
+ * The width of the grid.
2720
+ *
2721
+ * @param {string} mode
2722
+ * What type of data we're displaying. Must be one of: plain, group, pivot.
2723
+ *
2724
+ * @returns {RendererSpec}
2725
+ * A renderer that can be used to display the grid. Returns null if there aren't any options.
2726
+ */
2727
+
2728
+ Grid.prototype.findRenderer = function (width, mode) {
2729
+ var self = this,
2730
+ i, b;
2731
+
2732
+ var processRenderer = function (r) {
2733
+ var x = deepCopy(r);
2734
+ delete x.fn;
2735
+
2736
+ // If the `fn` property exists, call it to get properties that can override (or supplement)
2737
+ // those in the "main" object. This is how you can set the group renderer depending on whether
2738
+ // the grid is in summary or details mode.
2739
+
2740
+ if (typeof r.fn === 'function') {
2741
+ var spec = r.fn();
2742
+ x = deepDefaults(spec, x);
2743
+ }
2744
+
2745
+ return x;
2746
+ };
2747
+
2748
+ if (self.widthBreaks == null || self.widthBreaks.length === 0) {
2749
+ return null;
2750
+ }
2751
+
2752
+ // Find the entry with the largest minWidth that's still less than the current width, which also
2753
+ // supports the mode we're currently in.
2754
+
2755
+ for (i = self.widthBreaks.length - 1; i >= 0; i -= 1) {
2756
+ b = self.widthBreaks[i];
2757
+ if (b.minWidth <= width && (b.modes == null || b.modes.indexOf(mode) >= 0)) {
2758
+ return processRenderer(b.renderer);
2759
+ }
2760
+ }
2761
+
2762
+ // There aren't any renderers with a minWidth less than the current width; start at the bottom and
2763
+ // find the smallest that can handle the data.
2764
+
2765
+ for (i = 0; i < self.widthBreaks.length; i += 1) {
2766
+ b = self.widthBreaks[i];
2767
+ if (b.modes == null || b.modes.indexOf(mode) >= 0) {
2768
+ return processRenderer(b.renderer);
2769
+ }
2770
+ }
2771
+ };
2772
+
2773
+ // Exports {{{1
2774
+
2775
+ export {
2776
+ Grid
2777
+ };