vis-rails 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/.project +11 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/vis/rails/engine.rb +6 -0
- data/lib/vis/rails/version.rb +5 -0
- data/lib/vis/rails.rb +7 -0
- data/vendor/assets/javascripts/vis.js +1 -0
- data/vendor/assets/stylesheets/vis.css +3 -0
- data/vendor/assets/vis/DataSet.js +936 -0
- data/vendor/assets/vis/DataView.js +281 -0
- data/vendor/assets/vis/EventBus.js +89 -0
- data/vendor/assets/vis/events.js +116 -0
- data/vendor/assets/vis/graph/ClusterMixin.js +1019 -0
- data/vendor/assets/vis/graph/Edge.js +620 -0
- data/vendor/assets/vis/graph/Graph.js +2111 -0
- data/vendor/assets/vis/graph/Groups.js +80 -0
- data/vendor/assets/vis/graph/Images.js +41 -0
- data/vendor/assets/vis/graph/NavigationMixin.js +245 -0
- data/vendor/assets/vis/graph/Node.js +978 -0
- data/vendor/assets/vis/graph/Popup.js +105 -0
- data/vendor/assets/vis/graph/SectorsMixin.js +547 -0
- data/vendor/assets/vis/graph/SelectionMixin.js +515 -0
- data/vendor/assets/vis/graph/dotparser.js +829 -0
- data/vendor/assets/vis/graph/img/downarrow.png +0 -0
- data/vendor/assets/vis/graph/img/leftarrow.png +0 -0
- data/vendor/assets/vis/graph/img/minus.png +0 -0
- data/vendor/assets/vis/graph/img/plus.png +0 -0
- data/vendor/assets/vis/graph/img/rightarrow.png +0 -0
- data/vendor/assets/vis/graph/img/uparrow.png +0 -0
- data/vendor/assets/vis/graph/img/zoomExtends.png +0 -0
- data/vendor/assets/vis/graph/shapes.js +225 -0
- data/vendor/assets/vis/module/exports.js +68 -0
- data/vendor/assets/vis/module/header.js +24 -0
- data/vendor/assets/vis/module/imports.js +32 -0
- data/vendor/assets/vis/shim.js +252 -0
- data/vendor/assets/vis/timeline/Controller.js +172 -0
- data/vendor/assets/vis/timeline/Range.js +553 -0
- data/vendor/assets/vis/timeline/Stack.js +192 -0
- data/vendor/assets/vis/timeline/TimeStep.js +449 -0
- data/vendor/assets/vis/timeline/Timeline.js +476 -0
- data/vendor/assets/vis/timeline/component/Component.js +148 -0
- data/vendor/assets/vis/timeline/component/ContentPanel.js +113 -0
- data/vendor/assets/vis/timeline/component/CurrentTime.js +101 -0
- data/vendor/assets/vis/timeline/component/CustomTime.js +255 -0
- data/vendor/assets/vis/timeline/component/Group.js +129 -0
- data/vendor/assets/vis/timeline/component/GroupSet.js +546 -0
- data/vendor/assets/vis/timeline/component/ItemSet.js +612 -0
- data/vendor/assets/vis/timeline/component/Panel.js +112 -0
- data/vendor/assets/vis/timeline/component/RootPanel.js +215 -0
- data/vendor/assets/vis/timeline/component/TimeAxis.js +522 -0
- data/vendor/assets/vis/timeline/component/css/currenttime.css +5 -0
- data/vendor/assets/vis/timeline/component/css/customtime.css +6 -0
- data/vendor/assets/vis/timeline/component/css/groupset.css +59 -0
- data/vendor/assets/vis/timeline/component/css/item.css +93 -0
- data/vendor/assets/vis/timeline/component/css/itemset.css +17 -0
- data/vendor/assets/vis/timeline/component/css/panel.css +14 -0
- data/vendor/assets/vis/timeline/component/css/timeaxis.css +41 -0
- data/vendor/assets/vis/timeline/component/css/timeline.css +2 -0
- data/vendor/assets/vis/timeline/component/item/Item.js +81 -0
- data/vendor/assets/vis/timeline/component/item/ItemBox.js +302 -0
- data/vendor/assets/vis/timeline/component/item/ItemPoint.js +237 -0
- data/vendor/assets/vis/timeline/component/item/ItemRange.js +251 -0
- data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +91 -0
- data/vendor/assets/vis/util.js +673 -0
- data/vis-rails.gemspec +47 -0
- metadata +142 -0
@@ -0,0 +1,476 @@
|
|
1
|
+
/**
|
2
|
+
* Create a timeline visualization
|
3
|
+
* @param {HTMLElement} container
|
4
|
+
* @param {vis.DataSet | Array | DataTable} [items]
|
5
|
+
* @param {Object} [options] See Timeline.setOptions for the available options.
|
6
|
+
* @constructor
|
7
|
+
*/
|
8
|
+
function Timeline (container, items, options) {
|
9
|
+
var me = this;
|
10
|
+
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
|
11
|
+
this.options = {
|
12
|
+
orientation: 'bottom',
|
13
|
+
min: null,
|
14
|
+
max: null,
|
15
|
+
zoomMin: 10, // milliseconds
|
16
|
+
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
|
17
|
+
// moveable: true, // TODO: option moveable
|
18
|
+
// zoomable: true, // TODO: option zoomable
|
19
|
+
showMinorLabels: true,
|
20
|
+
showMajorLabels: true,
|
21
|
+
showCurrentTime: false,
|
22
|
+
showCustomTime: false,
|
23
|
+
autoResize: false
|
24
|
+
};
|
25
|
+
|
26
|
+
// controller
|
27
|
+
this.controller = new Controller();
|
28
|
+
|
29
|
+
// root panel
|
30
|
+
if (!container) {
|
31
|
+
throw new Error('No container element provided');
|
32
|
+
}
|
33
|
+
var rootOptions = Object.create(this.options);
|
34
|
+
rootOptions.height = function () {
|
35
|
+
// TODO: change to height
|
36
|
+
if (me.options.height) {
|
37
|
+
// fixed height
|
38
|
+
return me.options.height;
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
// auto height
|
42
|
+
return (me.timeaxis.height + me.content.height) + 'px';
|
43
|
+
}
|
44
|
+
};
|
45
|
+
this.rootPanel = new RootPanel(container, rootOptions);
|
46
|
+
this.controller.add(this.rootPanel);
|
47
|
+
|
48
|
+
// item panel
|
49
|
+
var itemOptions = Object.create(this.options);
|
50
|
+
itemOptions.left = function () {
|
51
|
+
return me.labelPanel.width;
|
52
|
+
};
|
53
|
+
itemOptions.width = function () {
|
54
|
+
return me.rootPanel.width - me.labelPanel.width;
|
55
|
+
};
|
56
|
+
itemOptions.top = null;
|
57
|
+
itemOptions.height = null;
|
58
|
+
this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
|
59
|
+
this.controller.add(this.itemPanel);
|
60
|
+
|
61
|
+
// label panel
|
62
|
+
var labelOptions = Object.create(this.options);
|
63
|
+
labelOptions.top = null;
|
64
|
+
labelOptions.left = null;
|
65
|
+
labelOptions.height = null;
|
66
|
+
labelOptions.width = function () {
|
67
|
+
if (me.content && typeof me.content.getLabelsWidth === 'function') {
|
68
|
+
return me.content.getLabelsWidth();
|
69
|
+
}
|
70
|
+
else {
|
71
|
+
return 0;
|
72
|
+
}
|
73
|
+
};
|
74
|
+
this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
|
75
|
+
this.controller.add(this.labelPanel);
|
76
|
+
|
77
|
+
// range
|
78
|
+
var rangeOptions = Object.create(this.options);
|
79
|
+
this.range = new Range(rangeOptions);
|
80
|
+
this.range.setRange(
|
81
|
+
now.clone().add('days', -3).valueOf(),
|
82
|
+
now.clone().add('days', 4).valueOf()
|
83
|
+
);
|
84
|
+
|
85
|
+
// TODO: reckon with options moveable and zoomable
|
86
|
+
// TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
|
87
|
+
this.range.subscribe(this.rootPanel, 'move', 'horizontal');
|
88
|
+
this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
|
89
|
+
this.range.on('rangechange', function (properties) {
|
90
|
+
var force = true;
|
91
|
+
me.controller.requestReflow(force);
|
92
|
+
me._trigger('rangechange', properties);
|
93
|
+
});
|
94
|
+
this.range.on('rangechanged', function (properties) {
|
95
|
+
var force = true;
|
96
|
+
me.controller.requestReflow(force);
|
97
|
+
me._trigger('rangechanged', properties);
|
98
|
+
});
|
99
|
+
|
100
|
+
// single select (or unselect) when tapping an item
|
101
|
+
// TODO: implement ctrl+click
|
102
|
+
this.rootPanel.on('tap', this._onSelectItem.bind(this));
|
103
|
+
|
104
|
+
// multi select when holding mouse/touch, or on ctrl+click
|
105
|
+
this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
|
106
|
+
|
107
|
+
// time axis
|
108
|
+
var timeaxisOptions = Object.create(rootOptions);
|
109
|
+
timeaxisOptions.range = this.range;
|
110
|
+
timeaxisOptions.left = null;
|
111
|
+
timeaxisOptions.top = null;
|
112
|
+
timeaxisOptions.width = '100%';
|
113
|
+
timeaxisOptions.height = null;
|
114
|
+
this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
|
115
|
+
this.timeaxis.setRange(this.range);
|
116
|
+
this.controller.add(this.timeaxis);
|
117
|
+
|
118
|
+
// current time bar
|
119
|
+
this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
|
120
|
+
this.controller.add(this.currenttime);
|
121
|
+
|
122
|
+
// custom time bar
|
123
|
+
this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
|
124
|
+
this.controller.add(this.customtime);
|
125
|
+
|
126
|
+
// create groupset
|
127
|
+
this.setGroups(null);
|
128
|
+
|
129
|
+
this.itemsData = null; // DataSet
|
130
|
+
this.groupsData = null; // DataSet
|
131
|
+
|
132
|
+
// apply options
|
133
|
+
if (options) {
|
134
|
+
this.setOptions(options);
|
135
|
+
}
|
136
|
+
|
137
|
+
// create itemset and groupset
|
138
|
+
if (items) {
|
139
|
+
this.setItems(items);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* Set options
|
145
|
+
* @param {Object} options TODO: describe the available options
|
146
|
+
*/
|
147
|
+
Timeline.prototype.setOptions = function (options) {
|
148
|
+
util.extend(this.options, options);
|
149
|
+
|
150
|
+
// force update of range (apply new min/max etc.)
|
151
|
+
// both start and end are optional
|
152
|
+
this.range.setRange(options.start, options.end);
|
153
|
+
|
154
|
+
this.controller.reflow();
|
155
|
+
this.controller.repaint();
|
156
|
+
};
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Set a custom time bar
|
160
|
+
* @param {Date} time
|
161
|
+
*/
|
162
|
+
Timeline.prototype.setCustomTime = function (time) {
|
163
|
+
this.customtime._setCustomTime(time);
|
164
|
+
};
|
165
|
+
|
166
|
+
/**
|
167
|
+
* Retrieve the current custom time.
|
168
|
+
* @return {Date} customTime
|
169
|
+
*/
|
170
|
+
Timeline.prototype.getCustomTime = function() {
|
171
|
+
return new Date(this.customtime.customTime.valueOf());
|
172
|
+
};
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Set items
|
176
|
+
* @param {vis.DataSet | Array | DataTable | null} items
|
177
|
+
*/
|
178
|
+
Timeline.prototype.setItems = function(items) {
|
179
|
+
var initialLoad = (this.itemsData == null);
|
180
|
+
|
181
|
+
// convert to type DataSet when needed
|
182
|
+
var newItemSet;
|
183
|
+
if (!items) {
|
184
|
+
newItemSet = null;
|
185
|
+
}
|
186
|
+
else if (items instanceof DataSet) {
|
187
|
+
newItemSet = items;
|
188
|
+
}
|
189
|
+
if (!(items instanceof DataSet)) {
|
190
|
+
newItemSet = new DataSet({
|
191
|
+
convert: {
|
192
|
+
start: 'Date',
|
193
|
+
end: 'Date'
|
194
|
+
}
|
195
|
+
});
|
196
|
+
newItemSet.add(items);
|
197
|
+
}
|
198
|
+
|
199
|
+
// set items
|
200
|
+
this.itemsData = newItemSet;
|
201
|
+
this.content.setItems(newItemSet);
|
202
|
+
|
203
|
+
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
|
204
|
+
// apply the data range as range
|
205
|
+
var dataRange = this.getItemRange();
|
206
|
+
|
207
|
+
// add 5% space on both sides
|
208
|
+
var start = dataRange.min;
|
209
|
+
var end = dataRange.max;
|
210
|
+
if (start != null && end != null) {
|
211
|
+
var interval = (end.valueOf() - start.valueOf());
|
212
|
+
if (interval <= 0) {
|
213
|
+
// prevent an empty interval
|
214
|
+
interval = 24 * 60 * 60 * 1000; // 1 day
|
215
|
+
}
|
216
|
+
start = new Date(start.valueOf() - interval * 0.05);
|
217
|
+
end = new Date(end.valueOf() + interval * 0.05);
|
218
|
+
}
|
219
|
+
|
220
|
+
// override specified start and/or end date
|
221
|
+
if (this.options.start != undefined) {
|
222
|
+
start = util.convert(this.options.start, 'Date');
|
223
|
+
}
|
224
|
+
if (this.options.end != undefined) {
|
225
|
+
end = util.convert(this.options.end, 'Date');
|
226
|
+
}
|
227
|
+
|
228
|
+
// apply range if there is a min or max available
|
229
|
+
if (start != null || end != null) {
|
230
|
+
this.range.setRange(start, end);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
};
|
234
|
+
|
235
|
+
/**
|
236
|
+
* Set groups
|
237
|
+
* @param {vis.DataSet | Array | DataTable} groups
|
238
|
+
*/
|
239
|
+
Timeline.prototype.setGroups = function(groups) {
|
240
|
+
var me = this;
|
241
|
+
this.groupsData = groups;
|
242
|
+
|
243
|
+
// switch content type between ItemSet or GroupSet when needed
|
244
|
+
var Type = this.groupsData ? GroupSet : ItemSet;
|
245
|
+
if (!(this.content instanceof Type)) {
|
246
|
+
// remove old content set
|
247
|
+
if (this.content) {
|
248
|
+
this.content.hide();
|
249
|
+
if (this.content.setItems) {
|
250
|
+
this.content.setItems(); // disconnect from items
|
251
|
+
}
|
252
|
+
if (this.content.setGroups) {
|
253
|
+
this.content.setGroups(); // disconnect from groups
|
254
|
+
}
|
255
|
+
this.controller.remove(this.content);
|
256
|
+
}
|
257
|
+
|
258
|
+
// create new content set
|
259
|
+
var options = Object.create(this.options);
|
260
|
+
util.extend(options, {
|
261
|
+
top: function () {
|
262
|
+
if (me.options.orientation == 'top') {
|
263
|
+
return me.timeaxis.height;
|
264
|
+
}
|
265
|
+
else {
|
266
|
+
return me.itemPanel.height - me.timeaxis.height - me.content.height;
|
267
|
+
}
|
268
|
+
},
|
269
|
+
left: null,
|
270
|
+
width: '100%',
|
271
|
+
height: function () {
|
272
|
+
if (me.options.height) {
|
273
|
+
// fixed height
|
274
|
+
return me.itemPanel.height - me.timeaxis.height;
|
275
|
+
}
|
276
|
+
else {
|
277
|
+
// auto height
|
278
|
+
return null;
|
279
|
+
}
|
280
|
+
},
|
281
|
+
maxHeight: function () {
|
282
|
+
// TODO: change maxHeight to be a css string like '100%' or '300px'
|
283
|
+
if (me.options.maxHeight) {
|
284
|
+
if (!util.isNumber(me.options.maxHeight)) {
|
285
|
+
throw new TypeError('Number expected for property maxHeight');
|
286
|
+
}
|
287
|
+
return me.options.maxHeight - me.timeaxis.height;
|
288
|
+
}
|
289
|
+
else {
|
290
|
+
return null;
|
291
|
+
}
|
292
|
+
},
|
293
|
+
labelContainer: function () {
|
294
|
+
return me.labelPanel.getContainer();
|
295
|
+
}
|
296
|
+
});
|
297
|
+
|
298
|
+
this.content = new Type(this.itemPanel, [this.timeaxis], options);
|
299
|
+
if (this.content.setRange) {
|
300
|
+
this.content.setRange(this.range);
|
301
|
+
}
|
302
|
+
if (this.content.setItems) {
|
303
|
+
this.content.setItems(this.itemsData);
|
304
|
+
}
|
305
|
+
if (this.content.setGroups) {
|
306
|
+
this.content.setGroups(this.groupsData);
|
307
|
+
}
|
308
|
+
this.controller.add(this.content);
|
309
|
+
}
|
310
|
+
};
|
311
|
+
|
312
|
+
/**
|
313
|
+
* Get the data range of the item set.
|
314
|
+
* @returns {{min: Date, max: Date}} range A range with a start and end Date.
|
315
|
+
* When no minimum is found, min==null
|
316
|
+
* When no maximum is found, max==null
|
317
|
+
*/
|
318
|
+
Timeline.prototype.getItemRange = function getItemRange() {
|
319
|
+
// calculate min from start filed
|
320
|
+
var itemsData = this.itemsData,
|
321
|
+
min = null,
|
322
|
+
max = null;
|
323
|
+
|
324
|
+
if (itemsData) {
|
325
|
+
// calculate the minimum value of the field 'start'
|
326
|
+
var minItem = itemsData.min('start');
|
327
|
+
min = minItem ? minItem.start.valueOf() : null;
|
328
|
+
|
329
|
+
// calculate maximum value of fields 'start' and 'end'
|
330
|
+
var maxStartItem = itemsData.max('start');
|
331
|
+
if (maxStartItem) {
|
332
|
+
max = maxStartItem.start.valueOf();
|
333
|
+
}
|
334
|
+
var maxEndItem = itemsData.max('end');
|
335
|
+
if (maxEndItem) {
|
336
|
+
if (max == null) {
|
337
|
+
max = maxEndItem.end.valueOf();
|
338
|
+
}
|
339
|
+
else {
|
340
|
+
max = Math.max(max, maxEndItem.end.valueOf());
|
341
|
+
}
|
342
|
+
}
|
343
|
+
}
|
344
|
+
|
345
|
+
return {
|
346
|
+
min: (min != null) ? new Date(min) : null,
|
347
|
+
max: (max != null) ? new Date(max) : null
|
348
|
+
};
|
349
|
+
};
|
350
|
+
|
351
|
+
/**
|
352
|
+
* Set selected items by their id. Replaces the current selection
|
353
|
+
* Unknown id's are silently ignored.
|
354
|
+
* @param {Array} [ids] An array with zero or more id's of the items to be
|
355
|
+
* selected. If ids is an empty array, all items will be
|
356
|
+
* unselected.
|
357
|
+
*/
|
358
|
+
Timeline.prototype.setSelection = function setSelection (ids) {
|
359
|
+
if (this.content) this.content.setSelection(ids);
|
360
|
+
};
|
361
|
+
|
362
|
+
/**
|
363
|
+
* Get the selected items by their id
|
364
|
+
* @return {Array} ids The ids of the selected items
|
365
|
+
*/
|
366
|
+
Timeline.prototype.getSelection = function getSelection() {
|
367
|
+
return this.content ? this.content.getSelection() : [];
|
368
|
+
};
|
369
|
+
|
370
|
+
/**
|
371
|
+
* Add event listener
|
372
|
+
* @param {String} event Event name. Available events:
|
373
|
+
* 'rangechange', 'rangechanged', 'select'
|
374
|
+
* @param {function} callback Callback function, invoked as callback(properties)
|
375
|
+
* where properties is an optional object containing
|
376
|
+
* event specific properties.
|
377
|
+
*/
|
378
|
+
Timeline.prototype.on = function on (event, callback) {
|
379
|
+
var available = ['rangechange', 'rangechanged', 'select'];
|
380
|
+
|
381
|
+
if (available.indexOf(event) == -1) {
|
382
|
+
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
|
383
|
+
}
|
384
|
+
|
385
|
+
events.addListener(this, event, callback);
|
386
|
+
};
|
387
|
+
|
388
|
+
/**
|
389
|
+
* Remove an event listener
|
390
|
+
* @param {String} event Event name
|
391
|
+
* @param {function} callback Callback function
|
392
|
+
*/
|
393
|
+
Timeline.prototype.off = function off (event, callback) {
|
394
|
+
events.removeListener(this, event, callback);
|
395
|
+
};
|
396
|
+
|
397
|
+
/**
|
398
|
+
* Trigger an event
|
399
|
+
* @param {String} event Event name, available events: 'rangechange',
|
400
|
+
* 'rangechanged', 'select'
|
401
|
+
* @param {Object} [properties] Event specific properties
|
402
|
+
* @private
|
403
|
+
*/
|
404
|
+
Timeline.prototype._trigger = function _trigger(event, properties) {
|
405
|
+
events.trigger(this, event, properties || {});
|
406
|
+
};
|
407
|
+
|
408
|
+
/**
|
409
|
+
* Handle selecting/deselecting an item when tapping it
|
410
|
+
* @param {Event} event
|
411
|
+
* @private
|
412
|
+
*/
|
413
|
+
Timeline.prototype._onSelectItem = function (event) {
|
414
|
+
var item = this._itemFromTarget(event);
|
415
|
+
|
416
|
+
var selection = item ? [item.id] : [];
|
417
|
+
this.setSelection(selection);
|
418
|
+
|
419
|
+
this._trigger('select', {
|
420
|
+
items: this.getSelection()
|
421
|
+
});
|
422
|
+
|
423
|
+
event.stopPropagation();
|
424
|
+
};
|
425
|
+
|
426
|
+
/**
|
427
|
+
* Handle selecting/deselecting multiple items when holding an item
|
428
|
+
* @param {Event} event
|
429
|
+
* @private
|
430
|
+
*/
|
431
|
+
Timeline.prototype._onMultiSelectItem = function (event) {
|
432
|
+
var selection,
|
433
|
+
item = this._itemFromTarget(event);
|
434
|
+
|
435
|
+
if (!item) {
|
436
|
+
// do nothing...
|
437
|
+
return;
|
438
|
+
}
|
439
|
+
|
440
|
+
selection = this.getSelection(); // current selection
|
441
|
+
var index = selection.indexOf(item.id);
|
442
|
+
if (index == -1) {
|
443
|
+
// item is not yet selected -> select it
|
444
|
+
selection.push(item.id);
|
445
|
+
}
|
446
|
+
else {
|
447
|
+
// item is already selected -> deselect it
|
448
|
+
selection.splice(index, 1);
|
449
|
+
}
|
450
|
+
this.setSelection(selection);
|
451
|
+
|
452
|
+
this._trigger('select', {
|
453
|
+
items: this.getSelection()
|
454
|
+
});
|
455
|
+
|
456
|
+
event.stopPropagation();
|
457
|
+
};
|
458
|
+
|
459
|
+
/**
|
460
|
+
* Find an item from an event target:
|
461
|
+
* searches for the attribute 'timeline-item' in the event target's element tree
|
462
|
+
* @param {Event} event
|
463
|
+
* @return {Item | null| item
|
464
|
+
* @private
|
465
|
+
*/
|
466
|
+
Timeline.prototype._itemFromTarget = function _itemFromTarget (event) {
|
467
|
+
var target = event.target;
|
468
|
+
while (target) {
|
469
|
+
if (target.hasOwnProperty('timeline-item')) {
|
470
|
+
return target['timeline-item'];
|
471
|
+
}
|
472
|
+
target = target.parentNode;
|
473
|
+
}
|
474
|
+
|
475
|
+
return null;
|
476
|
+
};
|
@@ -0,0 +1,148 @@
|
|
1
|
+
/**
|
2
|
+
* Prototype for visual components
|
3
|
+
*/
|
4
|
+
function Component () {
|
5
|
+
this.id = null;
|
6
|
+
this.parent = null;
|
7
|
+
this.depends = null;
|
8
|
+
this.controller = null;
|
9
|
+
this.options = null;
|
10
|
+
|
11
|
+
this.frame = null; // main DOM element
|
12
|
+
this.top = 0;
|
13
|
+
this.left = 0;
|
14
|
+
this.width = 0;
|
15
|
+
this.height = 0;
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Set parameters for the frame. Parameters will be merged in current parameter
|
20
|
+
* set.
|
21
|
+
* @param {Object} options Available parameters:
|
22
|
+
* {String | function} [className]
|
23
|
+
* {EventBus} [eventBus]
|
24
|
+
* {String | Number | function} [left]
|
25
|
+
* {String | Number | function} [top]
|
26
|
+
* {String | Number | function} [width]
|
27
|
+
* {String | Number | function} [height]
|
28
|
+
*/
|
29
|
+
Component.prototype.setOptions = function setOptions(options) {
|
30
|
+
if (options) {
|
31
|
+
util.extend(this.options, options);
|
32
|
+
|
33
|
+
if (this.controller) {
|
34
|
+
this.requestRepaint();
|
35
|
+
this.requestReflow();
|
36
|
+
}
|
37
|
+
}
|
38
|
+
};
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Get an option value by name
|
42
|
+
* The function will first check this.options object, and else will check
|
43
|
+
* this.defaultOptions.
|
44
|
+
* @param {String} name
|
45
|
+
* @return {*} value
|
46
|
+
*/
|
47
|
+
Component.prototype.getOption = function getOption(name) {
|
48
|
+
var value;
|
49
|
+
if (this.options) {
|
50
|
+
value = this.options[name];
|
51
|
+
}
|
52
|
+
if (value === undefined && this.defaultOptions) {
|
53
|
+
value = this.defaultOptions[name];
|
54
|
+
}
|
55
|
+
return value;
|
56
|
+
};
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Get the container element of the component, which can be used by a child to
|
60
|
+
* add its own widgets. Not all components do have a container for childs, in
|
61
|
+
* that case null is returned.
|
62
|
+
* @returns {HTMLElement | null} container
|
63
|
+
*/
|
64
|
+
// TODO: get rid of the getContainer and getFrame methods, provide these via the options
|
65
|
+
Component.prototype.getContainer = function getContainer() {
|
66
|
+
// should be implemented by the component
|
67
|
+
return null;
|
68
|
+
};
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Get the frame element of the component, the outer HTML DOM element.
|
72
|
+
* @returns {HTMLElement | null} frame
|
73
|
+
*/
|
74
|
+
Component.prototype.getFrame = function getFrame() {
|
75
|
+
return this.frame;
|
76
|
+
};
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Repaint the component
|
80
|
+
* @return {Boolean} changed
|
81
|
+
*/
|
82
|
+
Component.prototype.repaint = function repaint() {
|
83
|
+
// should be implemented by the component
|
84
|
+
return false;
|
85
|
+
};
|
86
|
+
|
87
|
+
/**
|
88
|
+
* Reflow the component
|
89
|
+
* @return {Boolean} resized
|
90
|
+
*/
|
91
|
+
Component.prototype.reflow = function reflow() {
|
92
|
+
// should be implemented by the component
|
93
|
+
return false;
|
94
|
+
};
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Hide the component from the DOM
|
98
|
+
* @return {Boolean} changed
|
99
|
+
*/
|
100
|
+
Component.prototype.hide = function hide() {
|
101
|
+
if (this.frame && this.frame.parentNode) {
|
102
|
+
this.frame.parentNode.removeChild(this.frame);
|
103
|
+
return true;
|
104
|
+
}
|
105
|
+
else {
|
106
|
+
return false;
|
107
|
+
}
|
108
|
+
};
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Show the component in the DOM (when not already visible).
|
112
|
+
* A repaint will be executed when the component is not visible
|
113
|
+
* @return {Boolean} changed
|
114
|
+
*/
|
115
|
+
Component.prototype.show = function show() {
|
116
|
+
if (!this.frame || !this.frame.parentNode) {
|
117
|
+
return this.repaint();
|
118
|
+
}
|
119
|
+
else {
|
120
|
+
return false;
|
121
|
+
}
|
122
|
+
};
|
123
|
+
|
124
|
+
/**
|
125
|
+
* Request a repaint. The controller will schedule a repaint
|
126
|
+
*/
|
127
|
+
Component.prototype.requestRepaint = function requestRepaint() {
|
128
|
+
if (this.controller) {
|
129
|
+
this.controller.requestRepaint();
|
130
|
+
}
|
131
|
+
else {
|
132
|
+
throw new Error('Cannot request a repaint: no controller configured');
|
133
|
+
// TODO: just do a repaint when no parent is configured?
|
134
|
+
}
|
135
|
+
};
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Request a reflow. The controller will schedule a reflow
|
139
|
+
*/
|
140
|
+
Component.prototype.requestReflow = function requestReflow() {
|
141
|
+
if (this.controller) {
|
142
|
+
this.controller.requestReflow();
|
143
|
+
}
|
144
|
+
else {
|
145
|
+
throw new Error('Cannot request a reflow: no controller configured');
|
146
|
+
// TODO: just do a reflow when no parent is configured?
|
147
|
+
}
|
148
|
+
};
|