vis-rails 0.0.4 → 0.0.5

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 (58) hide show
  1. checksums.yaml +5 -13
  2. data/lib/vis/rails/version.rb +1 -1
  3. data/vendor/assets/component/emitter.js +162 -0
  4. data/vendor/assets/javascripts/vis.js +1 -0
  5. data/vendor/assets/vis/DataSet.js +8 -2
  6. data/vendor/assets/vis/DataView.js +8 -4
  7. data/vendor/assets/vis/graph/Edge.js +210 -78
  8. data/vendor/assets/vis/graph/Graph.js +474 -652
  9. data/vendor/assets/vis/graph/Node.js +119 -82
  10. data/vendor/assets/vis/graph/css/graph-manipulation.css +128 -0
  11. data/vendor/assets/vis/graph/css/graph-navigation.css +62 -0
  12. data/vendor/assets/vis/graph/graphMixins/ClusterMixin.js +1141 -0
  13. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +296 -0
  14. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +433 -0
  15. data/vendor/assets/vis/graph/graphMixins/MixinLoader.js +201 -0
  16. data/vendor/assets/vis/graph/graphMixins/NavigationMixin.js +173 -0
  17. data/vendor/assets/vis/graph/graphMixins/SectorsMixin.js +552 -0
  18. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +558 -0
  19. data/vendor/assets/vis/graph/graphMixins/physics/BarnesHut.js +373 -0
  20. data/vendor/assets/vis/graph/graphMixins/physics/HierarchialRepulsion.js +64 -0
  21. data/vendor/assets/vis/graph/graphMixins/physics/PhysicsMixin.js +513 -0
  22. data/vendor/assets/vis/graph/graphMixins/physics/Repulsion.js +66 -0
  23. data/vendor/assets/vis/graph/img/acceptDeleteIcon.png +0 -0
  24. data/vendor/assets/vis/graph/img/addNodeIcon.png +0 -0
  25. data/vendor/assets/vis/graph/img/backIcon.png +0 -0
  26. data/vendor/assets/vis/graph/img/connectIcon.png +0 -0
  27. data/vendor/assets/vis/graph/img/cross.png +0 -0
  28. data/vendor/assets/vis/graph/img/cross2.png +0 -0
  29. data/vendor/assets/vis/graph/img/deleteIcon.png +0 -0
  30. data/vendor/assets/vis/graph/img/downArrow.png +0 -0
  31. data/vendor/assets/vis/graph/img/editIcon.png +0 -0
  32. data/vendor/assets/vis/graph/img/leftArrow.png +0 -0
  33. data/vendor/assets/vis/graph/img/rightArrow.png +0 -0
  34. data/vendor/assets/vis/graph/img/upArrow.png +0 -0
  35. data/vendor/assets/vis/module/exports.js +0 -2
  36. data/vendor/assets/vis/module/header.js +2 -2
  37. data/vendor/assets/vis/module/imports.js +1 -2
  38. data/vendor/assets/vis/timeline/Controller.js +56 -45
  39. data/vendor/assets/vis/timeline/Range.js +68 -62
  40. data/vendor/assets/vis/timeline/Stack.js +11 -13
  41. data/vendor/assets/vis/timeline/TimeStep.js +43 -38
  42. data/vendor/assets/vis/timeline/Timeline.js +215 -93
  43. data/vendor/assets/vis/timeline/component/Component.js +19 -3
  44. data/vendor/assets/vis/timeline/component/CurrentTime.js +1 -1
  45. data/vendor/assets/vis/timeline/component/CustomTime.js +39 -120
  46. data/vendor/assets/vis/timeline/component/GroupSet.js +35 -1
  47. data/vendor/assets/vis/timeline/component/ItemSet.js +272 -9
  48. data/vendor/assets/vis/timeline/component/RootPanel.js +59 -47
  49. data/vendor/assets/vis/timeline/component/TimeAxis.js +10 -0
  50. data/vendor/assets/vis/timeline/component/css/item.css +53 -22
  51. data/vendor/assets/vis/timeline/component/item/Item.js +40 -5
  52. data/vendor/assets/vis/timeline/component/item/ItemBox.js +3 -1
  53. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +3 -1
  54. data/vendor/assets/vis/timeline/component/item/ItemRange.js +67 -3
  55. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +37 -9
  56. data/vendor/assets/vis/timeline/img/delete.png +0 -0
  57. data/vendor/assets/vis/util.js +169 -30
  58. metadata +39 -12
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @constructor Stack
3
3
  * Stacks items on top of each other.
