vis-rails 0.0.6 → 1.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 (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