vis-rails 0.0.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/vis/rails/version.rb +1 -1
  3. data/vendor/assets/javascripts/vis.js +2 -9
  4. data/vendor/assets/vis/DataSet.js +17 -9
  5. data/vendor/assets/vis/graph/Edge.js +49 -24
  6. data/vendor/assets/vis/graph/Graph.js +268 -64
  7. data/vendor/assets/vis/graph/Groups.js +1 -1
  8. data/vendor/assets/vis/graph/Node.js +18 -67
  9. data/vendor/assets/vis/graph/Popup.js +40 -13
  10. data/vendor/assets/vis/graph/css/graph-navigation.css +18 -14
  11. data/vendor/assets/vis/graph/graphMixins/ClusterMixin.js +7 -5
  12. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +20 -5
  13. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +33 -33
  14. data/vendor/assets/vis/graph/graphMixins/MixinLoader.js +30 -32
  15. data/vendor/assets/vis/graph/graphMixins/NavigationMixin.js +33 -1
  16. data/vendor/assets/vis/graph/graphMixins/SectorsMixin.js +2 -2
  17. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +72 -60
  18. data/vendor/assets/vis/graph/graphMixins/physics/BarnesHut.js +43 -18
  19. data/vendor/assets/vis/graph/graphMixins/physics/HierarchialRepulsion.js +8 -8
  20. data/vendor/assets/vis/graph/graphMixins/physics/PhysicsMixin.js +309 -129
  21. data/vendor/assets/vis/graph/graphMixins/physics/Repulsion.js +10 -10
  22. data/vendor/assets/vis/module/exports.js +1 -2
  23. data/vendor/assets/vis/module/header.js +2 -2
  24. data/vendor/assets/vis/timeline/Range.js +53 -93
  25. data/vendor/assets/vis/timeline/Timeline.js +328 -224
  26. data/vendor/assets/vis/timeline/component/Component.js +17 -95
  27. data/vendor/assets/vis/timeline/component/CurrentTime.js +54 -59
  28. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -83
  29. data/vendor/assets/vis/timeline/component/Group.js +398 -75
  30. data/vendor/assets/vis/timeline/component/ItemSet.js +662 -403
  31. data/vendor/assets/vis/timeline/component/Panel.js +118 -60
  32. data/vendor/assets/vis/timeline/component/RootPanel.js +80 -132
  33. data/vendor/assets/vis/timeline/component/TimeAxis.js +191 -277
  34. data/vendor/assets/vis/timeline/component/css/item.css +16 -23
  35. data/vendor/assets/vis/timeline/component/css/itemset.css +25 -4
  36. data/vendor/assets/vis/timeline/component/css/labelset.css +34 -0
  37. data/vendor/assets/vis/timeline/component/css/panel.css +15 -1
  38. data/vendor/assets/vis/timeline/component/css/timeaxis.css +8 -8
  39. data/vendor/assets/vis/timeline/component/item/Item.js +48 -26
  40. data/vendor/assets/vis/timeline/component/item/ItemBox.js +156 -230
  41. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +118 -166
  42. data/vendor/assets/vis/timeline/component/item/ItemRange.js +135 -187
  43. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +29 -92
  44. data/vendor/assets/vis/timeline/stack.js +112 -0
  45. data/vendor/assets/vis/util.js +136 -38
  46. metadata +4 -18
  47. data/vendor/assets/vis/.gitignore +0 -1
  48. data/vendor/assets/vis/EventBus.js +0 -89
  49. data/vendor/assets/vis/events.js +0 -116
  50. data/vendor/assets/vis/graph/ClusterMixin.js +0 -1019
  51. data/vendor/assets/vis/graph/NavigationMixin.js +0 -245
  52. data/vendor/assets/vis/graph/SectorsMixin.js +0 -547
  53. data/vendor/assets/vis/graph/SelectionMixin.js +0 -515
  54. data/vendor/assets/vis/graph/img/downarrow.png +0 -0
  55. data/vendor/assets/vis/graph/img/leftarrow.png +0 -0
  56. data/vendor/assets/vis/graph/img/rightarrow.png +0 -0
  57. data/vendor/assets/vis/graph/img/uparrow.png +0 -0
  58. data/vendor/assets/vis/timeline/Controller.js +0 -183
  59. data/vendor/assets/vis/timeline/Stack.js +0 -190
  60. data/vendor/assets/vis/timeline/component/ContentPanel.js +0 -113
  61. data/vendor/assets/vis/timeline/component/GroupSet.js +0 -580
  62. data/vendor/assets/vis/timeline/component/css/groupset.css +0 -59
@@ -1,75 +1,72 @@
1
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
2
+
1
3
  /**
2
4
  * An ItemSet holds a set of items and ranges which can be displayed in a
3
5
  * range. The width is determined by the parent of the ItemSet, and the height
4
6
  * is determined by the size of the items.
5
- * @param {Component} parent
6
- * @param {Component[]} [depends] Components on which this components depends
7
- * (except for the parent)
8
- * @param {Object} [options] See ItemSet.setOptions for the available
9
- * options.
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
12
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
10
13
  * @constructor ItemSet
11
14
  * @extends Panel
12
15
  */
