Browse Source

Merge remote-tracking branch 'origin/develop' into develop

v3_develop
Alex de Mulder 9 years ago
parent
commit
17e233d34c
29 changed files with 25683 additions and 25241 deletions
  1. +16
    -0
      CONTRIBUTING.md
  2. +21
    -4
      HISTORY.md
  3. +2
    -0
      dist/vis.css
  4. +25247
    -25113
      dist/vis.js
  5. +1
    -1
      dist/vis.map
  6. +1
    -1
      dist/vis.min.css
  7. +15
    -15
      dist/vis.min.js
  8. +3
    -2
      docs/dataset.html
  9. +3
    -2
      docs/dataview.html
  10. +1
    -1
      docs/graph2d.html
  11. +22
    -13
      docs/timeline.html
  12. +1
    -0
      examples/timeline/08_edit_items.html
  13. +80
    -0
      examples/timeline/35_item_ordering.html
  14. +1
    -0
      examples/timeline/index.html
  15. +11
    -3
      lib/DataSet.js
  16. +9
    -7
      lib/DataView.js
  17. +5
    -3
      lib/network/Network.js
  18. +2
    -2
      lib/network/mixins/ManipulationMixin.js
  19. +23
    -5
      lib/timeline/Core.js
  20. +2
    -1
      lib/timeline/Timeline.js
  21. +35
    -6
      lib/timeline/component/Group.js
  22. +107
    -36
      lib/timeline/component/ItemSet.js
  23. +2
    -0
      lib/timeline/component/css/item.css
  24. +10
    -3
      lib/timeline/component/item/BackgroundItem.js
  25. +3
    -9
      lib/timeline/component/item/BoxItem.js
  26. +0
    -3
      lib/timeline/component/item/PointItem.js
  27. +14
    -10
      lib/timeline/component/item/RangeItem.js
  28. +45
    -1
      test/DataView.test.js
  29. +1
    -0
      test/timeline.html

+ 16
- 0
CONTRIBUTING.md View File

