-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathDataSourceTreeview.js
More file actions
347 lines (306 loc) · 14.1 KB
/
DataSourceTreeview.js
File metadata and controls
347 lines (306 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
'use strict';
var DataSourceIndexed = require('./DataSourceIndexed');
/** @typedef columnAddress
* @property {string} name - The name of a column listed in the fields array. See the {@link DataSourceTreeview#getFields|getFields()} method.
* @property {number} index - The index of the column in the fields array. See the {@link DataSourceTreeview#getFields|getFields()} method.
*/
/**
* @classdesc For proper sorting, include `DataSourceTreeviewSorter` in your data source pipeline, _ahead of_ (closer to the data than) this data source.
*
* For proper filtering, include `DataSourceTreeviewFilter` in your data source pipeline, _ahead of_ `DataSourceTreeviewSorter`, if included; or at any rate ahead of this data source.
* @constructor
* @param dataSource
* @extends DataSourceIndexed
*/
var DataSourceTreeview = DataSourceIndexed.extend('DataSourceTreeview', {
/** @summary Initialize a new instance.
* @desc Set up {@link DataSourceTreeviewSorter} access to this object. Access is provided to the whole object although only instance variables `joined`, `idColumn`, and `parentIdColumn` are needed by the sorter. The two ID columns are passed to the {@link DataSourceDepthSorter} constructor. (If dataSource is not the sorter, this is not used but harmless.)
*
* Note that all ancestor classes' `initialize` methods are called (top-down) before this one. See {@link http://npmjs.org/extend-me} for more info.
* @param dataSource
* @memberOf DataSourceTreeview#
*/
initialize: function(dataSource) {
while (dataSource) {
if (/treeview/i.test(dataSource.$$CLASS_NAME)) {
dataSource.treeview = this;
}
dataSource = dataSource.dataSource;
}
},
/** @summary Reference to the primary key column address object.
* @desc The primary key column uniquely identifies a data row.
* Used to relate a child row to a parent row.
*
* Redefined each time tree-view is turned *ON* by a call to {@link DataSourceTreeview#setRelation|setRelation()}.
* @param {number|string} indexOrName
* @returns {columnAddress} Getter returns column address object; setter however always returns its input.
*/
set idColumn(indexOrName) {
this._idColumn = this.getColumnInfo(indexOrName || 'ID');
},
get idColumn() {
return this._idColumn;
},
/** @summary Reference to the foreign key column address object.
* @desc The foreign key column defines grouping; it relates this tree node row to its parent tree node row. Top-level tree nodes have no parent. In that case the value in the column is `null`.
*
* Redefined each time tree-view is turned *ON* by a call to {@link DataSourceTreeview#setRelation|setRelation()}.
* @param {number|string} indexOrName
* @returns {columnAddress} Getter returns column address object; setter however always returns its input.
*/
set parentIdColumn(indexOrName) {
this._parentIdColumn = this.getColumnInfo(indexOrName || 'parentID');
},
get parentIdColumn() {
return this._parentIdColumn;
},
/** @summary Reference to the drill-down column address object.
* @desc The drill-down column is the column that is indented and decorated with drill-down controls (triangles). A column with the given index or name must exist.
*
* Redefined each time tree-view is turned *ON* by a call to {@link DataSourceTreeview#setRelation|setRelation()}.
* @param {number|string} indexOrName
* @returns {columnAddress} Getter returns column address object; setter however always returns its input.
*/
set treeColumn(indexOrName) {
this._treeColumn = this.getColumnInfo(indexOrName || 'name');
},
get treeColumn() {
return this._treeColumn;
},
/**
/** @summary Reference to the group name column address object.
* @desc The group name column is the column whose content describes the group. A column with the given index or name must exist.
*
* The treeview sorter treats the group name column differently than other columns,
* apply a "group sort" to it, which means only the group rows (rows with children)
* are sorted and the leaves are left alone (stable sorted).
*
* Normally refers to the same column as {@link DataSourceTreeview#treeColumn|treeColumn}.
*
* Redefined each time tree-view is turned *ON* by a call to {@link DataSourceTreeview#setRelation|setRelation()}.
* @param {number|string} indexOrName
* @returns {columnAddress} Getter returns column address object; setter however always returns its input.
*/
set groupColumn(indexOrName) {
this._groupColumn = this.getColumnInfo(indexOrName || this._treeColumn.name);
},
get groupColumn() {
return this._groupColumn;
},
/**
* TEMPORARY. This function included here until next version of base is published.
* The change was to use schema rather than getFields().
* (The current version in base is not in use because it's only used from here.)
*
* Get new object with name and index given the name or the index.
* @param {string|number} columnOrIndex - Column name or index.
* @returns {{name: string, index: number}}
*/
getColumnInfo: function(columnOrIndex) {
var name, index, result;
if (typeof columnOrIndex === 'number') {
index = columnOrIndex;
name = this.schema[index].name;
} else {
name = columnOrIndex;
index = this.schema.findIndex(function(columnSchema) {
return columnSchema.name === name;
});
}
if (name && index >= 0) {
result = {
name: name,
index: index
};
}
return result;
},
/**
* @summary Toggle the tree-view.
* @desc Calculates or recalculates nesting depth of each row and marks it as "expandable" iff it has children.
*
* If resetting previously set data, the state of expansion of all rows that still have children is retained. (All expanded rows will still be expanded when tree-view is turned back *ON*.)
*
* All of the columns referenced by the `options` properties `idColumn`, `parentIdColumn`, `treeColumn`, and `groupColumn` must exist. These four columns have default references (names) as listed below. The references may be overridden in `options` by supplying alternate column names or indexes.
*
* @param {boolean|object} [options] - Falsy value (or omitted) turns tree-view **OFF**. Truthy value turns tree-view **ON** using following options:
* @param {number|string} [options.idColumn='ID'] - Name or index of the primary key column.
* @param {number|string} [options.parentIdColumn='parentID'] - Name or index of the foreign key column for grouping.
* @param {number|string} [options.treeColumn='name'] - Name or index of the drill-down column to decorate with triangles.
* @param {number|string} [options.groupColumn=this._treeColumn.name] - Name or index of the column that contains the group names. This is normally the same as the drill-down column. You only need to specify a different value when you want the drill down to this column, such as when the drill-down is in a column of its own. See {@link http://openfin.github.io/fin-hypergrid/tree-view-separate-drill-down.html} for an example.
* @returns {boolean} Joined state.
*
* @memberOf DataSourceTreeview#
*/
setRelation: function(options) {
var r, parentID, depth, leafRow, row, ID;
// successful join requires that options object be given and that all columns exist
if (!options) {
this.joined = false;
} else {
this.idColumn = options.idColumn;
this.parentIdColumn = options.parentIdColumn;
this.treeColumn = options.treeColumn;
this.groupColumn = options.groupColumn;
this.joined = !!(this.idColumn && this.parentIdColumn && this.treeColumn && this.groupColumn);
}
this.buildIndex(); // make all rows visible to getRow()
r = this.getRowCount();
if (this.joined) {
// mutate data row with __DEPTH (all rows) and __EXPANDED (all "parent" rows)
var idColumnName = this.idColumn.name,
parentIdColumnName = this.parentIdColumn.name;
this.maxDepth = 0;
while (r--) {
depth = 0;
leafRow = this.getRow(r);
row = leafRow;
ID = row[idColumnName];
while ((parentID = row[parentIdColumnName]) != null) {
row = this.findRow(idColumnName, parentID);
++depth;
}
if (this.maxDepth < depth) {
this.maxDepth = depth;
}
leafRow.__DEPTH = depth;
if (!this.findRow(parentIdColumnName, ID)) {
delete leafRow.__EXPANDED; // no longer expandable
} else if (leafRow.__EXPANDED === undefined) { // retain previous setting for old rows
leafRow.__EXPANDED = false; // default for new row is unexpanded
}
}
} else {
// flatten the tree so group sorter sees it as a single group
while (r--) {
this.getRow(r).__DEPTH = 0;
}
}
// look for DataSourceTreeviewFilter
return this.joined;
},
/**
* @summary Rebuild the index.
* @desc Rebuild the index to show only "revealed" rows. (Rows that are not inside a collapsed parent node row.)
* @memberOf DataSourceTreeview#
*/
apply: function() {
if (!this.viewMakesSense()) {
this.clearIndex();
} else {
this.buildIndex(this.joined && rowIsRevealed);
}
},
/**
* @summary Get the value for the specified cell.
* @desc Intercepts tree column values and indents and decorates them.
* @param x
* @param y
* @returns {*}
* @memberOf DataSourceTreeview#
*/
getValue: function(x, y) {
var value = DataSourceIndexed.prototype.getValue.call(this, x, y);
if (this.viewMakesSense() && x === this._treeColumn.index) {
var row = this.getRow(y);
if (!(value === '' && row.__EXPANDED === undefined)) {
value = Array(row.__DEPTH + 1).join(' ') + this.drillDownCharMap[row.__EXPANDED ? 'OPEN' : 'CLOSE'] + value;
}
}
return value;
},
viewMakesSense: function() {
return this.joined;
},
/**
* @memberOf DataSourceTreeview#
* @param {number} columnIndex
* @returns {*|boolean}
*/
isDrillDown: function(columnIndex) {
var result = this.viewMakesSense();
if (result && columnIndex) {
result = columnIndex === this.treeColumnIndex;
}
return result;
},
isDrillDownCol: function (event) {
return event && event.dataCell.x === this._treeColumn.index;
},
/**
* @summary Handle a click event in the drill-down column.
* @desc Operates only on the following rows:
* * Expandable rows - Rows with a drill-down control.
* * Revealed rows - Rows not hidden inside of collapsed drill-downs.
* @param y - Revealed row number. (This is not the row ID.)
* @param {boolean} [expand] - One of:
* * `true` - Expand all rows that are currently collapsed.
* * `false` - Collapse all rows that are currently expanded.
* * `undefined` (or omitted) - Expand all currently collapsed rows; collapse all currently expanded rows.
* @param {number} [depth=Infinity] - One of:
* * number > 0 - Apply only if row depth is above the given depth.
* * number <= 0 - Apply only if row depth is below the given depth.
* @returns {undefined|boolean} One of:
* * `undefined` - Row was not expandable.
* * `true` - Row had drill-down _and_ state changed.
* * `false` - Row had drill-down _but_ state did _not_ change.
* @memberOf DataSourceTreeview#
*/
click: function(y, expand, depth) {
if (!this.viewMakesSense()) {
return this.dataSource.click.apply(this.dataSource, arguments);
}
var changed, row = this.getRow(y);
if (row && row.__EXPANDED !== undefined) {
if (depth !== undefined && (
depth > 0 && row.__DEPTH >= depth ||
depth <= 0 && row.__DEPTH < -depth
)) {
changed = false;
} else {
if (expand === undefined) {
expand = !row.__EXPANDED;
}
changed = row.__EXPANDED && !expand || !row.__EXPANDED && expand;
row.__EXPANDED = expand;
}
}
return changed;
},
/**
* @summary Expand nested drill-downs containing this row.
* @param ID - The unique row ID.
* @returns {boolean} If any rows expanded.
* @memberOf DataSourceTreeview#
*/
revealRow: function(ID) {
if (!this.viewMakesSense()) {
return this.dataSource.revealRow.apply(this.dataSource, arguments);
}
var row, parent, changed = false;
while ((row = this.findRow(this._idColumn.name, ID))) {
if (parent && row.__EXPANDED === false) {
row.__EXPANDED = changed = true;
}
parent = true;
ID = row[this._parentIdColumn.name];
}
return changed;
}
});
function rowIsRevealed(r, row) {
var parentID;
// are any of the row's ancestors collapsed?
while ((parentID = row[this._parentIdColumn.name]) != null) {
// walk up through each parent...
row = this.findRow(this._idColumn.name, parentID);
if (row.__EXPANDED === false) { // an ancestor is collapsed
return false; // exclude row from build
}
}
// no ancestors were collapsed
return true; // include row in build
}
Object.defineProperty(DataSourceTreeview.prototype, 'type', { value: 'treeviewer' }); // read-only property
module.exports = DataSourceTreeview;