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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +2 -0
  4. data/lib/vis/rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
  6. data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
  7. data/vendor/assets/javascripts/vis.js +3 -3
  8. data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
  9. data/vendor/assets/vis/DataSet.js +106 -130
  10. data/vendor/assets/vis/DataView.js +35 -37
  11. data/vendor/assets/vis/graph/Edge.js +225 -45
  12. data/vendor/assets/vis/graph/Graph.js +120 -24
  13. data/vendor/assets/vis/graph/Node.js +16 -16
  14. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
  15. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
  16. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
  17. data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
  18. data/vendor/assets/vis/module/exports.js +2 -3
  19. data/vendor/assets/vis/timeline/Range.js +93 -80
  20. data/vendor/assets/vis/timeline/Timeline.js +525 -428
  21. data/vendor/assets/vis/timeline/component/Component.js +19 -53
  22. data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
  23. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
  24. data/vendor/assets/vis/timeline/component/Group.js +47 -50
  25. data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
  26. data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
  27. data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
  28. data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
  29. data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
  30. data/vendor/assets/vis/timeline/component/css/item.css +1 -11
  31. data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
  32. data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
  33. data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
  34. data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
  35. data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
  36. data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
  37. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
  38. data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
  39. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
  40. data/vendor/assets/vis/timeline/stack.js +5 -5
  41. data/vendor/assets/vis/util.js +81 -35
  42. metadata +7 -4
  43. data/vendor/assets/vis/timeline/component/Panel.js +0 -170
  44. 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 {RootPanel} root Root panel, used to subscribe to events
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(root, parent, options) {
11
- this.id = util.randomUUID();
12
- this.start = null; // Number
13
- this.end = null; // Number
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.root = root;
16
- this.parent = parent;
17
- this.options = options || {};
30
+ this.props = {
31
+ touch: {}
32
+ };
18
33
 
19
34
  // drag listeners for dragging
20
- this.root.on('dragstart', this._onDragStart.bind(this));
21
- this.root.on('drag', this._onDrag.bind(this));
22
- this.root.on('dragend', this._onDragEnd.bind(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.root.on('hold', this._onHold.bind(this));
40
+ this.body.emitter.on('hold', this._onHold.bind(this));
26
41
 
27
42
  // mouse wheel for zooming
28
- this.root.on('mousewheel', this._onMouseWheel.bind(this));
29
- this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
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.root.on('touch', this._onTouch.bind(this));
33
- this.root.on('pinch', this._onPinch.bind(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
- // turn Range into an event emitter
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
- util.extend(this.options, options);
53
-
54
- // re-apply range with new limitations
55
- if (this.start !== null && this.end !== null) {
56
- this.setRange(this.start, this.end);
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 (touchParams.ignore) return;
255
-
256
- // TODO: reckon with option movable
278
+ if (!this.props.touch.allowDragging) return;
257
279
 
258
- touchParams.start = this.start;
259
- touchParams.end = this.end;
280
+ this.props.touch.start = this.start;
281
+ this.props.touch.end = this.end;
260
282
 
261
- var frame = this.parent.frame;
262
- if (frame) {
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 operating.
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 (touchParams.ignore) return;
302
+ if (!this.props.touch.allowDragging) return;
282
303
 
283
304
  var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
284
- interval = (touchParams.end - touchParams.start),
285
- width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
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(touchParams.start + diffRange, touchParams.end + diffRange);
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 operating.
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 (touchParams.ignore) return;
328
+ if (!this.props.touch.allowDragging) return;
305
329
 
306
- // TODO: reckon with option movable
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
- // TODO: reckon with option zoomable
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.parent.frame),
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
- touchParams.start = this.start;
373
- touchParams.end = this.end;
374
- touchParams.ignore = false;
375
- touchParams.center = null;
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
- touchParams.ignore = true;
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
- var direction = this.options.direction;
400
- touchParams.ignore = true;
415
+ // only allow zooming when configured as zoomable and moveable
416
+ if (!(this.options.zoomable && this.options.moveable)) return;
401
417
 
402
- // TODO: reckon with option zoomable
418
+ this.props.touch.allowDragging = false;
403
419
 
404
420
  if (event.gesture.touches.length > 1) {
405
- if (!touchParams.center) {
406
- touchParams.center = getPointer(event.gesture.center, this.parent.frame);
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(touchParams.center),
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 + (touchParams.start - initDate) * scale);
417
- var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
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.parent.width;
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.parent.height;
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
- orientation: 'bottom',
16
- direction: 'horizontal', // 'horizontal' or 'vertical'
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
- showMinorLabels: true,
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
- this.options = {};
71
- util.deepExtend(this.options, this.defaultOptions);
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
- // single select (or unselect) when tapping an item
96
- this.rootPanel.on('tap', this._onSelectItem.bind(this));
27
+ // all components listed here will be repainted automatically
28
+ this.components = [];
97
29
 
98
- // multi select when holding mouse/touch, or on ctrl+click
99
- this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
100
-
101
- // add item on doubletap
102
- this.rootPanel.on('doubletap', this._onAddItem.bind(this));
103
-
104
- // side panel
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
- className: function () {
124
- return 'side' + (me.groupsData ? '' : ' hidden');
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
- // TODO: move range inside rootPanel?
148
- var rangeOptions = Object.create(this.options);
149
- this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
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
- // panel with time axis
164
- var timeAxisOptions = util.extend(Object.create(rootOptions), {
165
- range: this.range,
166
- left: null,
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
- // Note: time bar will be attached in this.setOptions when selected
247
- this.currentTime = new CurrentTime(this.range, rootOptions);
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(rootOptions);
252
- this.customTime.on('timechange', function (time) {
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
- // itemset containing items and groups
260
- var itemOptions = util.extend(Object.create(this.options), {
261
- left: null,
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
- * Set options
292
- * @param {Object} options TODO: describe the available options
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.setOptions = function (options) {
295
- util.deepExtend(this.options, options);
296
-
297
- if ('editable' in options) {
298
- var isBoolean = typeof options.editable === 'boolean';
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
- this.options.editable = {
301
- updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
302
- updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
303
- add: isBoolean ? options.editable : (options.editable.add || false),
304
- remove: isBoolean ? options.editable : (options.editable.remove || false)
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
- // force update of range (apply new min/max etc.)
309
- // both start and end are optional
310
- this.range.setRange(options.start, options.end);
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
- if ('editable' in options || 'selectable' in options) {
313
- if (this.options.selectable) {
314
- // force update of selection
315
- this.setSelection(this.getSelection());
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
- // force the itemSet to refresh: options like orientation and margins may be changed
324
- this.itemSet.markDirty();
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
- // validate the callback functions
327
- var validateCallback = (function (fn) {
328
- if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
329
- throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
330
- }
331
- }).bind(this);
332
- ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
333
-
334
- // add/remove the current time bar
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
- // add/remove the custom time bar
349
- if (this.options.showCustomTime) {
350
- if (!this.mainPanel.hasChild(this.customTime)) {
351
- this.mainPanel.appendChild(this.customTime);
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
- else {
355
- if (this.mainPanel.hasChild(this.customTime)) {
356
- this.mainPanel.removeChild(this.customTime);
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
- // repaint everything
366
- this.rootPanel.repaint();
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
- convert: {
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.start == undefined || this.options.end == undefined)) {
346
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
423
347
  this.fit();
424
348
 
425
- var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
426
- var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
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 setGroups(groups) {
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 clear(what) {
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.setOptions(this.defaultOptions);
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 fit() {
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 getItemRange() {
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 setSelection (ids) {
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 getSelection() {
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 setWindow(start, end) {
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 setWindow() {
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 repaint of the Timeline. Can be useful to manually repaint when
533
+ * Force a redraw of the Timeline. Can be useful to manually redraw when
604
534
  * option autoResize=false
605
535
  */
606
- Timeline.prototype.repaint = function repaint() {
607
- this.rootPanel.repaint();
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
- * Handle selecting/deselecting an item when tapping it
612
- * @param {Event} event
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 ItemSet
616
- Timeline.prototype._onSelectItem = function (event) {
617
- if (!this.options.selectable) return;
618
-
619
- var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
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
- var newSelection = this.getSelection();
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
- // if selection is changed, emit a select event
635
- if (!util.equalArray(oldSelection, newSelection)) {
636
- this.emit('select', {
637
- items: this.getSelection()
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
- * Handle creation and updates of an item on double tap
646
- * @param event
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._onAddItem = function (event) {
650
- if (!this.options.selectable) return;
651
- if (!this.options.editable.add) return;
716
+ Timeline.prototype._startAutoResize = function () {
717
+ var me = this;
652
718
 
653
- var me = this,
654
- item = ItemSet.itemFromTarget(event);
719
+ this._stopAutoResize();
655
720
 
656
- if (item) {
657
- // update item
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
- // execute async handler to update the item (or cancel it)
660
- var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
661
- this.options.onUpdate(itemData, function (itemData) {
662
- if (itemData) {
663
- me.itemsData.update(itemData);
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
- // when default type is a range, add a default end date to the new item
677
- if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
678
- newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
735
+ me.emit('change');
736
+ }
679
737
  }
738
+ };
680
739
 
681
- var id = util.randomUUID();
682
- newItem[this.itemsData.fieldId] = id;
740
+ // add event listener to window resize
741
+ util.addEventListener(window, 'resize', this._onResize);
683
742
 
684
- var group = ItemSet.groupFromTarget(event);
685
- if (group) {
686
- newItem.group = group.groupId;
687
- }
743
+ this.watchTimer = setInterval(this._onResize, 1000);
744
+ };
688
745
 
689
- // execute async handler to customize (or cancel) adding an item
690
- this.options.onAdd(newItem, function (item) {
691
- if (item) {
692
- me.itemsData.add(newItem);
693
- // TODO: need to trigger a repaint?
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
- * Handle selecting/deselecting multiple items when holding an item
762
+ * Start moving the timeline vertically
701
763
  * @param {Event} event
702
764
  * @private
703
765
  */
704
- // TODO: move this function to ItemSet
705
- Timeline.prototype._onMultiSelectItem = function (event) {
706
- if (!this.options.selectable) return;
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
- this.emit('select', {
726
- items: this.getSelection()
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
- event.stopPropagation();
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
- * Convert a position on screen (pixels) to a datetime
735
- * @param {int} x Position on the screen in pixels
736
- * @return {Date} time The datetime the corresponds with given position x
809
+ * Apply a scrollTop
810
+ * @param {Number} scrollTop
811
+ * @returns {Number} scrollTop Returns the applied scrollTop
737
812
  * @private
738
813
  */
739
- Timeline.prototype._toTime = function _toTime(x) {
740
- var conversion = this.range.conversion(this.mainPanel.width);
741
- return new Date(x / conversion.scale + conversion.offset);
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
- * Convert a datetime (Date object) into a position on the screen
746
- * @param {Date} time A date
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._toScreen = function _toScreen(time) {
752
- var conversion = this.range.conversion(this.mainPanel.width);
753
- return (time.valueOf() - conversion.offset) * conversion.scale;
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
  };