@ -0,0 +1,16 @@
## Contributing
Contributions to the vis.js library are very welcome! We can't do this alone.
You can contribute in different ways: spread the word, report bugs, come up with
ideas and suggestions, and contribute to the code.
There are a few preferences regarding code contributions:
- vis.js follows the node.js code style as described
[here](http://nodeguide.com/style.html).
- When implementing new features, please update the documentation accordingly.
- Send pull requests to the `develop` branch, not the `master` branch.
- Only commit changes done in the source files under `lib`, not to the builds
which are located in the folder `dist`.
Thanks!

+ 21
- 4
HISTORY.md View File

@ -14,15 +14,32 @@ http://visjs.org
- Added pull request for usage of Icons. Thanks @Dude9177! - Added pull request for usage of Icons. Thanks @Dude9177!
- Allow hierarchical view to be set in setOptions. - Allow hierarchical view to be set in setOptions.
- Fixed manipulation bar for mobile. - Fixed manipulation bar for mobile.
### Graph2d
- Fixed #670: Bug when updating data in a DataSet, when Network is connected to the DataSet via a DataView.
- Fixed #688: Added a css class to be able to distinguish buttons "Edit node"
and "Edit edge".
### Timeline ### Timeline
- Implemented orientation option `'both'`, displaying a time axis both on top
and bottom (#665).
- Implemented creating new range items by dragging in an empty space with the
ctrl key down.
- Implemented configuration option `order: function` to define a custom ordering
for the items (see #538, #234).
- Fixed not property initializing with a DataView for groups. - Fixed not property initializing with a DataView for groups.
- Merged add custom timebar functionality, thanks @aytech! - Merged add custom timebar functionality, thanks @aytech!
- Fixed #664: end of item not restored when canceling a move event.
- Fixed #609: reduce the left/right dragarea when an item range is very small,
so you can still move it as a whole.
- Fixed #676: misalignment of background items when using subgroups and the
group label's height is larger than the contents.
### DataSet/DataView
- Implemented support for mapping field names. Thanks @spatialillusions.
- Fixed #670: DataView not passing a data property on update events (see #670)
## 2015-02-11, version 3.10.0 ## 2015-02-11, version 3.10.0

+ 2
- 0
dist/vis.css View File

@ -266,6 +266,7 @@
.vis.timeline .item.range .drag-left { .vis.timeline .item.range .drag-left {
position: absolute; position: absolute;
width: 24px; width: 24px;
max-width: 20%;
height: 100%; height: 100%;
top: 0; top: 0;
left: -4px; left: -4px;
@ -276,6 +277,7 @@
.vis.timeline .item.range .drag-right { .vis.timeline .item.range .drag-right {
position: absolute; position: absolute;
width: 24px; width: 24px;
max-width: 20%;
height: 100%; height: 100%;
top: 0; top: 0;
right: -4px; right: -4px;

+ 25247
- 25113
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.map
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 15
- 15
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 3
- 2
docs/dataset.html View File

@ -740,9 +740,10 @@ DataSet.map(callback [, options]);
<tr> <tr>
<td>fields</td> <td>fields</td>
<td>String[&nbsp;]</td>
<td>String[&nbsp;] | Object.&lt;String,&nbsp;String&gt;</td>
<td> <td>
An array with field names.
An array with field names, or an object with current field name and
new field name that the field is returned as.
By default, all properties of the items are emitted. By default, all properties of the items are emitted.
When <code>fields</code> is defined, only the properties When <code>fields</code> is defined, only the properties
whose name is specified in <code>fields</code> will be included whose name is specified in <code>fields</code> will be included

+ 3
- 2
docs/dataview.html View File

@ -129,9 +129,10 @@ var data = new vis.DataView(dataset, options)
<tr> <tr>
<td>fields</td> <td>fields</td>
<td>String[&nbsp;]</td>
<td>String[&nbsp;] | Object.&lt;String,&nbsp;String&gt;</td>
<td> <td>
An array with field names.
An array with field names, or an object with current field name and
new field name that the field is returned as.
By default, all properties of the items are emitted. By default, all properties of the items are emitted.
When <code>fields</code> is defined, only the properties When <code>fields</code> is defined, only the properties
whose name is specified in <code>fields</code> will be included whose name is specified in <code>fields</code> will be included

+ 1
- 1
docs/graph2d.html View File

@ -678,7 +678,7 @@ The options colored in green can also be used as options for the groups. All opt
<td>orientation</td> <td>orientation</td>
<td>String</td> <td>String</td>
<td>'bottom'</td> <td>'bottom'</td>
<td>Orientation of the timeline: 'top' or 'bottom' (default). If orientation is 'bottom', the time axis is drawn at the bottom, and if 'top', the axis is drawn on top.</td>
<td>Orientation of the timeline: 'top', 'bottom' (default), or 'both'. If orientation is 'bottom', the time axis is drawn at the bottom. When 'top', the axis is drawn on top. When 'both', two axes are drawn, both on top and at the bottom.</td>
</tr> </tr>
<tr> <tr>

+ 22
- 13
docs/timeline.html View File

@ -205,7 +205,7 @@ var items = [
</tr> </tr>
<tr> <tr>
<td>end</td> <td>end</td>
<td>Date</td>
<td>Date | number | string | Moment</td>
<td>no</td> <td>no</td>
<td>The end date of the item. The end date is optional, and can be left <code>null</code>. <td>The end date of the item. The end date is optional, and can be left <code>null</code>.
If end date is provided, the item is displayed as a range. If end date is provided, the item is displayed as a range.
@ -232,7 +232,7 @@ var items = [
</tr> </tr>
<tr> <tr>
<td>start</td> <td>start</td>
<td>Date</td>
<td>Date | number | string | Moment</td>
<td>yes</td> <td>yes</td>
<td>The start date of the item, for example <code>new Date(2010,9,23)</code>.</td> <td>The start date of the item, for example <code>new Date(2010,9,23)</code>.</td>
</tr> </tr>
@ -468,7 +468,7 @@ var options = {
<tr> <tr>
<td>end</td> <td>end</td>
<td>Date | Number | String</td>
<td>Date | Number | String | Moment</td>
<td>none</td> <td>none</td>
<td>The initial end date for the axis of the timeline. <td>The initial end date for the axis of the timeline.
If not provided, the latest date present in the items set is taken as If not provided, the latest date present in the items set is taken as
@ -584,7 +584,7 @@ var options = {
<tr> <tr>
<td>max</td> <td>max</td>
<td>Date | Number | String</td>
<td>Date | Number | String | Moment</td>
<td>none</td> <td>none</td>
<td>Set a maximum Date for the visible range. <td>Set a maximum Date for the visible range.
It will not be possible to move beyond this maximum. It will not be possible to move beyond this maximum.
@ -600,7 +600,7 @@ var options = {
<tr> <tr>
<td>min</td> <td>min</td>
<td>Date | Number | String</td>
<td>Date | Number | String | Moment</td>
<td>none</td> <td>none</td>
<td>Set a minimum Date for the visible range. <td>Set a minimum Date for the visible range.
It will not be possible to move beyond this minimum. It will not be possible to move beyond this minimum.
@ -664,24 +664,25 @@ var options = {
</td> </td>
</tr> </tr>
<!-- TODO: cleanup option order
<tr> <tr>
<td>order</td> <td>order</td>
<td>Function</td> <td>Function</td>
<td>none</td> <td>none</td>
<td>Provide a custom sort function to order the items. The order of the
<td>
<p>Provide a custom sort function to order the items. The order of the
items is determining the way they are stacked. The function items is determining the way they are stacked. The function
order is called with two parameters, both of type
`vis.components.items.Item`.
order is called with two arguments containing the data of two items to be
compared.
</p>
<p style="font-style: italic">WARNING: Use with caution. Custom ordering is not suitable for large amounts of items. On load, the Timeline will render all items once to determine their width and height. Keep the number of items in this configuration limited to a maximum of a few hundred items.</p>
</td> </td>
</tr> </tr>
-->
<tr> <tr>
<td>orientation</td> <td>orientation</td>
<td>String</td> <td>String</td>
<td>'bottom'</td> <td>'bottom'</td>
<td>Orientation of the timeline: 'top' or 'bottom' (default). If orientation is 'bottom', the time axis is drawn at the bottom, and if 'top', the axis is drawn on top.</td>
<td>Orientation of the timeline: 'top', 'bottom' (default), or 'both'. If orientation is 'bottom', the time axis is drawn at the bottom. When 'top', the axis is drawn on top. When 'both', two axes are drawn, both on top and at the bottom.</td>
</tr> </tr>
<tr> <tr>
@ -762,7 +763,7 @@ var options = {
<tr> <tr>
<td>start</td> <td>start</td>
<td>Date | Number | String</td>
<td>Date | Number | String | Moment</td>
<td>none</td> <td>none</td>
<td>The initial start date for the axis of the timeline. <td>The initial start date for the axis of the timeline.
If not provided, the earliest date present in the events is taken as start date.</td> If not provided, the earliest date present in the events is taken as start date.</td>
@ -1170,8 +1171,16 @@ timeline.off('select', onSelect);
<h2 id="Editing_Items">Editing Items</h2> <h2 id="Editing_Items">Editing Items</h2>
<p> <p>
When the Timeline is configured to be editable (both options <code>selectable</code> and <code>editable</code> are <code>true</code>), the user can move items by dragging them, can create a new item by double tapping on an empty space, can update an item by double tapping it, and can delete a selected item by clicking the delete button on the top right.
When the Timeline is configured to be editable (both options <code>selectable</code> and <code>editable</code> are <code>true</code>), the user can:
</p> </p>
<ul>
<li>Select an item by clicking it, and use ctrl+click to or shift+click to select multiple items</li>
<li>Move selected items by dragging them.</li>
<li>Create a new item by double tapping on an empty space.</li>
<li>Create a new range item by dragging on an empty space with the ctrl key down.</li>
<li>Update an item by double tapping it.</li>
<li>Delete a selected item by clicking the delete button on the top right.</li>
</ul>
<p>Option <code>editable</code> accepts a boolean or an object. When <code>editable</code> is a boolean, all manipulation actions will be either enabled or disabled. When <code>editable</code> is an object, one can enable individual manipulation actions:</p> <p>Option <code>editable</code> accepts a boolean or an object. When <code>editable</code> is a boolean, all manipulation actions will be either enabled or disabled. When <code>editable</code> is an object, one can enable individual manipulation actions:</p>

+ 1
- 0
examples/timeline/08_edit_items.html View File

@ -64,6 +64,7 @@
onMoving: function (item, callback) { onMoving: function (item, callback) {
if (item.start < min) item.start = min; if (item.start < min) item.start = min;
if (item.start > max) item.start = max; if (item.start > max) item.start = max;
if (item.end > max) item.end = max;
callback(item); // send back the (possibly) changed item callback(item); // send back the (possibly) changed item
}, },

+ 80
- 0
examples/timeline/35_item_ordering.html View File

@ -0,0 +1,80 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Item ordering</title>
<style type="text/css">
body, html {
font-family: sans-serif;
}
p {
max-width: 800px;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>Item ordering</h1>
<p>
By default, the items displayed on the Timeline are unordered. They are
stacked in the order that they where loaded. This means that way items are
stacked can change while moving and zooming the Timeline.
</p>
<p>
To display and stack the items in a controlled order, you can provide a
custom sorting function via the configuration option <code>order</code>.
</p>
<p>
WARNING: Custom ordering is only suitable for small amounts of items (up to a few
hundred), as the Timeline has to render <i>all</i> items once on load to
determine their width and height.
</p>
<p>
<label for="ordering"><input type="checkbox" id="ordering" checked/> Apply custom ordering. Order items by their id.</label>
</p>
<div id="visualization"></div>
<script type="text/javascript">
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet();
var date = vis.moment('2015-03-02');
for (var i = 0; i < 100; i++) {
date.add(Math.round(Math.random() * 2), 'hour');
items.add({
id: i,
content: 'Item ' + i,
start: date.clone(),
end: date.clone().add(4, 'hour')
});
}
function customOrder (a, b) {
// order by id
return a.id - b.id;
}
// Configuration for the Timeline
var options = {
order: customOrder,
editable: true,
margin: {item: 0}
};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
var ordering = document.getElementById('ordering');
ordering.onchange = function () {
timeline.setOptions({
order: ordering.checked ? customOrder: null
});
};
</script>
</body>
</html>

+ 1
- 0
examples/timeline/index.html View File

@ -45,6 +45,7 @@
<p><a href="32_grid_styling.html">32_grid_styling.html</a></p> <p><a href="32_grid_styling.html">32_grid_styling.html</a></p>
<p><a href="33_custom_snapping.html">33_custom_snapping.html</a></p> <p><a href="33_custom_snapping.html">33_custom_snapping.html</a></p>
<p><a href="34_add_custom_timebar.html">34_add_custom_timebar.html</a></p> <p><a href="34_add_custom_timebar.html">34_add_custom_timebar.html</a></p>
<p><a href="35_item_ordering.html">35_item_ordering.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p> <p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>
</div> </div>

+ 11
- 3
lib/DataSet.js View File

@ -663,9 +663,17 @@ DataSet.prototype._filterFields = function (item, fields) {
var filteredItem = {}; var filteredItem = {};
for (var field in item) {
if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
filteredItem[field] = item[field];
if(Array.isArray(fields)){
for (var field in item) {
if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
filteredItem[field] = item[field];
}
}
}else{
for (var field in item) {
if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) {
filteredItem[fields[field]] = item[field];
}
} }
} }

+ 9
- 7
lib/DataView.js View File

@ -258,12 +258,13 @@ DataView.prototype.getDataSet = function () {
* @private * @private
*/ */
DataView.prototype._onEvent = function (event, params, senderId) { DataView.prototype._onEvent = function (event, params, senderId) {
var i, len, id, item,
ids = params && params.items,
data = this._data,
added = [],
updated = [],
removed = [];
var i, len, id, item;
var ids = params && params.items;
var data = this._data;
var updatedData = [];
var added = [];
var updated = [];
var removed = [];
if (ids && data) { if (ids && data) {
switch (event) { switch (event) {
@ -290,6 +291,7 @@ DataView.prototype._onEvent = function (event, params, senderId) {
if (item) { if (item) {
if (this._ids[id]) { if (this._ids[id]) {
updated.push(id); updated.push(id);
updatedData.push(params.data[i]);
} }
else { else {
this._ids[id] = true; this._ids[id] = true;
@ -328,7 +330,7 @@ DataView.prototype._onEvent = function (event, params, senderId) {
this._trigger('add', {items: added}, senderId); this._trigger('add', {items: added}, senderId);
} }
if (updated.length) { if (updated.length) {
this._trigger('update', {items: updated}, senderId);
this._trigger('update', {items: updated, data: updatedData}, senderId);
} }
if (removed.length) { if (removed.length) {
this._trigger('remove', {items: removed}, senderId); this._trigger('remove', {items: removed}, senderId);

+ 5
- 3
lib/network/Network.js View File

@ -329,7 +329,7 @@ function Network (container, data, options) {
network.start(); network.start();
}, },
'update': function (event, params) { 'update': function (event, params) {
network._updateNodes(params.items, params.data);
network._updateNodes(params.items);
network.start(); network.start();
}, },
'remove': function (event, params) { 'remove': function (event, params) {
@ -1687,12 +1687,14 @@ Network.prototype._addNodes = function(ids) {
* @param {Number[] | String[]} ids * @param {Number[] | String[]} ids
* @private * @private
*/ */
Network.prototype._updateNodes = function(ids,changedData) {
Network.prototype._updateNodes = function(ids) {
var nodesData = this.nodesData.get(ids);
var nodes = this.nodes; var nodes = this.nodes;
for (var i = 0, len = ids.length; i < len; i++) { for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i]; var id = ids[i];
var node = nodes[id]; var node = nodes[id];
var data = changedData[i];
var data = nodesData[i];
if (node) { if (node) {
// update node // update node
node.setProperties(data, this.constants); node.setProperties(data, this.constants);

+ 2
- 2
lib/network/mixins/ManipulationMixin.js View File

@ -138,7 +138,7 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine';
this.manipulationDOM['editNodeSpan'] = document.createElement('div'); this.manipulationDOM['editNodeSpan'] = document.createElement('div');
this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit';
this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit node';
this.manipulationDOM['editNodeLabelSpan'] = document.createElement('div'); this.manipulationDOM['editNodeLabelSpan'] = document.createElement('div');
this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode']; this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode'];
@ -152,7 +152,7 @@ exports._createManipulatorBar = function() {
this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine'; this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine';
this.manipulationDOM['editEdgeSpan'] = document.createElement('div'); this.manipulationDOM['editEdgeSpan'] = document.createElement('div');
this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit';
this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit edge';
this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('div'); this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('div');
this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel'; this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel';
this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge']; this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge'];

+ 23
- 5
lib/timeline/Core.js View File

@ -5,6 +5,7 @@ var DataSet = require('../DataSet');
var DataView = require('../DataView'); var DataView = require('../DataView');
var Range = require('./Range'); var Range = require('./Range');
var ItemSet = require('./component/ItemSet'); var ItemSet = require('./component/ItemSet');
var TimeAxis = require('./component/TimeAxis');
var Activator = require('../shared/Activator'); var Activator = require('../shared/Activator');
var DateUtil = require('./DateUtil'); var DateUtil = require('./DateUtil');
var CustomTime = require('./component/CustomTime'); var CustomTime = require('./component/CustomTime');
@ -192,6 +193,28 @@ Core.prototype.setOptions = function (options) {
var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes', 'hiddenDates']; var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes', 'hiddenDates'];
util.selectiveExtend(fields, this.options, options); util.selectiveExtend(fields, this.options, options);
if (this.options.orientation === 'both') {
if (!this.timeAxis2) {
var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body);
timeAxis2.setOptions = function (options) {
var _options = options ? util.extend({}, options) : {};
_options.orientation = 'top'; // override the orientation option, always top
TimeAxis.prototype.setOptions.call(timeAxis2, _options);
};
this.components.push(timeAxis2);
}
}
else {
if (this.timeAxis2) {
var index = this.components.indexOf(this.timeAxis2);
if (index !== -1) {
this.components.splice(index, 1);
}
this.timeAxis2.destroy();
this.timeAxis2 = null;
}
}
if ('hiddenDates' in this.options) { if ('hiddenDates' in this.options) {
DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates);
} }
@ -219,11 +242,6 @@ Core.prototype.setOptions = function (options) {
component.setOptions(options); component.setOptions(options);
}); });
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
// redraw everything // redraw everything
this._redraw(); this._redraw();
}; };

+ 2
- 1
lib/timeline/Timeline.js View File

@ -38,7 +38,7 @@ function Timeline (container, items, groups, options) {
autoResize: true, autoResize: true,
orientation: 'bottom',
orientation: 'bottom', // 'bottom', 'top', or 'both'
width: null, width: null,
height: null, height: null,
maxHeight: null, maxHeight: null,
@ -83,6 +83,7 @@ function Timeline (container, items, groups, options) {
// time axis // time axis
this.timeAxis = new TimeAxis(this.body); this.timeAxis = new TimeAxis(this.body);
this.timeAxis2 = null; // used in case of orientation option 'both'
this.components.push(this.timeAxis); this.components.push(this.timeAxis);
// current time bar // current time bar

+ 35
- 6
lib/timeline/component/Group.js View File

@ -148,8 +148,6 @@ Group.prototype.getLabelWidth = function() {
Group.prototype.redraw = function(range, margin, restack) { Group.prototype.redraw = function(range, margin, restack) {
var resized = false; var resized = false;
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
// force recalculation of the height of the items when the marker height changed // force recalculation of the height of the items when the marker height changed
// (due to the Timeline being attached to the DOM or changed from display:none to visible) // (due to the Timeline being attached to the DOM or changed from display:none to visible)
var markerHeight = this.dom.marker.clientHeight; var markerHeight = this.dom.marker.clientHeight;
@ -165,11 +163,42 @@ Group.prototype.redraw = function(range, margin, restack) {
} }
// reposition visible items vertically // reposition visible items vertically
if (this.itemSet.options.stack) { // TODO: ugly way to access options...
stack.stack(this.visibleItems, margin, restack);
if (typeof this.itemSet.options.order === 'function') {
// a custom order function
if (restack) {
// brute force restack of all items
// show all items
var me = this;
var limitSize = false;
util.forEach(this.items, function (item) {
if (!item.displayed) {
item.redraw();
me.visibleItems.push(item);
}
item.repositionX(limitSize);
});
// order all items and force a restacking
var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) {
return me.itemSet.options.order(a.data, b.data);
});
stack.stack(customOrderedItems, margin, true /* restack=true */);
}
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
} }
else { // no stacking
stack.nostack(this.visibleItems, margin, this.subgroups);
else {
// no custom order function, lazy stacking
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
if (this.itemSet.options.stack) { // TODO: ugly way to access options...
stack.stack(this.visibleItems, margin, restack);
}
else { // no stacking
stack.nostack(this.visibleItems, margin, this.subgroups);
}
} }
// recalculate the height of the group // recalculate the height of the group

+ 107
- 36
lib/timeline/component/ItemSet.js View File

@ -274,7 +274,7 @@ ItemSet.prototype._create = function(){
ItemSet.prototype.setOptions = function(options) { ItemSet.prototype.setOptions = function(options) {
if (options) { if (options) {
// copy all options that we know // copy all options that we know
var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap'];
var fields = ['type', 'align', 'orientation', 'order', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap'];
util.selectiveExtend(fields, this.options, options); util.selectiveExtend(fields, this.options, options);
if ('margin' in options) { if ('margin' in options) {
@ -1162,6 +1162,54 @@ ItemSet.prototype._onDragStart = function (event) {
event.stopPropagation(); event.stopPropagation();
} }
else if (this.options.editable.add && event.gesture.srcEvent.ctrlKey) {
// create a new range item when dragging with ctrl key down
this._onDragStartAddItem(event);
}
};
/**
* Start creating a new range item by dragging.
* @param {Event} event
* @private
*/
ItemSet.prototype._onDragStartAddItem = function (event) {
var snap = this.options.snap || null;
var xAbs = util.getAbsoluteLeft(this.dom.frame);
var x = event.gesture.center.pageX - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px
var time = this.body.util.toTime(x);
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
var start = snap ? snap(time, scale, step) : start;
var end = start;
var itemData = {
type: 'range',
start: start,
end: end,
content: 'new item'
};
var id = util.randomUUID();
itemData[this.itemsData._fieldId] = id;
var group = this.groupFromTarget(event);
if (group) {
itemData.group = group.groupId;
}
var newItem = new RangeItem(itemData, this.conversion, this.options);
newItem.id = id; // TODO: not so nice setting id afterwards
this._addItem(newItem);
var props = {
item: newItem,
end: end.valueOf(),
initialX: event.gesture.center.clientX
};
this.touchParams.itemProps = [props];
event.stopPropagation();
}; };
/** /**
@ -1229,8 +1277,15 @@ ItemSet.prototype._onDrag = function (event) {
*/ */
ItemSet.prototype._updateItemProps = function(item, props) { ItemSet.prototype._updateItemProps = function(item, props) {
// TODO: copy all properties from props to item? (also new ones) // TODO: copy all properties from props to item? (also new ones)
if ('start' in props) item.data.start = props.start;
if ('end' in props) item.data.end = props.end;
if ('start' in props) {
item.data.start = props.start;
}
if ('end' in props) {
item.data.end = props.end;
}
else if ('duration' in props) {
item.data.end = new Date(props.start.valueOf() + props.duration);
}
if ('group' in props && item.data.group != props.group) { if ('group' in props && item.data.group != props.group) {
this._moveToGroup(item, props.group) this._moveToGroup(item, props.group)
} }
@ -1265,49 +1320,65 @@ ItemSet.prototype._onDragEnd = function (event) {
if (this.touchParams.itemProps) { if (this.touchParams.itemProps) {
// prepare a change set for the changed items // prepare a change set for the changed items
var changes = [],
me = this,
dataset = this.itemsData.getDataSet();
var changes = [];
var me = this;
var dataset = this.itemsData.getDataSet();
var itemProps = this.touchParams.itemProps ; var itemProps = this.touchParams.itemProps ;
this.touchParams.itemProps = null; this.touchParams.itemProps = null;
itemProps.forEach(function (props) { itemProps.forEach(function (props) {
var id = props.item.id,
itemData = me.itemsData.get(id, me.itemOptions);
var changed = false;
if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf());
itemData.start = util.convert(props.item.data.start,
dataset._options.type && dataset._options.type.start || 'Date');
}
if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf());
itemData.end = util.convert(props.item.data.end,
dataset._options.type && dataset._options.type.end || 'Date');
}
if ('group' in props.item.data) {
changed = changed || (props.group != props.item.data.group);
itemData.group = props.item.data.group;
}
var id = props.item.id;
var itemData = me.itemsData.get(id, me.itemOptions);
// only apply changes when start or end is actually changed
if (changed) {
me.options.onMove(itemData, function (itemData) {
if (!itemData) {
// add a new item
me.options.onAdd(props.item.data, function (itemData) {
me._removeItem(props.item); // remove temporary item
if (itemData) { if (itemData) {
// apply changes
itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
me.itemsData.getDataSet().add(itemData);
} }
else {
// restore original values
me._updateItemProps(props.item, props);
me.stackDirty = true; // force re-stacking of all items next redraw
me.body.emitter.emit('change');
}
// force re-stacking of all items next redraw
me.stackDirty = true;
me.body.emitter.emit('change');
}); });
} }
else {
// update existing item
var changed = false;
if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf());
itemData.start = util.convert(props.item.data.start,
dataset._options.type && dataset._options.type.start || 'Date');
}
if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf());
itemData.end = util.convert(props.item.data.end,
dataset._options.type && dataset._options.type.end || 'Date');
}
if ('group' in props.item.data) {
changed = changed || (props.group != props.item.data.group);
itemData.group = props.item.data.group;
}
// only apply changes when start or end is actually changed
if (changed) {
me.options.onMove(itemData, function (itemData) {
if (itemData) {
// apply changes
itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
}
else {
// restore original values
me._updateItemProps(props.item, props);
me.stackDirty = true; // force re-stacking of all items next redraw
me.body.emitter.emit('change');
}
});
}
}
}); });
// apply the changes to the data (if there are changes) // apply the changes to the data (if there are changes)

+ 2
- 0
lib/timeline/component/css/item.css View File

@ -99,6 +99,7 @@
.vis.timeline .item.range .drag-left { .vis.timeline .item.range .drag-left {
position: absolute; position: absolute;
width: 24px; width: 24px;
max-width: 20%;
height: 100%; height: 100%;
top: 0; top: 0;
left: -4px; left: -4px;
@ -109,6 +110,7 @@
.vis.timeline .item.range .drag-right { .vis.timeline .item.range .drag-right {
position: absolute; position: absolute;
width: 24px; width: 24px;
max-width: 20%;
height: 100%; height: 100%;
top: 0; top: 0;
right: -4px; right: -4px;

+ 10
- 3
lib/timeline/component/item/BackgroundItem.js View File

@ -147,6 +147,8 @@ BackgroundItem.prototype.repositionY = function(margin) {
// special positioning for subgroups // special positioning for subgroups
if (this.data.subgroup !== undefined) { if (this.data.subgroup !== undefined) {
// TODO: instead of calculating the top position of the subgroups here for every BackgroundItem, calculate the top of the subgroup once in Itemset
var itemSubgroup = this.data.subgroup; var itemSubgroup = this.data.subgroup;
var subgroups = this.parent.subgroups; var subgroups = this.parent.subgroups;
var subgroupIndex = subgroups[itemSubgroup].index; var subgroupIndex = subgroups[itemSubgroup].index;
@ -172,15 +174,20 @@ BackgroundItem.prototype.repositionY = function(margin) {
// and when the orientation is bottom: // and when the orientation is bottom:
else { else {
var newTop = this.parent.top; var newTop = this.parent.top;
var totalHeight = 0;
for (var subgroup in subgroups) { for (var subgroup in subgroups) {
if (subgroups.hasOwnProperty(subgroup)) { if (subgroups.hasOwnProperty(subgroup)) {
if (subgroups[subgroup].visible == true && subgroups[subgroup].index > subgroupIndex) {
newTop += subgroups[subgroup].height + margin.item.vertical;
if (subgroups[subgroup].visible == true) {
var newHeight = subgroups[subgroup].height + margin.item.vertical;
totalHeight += newHeight;
if (subgroups[subgroup].index > subgroupIndex) {
newTop += newHeight;
}
} }
} }
} }
height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical;
this.dom.box.style.top = newTop + 'px';
this.dom.box.style.top = (this.parent.height - totalHeight + newTop) + 'px';
this.dom.box.style.bottom = ''; this.dom.box.style.bottom = '';
} }
} }

+ 3
- 9
lib/timeline/component/item/BoxItem.js View File

@ -151,9 +151,6 @@ BoxItem.prototype.hide = function() {
if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
this.top = null;
this.left = null;
this.displayed = false; this.displayed = false;
} }
}; };
@ -166,9 +163,6 @@ BoxItem.prototype.repositionX = function() {
var start = this.conversion.toScreen(this.data.start); var start = this.conversion.toScreen(this.data.start);
var align = this.options.align; var align = this.options.align;
var left; var left;
var box = this.dom.box;
var line = this.dom.line;
var dot = this.dom.dot;
// calculate left position of the box // calculate left position of the box
if (align == 'right') { if (align == 'right') {
@ -183,13 +177,13 @@ BoxItem.prototype.repositionX = function() {
} }
// reposition box // reposition box
box.style.left = this.left + 'px';
this.dom.box.style.left = this.left + 'px';
// reposition line // reposition line
line.style.left = (start - this.props.line.width / 2) + 'px';
this.dom.line.style.left = (start - this.props.line.width / 2) + 'px';
// reposition dot // reposition dot
dot.style.left = (start - this.props.dot.width / 2) + 'px';
this.dom.dot.style.left = (start - this.props.dot.width / 2) + 'px';
}; };
/** /**

+ 0
- 3
lib/timeline/component/item/PointItem.js View File

@ -144,9 +144,6 @@ PointItem.prototype.hide = function() {
this.dom.point.parentNode.removeChild(this.dom.point); this.dom.point.parentNode.removeChild(this.dom.point);
} }
this.top = null;
this.left = null;
this.displayed = false; this.displayed = false;
} }
}; };

+ 14
- 10
lib/timeline/component/item/RangeItem.js View File

@ -140,30 +140,34 @@ RangeItem.prototype.hide = function() {
box.parentNode.removeChild(box); box.parentNode.removeChild(box);
} }
this.top = null;
this.left = null;
this.displayed = false; this.displayed = false;
} }
}; };
/** /**
* Reposition the item horizontally * Reposition the item horizontally
* @param {boolean} [limitSize=true] If true (default), the width of the range
* item will be limited, as the browser cannot
* display very wide divs. This means though
* that the applied left and width may
* not correspond to the ranges start and end
* @Override * @Override
*/ */
RangeItem.prototype.repositionX = function() {
RangeItem.prototype.repositionX = function(limitSize) {
var parentWidth = this.parent.width; var parentWidth = this.parent.width;
var start = this.conversion.toScreen(this.data.start); var start = this.conversion.toScreen(this.data.start);
var end = this.conversion.toScreen(this.data.end); var end = this.conversion.toScreen(this.data.end);
var contentLeft; var contentLeft;
var contentWidth; var contentWidth;
// limit the width of the this, as browsers cannot draw very wide divs
if (start < -parentWidth) {
start = -parentWidth;
}
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
// limit the width of the range, as browsers cannot draw very wide divs
if (limitSize === undefined || limitSize === true) {
if (start < -parentWidth) {
start = -parentWidth;
}
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
}
} }
var boxWidth = Math.max(end - start, 1); var boxWidth = Math.max(end - start, 1);

+ 45
- 1
test/DataView.test.js View File

@ -146,5 +146,49 @@ describe('DataView', function () {
assert.deepEqual(added, [2, 3]); assert.deepEqual(added, [2, 3]);
assert.deepEqual(updated, []); assert.deepEqual(updated, []);
assert.deepEqual(removed, []); assert.deepEqual(removed, []);
})
});
it('should pass data of changed items when updating a DataSet', function () {
var data = new DataSet([
{id: 1, title: 'Item 1', group: 1},
{id: 2, title: 'Item 2', group: 2},
{id: 3, title: 'Item 3', group: 2}
]);
var view = new DataView(data, {
filter: function (item) {
return item.group === 2;
}
});
var dataUpdates = [];
var viewUpdates = [];
data.on('update', function (event, properties, senderId) {
dataUpdates.push([event, properties]);
});
view.on('update', function (event, properties, senderId) {
viewUpdates.push([event, properties]);
});
// make a change not affecting the DataView
data.update({id: 1, title: 'Item 1 (changed)'});
assert.deepEqual(dataUpdates, [
['update', {items: [1], data: [{id: 1, title: 'Item 1 (changed)'}]}]
]);
assert.deepEqual(viewUpdates, []);
// make a change affecting the DataView
data.update({id: 2, title: 'Item 2 (changed)'});
assert.deepEqual(dataUpdates, [
['update', {items: [1], data: [{id: 1, title: 'Item 1 (changed)'}]}],
['update', {items: [2], data: [{id: 2, title: 'Item 2 (changed)'}]}]
]);
assert.deepEqual(viewUpdates, [
['update', {items: [2], data: [{id: 2, title: 'Item 2 (changed)'}]}]
]);
});
}); });

+ 1
- 0
test/timeline.html View File

@ -102,6 +102,7 @@
var options = { var options = {
editable: true, editable: true,
//orientation: 'top', //orientation: 'top',
orientation: 'both',
start: now.clone().add(-7, 'days'), start: now.clone().add(-7, 'days'),
end: now.clone().add(7, 'days'), end: now.clone().add(7, 'days'),
//maxHeight: 200, //maxHeight: 200,

Loading…
Cancel
Save