13
- // TODO: improve performance by replacing all Array.forEach with a for loop
14
- function ItemSet(parent, depends, options) {
16
+ function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
15
17
  this.id = util.randomUUID();
16
- this.parent = parent;
17
- this.depends = depends;
18
-
19
- // event listeners
20
- this.eventListeners = {
21
- dragstart: this._onDragStart.bind(this),
22
- drag: this._onDrag.bind(this),
23
- dragend: this._onDragEnd.bind(this)
24
- };
25
18
 
26
19
  // one options object is shared by this itemset and all its items
27
20
  this.options = options || {};
28
- this.defaultOptions = {
29
- type: 'box',
30
- align: 'center',
31
- orientation: 'bottom',
32
- margin: {
33
- axis: 20,
34
- item: 10
35
- },
36
- padding: 5
37
- };
38
-
21
+ this.backgroundPanel = backgroundPanel;
22
+ this.axisPanel = axisPanel;
23
+ this.sidePanel = sidePanel;
24
+ this.itemOptions = Object.create(this.options);
39
25
  this.dom = {};
26
+ this.hammer = null;
40
27
 
41
28
  var me = this;
42
- this.itemsData = null; // DataSet
43
- this.range = null; // Range or Object {start: number, end: number}
29
+ this.itemsData = null; // DataSet
30
+ this.groupsData = null; // DataSet
31
+ this.range = null; // Range or Object {start: number, end: number}
44
32
 
45
- // data change listeners
46
- this.listeners = {
33
+ // listeners for the DataSet of the items
34
+ this.itemListeners = {
47
35
  'add': function (event, params, senderId) {
48
- if (senderId != me.id) {
49
- me._onAdd(params.items);
50
- }
36
+ if (senderId != me.id) me._onAdd(params.items);
51
37
  },
52
38
  'update': function (event, params, senderId) {
53
- if (senderId != me.id) {
54
- me._onUpdate(params.items);
55
- }
39
+ if (senderId != me.id) me._onUpdate(params.items);
56
40
  },
57
41
  'remove': function (event, params, senderId) {
58
- if (senderId != me.id) {
59
- me._onRemove(params.items);
60
- }
42
+ if (senderId != me.id) me._onRemove(params.items);
43
+ }
44
+ };
45
+
46
+ // listeners for the DataSet of the groups
47
+ this.groupListeners = {
48
+ 'add': function (event, params, senderId) {
49
+ if (senderId != me.id) me._onAddGroups(params.items);
50
+ },
51
+ 'update': function (event, params, senderId) {
52
+ if (senderId != me.id) me._onUpdateGroups(params.items);
53
+ },
54
+ 'remove': function (event, params, senderId) {
55
+ if (senderId != me.id) me._onRemoveGroups(params.items);
61
56
  }
62
57
  };
63
58
 
64
59
  this.items = {}; // object with an Item for every data item
60
+ this.groups = {}; // Group object for every group
61
+ this.groupIds = [];
62
+
65
63
  this.selection = []; // list with the ids of all selected nodes
66
- this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
67
- this.stack = new Stack(this, Object.create(this.options));
68
- this.conversion = null;
64
+ this.stackDirty = true; // if true, all items will be restacked on next repaint
69
65
 
70
66
  this.touchParams = {}; // stores properties while dragging
67
+ // create the HTML DOM
71
68
 
72
- // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
69
+ this._create();
73
70
  }
74
71
 
75
72
  ItemSet.prototype = new Panel();
@@ -82,6 +79,51 @@ ItemSet.types = {
82
79
  point: ItemPoint
83
80
  };
84
81
 
82
+ /**
83
+ * Create the HTML DOM for the ItemSet
84
+ */
85
+ ItemSet.prototype._create = function _create(){
86
+ var frame = document.createElement('div');
87
+ frame['timeline-itemset'] = this;
88
+ this.frame = frame;
89
+
90
+ // create background panel
91
+ var background = document.createElement('div');
92
+ background.className = 'background';
93
+ this.backgroundPanel.frame.appendChild(background);
94
+ this.dom.background = background;
95
+
96
+ // create foreground panel
97
+ var foreground = document.createElement('div');
98
+ foreground.className = 'foreground';
99
+ frame.appendChild(foreground);
100
+ this.dom.foreground = foreground;
101
+
102
+ // create axis panel
103
+ var axis = document.createElement('div');
104
+ axis.className = 'axis';
105
+ this.dom.axis = axis;
106
+ this.axisPanel.frame.appendChild(axis);
107
+
108
+ // create labelset
109
+ var labelSet = document.createElement('div');
110
+ labelSet.className = 'labelset';
111
+ this.dom.labelSet = labelSet;
112
+ this.sidePanel.frame.appendChild(labelSet);
113
+
114
+ // create ungrouped Group
115
+ this._updateUngrouped();
116
+
117
+ // attach event listeners
118
+ // TODO: use event listeners from the rootpanel to improve performance?
119
+ this.hammer = Hammer(frame, {
120
+ prevent_default: true
121
+ });
122
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
123
+ this.hammer.on('drag', this._onDrag.bind(this));
124
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
125
+ };
126
+
85
127
  /**
86
128
  * Set options for the ItemSet. Existing options will be extended/overwritten.
87
129
  * @param {Object} [options] The following options are available:
@@ -110,56 +152,58 @@ ItemSet.types = {
110
152
  * Function to let items snap to nice dates when
111
153
  * dragging items.
112
154
  */
113
- ItemSet.prototype.setOptions = Component.prototype.setOptions;
114
-
115
-
155
+ ItemSet.prototype.setOptions = function setOptions(options) {
156
+ Component.prototype.setOptions.call(this, options);
157
+ };
116
158
 
117
159
  /**
118
- * Set controller for this component
119
- * @param {Controller | null} controller
160
+ * Mark the ItemSet dirty so it will refresh everything with next repaint
120
161
  */
121
- ItemSet.prototype.setController = function setController (controller) {
122
- var event;
162
+ ItemSet.prototype.markDirty = function markDirty() {
163
+ this.groupIds = [];
164
+ this.stackDirty = true;
165
+ };
123
166
 
124
- // unregister old event listeners
125
- if (this.controller) {
126
- for (event in this.eventListeners) {
127
- if (this.eventListeners.hasOwnProperty(event)) {
128
- this.controller.off(event, this.eventListeners[event]);
129
- }
130
- }
167
+ /**
168
+ * Hide the component from the DOM
169
+ */
170
+ ItemSet.prototype.hide = function hide() {
171
+ // remove the axis with dots
172
+ if (this.dom.axis.parentNode) {
173
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
131
174
  }
132
175
 
133
- this.controller = controller || null;
176
+ // remove the background with vertical lines
177
+ if (this.dom.background.parentNode) {
178
+ this.dom.background.parentNode.removeChild(this.dom.background);
179
+ }
134
180
 
135
- // register new event listeners
136
- if (this.controller) {
137
- for (event in this.eventListeners) {
138
- if (this.eventListeners.hasOwnProperty(event)) {
139
- this.controller.on(event, this.eventListeners[event]);
140
- }
141
- }
181
+ // remove the labelset containing all group labels
182
+ if (this.dom.labelSet.parentNode) {
183
+ this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
142
184
  }
143
185
  };
144
186
 
145
- // attach event listeners for dragging items to the controller
146
- (function (me) {
147
- var _controller = null;
148
- var _onDragStart = null;
149
- var _onDrag = null;
150
- var _onDragEnd = null;
151
-
152
- Object.defineProperty(me, 'controller', {
153
- get: function () {
154
- return _controller;
155
- },
156
-
157
- set: function (controller) {
187
+ /**
188
+ * Show the component in the DOM (when not already visible).
189
+ * @return {Boolean} changed
190
+ */
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);
195
+ }
158
196
 
159
- }
160
- });
161
- }) (this);
197
+ // show background with vertical lines
198
+ if (!this.dom.background.parentNode) {
199
+ this.backgroundPanel.frame.appendChild(this.dom.background);
200
+ }
162
201
 
202
+ // show labelset containing labels
203
+ if (!this.dom.labelSet.parentNode) {
204
+ this.sidePanel.frame.appendChild(this.dom.labelSet);
205
+ }
206
+ };
163
207
 
164
208
  /**
165
209
  * Set range (start and end).
@@ -181,7 +225,7 @@ ItemSet.prototype.setRange = function setRange(range) {
181
225
  * unselected.
182
226
  */
183
227
  ItemSet.prototype.setSelection = function setSelection(ids) {
184
- var i, ii, id, item, selection;
228
+ var i, ii, id, item;
185
229
 
186
230
  if (ids) {
187
231
  if (!Array.isArray(ids)) {
@@ -205,10 +249,6 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
205
249
  item.select();
206
250
  }
207
251
  }
208
-
209
- if (this.controller) {
210
- this.requestRepaint();
211
- }
212
252
  }
213
253
  };
