Browse Source

feat: #2300 Added nested groups (#2416)

* Fix groupEditable
* Fix group drag in vertical direction bug
* Fix indentation
* Add initial nestedGroups property
* Fix redraw order
* Get nested groups hiding working
* Add order to nestedGroups
* Fix up example
* Add support for intially closed nested groups
* Add nested groups indentation
* Fix indents
* Add nested groups docs
* Fix example
* Revert indents
* Add classes to nested and nesting group labels and add nesting group css
* Use pure css for nested group icons
* Remove commented off lines and unnecessary btn
* Fix indents
* Add support to showNested:false in group
* Add semicolon
* Remove empty lines
readme-improvements
yotamberk 7 years ago
committed by Alexander Wunschik
parent
commit
8ab1d183cb
8 changed files with 301 additions and 26 deletions
  1. +13
    -1
      docs/timeline/index.html
  2. +113
    -0
      examples/timeline/groups/nestedGroups.html
  3. +1
    -2
      lib/timeline/Core.js
  4. +4
    -2
      lib/timeline/Timeline.js
  5. +40
    -4
      lib/timeline/component/Group.js
  6. +109
    -17
      lib/timeline/component/ItemSet.js
  7. +20
    -0
      lib/timeline/component/css/itemset.css
  8. +1
    -0
      lib/timeline/component/item/Item.js

+ 13
- 1
docs/timeline/index.html View File

