vis-rails 1.0.2 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +2 -0
- data/lib/vis/rails/version.rb +1 -1
- data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
- data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
- data/vendor/assets/javascripts/vis.js +3 -3
- data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
- data/vendor/assets/vis/DataSet.js +106 -130
- data/vendor/assets/vis/DataView.js +35 -37
- data/vendor/assets/vis/graph/Edge.js +225 -45
- data/vendor/assets/vis/graph/Graph.js +120 -24
- data/vendor/assets/vis/graph/Node.js +16 -16
- data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
- data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
- data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
- data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
- data/vendor/assets/vis/module/exports.js +2 -3
- data/vendor/assets/vis/timeline/Range.js +93 -80
- data/vendor/assets/vis/timeline/Timeline.js +525 -428
- data/vendor/assets/vis/timeline/component/Component.js +19 -53
- data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
- data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
- data/vendor/assets/vis/timeline/component/Group.js +47 -50
- data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
- data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
- data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
- data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
- data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
- data/vendor/assets/vis/timeline/component/css/item.css +1 -11
- data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
- data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
- data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
- data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
- data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
- data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
- data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
- data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
- data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
- data/vendor/assets/vis/timeline/stack.js +5 -5
- data/vendor/assets/vis/util.js +81 -35
- metadata +7 -4
- data/vendor/assets/vis/timeline/component/Panel.js +0 -170
- data/vendor/assets/vis/timeline/component/RootPanel.js +0 -176
@@ -20,8 +20,6 @@ var vis = {
|
|
20
20
|
},
|
21
21
|
|
22
22
|
Component: Component,
|
23
|
-
Panel: Panel,
|
24
|
-
RootPanel: RootPanel,
|
25
23
|
ItemSet: ItemSet,
|
26
24
|
TimeAxis: TimeAxis
|
27
25
|
},
|
@@ -35,7 +33,8 @@ var vis = {
|
|
35
33
|
},
|
36
34
|
|
37
35
|
Timeline: Timeline,
|
38
|
-
Graph: Graph
|
36
|
+
Graph: Graph,
|
37
|
+
Graph3d: Graph3d
|
39
38
|
};
|
40
39
|
|
41
40
|
/**
|
@@ -3,57 +3,81 @@
|
|
3
3
|
* A Range controls a numeric range with a start and end value.
|
4
4
|
* The Range adjusts the range based on mouse events or programmatic changes,
|
5
5
|
* and triggers events when the range is changing or has been changed.
|
6
|
-
* @param {
|
7
|
-
* @param {Panel} parent Parent panel, used to attach to the DOM
|
6
|
+
* @param {{dom: Object, domProps: Object, emitter: Emitter}} body
|
8
7
|
* @param {Object} [options] See description at Range.setOptions
|
9
8
|
*/
|
10
|
-
function Range(
|
11
|
-
|
12
|
-
this.start =
|
13
|
-
this.end =
|
9
|
+
function Range(body, options) {
|
10
|
+
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
|
11
|
+
this.start = now.clone().add('days', -3).valueOf(); // Number
|
12
|
+
this.end = now.clone().add('days', 4).valueOf(); // Number
|
13
|
+
|
14
|
+
this.body = body;
|
15
|
+
|
16
|
+
// default options
|
17
|
+
this.defaultOptions = {
|
18
|
+
start: null,
|
19
|
+
end: null,
|
20
|
+
direction: 'horizontal', // 'horizontal' or 'vertical'
|
21
|
+
moveable: true,
|
22
|
+
zoomable: true,
|
23
|
+
min: null,
|
24
|
+
max: null,
|
25
|
+
zoomMin: 10, // milliseconds
|
26
|
+
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
|
27
|
+
};
|
28
|
+
this.options = util.extend({}, this.defaultOptions);
|
14
29
|
|
15
|
-
this.
|
16
|
-
|
17
|
-
|
30
|
+
this.props = {
|
31
|
+
touch: {}
|
32
|
+
};
|
18
33
|
|
19
34
|
// drag listeners for dragging
|
20
|
-
this.
|
21
|
-
this.
|
22
|
-
this.
|
35
|
+
this.body.emitter.on('dragstart', this._onDragStart.bind(this));
|
36
|
+
this.body.emitter.on('drag', this._onDrag.bind(this));
|
37
|
+
this.body.emitter.on('dragend', this._onDragEnd.bind(this));
|
23
38
|
|
24
39
|
// ignore dragging when holding
|
25
|
-
this.
|
40
|
+
this.body.emitter.on('hold', this._onHold.bind(this));
|
26
41
|
|
27
42
|
// mouse wheel for zooming
|
28
|
-
this.
|
29
|
-
this.
|
43
|
+
this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
|
44
|
+
this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
|
30
45
|
|
31
46
|
// pinch to zoom
|
32
|
-
this.
|
33
|
-
this.
|
47
|
+
this.body.emitter.on('touch', this._onTouch.bind(this));
|
48
|
+
this.body.emitter.on('pinch', this._onPinch.bind(this));
|
34
49
|
|
35
50
|
this.setOptions(options);
|
36
51
|
}
|
37
52
|
|
38
|
-
|
39
|
-
Emitter(Range.prototype);
|
53
|
+
Range.prototype = new Component();
|
40
54
|
|
41
55
|
/**
|
42
56
|
* Set options for the range controller
|
43
57
|
* @param {Object} options Available options:
|
58
|
+
* {Number | Date | String} start Start date for the range
|
59
|
+
* {Number | Date | String} end End date for the range
|
44
60
|
* {Number} min Minimum value for start
|
45
61
|
* {Number} max Maximum value for end
|
46
62
|
* {Number} zoomMin Set a minimum value for
|
47
63
|
* (end - start).
|
48
64
|
* {Number} zoomMax Set a maximum value for
|
49
65
|
* (end - start).
|
66
|
+
* {Boolean} moveable Enable moving of the range
|
67
|
+
* by dragging. True by default
|
68
|
+
* {Boolean} zoomable Enable zooming of the range
|
69
|
+
* by pinching/scrolling. True by default
|
50
70
|
*/
|
51
71
|
Range.prototype.setOptions = function (options) {
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
72
|
+
if (options) {
|
73
|
+
// copy the options that we know
|
74
|
+
var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
|
75
|
+
util.selectiveExtend(fields, this.options, options);
|
76
|
+
|
77
|
+
if ('start' in options || 'end' in options) {
|
78
|
+
// apply a new range. both start and end are optional
|
79
|
+
this.setRange(options.start, options.end);
|
80
|
+
}
|
57
81
|
}
|
58
82
|
};
|
59
83
|
|
@@ -80,8 +104,8 @@ Range.prototype.setRange = function(start, end) {
|
|
80
104
|
start: new Date(this.start),
|
81
105
|
end: new Date(this.end)
|
82
106
|
};
|
83
|
-
this.emit('rangechange', params);
|
84
|
-
this.emit('rangechanged', params);
|
107
|
+
this.body.emitter.emit('rangechange', params);
|
108
|
+
this.body.emitter.emit('rangechanged', params);
|
85
109
|
}
|
86
110
|
};
|
87
111
|
|
@@ -240,77 +264,75 @@ Range.conversion = function (start, end, width) {
|
|
240
264
|
}
|
241
265
|
};
|
242
266
|
|
243
|
-
// global (private) object to store drag params
|
244
|
-
var touchParams = {};
|
245
|
-
|
246
267
|
/**
|
247
268
|
* Start dragging horizontally or vertically
|
248
269
|
* @param {Event} event
|
249
270
|
* @private
|
250
271
|
*/
|
251
272
|
Range.prototype._onDragStart = function(event) {
|
273
|
+
// only allow dragging when configured as movable
|
274
|
+
if (!this.options.moveable) return;
|
275
|
+
|
252
276
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
253
277
|
// when releasing the fingers in opposite order from the touch screen
|
254
|
-
if (
|
255
|
-
|
256
|
-
// TODO: reckon with option movable
|
278
|
+
if (!this.props.touch.allowDragging) return;
|
257
279
|
|
258
|
-
|
259
|
-
|
280
|
+
this.props.touch.start = this.start;
|
281
|
+
this.props.touch.end = this.end;
|
260
282
|
|
261
|
-
|
262
|
-
|
263
|
-
frame.style.cursor = 'move';
|
283
|
+
if (this.body.dom.root) {
|
284
|
+
this.body.dom.root.style.cursor = 'move';
|
264
285
|
}
|
265
286
|
};
|
266
287
|
|
267
288
|
/**
|
268
|
-
* Perform dragging
|
289
|
+
* Perform dragging operation
|
269
290
|
* @param {Event} event
|
270
291
|
* @private
|
271
292
|
*/
|
272
293
|
Range.prototype._onDrag = function (event) {
|
294
|
+
// only allow dragging when configured as movable
|
295
|
+
if (!this.options.moveable) return;
|
296
|
+
|
273
297
|
var direction = this.options.direction;
|
274
298
|
validateDirection(direction);
|
275
299
|
|
276
|
-
// TODO: reckon with option movable
|
277
|
-
|
278
|
-
|
279
300
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
280
301
|
// when releasing the fingers in opposite order from the touch screen
|
281
|
-
if (
|
302
|
+
if (!this.props.touch.allowDragging) return;
|
282
303
|
|
283
304
|
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
|
284
|
-
interval = (
|
285
|
-
width = (direction == 'horizontal') ? this.
|
305
|
+
interval = (this.props.touch.end - this.props.touch.start),
|
306
|
+
width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
|
286
307
|
diffRange = -delta / width * interval;
|
287
308
|
|
288
|
-
this._applyRange(
|
309
|
+
this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
|
289
310
|
|
290
|
-
this.emit('rangechange', {
|
311
|
+
this.body.emitter.emit('rangechange', {
|
291
312
|
start: new Date(this.start),
|
292
313
|
end: new Date(this.end)
|
293
314
|
});
|
294
315
|
};
|
295
316
|
|
296
317
|
/**
|
297
|
-
* Stop dragging
|
318
|
+
* Stop dragging operation
|
298
319
|
* @param {event} event
|
299
320
|
* @private
|
300
321
|
*/
|
301
322
|
Range.prototype._onDragEnd = function (event) {
|
323
|
+
// only allow dragging when configured as movable
|
324
|
+
if (!this.options.moveable) return;
|
325
|
+
|
302
326
|
// refuse to drag when we where pinching to prevent the timeline make a jump
|
303
327
|
// when releasing the fingers in opposite order from the touch screen
|
304
|
-
if (
|
328
|
+
if (!this.props.touch.allowDragging) return;
|
305
329
|
|
306
|
-
|
307
|
-
|
308
|
-
if (this.parent.frame) {
|
309
|
-
this.parent.frame.style.cursor = 'auto';
|
330
|
+
if (this.body.dom.root) {
|
331
|
+
this.body.dom.root.style.cursor = 'auto';
|
310
332
|
}
|
311
333
|
|
312
334
|
// fire a rangechanged event
|
313
|
-
this.emit('rangechanged', {
|
335
|
+
this.body.emitter.emit('rangechanged', {
|
314
336
|
start: new Date(this.start),
|
315
337
|
end: new Date(this.end)
|
316
338
|
});
|
@@ -323,7 +345,8 @@ Range.prototype._onDragEnd = function (event) {
|
|
323
345
|
* @private
|
324
346
|
*/
|
325
347
|
Range.prototype._onMouseWheel = function(event) {
|
326
|
-
//
|
348
|
+
// only allow zooming when configured as zoomable and moveable
|
349
|
+
if (!(this.options.zoomable && this.options.moveable)) return;
|
327
350
|
|
328
351
|
// retrieve delta
|
329
352
|
var delta = 0;
|
@@ -353,7 +376,7 @@ Range.prototype._onMouseWheel = function(event) {
|
|
353
376
|
|
354
377
|
// calculate center, the date to zoom around
|
355
378
|
var gesture = util.fakeGesture(this, event),
|
356
|
-
pointer = getPointer(gesture.center, this.
|
379
|
+
pointer = getPointer(gesture.center, this.body.dom.center),
|
357
380
|
pointerDate = this._pointerToDate(pointer);
|
358
381
|
|
359
382
|
this.zoom(scale, pointerDate);
|
@@ -369,17 +392,10 @@ Range.prototype._onMouseWheel = function(event) {
|
|
369
392
|
* @private
|
370
393
|
*/
|
371
394
|
Range.prototype._onTouch = function (event) {
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
// don't move the range when dragging a selected event
|
378
|
-
// TODO: it's not so neat to have to know about the state of the ItemSet
|
379
|
-
var item = ItemSet.itemFromTarget(event);
|
380
|
-
if (item && item.selected && this.options.editable) {
|
381
|
-
touchParams.ignore = true;
|
382
|
-
}
|
395
|
+
this.props.touch.start = this.start;
|
396
|
+
this.props.touch.end = this.end;
|
397
|
+
this.props.touch.allowDragging = true;
|
398
|
+
this.props.touch.center = null;
|
383
399
|
};
|
384
400
|
|
385
401
|
/**
|
@@ -387,7 +403,7 @@ Range.prototype._onTouch = function (event) {
|
|
387
403
|
* @private
|
388
404
|
*/
|
389
405
|
Range.prototype._onHold = function () {
|
390
|
-
|
406
|
+
this.props.touch.allowDragging = false;
|
391
407
|
};
|
392
408
|
|
393
409
|
/**
|
@@ -396,25 +412,22 @@ Range.prototype._onHold = function () {
|
|
396
412
|
* @private
|
397
413
|
*/
|
398
414
|
Range.prototype._onPinch = function (event) {
|
399
|
-
|
400
|
-
|
415
|
+
// only allow zooming when configured as zoomable and moveable
|
416
|
+
if (!(this.options.zoomable && this.options.moveable)) return;
|
401
417
|
|
402
|
-
|
418
|
+
this.props.touch.allowDragging = false;
|
403
419
|
|
404
420
|
if (event.gesture.touches.length > 1) {
|
405
|
-
if (!
|
406
|
-
|
421
|
+
if (!this.props.touch.center) {
|
422
|
+
this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
|
407
423
|
}
|
408
424
|
|
409
425
|
var scale = 1 / event.gesture.scale,
|
410
|
-
initDate = this._pointerToDate(
|
411
|
-
center = getPointer(event.gesture.center, this.parent.frame),
|
412
|
-
date = this._pointerToDate(this.parent, center),
|
413
|
-
delta = date - initDate; // TODO: utilize delta
|
426
|
+
initDate = this._pointerToDate(this.props.touch.center);
|
414
427
|
|
415
428
|
// calculate new start and end
|
416
|
-
var newStart = parseInt(initDate + (
|
417
|
-
var newEnd = parseInt(initDate + (
|
429
|
+
var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
|
430
|
+
var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
|
418
431
|
|
419
432
|
// apply new range
|
420
433
|
this.setRange(newStart, newEnd);
|
@@ -434,12 +447,12 @@ Range.prototype._pointerToDate = function (pointer) {
|
|
434
447
|
validateDirection(direction);
|
435
448
|
|
436
449
|
if (direction == 'horizontal') {
|
437
|
-
var width = this.
|
450
|
+
var width = this.body.domProps.center.width;
|
438
451
|
conversion = this.conversion(width);
|
439
452
|
return pointer.x / conversion.scale + conversion.offset;
|
440
453
|
}
|
441
454
|
else {
|
442
|
-
var height = this.
|
455
|
+
var height = this.body.domProps.center.height;
|
443
456
|
conversion = this.conversion(height);
|
444
457
|
return pointer.y / conversion.scale + conversion.offset;
|
445
458
|
}
|
@@ -6,269 +6,64 @@
|
|
6
6
|
* @constructor
|
7
7
|
*/
|
8
8
|
function Timeline (container, items, options) {
|
9
|
-
// validate arguments
|
10
|
-
if (!container) throw new Error('No container element provided');
|
11
|
-
|
12
9
|
var me = this;
|
13
|
-
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
|
14
10
|
this.defaultOptions = {
|
15
|
-
|
16
|
-
|
17
|
-
autoResize: true,
|
18
|
-
stack: true,
|
19
|
-
|
20
|
-
editable: {
|
21
|
-
updateTime: false,
|
22
|
-
updateGroup: false,
|
23
|
-
add: false,
|
24
|
-
remove: false
|
25
|
-
},
|
26
|
-
|
27
|
-
selectable: true,
|
28
|
-
|
29
|
-
min: null,
|
30
|
-
max: null,
|
31
|
-
zoomMin: 10, // milliseconds
|
32
|
-
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
|
33
|
-
// moveable: true, // TODO: option moveable
|
34
|
-
// zoomable: true, // TODO: option zoomable
|
11
|
+
start: null,
|
12
|
+
end: null,
|
35
13
|
|
36
|
-
|
37
|
-
showMajorLabels: true,
|
38
|
-
showCurrentTime: false,
|
39
|
-
showCustomTime: false,
|
40
|
-
|
41
|
-
groupOrder: null,
|
14
|
+
autoResize: true,
|
42
15
|
|
16
|
+
orientation: 'bottom',
|
43
17
|
width: null,
|
44
18
|
height: null,
|
45
19
|
maxHeight: null,
|
46
|
-
minHeight: null
|
47
|
-
|
48
|
-
type: 'box',
|
49
|
-
align: 'center',
|
50
|
-
margin: {
|
51
|
-
axis: 20,
|
52
|
-
item: 10
|
53
|
-
},
|
54
|
-
padding: 5,
|
55
|
-
|
56
|
-
onAdd: function (item, callback) {
|
57
|
-
callback(item);
|
58
|
-
},
|
59
|
-
onUpdate: function (item, callback) {
|
60
|
-
callback(item);
|
61
|
-
},
|
62
|
-
onMove: function (item, callback) {
|
63
|
-
callback(item);
|
64
|
-
},
|
65
|
-
onRemove: function (item, callback) {
|
66
|
-
callback(item);
|
67
|
-
}
|
20
|
+
minHeight: null
|
68
21
|
};
|
22
|
+
this.options = util.deepExtend({}, this.defaultOptions);
|
69
23
|
|
70
|
-
|
71
|
-
|
72
|
-
util.deepExtend(this.options, {
|
73
|
-
snap: null, // will be specified after timeaxis is created
|
74
|
-
|
75
|
-
toScreen: me._toScreen.bind(me),
|
76
|
-
toTime: me._toTime.bind(me)
|
77
|
-
});
|
78
|
-
|
79
|
-
// root panel
|
80
|
-
var rootOptions = util.extend(Object.create(this.options), {
|
81
|
-
height: function () {
|
82
|
-
if (me.options.height) {
|
83
|
-
// fixed height
|
84
|
-
return me.options.height;
|
85
|
-
}
|
86
|
-
else {
|
87
|
-
// auto height
|
88
|
-
// TODO: implement a css based solution to automatically have the right hight
|
89
|
-
return (me.timeAxis.height + me.contentPanel.height) + 'px';
|
90
|
-
}
|
91
|
-
}
|
92
|
-
});
|
93
|
-
this.rootPanel = new RootPanel(container, rootOptions);
|
24
|
+
// Create the DOM, props, and emitter
|
25
|
+
this._create(container);
|
94
26
|
|
95
|
-
//
|
96
|
-
this.
|
27
|
+
// all components listed here will be repainted automatically
|
28
|
+
this.components = [];
|
97
29
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
var sideOptions = util.extend(Object.create(this.options), {
|
106
|
-
top: function () {
|
107
|
-
return (sideOptions.orientation == 'top') ? '0' : '';
|
108
|
-
},
|
109
|
-
bottom: function () {
|
110
|
-
return (sideOptions.orientation == 'top') ? '' : '0';
|
111
|
-
},
|
112
|
-
left: '0',
|
113
|
-
right: null,
|
114
|
-
height: '100%',
|
115
|
-
width: function () {
|
116
|
-
if (me.itemSet) {
|
117
|
-
return me.itemSet.getLabelsWidth();
|
118
|
-
}
|
119
|
-
else {
|
120
|
-
return 0;
|
121
|
-
}
|
30
|
+
this.body = {
|
31
|
+
dom: this.dom,
|
32
|
+
domProps: this.props,
|
33
|
+
emitter: {
|
34
|
+
on: this.on.bind(this),
|
35
|
+
off: this.off.bind(this),
|
36
|
+
emit: this.emit.bind(this)
|
122
37
|
},
|
123
|
-
|
124
|
-
|
38
|
+
util: {
|
39
|
+
snap: null, // will be specified after TimeAxis is created
|
40
|
+
toScreen: me._toScreen.bind(me),
|
41
|
+
toTime: me._toTime.bind(me)
|
125
42
|
}
|
126
|
-
}
|
127
|
-
this.sidePanel = new Panel(sideOptions);
|
128
|
-
this.rootPanel.appendChild(this.sidePanel);
|
129
|
-
|
130
|
-
// main panel (contains time axis and itemsets)
|
131
|
-
var mainOptions = util.extend(Object.create(this.options), {
|
132
|
-
left: function () {
|
133
|
-
// we align left to enable a smooth resizing of the window
|
134
|
-
return me.sidePanel.width;
|
135
|
-
},
|
136
|
-
right: null,
|
137
|
-
height: '100%',
|
138
|
-
width: function () {
|
139
|
-
return me.rootPanel.width - me.sidePanel.width;
|
140
|
-
},
|
141
|
-
className: 'main'
|
142
|
-
});
|
143
|
-
this.mainPanel = new Panel(mainOptions);
|
144
|
-
this.rootPanel.appendChild(this.mainPanel);
|
43
|
+
};
|
145
44
|
|
146
45
|
// range
|
147
|
-
|
148
|
-
|
149
|
-
this.range =
|
150
|
-
this.range.setRange(
|
151
|
-
now.clone().add('days', -3).valueOf(),
|
152
|
-
now.clone().add('days', 4).valueOf()
|
153
|
-
);
|
154
|
-
this.range.on('rangechange', function (properties) {
|
155
|
-
me.rootPanel.repaint();
|
156
|
-
me.emit('rangechange', properties);
|
157
|
-
});
|
158
|
-
this.range.on('rangechanged', function (properties) {
|
159
|
-
me.rootPanel.repaint();
|
160
|
-
me.emit('rangechanged', properties);
|
161
|
-
});
|
46
|
+
this.range = new Range(this.body);
|
47
|
+
this.components.push(this.range);
|
48
|
+
this.body.range = this.range;
|
162
49
|
|
163
|
-
//
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
top: null,
|
168
|
-
width: null,
|
169
|
-
height: null
|
170
|
-
});
|
171
|
-
this.timeAxis = new TimeAxis(timeAxisOptions);
|
172
|
-
this.timeAxis.setRange(this.range);
|
173
|
-
this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
|
174
|
-
this.mainPanel.appendChild(this.timeAxis);
|
175
|
-
|
176
|
-
// content panel (contains itemset(s))
|
177
|
-
var contentOptions = util.extend(Object.create(this.options), {
|
178
|
-
top: function () {
|
179
|
-
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
|
180
|
-
},
|
181
|
-
bottom: function () {
|
182
|
-
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
|
183
|
-
},
|
184
|
-
left: null,
|
185
|
-
right: null,
|
186
|
-
height: null,
|
187
|
-
width: null,
|
188
|
-
className: 'content'
|
189
|
-
});
|
190
|
-
this.contentPanel = new Panel(contentOptions);
|
191
|
-
this.mainPanel.appendChild(this.contentPanel);
|
192
|
-
|
193
|
-
// content panel (contains the vertical lines of box items)
|
194
|
-
var backgroundOptions = util.extend(Object.create(this.options), {
|
195
|
-
top: function () {
|
196
|
-
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
|
197
|
-
},
|
198
|
-
bottom: function () {
|
199
|
-
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
|
200
|
-
},
|
201
|
-
left: null,
|
202
|
-
right: null,
|
203
|
-
height: function () {
|
204
|
-
return me.contentPanel.height;
|
205
|
-
},
|
206
|
-
width: null,
|
207
|
-
className: 'background'
|
208
|
-
});
|
209
|
-
this.backgroundPanel = new Panel(backgroundOptions);
|
210
|
-
this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
|
211
|
-
|
212
|
-
// panel with axis holding the dots of item boxes
|
213
|
-
var axisPanelOptions = util.extend(Object.create(rootOptions), {
|
214
|
-
left: 0,
|
215
|
-
top: function () {
|
216
|
-
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
|
217
|
-
},
|
218
|
-
bottom: function () {
|
219
|
-
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
|
220
|
-
},
|
221
|
-
width: '100%',
|
222
|
-
height: 0,
|
223
|
-
className: 'axis'
|
224
|
-
});
|
225
|
-
this.axisPanel = new Panel(axisPanelOptions);
|
226
|
-
this.mainPanel.appendChild(this.axisPanel);
|
227
|
-
|
228
|
-
// content panel (contains itemset(s))
|
229
|
-
var sideContentOptions = util.extend(Object.create(this.options), {
|
230
|
-
top: function () {
|
231
|
-
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
|
232
|
-
},
|
233
|
-
bottom: function () {
|
234
|
-
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
|
235
|
-
},
|
236
|
-
left: null,
|
237
|
-
right: null,
|
238
|
-
height: null,
|
239
|
-
width: null,
|
240
|
-
className: 'side-content'
|
241
|
-
});
|
242
|
-
this.sideContentPanel = new Panel(sideContentOptions);
|
243
|
-
this.sidePanel.appendChild(this.sideContentPanel);
|
50
|
+
// time axis
|
51
|
+
this.timeAxis = new TimeAxis(this.body);
|
52
|
+
this.components.push(this.timeAxis);
|
53
|
+
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
|
244
54
|
|
245
55
|
// current time bar
|
246
|
-
|
247
|
-
this.
|
56
|
+
this.currentTime = new CurrentTime(this.body);
|
57
|
+
this.components.push(this.currentTime);
|
248
58
|
|
249
59
|
// custom time bar
|
250
60
|
// Note: time bar will be attached in this.setOptions when selected
|
251
|
-
this.customTime = new CustomTime(
|
252
|
-
this.
|
253
|
-
me.emit('timechange', time);
|
254
|
-
});
|
255
|
-
this.customTime.on('timechanged', function (time) {
|
256
|
-
me.emit('timechanged', time);
|
257
|
-
});
|
61
|
+
this.customTime = new CustomTime(this.body);
|
62
|
+
this.components.push(this.customTime);
|
258
63
|
|
259
|
-
//
|
260
|
-
|
261
|
-
|
262
|
-
right: null,
|
263
|
-
top: null,
|
264
|
-
bottom: null,
|
265
|
-
width: null,
|
266
|
-
height: null
|
267
|
-
});
|
268
|
-
this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
|
269
|
-
this.itemSet.setRange(this.range);
|
270
|
-
this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
|
271
|
-
this.contentPanel.appendChild(this.itemSet);
|
64
|
+
// item set
|
65
|
+
this.itemSet = new ItemSet(this.body);
|
66
|
+
this.components.push(this.itemSet);
|
272
67
|
|
273
68
|
this.itemsData = null; // DataSet
|
274
69
|
this.groupsData = null; // DataSet
|
@@ -282,88 +77,217 @@ function Timeline (container, items, options) {
|
|
282
77
|
if (items) {
|
283
78
|
this.setItems(items);
|
284
79
|
}
|
80
|
+
else {
|
81
|
+
this.redraw();
|
82
|
+
}
|
285
83
|
}
|
286
84
|
|
287
85
|
// turn Timeline into an event emitter
|
288
86
|
Emitter(Timeline.prototype);
|
289
87
|
|
290
88
|
/**
|
291
|
-
*
|
292
|
-
*
|
89
|
+
* Create the main DOM for the Timeline: a root panel containing left, right,
|
90
|
+
* top, bottom, content, and background panel.
|
91
|
+
* @param {Element} container The container element where the Timeline will
|
92
|
+
* be attached.
|
93
|
+
* @private
|
293
94
|
*/
|
294
|
-
Timeline.prototype.
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
95
|
+
Timeline.prototype._create = function (container) {
|
96
|
+
this.dom = {};
|
97
|
+
|
98
|
+
this.dom.root = document.createElement('div');
|
99
|
+
this.dom.background = document.createElement('div');
|
100
|
+
this.dom.backgroundVertical = document.createElement('div');
|
101
|
+
this.dom.backgroundHorizontal = document.createElement('div');
|
102
|
+
this.dom.centerContainer = document.createElement('div');
|
103
|
+
this.dom.leftContainer = document.createElement('div');
|
104
|
+
this.dom.rightContainer = document.createElement('div');
|
105
|
+
this.dom.center = document.createElement('div');
|
106
|
+
this.dom.left = document.createElement('div');
|
107
|
+
this.dom.right = document.createElement('div');
|
108
|
+
this.dom.top = document.createElement('div');
|
109
|
+
this.dom.bottom = document.createElement('div');
|
110
|
+
this.dom.shadowTop = document.createElement('div');
|
111
|
+
this.dom.shadowBottom = document.createElement('div');
|
112
|
+
this.dom.shadowTopLeft = document.createElement('div');
|
113
|
+
this.dom.shadowBottomLeft = document.createElement('div');
|
114
|
+
this.dom.shadowTopRight = document.createElement('div');
|
115
|
+
this.dom.shadowBottomRight = document.createElement('div');
|
116
|
+
|
117
|
+
this.dom.background.className = 'vispanel background';
|
118
|
+
this.dom.backgroundVertical.className = 'vispanel background vertical';
|
119
|
+
this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
|
120
|
+
this.dom.centerContainer.className = 'vispanel center';
|
121
|
+
this.dom.leftContainer.className = 'vispanel left';
|
122
|
+
this.dom.rightContainer.className = 'vispanel right';
|
123
|
+
this.dom.top.className = 'vispanel top';
|
124
|
+
this.dom.bottom.className = 'vispanel bottom';
|
125
|
+
this.dom.left.className = 'content';
|
126
|
+
this.dom.center.className = 'content';
|
127
|
+
this.dom.right.className = 'content';
|
128
|
+
this.dom.shadowTop.className = 'shadow top';
|
129
|
+
this.dom.shadowBottom.className = 'shadow bottom';
|
130
|
+
this.dom.shadowTopLeft.className = 'shadow top';
|
131
|
+
this.dom.shadowBottomLeft.className = 'shadow bottom';
|
132
|
+
this.dom.shadowTopRight.className = 'shadow top';
|
133
|
+
this.dom.shadowBottomRight.className = 'shadow bottom';
|
134
|
+
|
135
|
+
this.dom.root.appendChild(this.dom.background);
|
136
|
+
this.dom.root.appendChild(this.dom.backgroundVertical);
|
137
|
+
this.dom.root.appendChild(this.dom.backgroundHorizontal);
|
138
|
+
this.dom.root.appendChild(this.dom.centerContainer);
|
139
|
+
this.dom.root.appendChild(this.dom.leftContainer);
|
140
|
+
this.dom.root.appendChild(this.dom.rightContainer);
|
141
|
+
this.dom.root.appendChild(this.dom.top);
|
142
|
+
this.dom.root.appendChild(this.dom.bottom);
|
143
|
+
|
144
|
+
this.dom.centerContainer.appendChild(this.dom.center);
|
145
|
+
this.dom.leftContainer.appendChild(this.dom.left);
|
146
|
+
this.dom.rightContainer.appendChild(this.dom.right);
|
147
|
+
|
148
|
+
this.dom.centerContainer.appendChild(this.dom.shadowTop);
|
149
|
+
this.dom.centerContainer.appendChild(this.dom.shadowBottom);
|
150
|
+
this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
|
151
|
+
this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
|
152
|
+
this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
|
153
|
+
this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
|
154
|
+
|
155
|
+
this.on('rangechange', this.redraw.bind(this));
|
156
|
+
this.on('change', this.redraw.bind(this));
|
157
|
+
this.on('touch', this._onTouch.bind(this));
|
158
|
+
this.on('pinch', this._onPinch.bind(this));
|
159
|
+
this.on('dragstart', this._onDragStart.bind(this));
|
160
|
+
this.on('drag', this._onDrag.bind(this));
|
161
|
+
|
162
|
+
// create event listeners for all interesting events, these events will be
|
163
|
+
// emitted via emitter
|
164
|
+
this.hammer = Hammer(this.dom.root, {
|
165
|
+
prevent_default: true
|
166
|
+
});
|
167
|
+
this.listeners = {};
|
299
168
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
169
|
+
var me = this;
|
170
|
+
var events = [
|
171
|
+
'touch', 'pinch',
|
172
|
+
'tap', 'doubletap', 'hold',
|
173
|
+
'dragstart', 'drag', 'dragend',
|
174
|
+
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
|
175
|
+
];
|
176
|
+
events.forEach(function (event) {
|
177
|
+
var listener = function () {
|
178
|
+
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
|
179
|
+
me.emit.apply(me, args);
|
305
180
|
};
|
306
|
-
|
181
|
+
me.hammer.on(event, listener);
|
182
|
+
me.listeners[event] = listener;
|
183
|
+
});
|
307
184
|
|
308
|
-
//
|
309
|
-
|
310
|
-
|
185
|
+
// size properties of each of the panels
|
186
|
+
this.props = {
|
187
|
+
root: {},
|
188
|
+
background: {},
|
189
|
+
centerContainer: {},
|
190
|
+
leftContainer: {},
|
191
|
+
rightContainer: {},
|
192
|
+
center: {},
|
193
|
+
left: {},
|
194
|
+
right: {},
|
195
|
+
top: {},
|
196
|
+
bottom: {},
|
197
|
+
border: {},
|
198
|
+
scrollTop: 0,
|
199
|
+
scrollTopMin: 0
|
200
|
+
};
|
201
|
+
this.touch = {}; // store state information needed for touch events
|
311
202
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
}
|
317
|
-
else {
|
318
|
-
// remove selection
|
319
|
-
this.setSelection([]);
|
320
|
-
}
|
321
|
-
}
|
203
|
+
// attach the root panel to the provided container
|
204
|
+
if (!container) throw new Error('No container provided');
|
205
|
+
container.appendChild(this.dom.root);
|
206
|
+
};
|
322
207
|
|
323
|
-
|
324
|
-
|
208
|
+
/**
|
209
|
+
* Destroy the Timeline, clean up all DOM elements and event listeners.
|
210
|
+
*/
|
211
|
+
Timeline.prototype.destroy = function () {
|
212
|
+
// unbind datasets
|
213
|
+
this.clear();
|
325
214
|
|
326
|
-
//
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
if (this.options.showCurrentTime) {
|
336
|
-
if (!this.mainPanel.hasChild(this.currentTime)) {
|
337
|
-
this.mainPanel.appendChild(this.currentTime);
|
338
|
-
this.currentTime.start();
|
339
|
-
}
|
340
|
-
}
|
341
|
-
else {
|
342
|
-
if (this.mainPanel.hasChild(this.currentTime)) {
|
343
|
-
this.currentTime.stop();
|
344
|
-
this.mainPanel.removeChild(this.currentTime);
|
345
|
-
}
|
215
|
+
// remove all event listeners
|
216
|
+
this.off();
|
217
|
+
|
218
|
+
// stop checking for changed size
|
219
|
+
this._stopAutoResize();
|
220
|
+
|
221
|
+
// remove from DOM
|
222
|
+
if (this.dom.root.parentNode) {
|
223
|
+
this.dom.root.parentNode.removeChild(this.dom.root);
|
346
224
|
}
|
225
|
+
this.dom = null;
|
347
226
|
|
348
|
-
//
|
349
|
-
|
350
|
-
if (
|
351
|
-
this.
|
227
|
+
// cleanup hammer touch events
|
228
|
+
for (var event in this.listeners) {
|
229
|
+
if (this.listeners.hasOwnProperty(event)) {
|
230
|
+
delete this.listeners[event];
|
352
231
|
}
|
353
232
|
}
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
233
|
+
this.listeners = null;
|
234
|
+
this.hammer = null;
|
235
|
+
|
236
|
+
// give all components the opportunity to cleanup
|
237
|
+
this.components.forEach(function (component) {
|
238
|
+
component.destroy();
|
239
|
+
});
|
240
|
+
|
241
|
+
this.body = null;
|
242
|
+
};
|
243
|
+
|
244
|
+
/**
|
245
|
+
* Set options. Options will be passed to all components loaded in the Timeline.
|
246
|
+
* @param {Object} [options]
|
247
|
+
* {String} orientation
|
248
|
+
* Vertical orientation for the Timeline,
|
249
|
+
* can be 'bottom' (default) or 'top'.
|
250
|
+
* {String | Number} width
|
251
|
+
* Width for the timeline, a number in pixels or
|
252
|
+
* a css string like '1000px' or '75%'. '100%' by default.
|
253
|
+
* {String | Number} height
|
254
|
+
* Fixed height for the Timeline, a number in pixels or
|
255
|
+
* a css string like '400px' or '75%'. If undefined,
|
256
|
+
* The Timeline will automatically size such that
|
257
|
+
* its contents fit.
|
258
|
+
* {String | Number} minHeight
|
259
|
+
* Minimum height for the Timeline, a number in pixels or
|
260
|
+
* a css string like '400px' or '75%'.
|
261
|
+
* {String | Number} maxHeight
|
262
|
+
* Maximum height for the Timeline, a number in pixels or
|
263
|
+
* a css string like '400px' or '75%'.
|
264
|
+
* {Number | Date | String} start
|
265
|
+
* Start date for the visible window
|
266
|
+
* {Number | Date | String} end
|
267
|
+
* End date for the visible window
|
268
|
+
*/
|
269
|
+
Timeline.prototype.setOptions = function (options) {
|
270
|
+
if (options) {
|
271
|
+
// copy the known options
|
272
|
+
var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
|
273
|
+
util.selectiveExtend(fields, this.options, options);
|
274
|
+
|
275
|
+
// enable/disable autoResize
|
276
|
+
this._initAutoResize();
|
358
277
|
}
|
359
278
|
|
279
|
+
// propagate options to all components
|
280
|
+
this.components.forEach(function (component) {
|
281
|
+
component.setOptions(options);
|
282
|
+
});
|
283
|
+
|
360
284
|
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
|
361
285
|
if (options && options.order) {
|
362
286
|
throw new Error('Option order is deprecated. There is no replacement for this feature.');
|
363
287
|
}
|
364
288
|
|
365
|
-
//
|
366
|
-
this.
|
289
|
+
// redraw everything
|
290
|
+
this.redraw();
|
367
291
|
};
|
368
292
|
|
369
293
|
/**
|
@@ -408,7 +332,7 @@ Timeline.prototype.setItems = function(items) {
|
|
408
332
|
else {
|
409
333
|
// turn an array into a dataset
|
410
334
|
newDataSet = new DataSet(items, {
|
411
|
-
|
335
|
+
type: {
|
412
336
|
start: 'Date',
|
413
337
|
end: 'Date'
|
414
338
|
}
|
@@ -417,13 +341,13 @@ Timeline.prototype.setItems = function(items) {
|
|
417
341
|
|
418
342
|
// set items
|
419
343
|
this.itemsData = newDataSet;
|
420
|
-
this.itemSet.setItems(newDataSet);
|
344
|
+
this.itemSet && this.itemSet.setItems(newDataSet);
|
421
345
|
|
422
|
-
if (initialLoad && (this.options
|
346
|
+
if (initialLoad && ('start' in this.options || 'end' in this.options)) {
|
423
347
|
this.fit();
|
424
348
|
|
425
|
-
var start = (this.options
|
426
|
-
var end = (this.options
|
349
|
+
var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
|
350
|
+
var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
|
427
351
|
|
428
352
|
this.setWindow(start, end);
|
429
353
|
}
|
@@ -433,7 +357,7 @@ Timeline.prototype.setItems = function(items) {
|
|
433
357
|
* Set groups
|
434
358
|
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
|
435
359
|
*/
|
436
|
-
Timeline.prototype.setGroups = function
|
360
|
+
Timeline.prototype.setGroups = function(groups) {
|
437
361
|
// convert to type DataSet when needed
|
438
362
|
var newDataSet;
|
439
363
|
if (!groups) {
|
@@ -461,7 +385,7 @@ Timeline.prototype.setGroups = function setGroups(groups) {
|
|
461
385
|
* @param {Object} [what] Optionally specify what to clear. By default:
|
462
386
|
* {items: true, groups: true, options: true}
|
463
387
|
*/
|
464
|
-
Timeline.prototype.clear = function
|
388
|
+
Timeline.prototype.clear = function(what) {
|
465
389
|
// clear items
|
466
390
|
if (!what || what.items) {
|
467
391
|
this.setItems(null);
|
@@ -472,16 +396,20 @@ Timeline.prototype.clear = function clear(what) {
|
|
472
396
|
this.setGroups(null);
|
473
397
|
}
|
474
398
|
|
475
|
-
// clear options
|
399
|
+
// clear options of timeline and of each of the components
|
476
400
|
if (!what || what.options) {
|
477
|
-
this.
|
401
|
+
this.components.forEach(function (component) {
|
402
|
+
component.setOptions(component.defaultOptions);
|
403
|
+
});
|
404
|
+
|
405
|
+
this.setOptions(this.defaultOptions); // this will also do a redraw
|
478
406
|
}
|
479
407
|
};
|
480
408
|
|
481
409
|
/**
|
482
410
|
* Set Timeline window such that it fits all items
|
483
411
|
*/
|
484
|
-
Timeline.prototype.fit = function
|
412
|
+
Timeline.prototype.fit = function() {
|
485
413
|
// apply the data range as range
|
486
414
|
var dataRange = this.getItemRange();
|
487
415
|
|
@@ -512,7 +440,7 @@ Timeline.prototype.fit = function fit() {
|
|
512
440
|
* When no minimum is found, min==null
|
513
441
|
* When no maximum is found, max==null
|
514
442
|
*/
|
515
|
-
Timeline.prototype.getItemRange = function
|
443
|
+
Timeline.prototype.getItemRange = function() {
|
516
444
|
// calculate min from start filed
|
517
445
|
var itemsData = this.itemsData,
|
518
446
|
min = null,
|
@@ -521,20 +449,22 @@ Timeline.prototype.getItemRange = function getItemRange() {
|
|
521
449
|
if (itemsData) {
|
522
450
|
// calculate the minimum value of the field 'start'
|
523
451
|
var minItem = itemsData.min('start');
|
524
|
-
min = minItem ? minItem.start.valueOf() : null;
|
452
|
+
min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
|
453
|
+
// Note: we convert first to Date and then to number because else
|
454
|
+
// a conversion from ISODate to Number will fail
|
525
455
|
|
526
456
|
// calculate maximum value of fields 'start' and 'end'
|
527
457
|
var maxStartItem = itemsData.max('start');
|
528
458
|
if (maxStartItem) {
|
529
|
-
max = maxStartItem.start.valueOf();
|
459
|
+
max = util.convert(maxStartItem.start, 'Date').valueOf();
|
530
460
|
}
|
531
461
|
var maxEndItem = itemsData.max('end');
|
532
462
|
if (maxEndItem) {
|
533
463
|
if (max == null) {
|
534
|
-
max = maxEndItem.end.valueOf();
|
464
|
+
max = util.convert(maxEndItem.end, 'Date').valueOf();
|
535
465
|
}
|
536
466
|
else {
|
537
|
-
max = Math.max(max, maxEndItem.end.valueOf());
|
467
|
+
max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
|
538
468
|
}
|
539
469
|
}
|
540
470
|
}
|
@@ -552,16 +482,16 @@ Timeline.prototype.getItemRange = function getItemRange() {
|
|
552
482
|
* selected. If ids is an empty array, all items will be
|
553
483
|
* unselected.
|
554
484
|
*/
|
555
|
-
Timeline.prototype.setSelection = function
|
556
|
-
this.itemSet.setSelection(ids);
|
485
|
+
Timeline.prototype.setSelection = function(ids) {
|
486
|
+
this.itemSet && this.itemSet.setSelection(ids);
|
557
487
|
};
|
558
488
|
|
559
489
|
/**
|
560
490
|
* Get the selected items by their id
|
561
491
|
* @return {Array} ids The ids of the selected items
|
562
492
|
*/
|
563
|
-
Timeline.prototype.getSelection = function
|
564
|
-
return this.itemSet.getSelection();
|
493
|
+
Timeline.prototype.getSelection = function() {
|
494
|
+
return this.itemSet && this.itemSet.getSelection() || [];
|
565
495
|
};
|
566
496
|
|
567
497
|
/**
|
@@ -577,7 +507,7 @@ Timeline.prototype.getSelection = function getSelection() {
|
|
577
507
|
* @param {Date | Number | String | Object} [start] Start date of visible window
|
578
508
|
* @param {Date | Number | String} [end] End date of visible window
|
579
509
|
*/
|
580
|
-
Timeline.prototype.setWindow = function
|
510
|
+
Timeline.prototype.setWindow = function(start, end) {
|
581
511
|
if (arguments.length == 1) {
|
582
512
|
var range = arguments[0];
|
583
513
|
this.range.setRange(range.start, range.end);
|
@@ -591,7 +521,7 @@ Timeline.prototype.setWindow = function setWindow(start, end) {
|
|
591
521
|
* Get the visible window
|
592
522
|
* @return {{start: Date, end: Date}} Visible range
|
593
523
|
*/
|
594
|
-
Timeline.prototype.getWindow = function
|
524
|
+
Timeline.prototype.getWindow = function() {
|
595
525
|
var range = this.range.getRange();
|
596
526
|
return {
|
597
527
|
start: new Date(range.start),
|
@@ -600,155 +530,322 @@ Timeline.prototype.getWindow = function setWindow() {
|
|
600
530
|
};
|
601
531
|
|
602
532
|
/**
|
603
|
-
* Force a
|
533
|
+
* Force a redraw of the Timeline. Can be useful to manually redraw when
|
604
534
|
* option autoResize=false
|
605
535
|
*/
|
606
|
-
Timeline.prototype.
|
607
|
-
|
536
|
+
Timeline.prototype.redraw = function() {
|
537
|
+
var resized = false,
|
538
|
+
options = this.options,
|
539
|
+
props = this.props,
|
540
|
+
dom = this.dom;
|
541
|
+
|
542
|
+
if (!dom) return; // when destroyed
|
543
|
+
|
544
|
+
// update class names
|
545
|
+
dom.root.className = 'vis timeline root ' + options.orientation;
|
546
|
+
|
547
|
+
// update root width and height options
|
548
|
+
dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
|
549
|
+
dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
|
550
|
+
dom.root.style.width = util.option.asSize(options.width, '');
|
551
|
+
|
552
|
+
// calculate border widths
|
553
|
+
props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
|
554
|
+
props.border.right = props.border.left;
|
555
|
+
props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
|
556
|
+
props.border.bottom = props.border.top;
|
557
|
+
var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
|
558
|
+
var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
|
559
|
+
|
560
|
+
// calculate the heights. If any of the side panels is empty, we set the height to
|
561
|
+
// minus the border width, such that the border will be invisible
|
562
|
+
props.center.height = dom.center.offsetHeight;
|
563
|
+
props.left.height = dom.left.offsetHeight;
|
564
|
+
props.right.height = dom.right.offsetHeight;
|
565
|
+
props.top.height = dom.top.clientHeight || -props.border.top;
|
566
|
+
props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
|
567
|
+
|
568
|
+
// TODO: compensate borders when any of the panels is empty.
|
569
|
+
|
570
|
+
// apply auto height
|
571
|
+
// TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
|
572
|
+
var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
|
573
|
+
var autoHeight = props.top.height + contentHeight + props.bottom.height +
|
574
|
+
borderRootHeight + props.border.top + props.border.bottom;
|
575
|
+
dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
|
576
|
+
|
577
|
+
// calculate heights of the content panels
|
578
|
+
props.root.height = dom.root.offsetHeight;
|
579
|
+
props.background.height = props.root.height - borderRootHeight;
|
580
|
+
var containerHeight = props.root.height - props.top.height - props.bottom.height -
|
581
|
+
borderRootHeight;
|
582
|
+
props.centerContainer.height = containerHeight;
|
583
|
+
props.leftContainer.height = containerHeight;
|
584
|
+
props.rightContainer.height = props.leftContainer.height;
|
585
|
+
|
586
|
+
// calculate the widths of the panels
|
587
|
+
props.root.width = dom.root.offsetWidth;
|
588
|
+
props.background.width = props.root.width - borderRootWidth;
|
589
|
+
props.left.width = dom.leftContainer.clientWidth || -props.border.left;
|
590
|
+
props.leftContainer.width = props.left.width;
|
591
|
+
props.right.width = dom.rightContainer.clientWidth || -props.border.right;
|
592
|
+
props.rightContainer.width = props.right.width;
|
593
|
+
var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
|
594
|
+
props.center.width = centerWidth;
|
595
|
+
props.centerContainer.width = centerWidth;
|
596
|
+
props.top.width = centerWidth;
|
597
|
+
props.bottom.width = centerWidth;
|
598
|
+
|
599
|
+
// resize the panels
|
600
|
+
dom.background.style.height = props.background.height + 'px';
|
601
|
+
dom.backgroundVertical.style.height = props.background.height + 'px';
|
602
|
+
dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
|
603
|
+
dom.centerContainer.style.height = props.centerContainer.height + 'px';
|
604
|
+
dom.leftContainer.style.height = props.leftContainer.height + 'px';
|
605
|
+
dom.rightContainer.style.height = props.rightContainer.height + 'px';
|
606
|
+
|
607
|
+
dom.background.style.width = props.background.width + 'px';
|
608
|
+
dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
|
609
|
+
dom.backgroundHorizontal.style.width = props.background.width + 'px';
|
610
|
+
dom.centerContainer.style.width = props.center.width + 'px';
|
611
|
+
dom.top.style.width = props.top.width + 'px';
|
612
|
+
dom.bottom.style.width = props.bottom.width + 'px';
|
613
|
+
|
614
|
+
// reposition the panels
|
615
|
+
dom.background.style.left = '0';
|
616
|
+
dom.background.style.top = '0';
|
617
|
+
dom.backgroundVertical.style.left = props.left.width + 'px';
|
618
|
+
dom.backgroundVertical.style.top = '0';
|
619
|
+
dom.backgroundHorizontal.style.left = '0';
|
620
|
+
dom.backgroundHorizontal.style.top = props.top.height + 'px';
|
621
|
+
dom.centerContainer.style.left = props.left.width + 'px';
|
622
|
+
dom.centerContainer.style.top = props.top.height + 'px';
|
623
|
+
dom.leftContainer.style.left = '0';
|
624
|
+
dom.leftContainer.style.top = props.top.height + 'px';
|
625
|
+
dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
|
626
|
+
dom.rightContainer.style.top = props.top.height + 'px';
|
627
|
+
dom.top.style.left = props.left.width + 'px';
|
628
|
+
dom.top.style.top = '0';
|
629
|
+
dom.bottom.style.left = props.left.width + 'px';
|
630
|
+
dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
|
631
|
+
|
632
|
+
// update the scrollTop, feasible range for the offset can be changed
|
633
|
+
// when the height of the Timeline or of the contents of the center changed
|
634
|
+
this._updateScrollTop();
|
635
|
+
|
636
|
+
// reposition the scrollable contents
|
637
|
+
var offset = this.props.scrollTop;
|
638
|
+
if (options.orientation == 'bottom') {
|
639
|
+
offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0);
|
640
|
+
}
|
641
|
+
dom.center.style.left = '0';
|
642
|
+
dom.center.style.top = offset + 'px';
|
643
|
+
dom.left.style.left = '0';
|
644
|
+
dom.left.style.top = offset + 'px';
|
645
|
+
dom.right.style.left = '0';
|
646
|
+
dom.right.style.top = offset + 'px';
|
647
|
+
|
648
|
+
// show shadows when vertical scrolling is available
|
649
|
+
var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
|
650
|
+
var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
|
651
|
+
dom.shadowTop.style.visibility = visibilityTop;
|
652
|
+
dom.shadowBottom.style.visibility = visibilityBottom;
|
653
|
+
dom.shadowTopLeft.style.visibility = visibilityTop;
|
654
|
+
dom.shadowBottomLeft.style.visibility = visibilityBottom;
|
655
|
+
dom.shadowTopRight.style.visibility = visibilityTop;
|
656
|
+
dom.shadowBottomRight.style.visibility = visibilityBottom;
|
657
|
+
|
658
|
+
// redraw all components
|
659
|
+
this.components.forEach(function (component) {
|
660
|
+
resized = component.redraw() || resized;
|
661
|
+
});
|
662
|
+
if (resized) {
|
663
|
+
// keep repainting until all sizes are settled
|
664
|
+
this.redraw();
|
665
|
+
}
|
666
|
+
};
|
667
|
+
|
668
|
+
// TODO: deprecated since version 1.1.0, remove some day
|
669
|
+
Timeline.prototype.repaint = function () {
|
670
|
+
throw new Error('Function repaint is deprecated. Use redraw instead.');
|
608
671
|
};
|
609
672
|
|
610
673
|
/**
|
611
|
-
*
|
612
|
-
* @param {
|
674
|
+
* Convert a position on screen (pixels) to a datetime
|
675
|
+
* @param {int} x Position on the screen in pixels
|
676
|
+
* @return {Date} time The datetime the corresponds with given position x
|
613
677
|
* @private
|
614
678
|
*/
|
615
|
-
// TODO: move this function to
|
616
|
-
Timeline.prototype.
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
|
621
|
-
if (ctrlKey || shiftKey) {
|
622
|
-
this._onMultiSelectItem(event);
|
623
|
-
return;
|
624
|
-
}
|
625
|
-
|
626
|
-
var oldSelection = this.getSelection();
|
627
|
-
|
628
|
-
var item = ItemSet.itemFromTarget(event);
|
629
|
-
var selection = item ? [item.id] : [];
|
630
|
-
this.setSelection(selection);
|
679
|
+
// TODO: move this function to Range
|
680
|
+
Timeline.prototype._toTime = function(x) {
|
681
|
+
var conversion = this.range.conversion(this.props.center.width);
|
682
|
+
return new Date(x / conversion.scale + conversion.offset);
|
683
|
+
};
|
631
684
|
|
632
|
-
|
685
|
+
/**
|
686
|
+
* Convert a datetime (Date object) into a position on the screen
|
687
|
+
* @param {Date} time A date
|
688
|
+
* @return {int} x The position on the screen in pixels which corresponds
|
689
|
+
* with the given date.
|
690
|
+
* @private
|
691
|
+
*/
|
692
|
+
// TODO: move this function to Range
|
693
|
+
Timeline.prototype._toScreen = function(time) {
|
694
|
+
var conversion = this.range.conversion(this.props.center.width);
|
695
|
+
return (time.valueOf() - conversion.offset) * conversion.scale;
|
696
|
+
};
|
633
697
|
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
698
|
+
/**
|
699
|
+
* Initialize watching when option autoResize is true
|
700
|
+
* @private
|
701
|
+
*/
|
702
|
+
Timeline.prototype._initAutoResize = function () {
|
703
|
+
if (this.options.autoResize == true) {
|
704
|
+
this._startAutoResize();
|
705
|
+
}
|
706
|
+
else {
|
707
|
+
this._stopAutoResize();
|
639
708
|
}
|
640
|
-
|
641
|
-
event.stopPropagation();
|
642
709
|
};
|
643
710
|
|
644
711
|
/**
|
645
|
-
*
|
646
|
-
*
|
712
|
+
* Watch for changes in the size of the container. On resize, the Panel will
|
713
|
+
* automatically redraw itself.
|
647
714
|
* @private
|
648
715
|
*/
|
649
|
-
Timeline.prototype.
|
650
|
-
|
651
|
-
if (!this.options.editable.add) return;
|
716
|
+
Timeline.prototype._startAutoResize = function () {
|
717
|
+
var me = this;
|
652
718
|
|
653
|
-
|
654
|
-
item = ItemSet.itemFromTarget(event);
|
719
|
+
this._stopAutoResize();
|
655
720
|
|
656
|
-
|
657
|
-
|
721
|
+
this._onResize = function() {
|
722
|
+
if (me.options.autoResize != true) {
|
723
|
+
// stop watching when the option autoResize is changed to false
|
724
|
+
me._stopAutoResize();
|
725
|
+
return;
|
726
|
+
}
|
658
727
|
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
me.
|
664
|
-
|
665
|
-
});
|
666
|
-
}
|
667
|
-
else {
|
668
|
-
// add item
|
669
|
-
var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
|
670
|
-
var x = event.gesture.center.pageX - xAbs;
|
671
|
-
var newItem = {
|
672
|
-
start: this.timeAxis.snap(this._toTime(x)),
|
673
|
-
content: 'new item'
|
674
|
-
};
|
728
|
+
if (me.dom.root) {
|
729
|
+
// check whether the frame is resized
|
730
|
+
if ((me.dom.root.clientWidth != me.props.lastWidth) ||
|
731
|
+
(me.dom.root.clientHeight != me.props.lastHeight)) {
|
732
|
+
me.props.lastWidth = me.dom.root.clientWidth;
|
733
|
+
me.props.lastHeight = me.dom.root.clientHeight;
|
675
734
|
|
676
|
-
|
677
|
-
|
678
|
-
newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
|
735
|
+
me.emit('change');
|
736
|
+
}
|
679
737
|
}
|
738
|
+
};
|
680
739
|
|
681
|
-
|
682
|
-
|
740
|
+
// add event listener to window resize
|
741
|
+
util.addEventListener(window, 'resize', this._onResize);
|
683
742
|
|
684
|
-
|
685
|
-
|
686
|
-
newItem.group = group.groupId;
|
687
|
-
}
|
743
|
+
this.watchTimer = setInterval(this._onResize, 1000);
|
744
|
+
};
|
688
745
|
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
746
|
+
/**
|
747
|
+
* Stop watching for a resize of the frame.
|
748
|
+
* @private
|
749
|
+
*/
|
750
|
+
Timeline.prototype._stopAutoResize = function () {
|
751
|
+
if (this.watchTimer) {
|
752
|
+
clearInterval(this.watchTimer);
|
753
|
+
this.watchTimer = undefined;
|
696
754
|
}
|
755
|
+
|
756
|
+
// remove event listener on window.resize
|
757
|
+
util.removeEventListener(window, 'resize', this._onResize);
|
758
|
+
this._onResize = null;
|
697
759
|
};
|
698
760
|
|
699
761
|
/**
|
700
|
-
*
|
762
|
+
* Start moving the timeline vertically
|
701
763
|
* @param {Event} event
|
702
764
|
* @private
|
703
765
|
*/
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
var selection,
|
709
|
-
item = ItemSet.itemFromTarget(event);
|
710
|
-
|
711
|
-
if (item) {
|
712
|
-
// multi select items
|
713
|
-
selection = this.getSelection(); // current selection
|
714
|
-
var index = selection.indexOf(item.id);
|
715
|
-
if (index == -1) {
|
716
|
-
// item is not yet selected -> select it
|
717
|
-
selection.push(item.id);
|
718
|
-
}
|
719
|
-
else {
|
720
|
-
// item is already selected -> deselect it
|
721
|
-
selection.splice(index, 1);
|
722
|
-
}
|
723
|
-
this.setSelection(selection);
|
766
|
+
Timeline.prototype._onTouch = function (event) {
|
767
|
+
this.touch.allowDragging = true;
|
768
|
+
};
|
724
769
|
|
725
|
-
|
726
|
-
|
727
|
-
|
770
|
+
/**
|
771
|
+
* Start moving the timeline vertically
|
772
|
+
* @param {Event} event
|
773
|
+
* @private
|
774
|
+
*/
|
775
|
+
Timeline.prototype._onPinch = function (event) {
|
776
|
+
this.touch.allowDragging = false;
|
777
|
+
};
|
778
|
+
|
779
|
+
/**
|
780
|
+
* Start moving the timeline vertically
|
781
|
+
* @param {Event} event
|
782
|
+
* @private
|
783
|
+
*/
|
784
|
+
Timeline.prototype._onDragStart = function (event) {
|
785
|
+
this.touch.initialScrollTop = this.props.scrollTop;
|
786
|
+
};
|
787
|
+
|
788
|
+
/**
|
789
|
+
* Move the timeline vertically
|
790
|
+
* @param {Event} event
|
791
|
+
* @private
|
792
|
+
*/
|
793
|
+
Timeline.prototype._onDrag = function (event) {
|
794
|
+
// refuse to drag when we where pinching to prevent the timeline make a jump
|
795
|
+
// when releasing the fingers in opposite order from the touch screen
|
796
|
+
if (!this.touch.allowDragging) return;
|
797
|
+
|
798
|
+
var delta = event.gesture.deltaY;
|
799
|
+
|
800
|
+
var oldScrollTop = this._getScrollTop();
|
801
|
+
var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
|
728
802
|
|
729
|
-
|
803
|
+
if (newScrollTop != oldScrollTop) {
|
804
|
+
this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
|
730
805
|
}
|
731
806
|
};
|
732
807
|
|
733
808
|
/**
|
734
|
-
*
|
735
|
-
* @param {
|
736
|
-
* @
|
809
|
+
* Apply a scrollTop
|
810
|
+
* @param {Number} scrollTop
|
811
|
+
* @returns {Number} scrollTop Returns the applied scrollTop
|
737
812
|
* @private
|
738
813
|
*/
|
739
|
-
Timeline.prototype.
|
740
|
-
|
741
|
-
|
814
|
+
Timeline.prototype._setScrollTop = function (scrollTop) {
|
815
|
+
this.props.scrollTop = scrollTop;
|
816
|
+
this._updateScrollTop();
|
817
|
+
return this.props.scrollTop;
|
742
818
|
};
|
743
819
|
|
744
820
|
/**
|
745
|
-
*
|
746
|
-
* @
|
747
|
-
* @return {int} x The position on the screen in pixels which corresponds
|
748
|
-
* with the given date.
|
821
|
+
* Update the current scrollTop when the height of the containers has been changed
|
822
|
+
* @returns {Number} scrollTop Returns the applied scrollTop
|
749
823
|
* @private
|
750
824
|
*/
|
751
|
-
Timeline.prototype.
|
752
|
-
|
753
|
-
|
825
|
+
Timeline.prototype._updateScrollTop = function () {
|
826
|
+
// recalculate the scrollTopMin
|
827
|
+
var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
|
828
|
+
if (scrollTopMin != this.props.scrollTopMin) {
|
829
|
+
// in case of bottom orientation, change the scrollTop such that the contents
|
830
|
+
// do not move relative to the time axis at the bottom
|
831
|
+
if (this.options.orientation == 'bottom') {
|
832
|
+
this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
|
833
|
+
}
|
834
|
+
this.props.scrollTopMin = scrollTopMin;
|
835
|
+
}
|
836
|
+
|
837
|
+
// limit the scrollTop to the feasible scroll range
|
838
|
+
if (this.props.scrollTop > 0) this.props.scrollTop = 0;
|
839
|
+
if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
|
840
|
+
|
841
|
+
return this.props.scrollTop;
|
842
|
+
};
|
843
|
+
|
844
|
+
/**
|
845
|
+
* Get the current scrollTop
|
846
|
+
* @returns {number} scrollTop
|
847
|
+
* @private
|
848
|
+
*/
|
849
|
+
Timeline.prototype._getScrollTop = function () {
|
850
|
+
return this.props.scrollTop;
|
754
851
|
};
|