214
254
 
@@ -235,184 +275,145 @@ ItemSet.prototype._deselect = function _deselect(id) {
235
275
  }
236
276
  };
237
277
 
278
+ /**
279
+ * Return the item sets frame
280
+ * @returns {HTMLElement} frame
281
+ */
282
+ ItemSet.prototype.getFrame = function getFrame() {
283
+ return this.frame;
284
+ };
285
+
238
286
  /**
239
287
  * Repaint the component
240
- * @return {Boolean} changed
288
+ * @return {boolean} Returns true if the component is resized
241
289
  */
242
290
  ItemSet.prototype.repaint = function repaint() {
243
- var changed = 0,
244
- update = util.updateProperty,
291
+ var margin = this.options.margin,
292
+ range = this.range,
245
293
  asSize = util.option.asSize,
294
+ asString = util.option.asString,
246
295
  options = this.options,
247
296
  orientation = this.getOption('orientation'),
248
- defaultOptions = this.defaultOptions,
297
+ resized = false,
249
298
  frame = this.frame;
250
299
 
251
- if (!frame) {
252
- frame = document.createElement('div');
253
- frame.className = 'itemset';
254
- frame['timeline-itemset'] = this;
255
-
256
- var className = options.className;
257
- if (className) {
258
- util.addClassName(frame, util.option.asString(className));
259
- }
260
-
261
- // create background panel
262
- var background = document.createElement('div');
263
- background.className = 'background';
264
- frame.appendChild(background);
265
- this.dom.background = background;
266
-
267
- // create foreground panel
268
- var foreground = document.createElement('div');
269
- foreground.className = 'foreground';
270
- frame.appendChild(foreground);
271
- this.dom.foreground = foreground;
272
-
273
- // create axis panel
274
- var axis = document.createElement('div');
275
- axis.className = 'itemset-axis';
276
- //frame.appendChild(axis);
277
- this.dom.axis = axis;
278
-
279
- this.frame = frame;
280
- changed += 1;
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
+ };
281
306
  }
282
307
 
283
- if (!this.parent) {
284
- throw new Error('Cannot repaint itemset: no parent attached');
285
- }
286
- var parentContainer = this.parent.getContainer();
287
- if (!parentContainer) {
288
- throw new Error('Cannot repaint itemset: parent has no container element');
289
- }
290
- if (!frame.parentNode) {
291
- parentContainer.appendChild(frame);
292
- changed += 1;
293
- }
294
- if (!this.dom.axis.parentNode) {
295
- parentContainer.appendChild(this.dom.axis);
296
- changed += 1;
297
- }
308
+ // update className
309
+ frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
310
+
311
+ // reorder the groups (if needed)
312
+ resized = this._orderGroups() || resized;
313
+
314
+ // check whether zoomed (in that case we need to re-stack everything)
315
+ // 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);
318
+ if (zoomed) this.stackDirty = true;
319
+ this.lastVisibleInterval = visibleInterval;
320
+ this.lastWidth = this.width;
321
+
322
+ // repaint all groups
323
+ var restack = this.stackDirty,
324
+ firstGroup = this._firstGroup(),
325
+ firstMargin = {
326
+ item: margin.item,
327
+ axis: margin.axis
328
+ },
329
+ nonFirstMargin = {
330
+ item: margin.item,
331
+ axis: margin.item / 2
332
+ },
333
+ height = 0,
334
+ minHeight = margin.axis + margin.item;
335
+ util.forEach(this.groups, function (group) {
336
+ var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
337
+ resized = group.repaint(range, groupMargin, restack) || resized;
338
+ height += group.height;
339
+ });
340
+ height = Math.max(height, minHeight);
341
+ this.stackDirty = false;
298
342
 
299
343
  // reposition frame
