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