vis-rails 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
  };