300
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
301
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
302
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
303
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
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%');
349
+ frame.style.height = asSize(height);
350
+ //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
351
+
352
+ // calculate actual size and position
353
+ this.top = frame.offsetTop;
354
+ this.left = frame.offsetLeft;
355
+ this.width = frame.offsetWidth;
356
+ this.height = height;
304
357
 
305
358
  // reposition axis
306
- changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
307
- changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
308
- if (orientation == 'bottom') {
309
- changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
310
- }
311
- else { // orientation == 'top'
312
- changed += update(this.dom.axis.style, 'top', this.top + 'px');
313
- }
314
-
315
- this._updateConversion();
316
-
317
- var me = this,
318
- queue = this.queue,
319
- itemsData = this.itemsData,
320
- items = this.items,
321
- dataOptions = {
322
- // TODO: cleanup
323
- // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
324
- };
325
-
326
- // show/hide added/changed/removed items
327
- for (var id in queue) {
328
- if (queue.hasOwnProperty(id)) {
329
- var entry = queue[id],
330
- item = items[id],
331
- action = entry.action;
332
-
333
- //noinspection FallthroughInSwitchStatementJS
334
- switch (action) {
335
- case 'add':
336
- case 'update':
337
- var itemData = itemsData && itemsData.get(id, dataOptions);
338
-
339
- if (itemData) {
340
- var type = itemData.type ||
341
- (itemData.start && itemData.end && 'range') ||
342
- options.type ||
343
- 'box';
344
- var constructor = ItemSet.types[type];
345
-
346
- // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
347
- if (item) {
348
- // update item
349
- if (!constructor || !(item instanceof constructor)) {
350
- // item type has changed, hide and delete the item
351
- changed += item.hide();
352
- item = null;
353
- }
354
- else {
355
- item.data = itemData; // TODO: create a method item.setData ?
356
- changed++;
357
- }
358
- }
359
-
360
- if (!item) {
361
- // create item
362
- if (constructor) {
363
- item = new constructor(me, itemData, options, defaultOptions);
364
- item.id = entry.id; // we take entry.id, as id itself is stringified
365
- changed++;
366
- }
367
- else {
368
- throw new TypeError('Unknown item type "' + type + '"');
369
- }
370
- }
371
-
372
- // force a repaint (not only a reposition)
373
- item.repaint();
374
-
375
- items[id] = item;
376
- }
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');
377
365
 
378
- // update queue
379
- delete queue[id];
380
- break;
366
+ // check if this component is resized
367
+ resized = this._isResized() || resized;
381
368
 
382
- case 'remove':
383
- if (item) {
384
- // remove the item from the set selected items
385
- if (item.selected) {
386
- me._deselect(id);
387
- }
369
+ return resized;
370
+ };
388
371
 
389
- // remove DOM of the item
390
- changed += item.hide();
391
- }
372
+ /**
373
+ * Get the first group, aligned with the axis
374
+ * @return {Group | null} firstGroup
375
+ * @private
376
+ */
377
+ ItemSet.prototype._firstGroup = function _firstGroup() {
378
+ var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
379
+ var firstGroupId = this.groupIds[firstGroupIndex];
380
+ var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
392
381
 
393
- // update lists
394
- delete items[id];
395
- delete queue[id];
396
- break;
382
+ return firstGroup || null;
383
+ };
397
384
 
398
- default:
399
- console.log('Error: unknown action "' + action + '"');
400
- }
385
+ /**
386
+ * Create or delete the group holding all ungrouped items. This group is used when
387
+ * there are no groups specified.
388
+ * @protected
389
+ */
390
+ ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
391
+ var ungrouped = this.groups[UNGROUPED];
392
+
393
+ if (this.groupsData) {
394
+ // remove the group holding all ungrouped items
395
+ if (ungrouped) {
396
+ ungrouped.hide();
397
+ delete this.groups[UNGROUPED];
401
398
  }
402
399
  }
400
+ else {
401
+ // create a group holding all (unfiltered) items
402
+ if (!ungrouped) {
403
+ var id = null;
404
+ var data = null;
405
+ ungrouped = new Group(id, data, this);
406
+ this.groups[UNGROUPED] = ungrouped;
407
+
408
+ for (var itemId in this.items) {
409
+ if (this.items.hasOwnProperty(itemId)) {
410
+ ungrouped.add(this.items[itemId]);
411
+ }
412
+ }
403
413
 
404
- // reposition all items. Show items only when in the visible area
405
- util.forEach(this.items, function (item) {
406
- if (item.visible) {
407
- changed += item.show();
408
- item.reposition();
414
+ ungrouped.show();
409
415
  }
410
- else {
411
- changed += item.hide();
412
- }
413
- });
414
-
415
- return (changed > 0);
416
+ }
416
417
  };
417
418
 
418
419
  /**
@@ -440,87 +441,11 @@ ItemSet.prototype.getAxis = function getAxis() {
440
441
  };
441
442
 
442
443
  /**
443
- * Reflow the component
444
- * @return {Boolean} resized
445
- */
446
- ItemSet.prototype.reflow = function reflow () {
447
- var changed = 0,
448
- options = this.options,
449
- marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.defaultOptions.margin.axis,
450
- marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.defaultOptions.margin.item,
451
- update = util.updateProperty,
452
- asNumber = util.option.asNumber,
453
- asSize = util.option.asSize,
454
- frame = this.frame;
455
-
456
- if (frame) {
457
- this._updateConversion();
458
-
459
- util.forEach(this.items, function (item) {
460
- changed += item.reflow();
461
- });
462
-
463
- // TODO: stack.update should be triggered via an event, in stack itself
464
- // TODO: only update the stack when there are changed items
465
- this.stack.update();
466
-
467
- var maxHeight = asNumber(options.maxHeight);
468
- var fixedHeight = (asSize(options.height) != null);
469
- var height;
470
- if (fixedHeight) {
471
- height = frame.offsetHeight;
472
- }
473
- else {
474
- // height is not specified, determine the height from the height and positioned items
475
- var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
476
- if (visibleItems.length) {
477
- var min = visibleItems[0].top;
478
- var max = visibleItems[0].top + visibleItems[0].height;
479
- util.forEach(visibleItems, function (item) {
480
- min = Math.min(min, item.top);
481
- max = Math.max(max, (item.top + item.height));
482
- });
483
- height = (max - min) + marginAxis + marginItem;
484
- }
485
- else {
486
- height = marginAxis + marginItem;
487
- }
488
- }
489
- if (maxHeight != null) {
490
- height = Math.min(height, maxHeight);
491
- }
492
- changed += update(this, 'height', height);
493
-
494
- // calculate height from items
495
- changed += update(this, 'top', frame.offsetTop);
496
- changed += update(this, 'left', frame.offsetLeft);
497
- changed += update(this, 'width', frame.offsetWidth);
498
- }
499
- else {
500
- changed += 1;
501
- }
502
-
503
- return (changed > 0);
504
- };
505
-
506
- /**
507
- * Hide this component from the DOM
508
- * @return {Boolean} changed
444
+ * Get the element for the labelset
445
+ * @return {HTMLElement} labelSet
509
446
  */
510
- ItemSet.prototype.hide = function hide() {
511
- var changed = false;
512
-
513
- // remove the DOM
514
- if (this.frame && this.frame.parentNode) {
515
- this.frame.parentNode.removeChild(this.frame);
516
- changed = true;
517
- }
518
- if (this.dom.axis && this.dom.axis.parentNode) {
519
- this.dom.axis.parentNode.removeChild(this.dom.axis);
520
- changed = true;
521
- }
522
-
523
- return changed;
447
+ ItemSet.prototype.getLabelSet = function getLabelSet() {
448
+ return this.dom.labelSet;
524
449
  };
525
450
 
526
451
  /**
@@ -540,12 +465,12 @@ ItemSet.prototype.setItems = function setItems(items) {
540
465
  this.itemsData = items;
541
466
  }
542
467
  else {
543
- throw new TypeError('Data must be an instance of DataSet');
468
+ throw new TypeError('Data must be an instance of DataSet or DataView');
544
469
  }
545
470
 
546
471
  if (oldItemsData) {
547
472
  // unsubscribe from old dataset
548
- util.forEach(this.listeners, function (callback, event) {
473
+ util.forEach(this.itemListeners, function (callback, event) {
549
474
  oldItemsData.unsubscribe(event, callback);
550
475
  });
551
476
 
@@ -557,24 +482,86 @@ ItemSet.prototype.setItems = function setItems(items) {
557
482
  if (this.itemsData) {
558
483
  // subscribe to new dataset
559
484
  var id = this.id;
560
- util.forEach(this.listeners, function (callback, event) {
485
+ util.forEach(this.itemListeners, function (callback, event) {
561
486
  me.itemsData.on(event, callback, id);
562
487
  });
563
488
 
564
- // draw all new items
489
+ // add all new items
565
490
  ids = this.itemsData.getIds();
566
491
  this._onAdd(ids);
492
+
493
+ // update the group holding all ungrouped items
494
+ this._updateUngrouped();
567
495
  }
568
496
  };
569
497
 
570
498
  /**
571
- * Get the current items items
499
+ * Get the current items
572
500
  * @returns {vis.DataSet | null}
573
501
  */
574
502
  ItemSet.prototype.getItems = function getItems() {
575
503
  return this.itemsData;
576
504
  };
577
505
 
506
+ /**
507
+ * Set groups
508
+ * @param {vis.DataSet} groups
509
+ */
510
+ ItemSet.prototype.setGroups = function setGroups(groups) {
511
+ var me = this,
512
+ ids;
513
+
514
+ // unsubscribe from current dataset
515
+ if (this.groupsData) {
516
+ util.forEach(this.groupListeners, function (callback, event) {
517
+ me.groupsData.unsubscribe(event, callback);
518
+ });
519
+
520
+ // remove all drawn groups
521
+ ids = this.groupsData.getIds();
522
+ this._onRemoveGroups(ids);
523
+ }
524
+
525
+ // replace the dataset
526
+ if (!groups) {
527
+ this.groupsData = null;
528
+ }
529
+ else if (groups instanceof DataSet || groups instanceof DataView) {
530
+ this.groupsData = groups;
531
+ }
532
+ else {
533
+ throw new TypeError('Data must be an instance of DataSet or DataView');
534
+ }
535
+
536
+ if (this.groupsData) {
537
+ // subscribe to new dataset
538
+ var id = this.id;
539
+ util.forEach(this.groupListeners, function (callback, event) {
540
+ me.groupsData.on(event, callback, id);
541
+ });
542
+
543
+ // draw all ms
544
+ ids = this.groupsData.getIds();
545
+ this._onAddGroups(ids);
546
+ }
547
+
548
+ // update the group holding all ungrouped items
549
+ this._updateUngrouped();
550
+
551
+ // update the order of all items in each group
552
+ this._order();
553
+
554
+ this.emit('change');
555
+ };
556
+
557
+ /**
558
+ * Get the current groups
559
+ * @returns {vis.DataSet | null} groups
560
+ */
561
+ ItemSet.prototype.getGroups = function getGroups() {
562
+ return this.groupsData;
563
+ };
564
+
578
565
  /**
579
566
  * Remove an item by its id
580
567
  * @param {String | Number} id
@@ -587,7 +574,9 @@ ItemSet.prototype.removeItem = function removeItem (id) {
587
574
  // confirm deletion
588
575
  this.options.onRemove(item, function (item) {
589
576
  if (item) {
590
- dataset.remove(item);
577
+ // remove by id here, it is possible that an item has no id defined
578
+ // itself, so better not delete by the item itself
579
+ dataset.remove(id);
591
580
  }
592
581
  });
593
582
  }
@@ -596,94 +585,307 @@ ItemSet.prototype.removeItem = function removeItem (id) {
596
585
  /**
597
586
  * Handle updated items
598
587
  * @param {Number[]} ids
599
- * @private
588
+ * @protected
600
589
  */
601
590
  ItemSet.prototype._onUpdate = function _onUpdate(ids) {
602
- this._toQueue('update', ids);
591
+ var me = this,
592
+ items = this.items,
593
+ itemOptions = this.itemOptions;
594
+
595
+ ids.forEach(function (id) {
596
+ var itemData = me.itemsData.get(id),
597
+ item = items[id],
598
+ type = itemData.type ||
599
+ (itemData.start && itemData.end && 'range') ||
600
+ me.options.type ||
601
+ 'box';
602
+
603
+ var constructor = ItemSet.types[type];
604
+
605
+ if (item) {
606
+ // update item
607
+ if (!constructor || !(item instanceof constructor)) {
608
+ // item type has changed, delete the item and recreate it
609
+ me._removeItem(item);
610
+ item = null;
611
+ }
612
+ else {
613
+ me._updateItem(item, itemData);
614
+ }
615
+ }
616
+
617
+ if (!item) {
618
+ // create item
619
+ if (constructor) {
620
+ item = new constructor(itemData, me.options, itemOptions);
621
+ item.id = id; // TODO: not so nice setting id afterwards
622
+ me._addItem(item);
623
+ }
624
+ else {
625
+ throw new TypeError('Unknown item type "' + type + '"');
626
+ }
627
+ }
628
+ });
629
+
630
+ this._order();
631
+ this.stackDirty = true; // force re-stacking of all items next repaint
632
+ this.emit('change');
603
633
  };
604
634
 
605
635
  /**
606
- * Handle changed items
636
+ * Handle added items
637
+ * @param {Number[]} ids
638
+ * @protected
639
+ */
640
+ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
641
+
642
+ /**
643
+ * Handle removed items
607
644
  * @param {Number[]} ids
645
+ * @protected
646
+ */
647
+ ItemSet.prototype._onRemove = function _onRemove(ids) {
648
+ var count = 0;
649
+ var me = this;
650
+ ids.forEach(function (id) {
651
+ var item = me.items[id];
652
+ if (item) {
653
+ count++;
654
+ me._removeItem(item);
655
+ }
656
+ });
657
+
658
+ if (count) {
659
+ // update order
660
+ this._order();
661
+ this.stackDirty = true; // force re-stacking of all items next repaint
662
+ this.emit('change');
663
+ }
664
+ };
665
+
666
+ /**
667
+ * Update the order of item in all groups
608
668
  * @private
609
669
  */
610
- ItemSet.prototype._onAdd = function _onAdd(ids) {
611
- this._toQueue('add', ids);
670
+ ItemSet.prototype._order = function _order() {
671
+ // reorder the items in all groups
672
+ // TODO: optimization: only reorder groups affected by the changed items
673
+ util.forEach(this.groups, function (group) {
674
+ group.order();
675
+ });
612
676
  };
613
677
 
614
678
  /**
615
- * Handle removed items
679
+ * Handle updated groups
616
680
  * @param {Number[]} ids
617
681
  * @private
618
682
  */
619
- ItemSet.prototype._onRemove = function _onRemove(ids) {
620
- this._toQueue('remove', ids);
683
+ ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
684
+ this._onAddGroups(ids);
621
685
  };
622
686
 
623
687
  /**
624
- * Put items in the queue to be added/updated/remove
625
- * @param {String} action can be 'add', 'update', 'remove'
688
+ * Handle changed groups
626
689
  * @param {Number[]} ids
690
+ * @private
627
691
  */
628
- ItemSet.prototype._toQueue = function _toQueue(action, ids) {
629
- var queue = this.queue;
692
+ ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
693
+ var me = this;
694
+
630
695
  ids.forEach(function (id) {
631
- queue[id] = {
632
- id: id,
633
- action: action
634
- };
696
+ var groupData = me.groupsData.get(id);
697
+ var group = me.groups[id];
698
+
699
+ if (!group) {
700
+ // check for reserved ids
701
+ if (id == UNGROUPED) {
702
+ throw new Error('Illegal group id. ' + id + ' is a reserved id.');
703
+ }
704
+
705
+ var groupOptions = Object.create(me.options);
706
+ util.extend(groupOptions, {
707
+ height: null
708
+ });
709
+
710
+ group = new Group(id, groupData, me);
711
+ me.groups[id] = group;
712
+
713
+ // add items with this groupId to the new group
714
+ for (var itemId in me.items) {
715
+ if (me.items.hasOwnProperty(itemId)) {
716
+ var item = me.items[itemId];
717
+ if (item.data.group == id) {
718
+ group.add(item);
719
+ }
720
+ }
721
+ }
722
+
723
+ group.order();
724
+ group.show();
725
+ }
726
+ else {
727
+ // update group
728
+ group.setData(groupData);
729
+ }
635
730
  });
636
731
 
637
- if (this.controller) {
638
- //this.requestReflow();
639
- this.requestRepaint();
640
- }
732
+ this.emit('change');
641
733
  };
642
734
 
643
735
  /**
644
- * Calculate the scale and offset to convert a position on screen to the
645
- * corresponding date and vice versa.
646
- * After the method _updateConversion is executed once, the methods toTime
647
- * and toScreen can be used.
736
+ * Handle removed groups
737
+ * @param {Number[]} ids
648
738
  * @private
649
739
  */
650
- ItemSet.prototype._updateConversion = function _updateConversion() {
651
- var range = this.range;
652
- if (!range) {
653
- throw new Error('No range configured');
654
- }
740
+ ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
741
+ var groups = this.groups;
742
+ ids.forEach(function (id) {
743
+ var group = groups[id];
744
+
745
+ if (group) {
746
+ group.hide();
747
+ delete groups[id];
748
+ }
749
+ });
750
+
751
+ this.markDirty();
655
752
 
656
- if (range.conversion) {
657
- this.conversion = range.conversion(this.width);
753
+ this.emit('change');
754
+ };
755
+
756
+ /**
757
+ * Reorder the groups if needed
758
+ * @return {boolean} changed
759
+ * @private
760
+ */
761
+ ItemSet.prototype._orderGroups = function () {
762
+ if (this.groupsData) {
763
+ // reorder the groups
764
+ var groupIds = this.groupsData.getIds({
765
+ order: this.options.groupOrder
766
+ });
767
+
768
+ var changed = !util.equalArray(groupIds, this.groupIds);
769
+ if (changed) {
770
+ // hide all groups, removes them from the DOM
771
+ var groups = this.groups;
772
+ groupIds.forEach(function (groupId) {
773
+ var group = groups[groupId];
774
+ group.hide();
775
+ });
776
+
777
+ // show the groups again, attach them to the DOM in correct order
778
+ groupIds.forEach(function (groupId) {
779
+ groups[groupId].show();
780
+ });
781
+
782
+ this.groupIds = groupIds;
783
+ }
784
+
785
+ return changed;
658
786
  }
659
787
  else {
660
- this.conversion = Range.conversion(range.start, range.end, this.width);
788
+ return false;
661
789
  }
662
790
  };
663
791
 
664
792
  /**
665
- * Convert a position on screen (pixels) to a datetime
666
- * Before this method can be used, the method _updateConversion must be
667
- * executed once.
668
- * @param {int} x Position on the screen in pixels
669
- * @return {Date} time The datetime the corresponds with given position x
793
+ * Add a new item
794
+ * @param {Item} item
795
+ * @private
670
796
  */
671
- ItemSet.prototype.toTime = function toTime(x) {
672
- var conversion = this.conversion;
673
- return new Date(x / conversion.scale + conversion.offset);
797
+ ItemSet.prototype._addItem = function _addItem(item) {
798
+ this.items[item.id] = item;
799
+
800
+ // add to group
801
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
802
+ var group = this.groups[groupId];
803
+ if (group) group.add(item);
674
804
  };
675
805
 
676
806
  /**
677
- * Convert a datetime (Date object) into a position on the screen
678
- * Before this method can be used, the method _updateConversion must be
679
- * executed once.
680
- * @param {Date} time A date
681
- * @return {int} x The position on the screen in pixels which corresponds
682
- * with the given date.
807
+ * Update an existing item
808
+ * @param {Item} item
809
+ * @param {Object} itemData
810
+ * @private
683
811
  */
684
- ItemSet.prototype.toScreen = function toScreen(time) {
685
- var conversion = this.conversion;
686
- return (time.valueOf() - conversion.offset) * conversion.scale;
812
+ ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
813
+ var oldGroupId = item.data.group;
814
+
815
+ item.data = itemData;
816
+ item.repaint();
817
+
818
+ // update group
819
+ if (oldGroupId != item.data.group) {
820
+ var oldGroup = this.groups[oldGroupId];
821
+ if (oldGroup) oldGroup.remove(item);
822
+
823
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
824
+ var group = this.groups[groupId];
825
+ if (group) group.add(item);
826
+ }
827
+ };
828
+
829
+ /**
830
+ * Delete an item from the ItemSet: remove it from the DOM, from the map
831
+ * with items, and from the map with visible items, and from the selection
832
+ * @param {Item} item
833
+ * @private
834
+ */
835
+ ItemSet.prototype._removeItem = function _removeItem(item) {
836
+ // remove from DOM
837
+ item.hide();
838
+
839
+ // remove from items
840
+ delete this.items[item.id];
841
+
842
+ // remove from selection
843
+ var index = this.selection.indexOf(item.id);
844
+ if (index != -1) this.selection.splice(index, 1);
845
+
846
+ // remove from group
847
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
848
+ var group = this.groups[groupId];
849
+ if (group) group.remove(item);
850
+ };
851
+
852
+ /**
853
+ * Create an array containing all items being a range (having an end date)
854
+ * @param array
855
+ * @returns {Array}
856
+ * @private
857
+ */
858
+ ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
859
+ var endArray = [];
860
+
861
+ for (var i = 0; i < array.length; i++) {
862
+ if (array[i] instanceof ItemRange) {
863
+ endArray.push(array[i]);
864
+ }
865
+ }
866
+ return endArray;
867
+ };
868
+
869
+ /**
870
+ * Get the width of the group labels
871
+ * @return {Number} width
872
+ */
873
+ ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
874
+ var width = 0;
875
+
876
+ util.forEach(this.groups, function (group) {
877
+ width = Math.max(width, group.getLabelWidth());
878
+ });
879
+
880
+ return width;
881
+ };
882
+
883
+ /**
884
+ * Get the height of the itemsets background
885
+ * @return {Number} height
886
+ */
887
+ ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
888
+ return this.height;
687
889
  };
688
890
 
689
891
  /**
@@ -692,28 +894,45 @@ ItemSet.prototype.toScreen = function toScreen(time) {
692
894
  * @private
693
895
  */
694
896
  ItemSet.prototype._onDragStart = function (event) {
695
- if (!this.options.editable) {
897
+ if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
696
898
  return;
697
899
  }
698
900
 
699
901
  var item = ItemSet.itemFromTarget(event),
700
- me = this;
902
+ me = this,
903
+ props;
701
904
 
702
905
  if (item && item.selected) {
703
906
  var dragLeftItem = event.target.dragLeftItem;
704
907
  var dragRightItem = event.target.dragRightItem;
705
908
 
706
909
  if (dragLeftItem) {
707
- this.touchParams.itemProps = [{
708
- item: dragLeftItem,
709
- start: item.data.start.valueOf()
710
- }];
910
+ props = {
911
+ item: dragLeftItem
912
+ };
913
+
914
+ if (me.options.editable.updateTime) {
915
+ props.start = item.data.start.valueOf();
916
+ }
917
+ if (me.options.editable.updateGroup) {
918
+ if ('group' in item.data) props.group = item.data.group;
919
+ }
920
+
921
+ this.touchParams.itemProps = [props];
711
922
  }
712
923
  else if (dragRightItem) {
713
- this.touchParams.itemProps = [{
714
- item: dragRightItem,
715
- end: item.data.end.valueOf()
716
- }];
924
+ props = {
925
+ item: dragRightItem
926
+ };
927
+
928
+ if (me.options.editable.updateTime) {
929
+ props.end = item.data.end.valueOf();
930
+ }
931
+ if (me.options.editable.updateGroup) {
932
+ if ('group' in item.data) props.group = item.data.group;
933
+ }
934
+
935
+ this.touchParams.itemProps = [props];
717
936
  }
718
937
  else {
719
938
  this.touchParams.itemProps = this.getSelection().map(function (id) {
@@ -722,11 +941,12 @@ ItemSet.prototype._onDragStart = function (event) {
722
941
  item: item
723
942
  };
724
943
 
725
- if ('start' in item.data) {
726
- props.start = item.data.start.valueOf()
944
+ if (me.options.editable.updateTime) {
945
+ if ('start' in item.data) props.start = item.data.start.valueOf();
946
+ if ('end' in item.data) props.end = item.data.end.valueOf();
727
947
  }
728
- if ('end' in item.data) {
729
- props.end = item.data.end.valueOf()
948
+ if (me.options.editable.updateGroup) {
949
+ if ('group' in item.data) props.group = item.data.group;
730
950
  }
731
951
 
732
952
  return props;
@@ -746,7 +966,8 @@ ItemSet.prototype._onDrag = function (event) {
746
966
  if (this.touchParams.itemProps) {
747
967
  var snap = this.options.snap || null,
748
968
  deltaX = event.gesture.deltaX,
749
- offset = deltaX / this.conversion.scale;
969
+ scale = (this.width / (this.range.end - this.range.start)),
970
+ offset = deltaX / scale;
750
971
 
751
972
  // move
752
973
  this.touchParams.itemProps.forEach(function (props) {
@@ -754,17 +975,31 @@ ItemSet.prototype._onDrag = function (event) {
754
975
  var start = new Date(props.start + offset);
755
976
  props.item.data.start = snap ? snap(start) : start;
756
977
  }
978
+
757
979
  if ('end' in props) {
758
980
  var end = new Date(props.end + offset);
759
981
  props.item.data.end = snap ? snap(end) : end;
760
982
  }
983
+
984
+ if ('group' in props) {
985
+ // drag from one group to another
986
+ var group = ItemSet.groupFromTarget(event);
987
+ if (group && group.groupId != props.item.data.group) {
988
+ var oldGroup = props.item.parent;
989
+ oldGroup.remove(props.item);
990
+ oldGroup.order();
991
+ group.add(props.item);
992
+ group.order();
993
+
994
+ props.item.data.group = group.groupId;
995
+ }
996
+ }
761
997
  });
762
998
 
763
999
  // TODO: implement onMoving handler
764
1000
 
765
- // TODO: implement dragging from one group to another
766
-
767
- this.requestReflow();
1001
+ this.stackDirty = true; // force re-stacking of all items next repaint
1002
+ this.emit('change');
768
1003
 
769
1004
  event.stopPropagation();
770
1005
  }
@@ -780,35 +1015,41 @@ ItemSet.prototype._onDragEnd = function (event) {
780
1015
  // prepare a change set for the changed items
781
1016
  var changes = [],
782
1017
  me = this,
783
- dataset = this._myDataSet(),
784
- type;
1018
+ dataset = this._myDataSet();
785
1019
 
786
1020
  this.touchParams.itemProps.forEach(function (props) {
787
1021
  var id = props.item.id,
788
- item = me.itemsData.get(id);
1022
+ itemData = me.itemsData.get(id);
789
1023
 
790
1024
  var changed = false;
791
1025
  if ('start' in props.item.data) {
792
1026
  changed = (props.start != props.item.data.start.valueOf());
793
- item.start = util.convert(props.item.data.start, dataset.convert['start']);
1027
+ itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
794
1028
  }
795
1029
  if ('end' in props.item.data) {
796
1030
  changed = changed || (props.end != props.item.data.end.valueOf());
797
- item.end = util.convert(props.item.data.end, dataset.convert['end']);
1031
+ itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
1032
+ }
1033
+ if ('group' in props.item.data) {
1034
+ changed = changed || (props.group != props.item.data.group);
1035
+ itemData.group = props.item.data.group;
798
1036
  }
799
1037
 
800
1038
  // only apply changes when start or end is actually changed
801
1039
  if (changed) {
802
- me.options.onMove(item, function (item) {
803
- if (item) {
1040
+ me.options.onMove(itemData, function (itemData) {
1041
+ if (itemData) {
804
1042
  // apply changes
805
- changes.push(item);
1043
+ itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
1044
+ changes.push(itemData);
806
1045
  }
807
1046
  else {
808
1047
  // restore original values
809
1048
  if ('start' in props) props.item.data.start = props.start;
810
1049
  if ('end' in props) props.item.data.end = props.end;
811
- me.requestReflow();
1050
+
1051
+ me.stackDirty = true; // force re-stacking of all items next repaint
1052
+ me.emit('change');
812
1053
  }
813
1054
  });
814
1055
  }
@@ -842,6 +1083,24 @@ ItemSet.itemFromTarget = function itemFromTarget (event) {
842
1083
  return null;
843
1084
  };
844
1085
 
1086
+ /**
1087
+ * Find the Group from an event target:
1088
+ * searches for the attribute 'timeline-group' in the event target's element tree
1089
+ * @param {Event} event
1090
+ * @return {Group | null} group
1091
+ */
1092
+ ItemSet.groupFromTarget = function groupFromTarget (event) {
1093
+ var target = event.target;
1094
+ while (target) {
1095
+ if (target.hasOwnProperty('timeline-group')) {
1096
+ return target['timeline-group'];
1097
+ }
1098
+ target = target.parentNode;
1099
+ }
1100
+
1101
+ return null;
1102
+ };
1103
+
845
1104
  /**
846
1105
  * Find the ItemSet from an event target:
847
1106
  * searches for the attribute 'timeline-itemset' in the event target's element tree