vis-rails 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +2 -0
  4. data/lib/vis/rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
  6. data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
  7. data/vendor/assets/javascripts/vis.js +3 -3
  8. data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
  9. data/vendor/assets/vis/DataSet.js +106 -130
  10. data/vendor/assets/vis/DataView.js +35 -37
  11. data/vendor/assets/vis/graph/Edge.js +225 -45
  12. data/vendor/assets/vis/graph/Graph.js +120 -24
  13. data/vendor/assets/vis/graph/Node.js +16 -16
  14. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
  15. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
  16. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
  17. data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
  18. data/vendor/assets/vis/module/exports.js +2 -3
  19. data/vendor/assets/vis/timeline/Range.js +93 -80
  20. data/vendor/assets/vis/timeline/Timeline.js +525 -428
  21. data/vendor/assets/vis/timeline/component/Component.js +19 -53
  22. data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
  23. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
  24. data/vendor/assets/vis/timeline/component/Group.js +47 -50
  25. data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
  26. data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
  27. data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
  28. data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
  29. data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
  30. data/vendor/assets/vis/timeline/component/css/item.css +1 -11
  31. data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
  32. data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
  33. data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
  34. data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
  35. data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
  36. data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
  37. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
  38. data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
  39. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
  40. data/vendor/assets/vis/timeline/stack.js +5 -5
  41. data/vendor/assets/vis/util.js +81 -35
  42. metadata +7 -4
  43. data/vendor/assets/vis/timeline/component/Panel.js +0 -170
  44. data/vendor/assets/vis/timeline/component/RootPanel.js +0 -176
@@ -4,55 +4,92 @@ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
4
4
  * An ItemSet holds a set of items and ranges which can be displayed in a
5
5
  * range. The width is determined by the parent of the ItemSet, and the height
6
6
  * is determined by the size of the items.
7
- * @param {Panel} backgroundPanel Panel which can be used to display the
8
- * vertical lines of box items.
9
- * @param {Panel} axisPanel Panel on the axis where the dots of box-items
10
- * can be displayed.
11
- * @param {Panel} sidePanel Left side panel holding labels
7
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
12
8
  * @param {Object} [options] See ItemSet.setOptions for the available options.
13
9
  * @constructor ItemSet
14
- * @extends Panel
10
+ * @extends Component
15
11
  */
16
- function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
17
- this.id = util.randomUUID();
18
-
19
- // one options object is shared by this itemset and all its items
20
- this.options = options || {};
21
- this.backgroundPanel = backgroundPanel;
22
- this.axisPanel = axisPanel;
23
- this.sidePanel = sidePanel;
24
- this.itemOptions = Object.create(this.options);
12
+ function ItemSet(body, options) {
13
+ this.body = body;
14
+
15
+ this.defaultOptions = {
16
+ type: 'box',
17
+ orientation: 'bottom', // 'top' or 'bottom'
18
+ align: 'center', // alignment of box items
19
+ stack: true,
20
+ groupOrder: null,
21
+
22
+ selectable: true,
23
+ editable: {
24
+ updateTime: false,
25
+ updateGroup: false,
26
+ add: false,
27
+ remove: false
28
+ },
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
+ },
42
+
43
+ margin: {
44
+ item: 10,
45
+ axis: 20
46
+ },
47
+ padding: 5
48
+ };
49
+
50
+ // options is shared by this ItemSet and all its items
51
+ this.options = util.extend({}, this.defaultOptions);
52
+
53
+ // options for getting items from the DataSet with the correct type
54
+ this.itemOptions = {
55
+ type: {start: 'Date', end: 'Date'}
56
+ };
57
+
58
+ this.conversion = {
59
+ toScreen: body.util.toScreen,
60
+ toTime: body.util.toTime
61
+ };
25
62
  this.dom = {};
63
+ this.props = {};
26
64
  this.hammer = null;
27
65
 
28
66
  var me = this;
29
67
  this.itemsData = null; // DataSet
30
68
  this.groupsData = null; // DataSet
31
- this.range = null; // Range or Object {start: number, end: number}
32
69
 
33
70
  // listeners for the DataSet of the items
34
71
  this.itemListeners = {
35
72
  'add': function (event, params, senderId) {
36
- if (senderId != me.id) me._onAdd(params.items);
73
+ me._onAdd(params.items);
37
74
  },
38
75
  'update': function (event, params, senderId) {
39
- if (senderId != me.id) me._onUpdate(params.items);
76
+ me._onUpdate(params.items);
40
77
  },
41
78
  'remove': function (event, params, senderId) {
42
- if (senderId != me.id) me._onRemove(params.items);
79
+ me._onRemove(params.items);
43
80
  }
44
81
  };
45
82
 
46
83
  // listeners for the DataSet of the groups
47
84
  this.groupListeners = {
48
85
  'add': function (event, params, senderId) {
49
- if (senderId != me.id) me._onAddGroups(params.items);
86
+ me._onAddGroups(params.items);
50
87
  },
51
88
  'update': function (event, params, senderId) {
52
- if (senderId != me.id) me._onUpdateGroups(params.items);
89
+ me._onUpdateGroups(params.items);
53
90
  },
54
91
  'remove': function (event, params, senderId) {
55
- if (senderId != me.id) me._onRemoveGroups(params.items);
92
+ me._onRemoveGroups(params.items);
56
93
  }
57
94
  };
58
95
 
@@ -61,15 +98,17 @@ function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
61
98
  this.groupIds = [];
62
99
 