@ -382,7 +382,7 @@ var groups = [
{
id: 1,
content: 'Group 1'
// Optional: a field 'className', 'style'
// Optional: a field 'className', 'style', 'order', [properties]
}
// more groups...
]);
@ -461,6 +461,18 @@ var groups = [
<td>no</td>
<td>Provides a means to toggle the whether a group is displayed or not. Defaults to <code>true</code>.</td>
</tr>
<tr>
<td>nestedGroups</td>
<td>Array</td>
<td>no</td>
<td>Array of group ids nested in the group. Nested groups will appear under this nesting group.</td>
</tr>
<tr>
<td>showNestedGroups</td>
<td>Boolean</td>
<td>no</td>
<td>Assuming the group has nested groups, this will set the initial state of the group - shown or collapsed. The <code>showNestedGroups</code> is defaulted to <code>true</code>.</td>
</tr>
</table>

+ 113
- 0
examples/timeline/groups/nestedGroups.html View File

@ -0,0 +1,113 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Nested Groups example</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
</style>
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.4/moment.min.js"></script>
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
This example demonstrate using groups. Note that a DataSet is used for both
items and groups, allowing to dynamically add, update or remove both items
and groups via the DataSet.
</p>
<div id="visualization"></div>
<script>
var now = moment().minutes(0).seconds(0).milliseconds(0);
var itemCount = 60;
// create a data set with groups
var groups = new vis.DataSet();
groups.add([
{
id: 1,
content: "Lee",
nestedGroups: [11,12,13]
},
{
id: 2,
content: "invisible group",
visible: false
},
{
id: 3,
content: "John",
nestedGroups: [14],
showNested: false
},
{
id: 4,
content: "Alson"
},
]);
groups.add([
{
id: 11,
content: "cook",
},
{
id: 12,
content: "shop",
},
{
id: 13,
content: "clean house",
},
{
id: 14,
content: "wash dishes",
}
]);
// create a dataset with items
var items = new vis.DataSet();
var groupIds = groups.getIds();
var types = [ 'box', 'point', 'range', 'background']
for (var i = 0; i < itemCount; i++) {
var start = now.clone().add(Math.random() * 200, 'hours');
var end = start.clone().add(2, 'hours');
var randomGroupId = groupIds[Math.floor(Math.random() * groupIds.length)];
var type = types[Math.floor(4 * Math.random())]
items.add({
id: i,
group: randomGroupId,
content: 'item ' + i,
start: start,
end: end,
type: type
});
}
// create visualization
var container = document.getElementById('visualization');
var options = {
groupOrder: 'content' // groupOrder can be a property name or a sorting function
};
var timeline = new vis.Timeline(container, items, groups, options);
</script>
</body>
</html>

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

@ -119,10 +119,9 @@ Core.prototype._create = function (container) {
this.on('panmove', this._onDrag.bind(this));
var me = this;
this._origRedraw = this._redraw.bind(this);
this._redraw = util.throttle(this._origRedraw);
this.on('_change', function (properties) {
if (me.itemSet && me.itemSet.initialItemSetDrawn && properties && properties.queue == true) {
me._redraw()

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

@ -434,8 +434,10 @@ Timeline.prototype.getItemRange = function () {
// calculate the date of the left side and right side of the items given
util.forEach(this.itemSet.items, function (item) {
item.show();
item.repositionX();
if (item.groupShowing) {
item.show();
item.repositionX();
}
var start = getStart(item);
var end = getEnd(item);

+ 40
- 4
lib/timeline/component/Group.js View File

@ -15,6 +15,17 @@ function Group (groupId, data, itemSet) {
this.subgroupOrderer = data && data.subgroupOrder;
this.itemSet = itemSet;
this.isVisible = null;
if (data && data.nestedGroups) {
this.nestedGroups = data.nestedGroups;
if (data.showNested == false) {
this.showNested = false;
} else {
this.showNested = true;
}
}
this.nestedInGroup = null;
this.dom = {};
this.props = {
@ -50,9 +61,9 @@ function Group (groupId, data, itemSet) {
Group.prototype._create = function() {
var label = document.createElement('div');
if (this.itemSet.options.groupEditable.order) {
label.className = 'vis-label draggable';
label.className = 'vis-label draggable';
} else {
label.className = 'vis-label';
label.className = 'vis-label';
}
this.dom.label = label;
@ -113,7 +124,6 @@ Group.prototype.setData = function(data) {
// update title
this.dom.label.title = data && data.title || '';
if (!this.dom.inner.firstChild) {
util.addClassName(this.dom.inner, 'vis-hidden');
}
@ -121,6 +131,33 @@ Group.prototype.setData = function(data) {
util.removeClassName(this.dom.inner, 'vis-hidden');
}
if (data && data.nestedGroups) {
if (data.showNested == false) {
this.showNested = false;
} else {
this.showNested = true;
}
util.addClassName(this.dom.label, 'vis-nesting-group');
if (this.showNested) {
util.removeClassName(this.dom.label, 'collapsed');
util.addClassName(this.dom.label, 'expanded');
} else {
util.removeClassName(this.dom.label, 'expanded');
var collapsedDirClassName = this.itemSet.options.rtl ? 'collapsed-rtl' : 'collapsed'
util.addClassName(this.dom.label, collapsedDirClassName);
}
}
if (data && data.nestedInGroup) {
util.addClassName(this.dom.label, 'vis-nested-group');
if (this.itemSet.options && this.itemSet.options.rtl) {
this.dom.inner.style.paddingRight = '30px';
} else {
this.dom.inner.style.paddingLeft = '30px';
}
}
// update className
var className = data && data.className || null;
if (className != this.className) {
@ -172,7 +209,6 @@ Group.prototype.redraw = function(range, margin, restack) {
var markerHeight = this.dom.marker.clientHeight;
if (markerHeight != this.lastMarkerHeight) {
this.lastMarkerHeight = markerHeight;
util.forEach(this.items, function (item) {
item.dirty = true;
if (item.displayed) item.redraw();

+ 109
- 17
lib/timeline/component/ItemSet.js View File

@ -240,6 +240,7 @@ ItemSet.prototype._create = function(){
this.groupHammer = new Hammer(this.body.dom.leftContainer);
}
this.groupHammer.on('tap', this._onGroupClick.bind(this));
this.groupHammer.on('panstart', this._onGroupDragStart.bind(this));
this.groupHammer.on('panmove', this._onGroupDrag.bind(this));
this.groupHammer.on('panend', this._onGroupDragEnd.bind(this));
@ -812,6 +813,26 @@ ItemSet.prototype.setGroups = function(groups) {
}
if (this.groupsData) {
// go over all groups nesting
var groupsData = this.groupsData;
if (this.groupsData instanceof DataView) {
groupsData = this.groupsData.getDataSet()
}
groupsData.get().forEach(function(group){
if (group.nestedGroups) {
group.nestedGroups.forEach(function(nestedGroupId) {
var updatedNestedGroup = groupsData.get(nestedGroupId);
updatedNestedGroup.nestedInGroup = group.id;
if (group.showNested == false) {
updatedNestedGroup.visible = false;
}
groupsData.update(updatedNestedGroup);
})
}
})
// subscribe to new dataset
var id = this.id;
util.forEach(this.groupListeners, function (callback, event) {
@ -904,7 +925,7 @@ ItemSet.prototype._onUpdate = function(ids) {
var selected;
if (item) {
// update item
// update item
if (!constructor || !(item instanceof constructor)) {
// item type has changed, delete the item and recreate it
selected = item.selected; // preserve selection of this item
@ -921,6 +942,7 @@ ItemSet.prototype._onUpdate = function(ids) {
if (constructor) {
item = new constructor(itemData, me.conversion, me.options);
item.id = id; // TODO: not so nice setting id afterwards
me._addItem(item);
if (selected) {
this.selection.push(id);
@ -1076,6 +1098,8 @@ ItemSet.prototype._orderGroups = function () {
order: this.options.groupOrder
});
groupIds = this._orderNestedGroups(groupIds);
var changed = !util.equalArray(groupIds, this.groupIds);
if (changed) {
// hide all groups, removes them from the DOM
@ -1099,6 +1123,33 @@ ItemSet.prototype._orderGroups = function () {
}
};
/**
* Reorder the nested groups
* @return {boolean} changed
* @private
*/
ItemSet.prototype._orderNestedGroups = function(groupIds) {
var newGroupIdsOrder = [];
groupIds.forEach(function(groupId){
var groupData = this.groupsData.get(groupId);
if (!groupData.nestedInGroup) {
newGroupIdsOrder.push(groupId)
}
if (groupData.nestedGroups) {
var nestedGroups = this.groupsData.get({
filter: function(nestedGroup) {
return nestedGroup.nestedInGroup == groupId;
}
});
var nestedGroupIds = nestedGroups.map(function(nestedGroup) { return nestedGroup.id })
newGroupIdsOrder = newGroupIdsOrder.concat(nestedGroupIds);
}
}, this)
return newGroupIdsOrder;
}
/**
* Add a new item
* @param {Item} item
@ -1110,6 +1161,13 @@ ItemSet.prototype._addItem = function(item) {
// add to group
var groupId = this._getGroupId(item.data);
var group = this.groups[groupId];
if (!group) {
item.groupShowing = false;
} else if (group && group.data && group.data.showNested) {
item.groupShowing = true;
}
if (group) group.add(item);
};
@ -1126,13 +1184,17 @@ ItemSet.prototype._updateItem = function(item, itemData) {
// update the items data (will redraw the item when displayed)
item.setData(itemData);
var groupId = this._getGroupId(item.data);
var group = this.groups[groupId];
if (!group) {
item.groupShowing = false;
} else if (group && group.data && group.data.showNested) {
item.groupShowing = true;
}
// update group
if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) {
var oldGroup = this.groups[oldGroupId];
if (oldGroup) oldGroup.remove(item);
var groupId = this._getGroupId(item.data);
var group = this.groups[groupId];
if (group) group.add(item);
}
};
@ -1561,6 +1623,35 @@ ItemSet.prototype._onDragEnd = function (event) {
}
};
ItemSet.prototype._onGroupClick = function (event) {
var group = this.groupFromTarget(event);
if (!group.nestedGroups) return;
var groupsData = this.groupsData;
if (this.groupsData instanceof DataView) {
groupsData = this.groupsData.getDataSet()
}
group.showNested = !group.showNested;
var nestedGroups = groupsData.get(group.nestedGroups).map(function(nestedGroup) {
if (nestedGroup.visible == undefined) { nestedGroup.visible = true; }
nestedGroup.visible = !!group.showNested;
return nestedGroup;
});
groupsData.update(nestedGroups);
if (group.showNested) {
util.removeClassName(group.dom.label, 'collapsed');
util.addClassName(group.dom.label, 'expanded');
} else {
util.removeClassName(group.dom.label, 'expanded');
var collapsedDirClassName = this.options.rtl ? 'collapsed-rtl' : 'collapsed'
util.addClassName(group.dom.label, collapsedDirClassName);
}
}
ItemSet.prototype._onGroupDragStart = function (event) {
if (this.options.groupEditable.order) {
this.groupTouchParams.group = this.groupFromTarget(event);
@ -1612,15 +1703,16 @@ ItemSet.prototype._onGroupDrag = function (event) {
// switch groups
if (draggedGroup && targetGroup) {
this.options.groupOrderSwap(draggedGroup, targetGroup, this.groupsData);
groupsData.update(draggedGroup);
groupsData.update(targetGroup);
this.options.groupOrderSwap(draggedGroup, targetGroup, groupsData);
groupsData.update(draggedGroup);
groupsData.update(targetGroup);
}
// fetch current order of groups
var newOrder = groupsData.getIds({
order: this.options.groupOrder
});
order: this.options.groupOrder
});
// in case of changes since _onGroupDragStart
if (!util.equalArray(newOrder, this.groupTouchParams.originalOrder)) {
@ -1715,14 +1807,14 @@ ItemSet.prototype._onGroupDragEnd = function (event) {
break;
}
// found a group that has the wrong position -> switch with the
// group at the position where other one should be, fix index arrays and continue
var slippedPosition = newOrder.indexOf(origOrder[curPos])
var switchGroup = dataset.get(newOrder[curPos]);
var shouldBeGroup = dataset.get(origOrder[curPos]);
me.options.groupOrderSwap(switchGroup, shouldBeGroup, dataset);
groupsData.update(switchGroup);
groupsData.update(shouldBeGroup);
// found a group that has the wrong position -> switch with the
// group at the position where other one should be, fix index arrays and continue
var slippedPosition = newOrder.indexOf(origOrder[curPos])
var switchGroup = dataset.get(newOrder[curPos]);
var shouldBeGroup = dataset.get(origOrder[curPos]);
me.options.groupOrderSwap(switchGroup, shouldBeGroup, dataset);
groupsData.update(switchGroup);
groupsData.update(shouldBeGroup);
var switchGroupId = newOrder[curPos];
newOrder[curPos] = origOrder[curPos];

+ 20
- 0
lib/timeline/component/css/itemset.css View File

@ -33,6 +33,26 @@
border-bottom: none;
}
.vis-nesting-group {
cursor: pointer;
}
.vis-nested-group {
background: #f5f5f5;
}
.vis-label.vis-nesting-group.expanded:before {
content: "\25BC";
}
.vis-label.vis-nesting-group.collapsed-rtl:before {
content: "\25C0";
}
.vis-label.vis-nesting-group.collapsed:before {
content: "\25B6";
}
.vis-overlay {
position: absolute;
top: 0;

+ 1
- 0
lib/timeline/component/item/Item.js View File

@ -21,6 +21,7 @@ function Item (data, conversion, options) {
this.options = options || {};
this.selected = false;
this.displayed = false;
this.groupShowing = true;
this.dirty = true;
this.top = null;

Loading…
Cancel
Save