4
- * @param {ItemSet} parent
4
+ * @param {ItemSet} itemset
5
5
  * @param {Object} [options]
6
6
  */
7
- function Stack (parent, options) {
8
- this.parent = parent;
7
+ function Stack (itemset, options) {
8
+ this.itemset = itemset;
9
9
 
10
10
  this.options = options || {};
11
11
  this.defaultOptions = {
@@ -43,14 +43,14 @@ function Stack (parent, options) {
43
43
  /**
44
44
  * Set options for the stack
45
45
  * @param {Object} options Available options:
46
- * {ItemSet} parent
46
+ * {ItemSet} itemset
47
47
  * {Number} margin
48
48
  * {function} order Stacking order
49
49
  */
50
50
  Stack.prototype.setOptions = function setOptions (options) {
51
51
  util.extend(this.options, options);
52
52
 
53
- // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
53
+ // TODO: register on data changes at the connected itemset, and update the changed part only and immediately
54
54
  };
55
55
 
56
56
  /**
@@ -63,16 +63,14 @@ Stack.prototype.update = function update() {
63
63
  };
64
64
 
65
65
  /**
66
- * Order the items. The items are ordered by width first, and by left position
67
- * second.
68
- * If a custom order function has been provided via the options, then this will
69
- * be used.
66
+ * Order the items. If a custom order function has been provided via the options,
67
+ * then this will be used.
70
68
  * @private
71
69
  */
72
70
  Stack.prototype._order = function _order () {
73
- var items = this.parent.items;
71
+ var items = this.itemset.items;
74
72
  if (!items) {
75
- throw new Error('Cannot stack items: parent does not contain items');
73
+ throw new Error('Cannot stack items: ItemSet does not contain items');
76
74
  }
77
75
 
78
76
  // TODO: store the sorted items, to have less work later on
@@ -185,8 +183,8 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
185
183
  * @return {boolean} true if a and b collide, else false
186
184
  */
187
185
  Stack.prototype.collision = function collision (a, b, margin) {
188
- return ((a.left - margin) < (b.left + b.getWidth()) &&
189
- (a.left + a.getWidth() + margin) > b.left &&
186
+ return ((a.left - margin) < (b.left + b.width) &&
187
+ (a.left + a.width + margin) > b.left &&
190
188
  (a.top - margin) < (b.top + b.height) &&
191
189
  (a.top + a.height + margin) > b.top);
192
190
  };
@@ -281,35 +281,38 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) {
281
281
  };
282
282
 
283
283
  /**
284
- * Snap a date to a rounded value. The snap intervals are dependent on the
285
- * current scale and step.
286
- * @param {Date} date the date to be snapped
284
+ * Snap a date to a rounded value.
285
+ * The snap intervals are dependent on the current scale and step.
286
+ * @param {Date} date the date to be snapped.
287
+ * @return {Date} snappedDate
287
288
  */
288
289
  TimeStep.prototype.snap = function(date) {
290
+ var clone = new Date(date.valueOf());
291
+
289
292
  if (this.scale == TimeStep.SCALE.YEAR) {
290
- var year = date.getFullYear() + Math.round(date.getMonth() / 12);
291
- date.setFullYear(Math.round(year / this.step) * this.step);
292
- date.setMonth(0);
293
- date.setDate(0);
294
- date.setHours(0);
295
- date.setMinutes(0);
296
- date.setSeconds(0);
297
- date.setMilliseconds(0);
293
+ var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
294
+ clone.setFullYear(Math.round(year / this.step) * this.step);
295
+ clone.setMonth(0);
296
+ clone.setDate(0);
297
+ clone.setHours(0);
298
+ clone.setMinutes(0);
299
+ clone.setSeconds(0);
300
+ clone.setMilliseconds(0);
298
301
  }
299
302
  else if (this.scale == TimeStep.SCALE.MONTH) {
300
- if (date.getDate() > 15) {
301
- date.setDate(1);
302
- date.setMonth(date.getMonth() + 1);
303
+ if (clone.getDate() > 15) {
304
+ clone.setDate(1);
305
+ clone.setMonth(clone.getMonth() + 1);
303
306
  // important: first set Date to 1, after that change the month.
304
307
  }
305
308
  else {
306
- date.setDate(1);
309
+ clone.setDate(1);
307
310
  }
308
311
 
309
- date.setHours(0);
310
- date.setMinutes(0);
311
- date.setSeconds(0);
312
- date.setMilliseconds(0);
312
+ clone.setHours(0);
313
+ clone.setMinutes(0);
314
+ clone.setSeconds(0);
315
+ clone.setMilliseconds(0);
313
316
  }
314
317
  else if (this.scale == TimeStep.SCALE.DAY ||
315
318
  this.scale == TimeStep.SCALE.WEEKDAY) {
@@ -317,56 +320,58 @@ TimeStep.prototype.snap = function(date) {
317
320
  switch (this.step) {
318
321
  case 5:
319
322
  case 2:
320
- date.setHours(Math.round(date.getHours() / 24) * 24); break;
323
+ clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
321
324
  default:
322
- date.setHours(Math.round(date.getHours() / 12) * 12); break;
325
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
323
326
  }
324
- date.setMinutes(0);
325
- date.setSeconds(0);
326
- date.setMilliseconds(0);
327
+ clone.setMinutes(0);
328
+ clone.setSeconds(0);
329
+ clone.setMilliseconds(0);
327
330
  }
328
331
  else if (this.scale == TimeStep.SCALE.HOUR) {
329
332
  switch (this.step) {
330
333
  case 4:
331
- date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
334
+ clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
332
335
  default:
333
- date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
336
+ clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
334
337
  }
335
- date.setSeconds(0);
336
- date.setMilliseconds(0);
338
+ clone.setSeconds(0);
339
+ clone.setMilliseconds(0);
337
340
  } else if (this.scale == TimeStep.SCALE.MINUTE) {
338
341
  //noinspection FallthroughInSwitchStatementJS
339
342
  switch (this.step) {
340
343
  case 15:
341
344
  case 10:
342
- date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
343
- date.setSeconds(0);
345
+ clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
346
+ clone.setSeconds(0);
344
347
  break;
345
348
  case 5:
346
- date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
349
+ clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
347
350
  default:
348
- date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
351
+ clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
349
352
  }
350
- date.setMilliseconds(0);
353
+ clone.setMilliseconds(0);
351
354
  }
352
355
  else if (this.scale == TimeStep.SCALE.SECOND) {
353
356
  //noinspection FallthroughInSwitchStatementJS
354
357
  switch (this.step) {
355
358
  case 15:
356
359
  case 10:
357
- date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
358
- date.setMilliseconds(0);
360
+ clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
361
+ clone.setMilliseconds(0);
359
362
  break;
360
363
  case 5:
361
- date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
364
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
362
365
  default:
363
- date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
366
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
364
367
  }
365
368
  }
366
369
  else if (this.scale == TimeStep.SCALE.MILLISECOND) {
367
370
  var step = this.step > 5 ? this.step / 2 : 1;
368
- date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
371
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
369
372
  }
373
+
374
+ return clone;
370
375
  };
371
376
 
372
377
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Create a timeline visualization
3
3
  * @param {HTMLElement} container
4
- * @param {vis.DataSet | Array | DataTable} [items]
4
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
5
5
  * @param {Object} [options] See Timeline.setOptions for the available options.
6
6
  * @constructor
7
7
  */
@@ -10,17 +10,35 @@ function Timeline (container, items, options) {
10
10
  var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
11
11
  this.options = {
12
12
  orientation: 'bottom',
13
+ autoResize: true,
14
+ editable: false,
15
+ selectable: true,
16
+ snap: null, // will be specified after timeaxis is created
17
+
13
18
  min: null,
14
19
  max: null,
15
20
  zoomMin: 10, // milliseconds
16
21
  zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
17
22
  // moveable: true, // TODO: option moveable
18
23
  // zoomable: true, // TODO: option zoomable
24
+
19
25
  showMinorLabels: true,
20
26
  showMajorLabels: true,
21
27
  showCurrentTime: false,
22
28
  showCustomTime: false,
23
- autoResize: false
29
+
30
+ onAdd: function (item, callback) {
31
+ callback(item);
32
+ },
33
+ onUpdate: function (item, callback) {
34
+ callback(item);
35
+ },
36
+ onMove: function (item, callback) {
37
+ callback(item);
38
+ },
39
+ onRemove: function (item, callback) {
40
+ callback(item);
41
+ }
24
42
  };
25
43
 
26
44
  // controller
@@ -45,6 +63,15 @@ function Timeline (container, items, options) {
45
63
  this.rootPanel = new RootPanel(container, rootOptions);
46
64
  this.controller.add(this.rootPanel);
47
65
 
66
+ // single select (or unselect) when tapping an item
67
+ this.controller.on('tap', this._onSelectItem.bind(this));
68
+
69
+ // multi select when holding mouse/touch, or on ctrl+click
70
+ this.controller.on('hold', this._onMultiSelectItem.bind(this));
71
+
72
+ // add item on doubletap
73
+ this.controller.on('doubletap', this._onAddItem.bind(this));
74
+
48
75
  // item panel
49
76
  var itemOptions = Object.create(this.options);
50
77
  itemOptions.left = function () {
@@ -82,28 +109,19 @@ function Timeline (container, items, options) {
82
109
  now.clone().add('days', 4).valueOf()
83
110
  );
84
111
 
85
- // TODO: reckon with options moveable and zoomable
86
- // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
87
- this.range.subscribe(this.rootPanel, 'move', 'horizontal');
88
- this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
112
+ this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
113
+ this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
89
114
  this.range.on('rangechange', function (properties) {
90
115
  var force = true;
91
- me.controller.requestReflow(force);
92
- me._trigger('rangechange', properties);
116
+ me.controller.emit('rangechange', properties);
117
+ me.controller.emit('request-reflow', force);
93
118
  });
94
119
  this.range.on('rangechanged', function (properties) {
95
120
  var force = true;
96
- me.controller.requestReflow(force);
97
- me._trigger('rangechanged', properties);
121
+ me.controller.emit('rangechanged', properties);
122
+ me.controller.emit('request-reflow', force);
98
123
  });
99
124
 
100
- // single select (or unselect) when tapping an item
101
- // TODO: implement ctrl+click
102
- this.rootPanel.on('tap', this._onSelectItem.bind(this));
103
-
104
- // multi select when holding mouse/touch, or on ctrl+click
105
- this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
106
-
107
125
  // time axis
108
126
  var timeaxisOptions = Object.create(rootOptions);
109
127
  timeaxisOptions.range = this.range;
@@ -114,6 +132,7 @@ function Timeline (container, items, options) {
114
132
  this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
115
133
  this.timeaxis.setRange(this.range);
116
134
  this.controller.add(this.timeaxis);
135
+ this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
117
136
 
118
137
  // current time bar
119
138
  this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
@@ -140,6 +159,25 @@ function Timeline (container, items, options) {
140
159
  }
141
160
  }
142
161
 
162
+ /**
163
+ * Add an event listener to the timeline
164
+ * @param {String} event Available events: select, rangechange, rangechanged,
165
+ * timechange, timechanged
166
+ * @param {function} callback
167
+ */
168
+ Timeline.prototype.on = function on (event, callback) {
169
+ this.controller.on(event, callback);
170
+ };
171
+
172
+ /**
173
+ * Add an event listener from the timeline
174
+ * @param {String} event
175
+ * @param {function} callback
176
+ */
177
+ Timeline.prototype.off = function off (event, callback) {
178
+ this.controller.off(event, callback);
179
+ };
180
+
143
181
  /**
144
182
  * Set options
145
183
  * @param {Object} options TODO: describe the available options
@@ -151,6 +189,25 @@ Timeline.prototype.setOptions = function (options) {
151
189
  // both start and end are optional
152
190
  this.range.setRange(options.start, options.end);
153
191
 
192
+ if ('editable' in options || 'selectable' in options) {
193
+ if (this.options.selectable) {
194
+ // force update of selection
195
+ this.setSelection(this.getSelection());
196
+ }
197
+ else {
198
+ // remove selection
199
+ this.setSelection([]);
200
+ }
201
+ }
202
+
203
+ // validate the callback functions
204
+ var validateCallback = (function (fn) {
205
+ if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
206
+ throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
207
+ }
208
+ }).bind(this);
209
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
210
+
154
211
  this.controller.reflow();
155
212
  this.controller.repaint();
156
213
  };
@@ -160,7 +217,11 @@ Timeline.prototype.setOptions = function (options) {
160
217
  * @param {Date} time
161
218
  */
162
219
  Timeline.prototype.setCustomTime = function (time) {
163
- this.customtime._setCustomTime(time);
220
+ if (!this.customtime) {
221
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
222
+ }
223
+
224
+ this.customtime.setCustomTime(time);
164
225
  };
165
226
 
166
227
  /**
@@ -168,37 +229,41 @@ Timeline.prototype.setCustomTime = function (time) {
168
229
  * @return {Date} customTime
169
230
  */
170
231
  Timeline.prototype.getCustomTime = function() {
171
- return new Date(this.customtime.customTime.valueOf());
232
+ if (!this.customtime) {
233
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
234
+ }
235
+
236
+ return this.customtime.getCustomTime();
172
237
  };
173
238
 
174
239
  /**
175
240
  * Set items
176
- * @param {vis.DataSet | Array | DataTable | null} items
241
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
177
242
  */
178
243
  Timeline.prototype.setItems = function(items) {
179
244
  var initialLoad = (this.itemsData == null);
180
245
 
181
246
  // convert to type DataSet when needed
182
- var newItemSet;
247
+ var newDataSet;
183
248
  if (!items) {
184
- newItemSet = null;
249
+ newDataSet = null;
185
250
  }
186
251
  else if (items instanceof DataSet) {
187
- newItemSet = items;
252
+ newDataSet = items;
188
253
  }
189
254
  if (!(items instanceof DataSet)) {
190
- newItemSet = new DataSet({
255
+ newDataSet = new DataSet({
191
256
  convert: {
192
257
  start: 'Date',
193
258
  end: 'Date'
194
259
  }
195
260
  });
196
- newItemSet.add(items);
261
+ newDataSet.add(items);
197
262
  }
198
263
 
199
264
  // set items
200
- this.itemsData = newItemSet;
201
- this.content.setItems(newItemSet);
265
+ this.itemsData = newDataSet;
266
+ this.content.setItems(newDataSet);
202
267
 
203
268
  if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
204
269
  // apply the data range as range
@@ -234,7 +299,7 @@ Timeline.prototype.setItems = function(items) {
234
299
 
235
300
  /**
236
301
  * Set groups
237
- * @param {vis.DataSet | Array | DataTable} groups
302
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
238
303
  */
239
304
  Timeline.prototype.setGroups = function(groups) {
240
305
  var me = this;
@@ -368,41 +433,25 @@ Timeline.prototype.getSelection = function getSelection() {
368
433
  };
369
434
 
370
435
  /**
371
- * Add event listener
372
- * @param {String} event Event name. Available events:
373
- * 'rangechange', 'rangechanged', 'select'
374
- * @param {function} callback Callback function, invoked as callback(properties)
375
- * where properties is an optional object containing
376
- * event specific properties.
436
+ * Set the visible window. Both parameters are optional, you can change only
437
+ * start or only end.
438
+ * @param {Date | Number | String} [start] Start date of visible window
439
+ * @param {Date | Number | String} [end] End date of visible window
377
440
  */
378
- Timeline.prototype.on = function on (event, callback) {
379
- var available = ['rangechange', 'rangechanged', 'select'];
380
-
381
- if (available.indexOf(event) == -1) {
382
- throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
383
- }
384
-
385
- events.addListener(this, event, callback);
441
+ Timeline.prototype.setWindow = function setWindow(start, end) {
442
+ this.range.setRange(start, end);
386
443
  };
387
444
 
388
445
  /**
389
- * Remove an event listener
390
- * @param {String} event Event name
391
- * @param {function} callback Callback function
392
- */
393
- Timeline.prototype.off = function off (event, callback) {
394
- events.removeListener(this, event, callback);
395
- };
396
-
397
- /**
398
- * Trigger an event
399
- * @param {String} event Event name, available events: 'rangechange',
400
- * 'rangechanged', 'select'
401
- * @param {Object} [properties] Event specific properties
402
- * @private
446
+ * Get the visible window
447
+ * @return {{start: Date, end: Date}} Visible range
403
448
  */
404
- Timeline.prototype._trigger = function _trigger(event, properties) {
405
- events.trigger(this, event, properties || {});
449
+ Timeline.prototype.getWindow = function setWindow() {
450
+ var range = this.range.getRange();
451
+ return {
452
+ start: new Date(range.start),
453
+ end: new Date(range.end)
454
+ };
406
455
  };
407
456
 
408
457
  /**
@@ -410,13 +459,23 @@ Timeline.prototype._trigger = function _trigger(event, properties) {
410
459
  * @param {Event} event
411
460
  * @private
412
461
  */
462
+ // TODO: move this function to ItemSet
413
463
  Timeline.prototype._onSelectItem = function (event) {
414
- var item = this._itemFromTarget(event);
464
+ if (!this.options.selectable) return;
465
+
466
+ var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
467
+ var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
468
+ if (ctrlKey || shiftKey) {
469
+ this._onMultiSelectItem(event);
470
+ return;
471
+ }
472
+
473
+ var item = ItemSet.itemFromTarget(event);
415
474
 
416
475
  var selection = item ? [item.id] : [];
417
476
  this.setSelection(selection);
418
477
 
419
- this._trigger('select', {
478
+ this.controller.emit('select', {
420
479
  items: this.getSelection()
421
480
  });
422
481
 
@@ -424,53 +483,116 @@ Timeline.prototype._onSelectItem = function (event) {
424
483
  };
425
484
 
426
485
  /**
427
- * Handle selecting/deselecting multiple items when holding an item
428
- * @param {Event} event
486
+ * Handle creation and updates of an item on double tap
487
+ * @param event
429
488
  * @private
430
489
  */
431
- Timeline.prototype._onMultiSelectItem = function (event) {
432
- var selection,
433
- item = this._itemFromTarget(event);
490
+ Timeline.prototype._onAddItem = function (event) {
491
+ if (!this.options.selectable) return;
492
+ if (!this.options.editable) return;
434
493
 
435
- if (!item) {
436
- // do nothing...
437
- return;
438
- }
494
+ var me = this,
495
+ item = ItemSet.itemFromTarget(event);
496
+
497
+ if (item) {
498
+ // update item
439
499
 
440
- selection = this.getSelection(); // current selection
441
- var index = selection.indexOf(item.id);
442
- if (index == -1) {
443
- // item is not yet selected -> select it
444
- selection.push(item.id);
500
+ // execute async handler to update the item (or cancel it)
501
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
502
+ this.options.onUpdate(itemData, function (itemData) {
503
+ if (itemData) {
504
+ me.itemsData.update(itemData);
505
+ }
506
+ });
445
507
  }
446
508
  else {
447
- // item is already selected -> deselect it
448
- selection.splice(index, 1);
449
- }
450
- this.setSelection(selection);
509
+ // add item
510
+ var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
511
+ var x = event.gesture.center.pageX - xAbs;
512
+ var newItem = {
513
+ start: this.timeaxis.snap(this._toTime(x)),
514
+ content: 'new item'
515
+ };
516
+
517
+ var id = util.randomUUID();
518
+ newItem[this.itemsData.fieldId] = id;
519
+
520
+ var group = GroupSet.groupFromTarget(event);
521
+ if (group) {
522
+ newItem.group = group.groupId;
523
+ }
451
524
 
452
- this._trigger('select', {
453
- items: this.getSelection()
454
- });
525
+ // execute async handler to customize (or cancel) adding an item
526
+ this.options.onAdd(newItem, function (item) {
527
+ if (item) {
528
+ me.itemsData.add(newItem);
455
529
 
456
- event.stopPropagation();
530
+ // select the created item after it is repainted
531
+ me.controller.once('repaint', function () {
532
+ me.setSelection([id]);
533
+
534
+ me.controller.emit('select', {
535
+ items: me.getSelection()
536
+ });
537
+ }.bind(me));
538
+ }
539
+ });
540
+ }
457
541
  };
458
542
 
459
543
  /**
460
- * Find an item from an event target:
461
- * searches for the attribute 'timeline-item' in the event target's element tree
544
+ * Handle selecting/deselecting multiple items when holding an item
462
545
  * @param {Event} event
463
- * @return {Item | null| item
464
546
  * @private
465
547
  */
466
- Timeline.prototype._itemFromTarget = function _itemFromTarget (event) {
467
- var target = event.target;
468
- while (target) {
469
- if (target.hasOwnProperty('timeline-item')) {
470
- return target['timeline-item'];
548
+ // TODO: move this function to ItemSet
549
+ Timeline.prototype._onMultiSelectItem = function (event) {
550
+ if (!this.options.selectable) return;
551
+
552
+ var selection,
553
+ item = ItemSet.itemFromTarget(event);
554
+
555
+ if (item) {
556
+ // multi select items
557
+ selection = this.getSelection(); // current selection
558
+ var index = selection.indexOf(item.id);
559
+ if (index == -1) {
560
+ // item is not yet selected -> select it
561
+ selection.push(item.id);
562
+ }
563
+ else {
564
+ // item is already selected -> deselect it
565
+ selection.splice(index, 1);
471
566
  }
472
- target = target.parentNode;
567
+ this.setSelection(selection);
568
+
569
+ this.controller.emit('select', {
570
+ items: this.getSelection()
571
+ });
572
+
573
+ event.stopPropagation();
473
574
  }
575
+ };
474
576
 
475
- return null;
476
- };
577
+ /**
578
+ * Convert a position on screen (pixels) to a datetime
579
+ * @param {int} x Position on the screen in pixels
580
+ * @return {Date} time The datetime the corresponds with given position x
581
+ * @private
582
+ */
583
+ Timeline.prototype._toTime = function _toTime(x) {
584
+ var conversion = this.range.conversion(this.content.width);
585
+ return new Date(x / conversion.scale + conversion.offset);
586
+ };
587
+
588
+ /**
589
+ * Convert a datetime (Date object) into a position on the screen
590
+ * @param {Date} time A date
591
+ * @return {int} x The position on the screen in pixels which corresponds
592
+ * with the given date.
593
+ * @private
594
+ */
595
+ Timeline.prototype._toScreen = function _toScreen(time) {
596
+ var conversion = this.range.conversion(this.content.width);
597
+ return (time.valueOf() - conversion.offset) * conversion.scale;
598
+ };