From 8ab1d183cbbcaffbcc195912c1b03f626bb77c17 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Thu, 29 Dec 2016 16:23:32 +0200 Subject: [PATCH] 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 --- docs/timeline/index.html | 14 ++- examples/timeline/groups/nestedGroups.html | 113 ++++++++++++++++++ lib/timeline/Core.js | 3 +- lib/timeline/Timeline.js | 6 +- lib/timeline/component/Group.js | 44 ++++++- lib/timeline/component/ItemSet.js | 126 ++++++++++++++++++--- lib/timeline/component/css/itemset.css | 20 ++++ lib/timeline/component/item/Item.js | 1 + 8 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 examples/timeline/groups/nestedGroups.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index e0d3291c..f8717d96 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -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 = [ no Provides a means to toggle the whether a group is displayed or not. Defaults to true. + + nestedGroups + Array + no + Array of group ids nested in the group. Nested groups will appear under this nesting group. + + + showNestedGroups + Boolean + no + Assuming the group has nested groups, this will set the initial state of the group - shown or collapsed. The showNestedGroups is defaulted to true. + diff --git a/examples/timeline/groups/nestedGroups.html b/examples/timeline/groups/nestedGroups.html new file mode 100644 index 00000000..f8abfb1a --- /dev/null +++ b/examples/timeline/groups/nestedGroups.html @@ -0,0 +1,113 @@ + + + + Timeline | Nested Groups example + + + + + + + + + + +

+ 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. +

+
+ + + + \ No newline at end of file diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index d3376e1e..0d42edf5 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -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() diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 0d4b62e3..20f708c4 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -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); diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 95b3e016..b2190f43 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -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(); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 24ba15ce..4041f16b 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -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]; diff --git a/lib/timeline/component/css/itemset.css b/lib/timeline/component/css/itemset.css index 45af6a30..3e9578f9 100644 --- a/lib/timeline/component/css/itemset.css +++ b/lib/timeline/component/css/itemset.css @@ -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; diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index a7522921..b8f9fe60 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -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;