vis-rails 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +2 -0
  4. data/lib/vis/rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/module/exports-only-timeline.js +55 -0
  6. data/vendor/assets/javascripts/vis-only-timeline.js +23 -0
  7. data/vendor/assets/javascripts/vis.js +3 -3
  8. data/vendor/assets/stylesheets/vis-only-timeline.css +3 -0
  9. data/vendor/assets/vis/DataSet.js +106 -130
  10. data/vendor/assets/vis/DataView.js +35 -37
  11. data/vendor/assets/vis/graph/Edge.js +225 -45
  12. data/vendor/assets/vis/graph/Graph.js +120 -24
  13. data/vendor/assets/vis/graph/Node.js +16 -16
  14. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +1 -1
  15. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +143 -0
  16. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +81 -3
  17. data/vendor/assets/vis/graph3d/Graph3d.js +3306 -0
  18. data/vendor/assets/vis/module/exports.js +2 -3
  19. data/vendor/assets/vis/timeline/Range.js +93 -80
  20. data/vendor/assets/vis/timeline/Timeline.js +525 -428
  21. data/vendor/assets/vis/timeline/component/Component.js +19 -53
  22. data/vendor/assets/vis/timeline/component/CurrentTime.js +57 -25
  23. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -19
  24. data/vendor/assets/vis/timeline/component/Group.js +47 -50
  25. data/vendor/assets/vis/timeline/component/ItemSet.js +402 -206
  26. data/vendor/assets/vis/timeline/component/TimeAxis.js +112 -169
  27. data/vendor/assets/vis/timeline/component/css/animation.css +33 -0
  28. data/vendor/assets/vis/timeline/component/css/currenttime.css +1 -1
  29. data/vendor/assets/vis/timeline/component/css/customtime.css +1 -1
  30. data/vendor/assets/vis/timeline/component/css/item.css +1 -11
  31. data/vendor/assets/vis/timeline/component/css/itemset.css +13 -18
  32. data/vendor/assets/vis/timeline/component/css/labelset.css +8 -6
  33. data/vendor/assets/vis/timeline/component/css/panel.css +56 -13
  34. data/vendor/assets/vis/timeline/component/css/timeaxis.css +15 -8
  35. data/vendor/assets/vis/timeline/component/item/Item.js +16 -15
  36. data/vendor/assets/vis/timeline/component/item/ItemBox.js +30 -30
  37. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +20 -21
  38. data/vendor/assets/vis/timeline/component/item/ItemRange.js +23 -24
  39. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +10 -10
  40. data/vendor/assets/vis/timeline/stack.js +5 -5
  41. data/vendor/assets/vis/util.js +81 -35
  42. metadata +7 -4
  43. data/vendor/assets/vis/timeline/component/Panel.js +0 -170
  44. data/vendor/assets/vis/timeline/component/RootPanel.js +0 -176
@@ -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) {