vis-rails 0.0.4 → 0.0.5

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