63
100
  this.selection = []; // list with the ids of all selected nodes
64
- this.stackDirty = true; // if true, all items will be restacked on next repaint
101
+ this.stackDirty = true; // if true, all items will be restacked on next redraw
65
102
 
66
103
  this.touchParams = {}; // stores properties while dragging
67
104
  // create the HTML DOM
68
105
 
69
106
  this._create();
107
+
108
+ this.setOptions(options);
70
109
  }
71
110
 
72
- ItemSet.prototype = new Panel();
111
+ ItemSet.prototype = new Component();
73
112
 
74
113
  // available item types will be registered here
75
114
  ItemSet.types = {
@@ -82,15 +121,16 @@ ItemSet.types = {
82
121
  /**
83
122
  * Create the HTML DOM for the ItemSet
84
123
  */
85
- ItemSet.prototype._create = function _create(){
124
+ ItemSet.prototype._create = function(){
86
125
  var frame = document.createElement('div');
126
+ frame.className = 'itemset';
87
127
  frame['timeline-itemset'] = this;
88
- this.frame = frame;
128
+ this.dom.frame = frame;
89
129
 
90
130
  // create background panel
91
131
  var background = document.createElement('div');
92
132
  background.className = 'background';
93
- this.backgroundPanel.frame.appendChild(background);
133
+ frame.appendChild(background);
94
134
  this.dom.background = background;
95
135
 
96
136
  // create foreground panel
@@ -103,33 +143,46 @@ ItemSet.prototype._create = function _create(){
103
143
  var axis = document.createElement('div');
104
144
  axis.className = 'axis';
105
145
  this.dom.axis = axis;
106
- this.axisPanel.frame.appendChild(axis);
107
146
 
108
147
  // create labelset
109
148
  var labelSet = document.createElement('div');
110
149
  labelSet.className = 'labelset';
111
150
  this.dom.labelSet = labelSet;
112
- this.sidePanel.frame.appendChild(labelSet);
113
151
 
114
152
  // create ungrouped Group
115
153
  this._updateUngrouped();
116
154
 
117
155
  // attach event listeners
118
- // TODO: use event listeners from the rootpanel to improve performance?
119
- this.hammer = Hammer(frame, {
156
+ // Note: we bind to the centerContainer for the case where the height
157
+ // of the center container is larger than of the ItemSet, so we
158
+ // can click in the empty area to create a new item or deselect an item.
159
+ this.hammer = Hammer(this.body.dom.centerContainer, {
120
160
  prevent_default: true
121
161
  });
162
+
163
+ // drag items when selected
164
+ this.hammer.on('touch', this._onTouch.bind(this));
122
165
  this.hammer.on('dragstart', this._onDragStart.bind(this));
123
166
  this.hammer.on('drag', this._onDrag.bind(this));
124
167
  this.hammer.on('dragend', this._onDragEnd.bind(this));
168
+
169
+ // single select (or unselect) when tapping an item
170
+ this.hammer.on('tap', this._onSelectItem.bind(this));
171
+
172
+ // multi select when holding mouse/touch, or on ctrl+click
173
+ this.hammer.on('hold', this._onMultiSelectItem.bind(this));
174
+
175
+ // add item on doubletap
176
+ this.hammer.on('doubletap', this._onAddItem.bind(this));
177
+
178
+ // attach to the DOM
179
+ this.show();
125
180
  };
126
181
 
127
182
  /**
128
183
  * Set options for the ItemSet. Existing options will be extended/overwritten.
129
184
  * @param {Object} [options] The following options are available:
130
- * {String | function} [className]
131
- * class name for the itemset
132
- * {String} [type]
185
+ * {String} type
133
186
  * Default type for the items. Choose from 'box'
134
187
  * (default), 'point', or 'range'. The default
135
188
  * Style can be overwritten by individual items.
@@ -140,44 +193,129 @@ ItemSet.prototype._create = function _create(){
140
193
  * {String} orientation
141
194
  * Orientation of the item set. Choose 'top' or
142
195
  * 'bottom' (default).
196
+ * {Function} groupOrder
197
+ * A sorting function for ordering groups
198
+ * {Boolean} stack
199
+ * If true (deafult), items will be stacked on
200
+ * top of each other.
143
201
  * {Number} margin.axis
144
202
  * Margin between the axis and the items in pixels.
145
203
  * Default is 20.
146
204
  * {Number} margin.item
147
205
  * Margin between items in pixels. Default is 10.
206
+ * {Number} margin
207
+ * Set margin for both axis and items in pixels.
148
208
  * {Number} padding
149
209
  * Padding of the contents of an item in pixels.
150
210
  * Must correspond with the items css. Default is 5.
151
- * {Function} snap
152
- * Function to let items snap to nice dates when
153
- * dragging items.
211
+ * {Boolean} selectable
212
+ * If true (default), items can be selected.
213
+ * {Boolean} editable
214
+ * Set all editable options to true or false
215
+ * {Boolean} editable.updateTime
216
+ * Allow dragging an item to an other moment in time
217
+ * {Boolean} editable.updateGroup
218
+ * Allow dragging an item to an other group
219
+ * {Boolean} editable.add
220
+ * Allow creating new items on double tap
221
+ * {Boolean} editable.remove
222
+ * Allow removing items by clicking the delete button
223
+ * top right of a selected item.
224
+ * {Function(item: Item, callback: Function)} onAdd
225
+ * Callback function triggered when an item is about to be added:
226
+ * when the user double taps an empty space in the Timeline.
227
+ * {Function(item: Item, callback: Function)} onUpdate
228
+ * Callback function fired when an item is about to be updated.
229
+ * This function typically has to show a dialog where the user
230
+ * change the item. If not implemented, nothing happens.
231
+ * {Function(item: Item, callback: Function)} onMove
232
+ * Fired when an item has been moved. If not implemented,
233
+ * the move action will be accepted.
234
+ * {Function(item: Item, callback: Function)} onRemove
235
+ * Fired when an item is about to be deleted.
236
+ * If not implemented, the item will be always removed.
154
237
  */
155
- ItemSet.prototype.setOptions = function setOptions(options) {
156
- Component.prototype.setOptions.call(this, options);
238
+ ItemSet.prototype.setOptions = function(options) {
239
+ if (options) {
240
+ // copy all options that we know
241
+ var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
242
+ util.selectiveExtend(fields, this.options, options);
243
+
244
+ if ('margin' in options) {
245
+ if (typeof options.margin === 'number') {
246
+ this.options.margin.axis = options.margin;
247
+ this.options.margin.item = options.margin;
248
+ }
249
+ else if (typeof options.margin === 'object'){
250
+ util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin);
251
+ }
252
+ }
253
+
254
+ if ('editable' in options) {
255
+ if (typeof options.editable === 'boolean') {
256
+ this.options.editable.updateTime = options.editable;
257
+ this.options.editable.updateGroup = options.editable;
258
+ this.options.editable.add = options.editable;
259
+ this.options.editable.remove = options.editable;
260
+ }
261
+ else if (typeof options.editable === 'object') {
262
+ util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
263
+ }
264
+ }
265
+
266
+ // callback functions
267
+ var addCallback = (function (name) {
268
+ if (name in options) {
269
+ var fn = options[name];
270
+ if (!(fn instanceof Function) || fn.length != 2) {
271
+ throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
272
+ }
273
+ this.options[name] = fn;
274
+ }
275
+ }).bind(this);
276
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
277
+
278
+ // force the itemSet to refresh: options like orientation and margins may be changed
279
+ this.markDirty();
280
+ }
157
281
  };
158
282
 
159
283
  /**
160
- * Mark the ItemSet dirty so it will refresh everything with next repaint
284
+ * Mark the ItemSet dirty so it will refresh everything with next redraw
161
285
  */
162
- ItemSet.prototype.markDirty = function markDirty() {
286
+ ItemSet.prototype.markDirty = function() {
163
287
  this.groupIds = [];
164
288
  this.stackDirty = true;
165
289
  };
166
290
 
291
+ /**
292
+ * Destroy the ItemSet
293
+ */
294
+ ItemSet.prototype.destroy = function() {
295
+ this.hide();
296
+ this.setItems(null);
297
+ this.setGroups(null);
298
+
299
+ this.hammer = null;
300
+
301
+ this.body = null;
302
+ this.conversion = null;
303
+ };
304
+
167
305
  /**
168
306
  * Hide the component from the DOM
169
307
  */
170
- ItemSet.prototype.hide = function hide() {
308
+ ItemSet.prototype.hide = function() {
309
+ // remove the frame containing the items
310
+ if (this.dom.frame.parentNode) {
311
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
312
+ }
313
+
171
314
  // remove the axis with dots
172
315
  if (this.dom.axis.parentNode) {
173
316
  this.dom.axis.parentNode.removeChild(this.dom.axis);
174
317
  }
175
318
 
176
- // remove the background with vertical lines
177
- if (this.dom.background.parentNode) {
178
- this.dom.background.parentNode.removeChild(this.dom.background);
179
- }
180
-
181
319
  // remove the labelset containing all group labels
182
320
  if (this.dom.labelSet.parentNode) {
183
321
  this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
@@ -188,35 +326,23 @@ ItemSet.prototype.hide = function hide() {
188
326
  * Show the component in the DOM (when not already visible).
189
327
  * @return {Boolean} changed
190
328
  */
191
- ItemSet.prototype.show = function show() {
192
- // show axis with dots
193
- if (!this.dom.axis.parentNode) {
194
- this.axisPanel.frame.appendChild(this.dom.axis);
329
+ ItemSet.prototype.show = function() {
330
+ // show frame containing the items
331
+ if (!this.dom.frame.parentNode) {
332
+ this.body.dom.center.appendChild(this.dom.frame);
195
333
  }
196
334
 
197
- // show background with vertical lines
198
- if (!this.dom.background.parentNode) {
199
- this.backgroundPanel.frame.appendChild(this.dom.background);
335
+ // show axis with dots
336
+ if (!this.dom.axis.parentNode) {
337
+ this.body.dom.backgroundVertical.appendChild(this.dom.axis);
200
338
  }
201
339
 
202
340
  // show labelset containing labels
203
341
  if (!this.dom.labelSet.parentNode) {
204
- this.sidePanel.frame.appendChild(this.dom.labelSet);
342
+ this.body.dom.left.appendChild(this.dom.labelSet);
205
343
  }
206
344
  };
207
345
 
208
- /**
209
- * Set range (start and end).
210
- * @param {Range | Object} range A Range or an object containing start and end.
211
- */
212
- ItemSet.prototype.setRange = function setRange(range) {
213
- if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
214
- throw new TypeError('Range must be an instance of Range, ' +
215
- 'or an object containing start and end.');
216
- }
217
- this.range = range;
218
- };
219
-
220
346
  /**
221
347
  * Set selected items by their id. Replaces the current selection
222
348
  * Unknown id's are silently ignored.
@@ -224,7 +350,7 @@ ItemSet.prototype.setRange = function setRange(range) {
224
350
  * selected. If ids is an empty array, all items will be
225
351
  * unselected.
226
352
  */
227
- ItemSet.prototype.setSelection = function setSelection(ids) {
353
+ ItemSet.prototype.setSelection = function(ids) {
228
354
  var i, ii, id, item;
229
355
 
230
356
  if (ids) {
@@ -256,7 +382,7 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
256
382
  * Get the selected items by their id
257
383
  * @return {Array} ids The ids of the selected items
258
384
  */
259
- ItemSet.prototype.getSelection = function getSelection() {
385
+ ItemSet.prototype.getSelection = function() {
260
386
  return this.selection.concat([]);
261
387
  };
262
388
 
@@ -265,7 +391,7 @@ ItemSet.prototype.getSelection = function getSelection() {
265
391
  * @param {String | Number} id
266
392
  * @private
267
393
  */
268
- ItemSet.prototype._deselect = function _deselect(id) {
394
+ ItemSet.prototype._deselect = function(id) {
269
395
  var selection = this.selection;
270
396
  for (var i = 0, ii = selection.length; i < ii; i++) {
271
397
  if (selection[i] == id) { // non-strict comparison!
@@ -275,51 +401,35 @@ ItemSet.prototype._deselect = function _deselect(id) {
275
401
  }
276
402
  };
277
403
 
278
- /**
279
- * Return the item sets frame
280
- * @returns {HTMLElement} frame
281
- */
282
- ItemSet.prototype.getFrame = function getFrame() {
283
- return this.frame;
284
- };
285
-
286
404
  /**
287
405
  * Repaint the component
288
406
  * @return {boolean} Returns true if the component is resized
289
407
  */
290
- ItemSet.prototype.repaint = function repaint() {
408
+ ItemSet.prototype.redraw = function() {
291
409
  var margin = this.options.margin,
292
- range = this.range,
410
+ range = this.body.range,
293
411
  asSize = util.option.asSize,
294
- asString = util.option.asString,
295
412
  options = this.options,
296
- orientation = this.getOption('orientation'),
413
+ orientation = options.orientation,
297
414
  resized = false,
298
- frame = this.frame;
299
-
300
- // TODO: document this feature to specify one margin for both item and axis distance
301
- if (typeof margin === 'number') {
302
- margin = {
303
- item: margin,
304
- axis: margin
305
- };
306
- }
415
+ frame = this.dom.frame,
416
+ editable = options.editable.updateTime || options.editable.updateGroup;
307
417
 
308
- // update className
309
- frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
418
+ // update class name
419
+ frame.className = 'itemset' + (editable ? ' editable' : '');
310
420
 
311
421
  // reorder the groups (if needed)
312
422
  resized = this._orderGroups() || resized;
313
423
 
314
424
  // check whether zoomed (in that case we need to re-stack everything)
315
425
  // TODO: would be nicer to get this as a trigger from Range
316
- var visibleInterval = this.range.end - this.range.start;
317
- var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
426
+ var visibleInterval = range.end - range.start;
427
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
318
428
  if (zoomed) this.stackDirty = true;
319
429
  this.lastVisibleInterval = visibleInterval;
320
- this.lastWidth = this.width;
430
+ this.props.lastWidth = this.props.width;
321
431
 
322
- // repaint all groups
432
+ // redraw all groups
323
433
  var restack = this.stackDirty,
324
434
  firstGroup = this._firstGroup(),
325
435
  firstMargin = {
@@ -334,34 +444,27 @@ ItemSet.prototype.repaint = function repaint() {
334
444
  minHeight = margin.axis + margin.item;
335
445
  util.forEach(this.groups, function (group) {
336
446
  var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
337
- resized = group.repaint(range, groupMargin, restack) || resized;
447
+ var groupResized = group.redraw(range, groupMargin, restack);
448
+ resized = groupResized || resized;
338
449
  height += group.height;
339
450
  });
340
451
  height = Math.max(height, minHeight);
341
452
  this.stackDirty = false;
342
453
 
343
- // reposition frame
344
- frame.style.left = asSize(options.left, '');
345
- frame.style.right = asSize(options.right, '');
346
- frame.style.top = asSize((orientation == 'top') ? '0' : '');
347
- frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
348
- frame.style.width = asSize(options.width, '100%');
454
+ // update frame height
349
455
  frame.style.height = asSize(height);
350
- //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
351
456
 
352
457
  // calculate actual size and position
353
- this.top = frame.offsetTop;
354
- this.left = frame.offsetLeft;
355
- this.width = frame.offsetWidth;
356
- this.height = height;
458
+ this.props.top = frame.offsetTop;
459
+ this.props.left = frame.offsetLeft;
460
+ this.props.width = frame.offsetWidth;
461
+ this.props.height = height;
357
462
 
358
463
  // reposition axis
359
- this.dom.axis.style.left = asSize(options.left, '0');
360
- this.dom.axis.style.right = asSize(options.right, '');
361
- this.dom.axis.style.width = asSize(options.width, '100%');
362
- this.dom.axis.style.height = asSize(0);
363
- this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
364
- this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
464
+ this.dom.axis.style.top = asSize((orientation == 'top') ?
465
+ (this.body.domProps.top.height + this.body.domProps.border.top) :
466
+ (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
467
+ this.dom.axis.style.left = this.body.domProps.border.left + 'px';
365
468
 
366
469
  // check if this component is resized
367
470
  resized = this._isResized() || resized;
@@ -374,7 +477,7 @@ ItemSet.prototype.repaint = function repaint() {
374
477
  * @return {Group | null} firstGroup
375
478
  * @private
376
479
  */
377
- ItemSet.prototype._firstGroup = function _firstGroup() {
480
+ ItemSet.prototype._firstGroup = function() {
378
481
  var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
379
482
  var firstGroupId = this.groupIds[firstGroupIndex];
380
483
  var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
@@ -387,7 +490,7 @@ ItemSet.prototype._firstGroup = function _firstGroup() {
387
490
  * there are no groups specified.
388
491
  * @protected
389
492
  */
390
- ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
493
+ ItemSet.prototype._updateUngrouped = function() {
391
494
  var ungrouped = this.groups[UNGROUPED];
392
495
 
393
496
  if (this.groupsData) {
@@ -416,35 +519,11 @@ ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
416
519
  }
417
520
  };
418
521
 
419
- /**
420
- * Get the foreground container element
421
- * @return {HTMLElement} foreground
422
- */
423
- ItemSet.prototype.getForeground = function getForeground() {
424
- return this.dom.foreground;
425
- };
426
-
427
- /**
428
- * Get the background container element
429
- * @return {HTMLElement} background
430
- */
431
- ItemSet.prototype.getBackground = function getBackground() {
432
- return this.dom.background;
433
- };
434
-
435
- /**
436
- * Get the axis container element
437
- * @return {HTMLElement} axis
438
- */
439
- ItemSet.prototype.getAxis = function getAxis() {
440
- return this.dom.axis;
441
- };
442
-
443
522
  /**
444
523
  * Get the element for the labelset
445
524
  * @return {HTMLElement} labelSet
446
525
  */
447
- ItemSet.prototype.getLabelSet = function getLabelSet() {
526
+ ItemSet.prototype.getLabelSet = function() {
448
527
  return this.dom.labelSet;
449
528
  };
450
529
 
@@ -452,7 +531,7 @@ ItemSet.prototype.getLabelSet = function getLabelSet() {
452
531
  * Set items
453
532
  * @param {vis.DataSet | null} items
454
533
  */
455
- ItemSet.prototype.setItems = function setItems(items) {
534
+ ItemSet.prototype.setItems = function(items) {
456
535
  var me = this,
457
536
  ids,
458
537
  oldItemsData = this.itemsData;
@@ -471,7 +550,7 @@ ItemSet.prototype.setItems = function setItems(items) {
471
550
  if (oldItemsData) {
472
551
  // unsubscribe from old dataset
473
552
  util.forEach(this.itemListeners, function (callback, event) {
474
- oldItemsData.unsubscribe(event, callback);
553
+ oldItemsData.off(event, callback);
475
554
  });
476
555
 
477
556
  // remove all drawn items
@@ -499,7 +578,7 @@ ItemSet.prototype.setItems = function setItems(items) {
499
578
  * Get the current items
500
579
  * @returns {vis.DataSet | null}
501
580
  */
502
- ItemSet.prototype.getItems = function getItems() {
581
+ ItemSet.prototype.getItems = function() {
503
582
  return this.itemsData;
504
583
  };
505
584
 
@@ -507,7 +586,7 @@ ItemSet.prototype.getItems = function getItems() {
507
586
  * Set groups
508
587
  * @param {vis.DataSet} groups
509
588
  */
510
- ItemSet.prototype.setGroups = function setGroups(groups) {
589
+ ItemSet.prototype.setGroups = function(groups) {
511
590
  var me = this,
512
591
  ids;
513
592
 
@@ -520,7 +599,7 @@ ItemSet.prototype.setGroups = function setGroups(groups) {
520
599
  // remove all drawn groups
521
600
  ids = this.groupsData.getIds();
522
601
  this.groupsData = null;
523
- this._onRemoveGroups(ids); // note: this will cause a repaint
602
+ this._onRemoveGroups(ids); // note: this will cause a redraw
524
603
  }
525
604
 
526
605
  // replace the dataset
@@ -552,14 +631,14 @@ ItemSet.prototype.setGroups = function setGroups(groups) {
552
631
  // update the order of all items in each group
553
632
  this._order();
554
633
 
555
- this.emit('change');
634
+ this.body.emitter.emit('change');
556
635
  };
557
636
 
558
637
  /**
559
638
  * Get the current groups
560
639
  * @returns {vis.DataSet | null} groups
561
640
  */
562
- ItemSet.prototype.getGroups = function getGroups() {
641
+ ItemSet.prototype.getGroups = function() {
563
642
  return this.groupsData;
564
643
  };
565
644
 
@@ -567,7 +646,7 @@ ItemSet.prototype.getGroups = function getGroups() {
567
646
  * Remove an item by its id
568
647
  * @param {String | Number} id
569
648
  */
570
- ItemSet.prototype.removeItem = function removeItem (id) {
649
+ ItemSet.prototype.removeItem = function(id) {
571
650
  var item = this.itemsData.get(id),
572
651
  dataset = this._myDataSet();
573
652
 
@@ -588,14 +667,12 @@ ItemSet.prototype.removeItem = function removeItem (id) {
588
667
  * @param {Number[]} ids
589
668
  * @protected
590
669
  */
591
- ItemSet.prototype._onUpdate = function _onUpdate(ids) {
592
- var me = this,
593
- items = this.items,
594
- itemOptions = this.itemOptions;
670
+ ItemSet.prototype._onUpdate = function(ids) {
671
+ var me = this;
595
672
 
596
673
  ids.forEach(function (id) {
597
- var itemData = me.itemsData.get(id),
598
- item = items[id],
674
+ var itemData = me.itemsData.get(id, me.itemOptions),
675
+ item = me.items[id],
599
676
  type = itemData.type ||
600
677
  (itemData.start && itemData.end && 'range') ||
601
678
  me.options.type ||
@@ -618,7 +695,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
618
695
  if (!item) {
619
696
  // create item
620
697
  if (constructor) {
621
- item = new constructor(itemData, me.options, itemOptions);
698
+ item = new constructor(itemData, me.conversion, me.options);
622
699
  item.id = id; // TODO: not so nice setting id afterwards
623
700
  me._addItem(item);
624
701
  }
@@ -629,8 +706,8 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
629
706
  });
630
707
 
631
708
  this._order();
632
- this.stackDirty = true; // force re-stacking of all items next repaint
633
- this.emit('change');
709
+ this.stackDirty = true; // force re-stacking of all items next redraw
710
+ this.body.emitter.emit('change');
634
711
  };
635
712
 
636
713
  /**
@@ -645,7 +722,7 @@ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
645
722
  * @param {Number[]} ids
646
723
  * @protected
647
724
  */
648
- ItemSet.prototype._onRemove = function _onRemove(ids) {
725
+ ItemSet.prototype._onRemove = function(ids) {
649
726
  var count = 0;
650
727
  var me = this;
651
728
  ids.forEach(function (id) {
@@ -659,8 +736,8 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
659
736
  if (count) {
660
737
  // update order
661
738
  this._order();
662
- this.stackDirty = true; // force re-stacking of all items next repaint
663
- this.emit('change');
739
+ this.stackDirty = true; // force re-stacking of all items next redraw
740
+ this.body.emitter.emit('change');
664
741
  }
665
742
  };
666
743
 
@@ -668,7 +745,7 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
668
745
  * Update the order of item in all groups
669
746
  * @private
670
747
  */
671
- ItemSet.prototype._order = function _order() {
748
+ ItemSet.prototype._order = function() {
672
749
  // reorder the items in all groups
673
750
  // TODO: optimization: only reorder groups affected by the changed items
674
751
  util.forEach(this.groups, function (group) {
@@ -681,7 +758,7 @@ ItemSet.prototype._order = function _order() {
681
758
  * @param {Number[]} ids
682
759
  * @private
683
760
  */
684
- ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
761
+ ItemSet.prototype._onUpdateGroups = function(ids) {
685
762
  this._onAddGroups(ids);
686
763
  };
687
764
 
@@ -690,7 +767,7 @@ ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
690
767
  * @param {Number[]} ids
691
768
  * @private
692
769
  */
693
- ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
770
+ ItemSet.prototype._onAddGroups = function(ids) {
694
771
  var me = this;
695
772
 
696
773
  ids.forEach(function (id) {
@@ -730,7 +807,7 @@ ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
730
807
  }
731
808
  });
732
809
 
733
- this.emit('change');
810
+ this.body.emitter.emit('change');
734
811
  };
735
812
 
736
813
  /**
@@ -738,7 +815,7 @@ ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
738
815
  * @param {Number[]} ids
739
816
  * @private
740
817
  */
741
- ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
818
+ ItemSet.prototype._onRemoveGroups = function(ids) {
742
819
  var groups = this.groups;
743
820
  ids.forEach(function (id) {
744
821
  var group = groups[id];
@@ -751,7 +828,7 @@ ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
751
828
 
752
829
  this.markDirty();
753
830
 
754
- this.emit('change');
831
+ this.body.emitter.emit('change');
755
832
  };
756
833
 
757
834
  /**
@@ -794,7 +871,7 @@ ItemSet.prototype._orderGroups = function () {
794
871
  * @param {Item} item
795
872
  * @private
796
873
  */
797
- ItemSet.prototype._addItem = function _addItem(item) {
874
+ ItemSet.prototype._addItem = function(item) {
798
875
  this.items[item.id] = item;
799
876
 
800
877
  // add to group
@@ -809,12 +886,12 @@ ItemSet.prototype._addItem = function _addItem(item) {
809
886
  * @param {Object} itemData
810
887
  * @private
811
888
  */
812
- ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
889
+ ItemSet.prototype._updateItem = function(item, itemData) {
813
890
  var oldGroupId = item.data.group;
814
891
 
815
892
  item.data = itemData;
816
893
  if (item.displayed) {
817
- item.repaint();
894
+ item.redraw();
818
895
  }
819
896
 
820
897
  // update group
@@ -834,7 +911,7 @@ ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
834
911
  * @param {Item} item
835
912
  * @private
836
913
  */
837
- ItemSet.prototype._removeItem = function _removeItem(item) {
914
+ ItemSet.prototype._removeItem = function(item) {
838
915
  // remove from DOM
839
916
  item.hide();
840
917
 
@@ -857,7 +934,7 @@ ItemSet.prototype._removeItem = function _removeItem(item) {
857
934
  * @returns {Array}
858
935
  * @private
859
936
  */
860
- ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
937
+ ItemSet.prototype._constructByEndArray = function(array) {
861
938
  var endArray = [];
862
939
 
863
940
  for (var i = 0; i < array.length; i++) {
@@ -869,25 +946,17 @@ ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
869
946
  };
870
947
 
871
948
  /**
872
- * Get the width of the group labels
873
- * @return {Number} width
874
- */
875
- ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
876
- var width = 0;
877
-
878
- util.forEach(this.groups, function (group) {
879
- width = Math.max(width, group.getLabelWidth());
880
- });
881
-
882
- return width;
883
- };
884
-
885
- /**
886
- * Get the height of the itemsets background
887
- * @return {Number} height
949
+ * Register the clicked item on touch, before dragStart is initiated.
950
+ *
951
+ * dragStart is initiated from a mousemove event, which can have left the item
952
+ * already resulting in an item == null
953
+ *
954
+ * @param {Event} event
955
+ * @private
888
956
  */
889
- ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
890
- return this.height;
957
+ ItemSet.prototype._onTouch = function (event) {
958
+ // store the touched item, used in _onDragStart
959
+ this.touchParams.item = ItemSet.itemFromTarget(event);
891
960
  };
892
961
 
893
962
  /**
@@ -900,7 +969,7 @@ ItemSet.prototype._onDragStart = function (event) {
900
969
  return;
901
970
  }
902
971
 
903
- var item = ItemSet.itemFromTarget(event),
972
+ var item = this.touchParams.item || null,
904
973
  me = this,
905
974
  props;
906
975
 
@@ -966,9 +1035,10 @@ ItemSet.prototype._onDragStart = function (event) {
966
1035
  */
967
1036
  ItemSet.prototype._onDrag = function (event) {
968
1037
  if (this.touchParams.itemProps) {
969
- var snap = this.options.snap || null,
1038
+ var range = this.body.range,
1039
+ snap = this.body.util.snap || null,
970
1040
  deltaX = event.gesture.deltaX,
971
- scale = (this.width / (this.range.end - this.range.start)),
1041
+ scale = (this.props.width / (range.end - range.start)),
972
1042
  offset = deltaX / scale;
973
1043
 
974
1044
  // move
@@ -1000,8 +1070,8 @@ ItemSet.prototype._onDrag = function (event) {
1000
1070
 
1001
1071
  // TODO: implement onMoving handler
1002
1072
 
1003
- this.stackDirty = true; // force re-stacking of all items next repaint
1004
- this.emit('change');
1073
+ this.stackDirty = true; // force re-stacking of all items next redraw
1074
+ this.body.emitter.emit('change');
1005
1075
 
1006
1076
  event.stopPropagation();
1007
1077
  }
@@ -1021,16 +1091,18 @@ ItemSet.prototype._onDragEnd = function (event) {
1021
1091
 
1022
1092
  this.touchParams.itemProps.forEach(function (props) {
1023
1093
  var id = props.item.id,
1024
- itemData = me.itemsData.get(id);
1094
+ itemData = me.itemsData.get(id, me.itemOptions);
1025
1095
 
1026
1096
  var changed = false;
1027
1097
  if ('start' in props.item.data) {
1028
1098
  changed = (props.start != props.item.data.start.valueOf());
1029
- itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
1099
+ itemData.start = util.convert(props.item.data.start,
1100
+ dataset._options.type && dataset._options.type.start || 'Date');
1030
1101
  }
1031
1102
  if ('end' in props.item.data) {
1032
1103
  changed = changed || (props.end != props.item.data.end.valueOf());
1033
- itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
1104
+ itemData.end = util.convert(props.item.data.end,
1105
+ dataset._options.type && dataset._options.type.end || 'Date');
1034
1106
  }
1035
1107
  if ('group' in props.item.data) {
1036
1108
  changed = changed || (props.group != props.item.data.group);
@@ -1042,7 +1114,7 @@ ItemSet.prototype._onDragEnd = function (event) {
1042
1114
  me.options.onMove(itemData, function (itemData) {
1043
1115
  if (itemData) {
1044
1116
  // apply changes
1045
- itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
1117
+ itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
1046
1118
  changes.push(itemData);
1047
1119
  }
1048
1120
  else {
@@ -1050,8 +1122,8 @@ ItemSet.prototype._onDragEnd = function (event) {
1050
1122
  if ('start' in props) props.item.data.start = props.start;
1051
1123
  if ('end' in props) props.item.data.end = props.end;
1052
1124
 
1053
- me.stackDirty = true; // force re-stacking of all items next repaint
1054
- me.emit('change');
1125
+ me.stackDirty = true; // force re-stacking of all items next redraw
1126
+ me.body.emitter.emit('change');
1055
1127
  }
1056
1128
  });
1057
1129
  }
@@ -1067,13 +1139,137 @@ ItemSet.prototype._onDragEnd = function (event) {
1067
1139
  }
1068
1140
  };
1069
1141
 
1142
+ /**
1143
+ * Handle selecting/deselecting an item when tapping it
1144
+ * @param {Event} event
1145
+ * @private
1146
+ */
1147
+ ItemSet.prototype._onSelectItem = function (event) {
1148
+ if (!this.options.selectable) return;
1149
+
1150
+ var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
1151
+ var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
1152
+ if (ctrlKey || shiftKey) {
1153
+ this._onMultiSelectItem(event);
1154
+ return;
1155
+ }
1156
+
1157
+ var oldSelection = this.getSelection();
1158
+
1159
+ var item = ItemSet.itemFromTarget(event);
1160
+ var selection = item ? [item.id] : [];
1161
+ this.setSelection(selection);
1162
+
1163
+ var newSelection = this.getSelection();
1164
+
1165
+ // emit a select event,
1166
+ // except when old selection is empty and new selection is still empty
1167
+ if (newSelection.length > 0 || oldSelection.length > 0) {
1168
+ this.body.emitter.emit('select', {
1169
+ items: this.getSelection()
1170
+ });
1171
+ }
1172
+
1173
+ event.stopPropagation();
1174
+ };
1175
+
1176
+ /**
1177
+ * Handle creation and updates of an item on double tap
1178
+ * @param event
1179
+ * @private
1180
+ */
1181
+ ItemSet.prototype._onAddItem = function (event) {
1182
+ if (!this.options.selectable) return;
1183
+ if (!this.options.editable.add) return;
1184
+
1185
+ var me = this,
1186
+ snap = this.body.util.snap || null,
1187
+ item = ItemSet.itemFromTarget(event);
1188
+
1189
+ if (item) {
1190
+ // update item
1191
+
1192
+ // execute async handler to update the item (or cancel it)
1193
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
1194
+ this.options.onUpdate(itemData, function (itemData) {
1195
+ if (itemData) {
1196
+ me.itemsData.update(itemData);
1197
+ }
1198
+ });
1199
+ }
1200
+ else {
1201
+ // add item
1202
+ var xAbs = vis.util.getAbsoluteLeft(this.dom.frame);
1203
+ var x = event.gesture.center.pageX - xAbs;
1204
+ var start = this.body.util.toTime(x);
1205
+ var newItem = {
1206
+ start: snap ? snap(start) : start,
1207
+ content: 'new item'
1208
+ };
1209
+
1210
+ // when default type is a range, add a default end date to the new item
1211
+ if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
1212
+ var end = this.body.util.toTime(x + this.props.width / 5);
1213
+ newItem.end = snap ? snap(end) : end;
1214
+ }
1215
+
1216
+ newItem[this.itemsData.fieldId] = util.randomUUID();
1217
+
1218
+ var group = ItemSet.groupFromTarget(event);
1219
+ if (group) {
1220
+ newItem.group = group.groupId;
1221
+ }
1222
+
1223
+ // execute async handler to customize (or cancel) adding an item
1224
+ this.options.onAdd(newItem, function (item) {
1225
+ if (item) {
1226
+ me.itemsData.add(newItem);
1227
+ // TODO: need to trigger a redraw?
1228
+ }
1229
+ });
1230
+ }
1231
+ };
1232
+
1233
+ /**
1234
+ * Handle selecting/deselecting multiple items when holding an item
1235
+ * @param {Event} event
1236
+ * @private
1237
+ */
1238
+ ItemSet.prototype._onMultiSelectItem = function (event) {
1239
+ if (!this.options.selectable) return;
1240
+
1241
+ var selection,
1242
+ item = ItemSet.itemFromTarget(event);
1243
+
1244
+ if (item) {
1245
+ // multi select items
1246
+ selection = this.getSelection(); // current selection
1247
+ var index = selection.indexOf(item.id);
1248
+ if (index == -1) {
1249
+ // item is not yet selected -> select it
1250
+ selection.push(item.id);
1251
+ }
1252
+ else {
1253
+ // item is already selected -> deselect it
1254
+ selection.splice(index, 1);
1255
+ }
1256
+ this.setSelection(selection);
1257
+
1258
+ this.body.emitter.emit('select', {
1259
+ items: this.getSelection()
1260
+ });
1261
+
1262
+ event.stopPropagation();
1263
+ }
1264
+ };
1265
+
1070
1266
  /**
1071
1267
  * Find an item from an event target:
1072
1268
  * searches for the attribute 'timeline-item' in the event target's element tree
1073
1269
  * @param {Event} event
1074
1270
  * @return {Item | null} item
1075
1271
  */
1076
- ItemSet.itemFromTarget = function itemFromTarget (event) {
1272
+ ItemSet.itemFromTarget = function(event) {
1077
1273
  var target = event.target;
1078
1274
  while (target) {
1079
1275
  if (target.hasOwnProperty('timeline-item')) {
@@ -1091,7 +1287,7 @@ ItemSet.itemFromTarget = function itemFromTarget (event) {
1091
1287
  * @param {Event} event
1092
1288
  * @return {Group | null} group
1093
1289
  */
1094
- ItemSet.groupFromTarget = function groupFromTarget (event) {
1290
+ ItemSet.groupFromTarget = function(event) {
1095
1291
  var target = event.target;
1096
1292
  while (target) {
1097
1293
  if (target.hasOwnProperty('timeline-group')) {
@@ -1109,7 +1305,7 @@ ItemSet.groupFromTarget = function groupFromTarget (event) {
1109
1305
  * @param {Event} event
1110
1306
  * @return {ItemSet | null} item
1111
1307
  */
1112
- ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
1308
+ ItemSet.itemSetFromTarget = function(event) {
1113
1309
  var target = event.target;
1114
1310
  while (target) {
1115
1311
  if (target.hasOwnProperty('timeline-itemset')) {
@@ -1126,7 +1322,7 @@ ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
1126
1322
  * @returns {null | DataSet} dataset
1127
1323
  * @private
1128
1324
  */
1129
- ItemSet.prototype._myDataSet = function _myDataSet() {
1325
+ ItemSet.prototype._myDataSet = function() {
1130
1326
  // find the root DataSet
1131
1327
  var dataset = this.itemsData;
1132
1328
  while (dataset instanceof DataView) {