Browse Source

Inserted some more group functionality to ItemSet

css_transitions
jos 10 years ago
parent
commit
efd861003f
4 changed files with 267 additions and 199 deletions
  1. +21
    -8
      src/timeline/Timeline.js
  2. +15
    -174
      src/timeline/component/Group.js
  3. +230
    -17
      src/timeline/component/ItemSet.js
  4. +1
    -0
      src/timeline/component/item/Item.js

+ 21
- 8
src/timeline/Timeline.js View File

@ -368,17 +368,17 @@ Timeline.prototype.setItems = function(items) {
if (!items) { if (!items) {
newDataSet = null; newDataSet = null;
} }
else if (items instanceof DataSet) {
else if (items instanceof DataSet || items instanceof DataView) {
newDataSet = items; newDataSet = items;
} }
if (!(items instanceof DataSet)) {
newDataSet = new DataSet({
else {
// turn an array into a dataset
newDataSet = new DataSet(items, {
convert: { convert: {
start: 'Date', start: 'Date',
end: 'Date' end: 'Date'
} }
}); });
newDataSet.add(items);
} }
// set items // set items
@ -432,11 +432,24 @@ Timeline.prototype.setItems = function(items) {
/** /**
* Set groups * Set groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/ */
Timeline.prototype.setGroups = function(groupSet) {
this.groupsData = groupSet;
this.itemSet.setGroups(groupSet);
Timeline.prototype.setGroups = function(groups) {
// convert to type DataSet when needed
var newDataSet;
if (!groups) {
newDataSet = null;
}
else if (groups instanceof DataSet || groups instanceof DataView) {
newDataSet = groups;
}
else {
// turn an array into a dataset
newDataSet = new DataSet(groups);
}
this.groupsData = newDataSet;
this.itemSet.setGroups(newDataSet);
}; };
/** /**

+ 15
- 174
src/timeline/component/Group.js View File

@ -1,48 +1,16 @@
/** /**
* @constructor Group * @constructor Group
* @param {Panel} groupPanel
* @param {Panel} labelPanel
* @param {Panel} backgroundPanel
* @param {Panel} axisPanel
* @param {Number | String} groupId * @param {Number | String} groupId
* @param {Object} [options] Options to set initial property values
* // TODO: describe available options
* @extends Component
*/ */
function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
this.id = util.randomUUID();
this.groupPanel = groupPanel;
this.labelPanel = labelPanel;
this.backgroundPanel = backgroundPanel;
this.axisPanel = axisPanel;
function Group (groupId) {
this.groupId = groupId; this.groupId = groupId;
this.itemSet = null; // ItemSet
this.options = options || {};
this.options.top = 0;
this.props = {
label: {
width: 0,
height: 0
}
};
this.dom = {}; this.dom = {};
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
this.items = {}; // items filtered by groupId of this group
this._create(); this._create();
} }
Group.prototype = new Component();
// TODO: comment
Group.prototype.setOptions = Component.prototype.setOptions;
/** /**
* Create DOM elements for the group * Create DOM elements for the group
* @private * @private
@ -56,158 +24,31 @@ Group.prototype._create = function() {
inner.className = 'inner'; inner.className = 'inner';
label.appendChild(inner); label.appendChild(inner);
this.dom.inner = inner; this.dom.inner = inner;
};
/**
* Set the group data for this group
* @param {Object} data Group data, can contain properties content and className
*/
Group.prototype.setData = function setData(data) {
// update contents
var content = data && data.content;
if (content instanceof Element) {
this.dom.inner.appendChild(content);
}
else if (content != undefined) {
this.dom.inner.innerHTML = content;
}
else {
this.dom.inner.innerHTML = this.groupId;
}
// update className
var className = data && data.className;
if (className) {
util.addClassName(this.dom.label, className);
}
};
/**
* Set item set for the group. The group will create a view on the itemSet,
* filtered by the groups id.
* @param {DataSet | DataView} itemsData
*/
Group.prototype.setItems = function setItems(itemsData) {
if (this.itemSet) {
// remove current item set
this.itemSet.setItems();
this.itemSet.hide();
this.groupPanel.frame.removeChild(this.itemSet.getFrame());
this.itemSet = null;
}
if (itemsData) {
var groupId = this.groupId;
var me = this;
var itemSetOptions = util.extend(this.options, {
height: function () {
// FIXME: setting height doesn't yet work
return Math.max(me.props.label.height, me.itemSet.height);
}
});
this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
this.itemSet.parent = this;
this.groupPanel.frame.appendChild(this.itemSet.getFrame());
if (this.range) this.itemSet.setRange(this.range);
this.view = new DataView(itemsData, {
filter: function (item) {
return item.group == groupId;
}
});
this.itemSet.setItems(this.view);
}
};
/**
* hide the group, detach from DOM if needed
*/
Group.prototype.show = function show() {
if (!this.dom.label.parentNode) {
this.labelPanel.frame.appendChild(this.dom.label);
}
var itemSetFrame = this.itemSet && this.itemSet.getFrame();
if (itemSetFrame) {
if (itemSetFrame.parentNode) {
itemSetFrame.parentNode.removeChild(itemSetFrame);
}
this.groupPanel.frame.appendChild(itemSetFrame);
this.itemSet.show();
}
this.dom.group = document.createElement('div');
}; };
/** /**
* hide the group, detach from DOM if needed
*/
Group.prototype.hide = function hide() {
if (this.dom.label.parentNode) {
this.dom.label.parentNode.removeChild(this.dom.label);
}
if (this.itemSet) {
this.itemSet.hide();
}
var itemSetFrame = this.itemset && this.itemSet.getFrame();
if (itemSetFrame && itemSetFrame.parentNode) {
itemSetFrame.parentNode.removeChild(itemSetFrame);
}
};
/**
* Set range (start and end).
* @param {Range | Object} range A Range or an object containing start and end.
* Repaint the group
* @return {boolean} Returns true if the component is resized
*/ */
Group.prototype.setRange = function (range) {
this.range = range;
if (this.itemSet) this.itemSet.setRange(range);
Group.prototype.repaint = function repaint() {
// TODO: implement Group.repaint
}; };
/** /**
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
* Add an item to the group
* @param {Item} item
*/ */
Group.prototype.setSelection = function setSelection(ids) {
if (this.itemSet) this.itemSet.setSelection(ids);
Group.prototype.add = function add(item) {
this.items[item.id] = item;
}; };
/** /**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
* Remove an item from the group
* @param {Item} item
*/ */
Group.prototype.getSelection = function getSelection() {
return this.itemSet ? this.itemSet.getSelection() : [];
Group.prototype.remove = function remove(item) {
delete this.items[item.id];
}; };
/**
* Repaint the group
* @return {boolean} Returns true if the component is resized
*/
Group.prototype.repaint = function repaint() {
var resized = false;
this.show();
if (this.itemSet) {
resized = this.itemSet.repaint() || resized;
}
// calculate inner size of the label
resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
this.height = this.itemSet ? this.itemSet.height : 0;
this.dom.label.style.height = this.height + 'px';
return resized;
};

+ 230
- 17
src/timeline/component/ItemSet.js View File

@ -27,11 +27,12 @@ function ItemSet(backgroundPanel, axisPanel, options) {
this.hammer = null; this.hammer = null;
var me = this; var me = this;
this.itemsData = null; // DataSet
this.range = null; // Range or Object {start: number, end: number}
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
this.range = null; // Range or Object {start: number, end: number}
// data change listeners
this.listeners = {
// listeners for the DataSet of the items
this.itemListeners = {
'add': function (event, params, senderId) { 'add': function (event, params, senderId) {
if (senderId != me.id) me._onAdd(params.items); if (senderId != me.id) me._onAdd(params.items);
}, },
@ -43,15 +44,30 @@ function ItemSet(backgroundPanel, axisPanel, options) {
} }
}; };
// listeners for the DataSet of the groups
this.groupListeners = {
'add': function (event, params, senderId) {
if (senderId != me.id) me._onAddGroups(params.items);
},
'update': function (event, params, senderId) {
if (senderId != me.id) me._onUpdateGroups(params.items);
},
'remove': function (event, params, senderId) {
if (senderId != me.id) me._onRemoveGroups(params.items);
}
};
this.items = {}; // object with an Item for every data item this.items = {}; // object with an Item for every data item
this.orderedItems = { this.orderedItems = {
byStart: [], byStart: [],
byEnd: [] byEnd: []
}; };
// this.systemLoaded = false;
this.groups = {}; // Group object for every group
this.groupIds = [];
this.visibleItems = []; // visible, ordered items this.visibleItems = []; // visible, ordered items
this.selection = []; // list with the ids of all selected nodes this.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
this.stack = new Stack(Object.create(this.options)); this.stack = new Stack(Object.create(this.options));
this.stackDirty = true; // if true, all items will be restacked on next repaint this.stackDirty = true; // if true, all items will be restacked on next repaint
@ -508,12 +524,12 @@ ItemSet.prototype.setItems = function setItems(items) {
this.itemsData = items; this.itemsData = items;
} }
else { else {
throw new TypeError('Data must be an instance of DataSet');
throw new TypeError('Data must be an instance of DataSet or DataView');
} }
if (oldItemsData) { if (oldItemsData) {
// unsubscribe from old dataset // unsubscribe from old dataset
util.forEach(this.listeners, function (callback, event) {
util.forEach(this.itemListeners, function (callback, event) {
oldItemsData.unsubscribe(event, callback); oldItemsData.unsubscribe(event, callback);
}); });
@ -525,7 +541,7 @@ ItemSet.prototype.setItems = function setItems(items) {
if (this.itemsData) { if (this.itemsData) {
// subscribe to new dataset // subscribe to new dataset
var id = this.id; var id = this.id;
util.forEach(this.listeners, function (callback, event) {
util.forEach(this.itemListeners, function (callback, event) {
me.itemsData.on(event, callback, id); me.itemsData.on(event, callback, id);
}); });
@ -536,13 +552,66 @@ ItemSet.prototype.setItems = function setItems(items) {
}; };
/** /**
* Get the current items items
* Get the current items
* @returns {vis.DataSet | null} * @returns {vis.DataSet | null}
*/ */
ItemSet.prototype.getItems = function getItems() { ItemSet.prototype.getItems = function getItems() {
return this.itemsData; return this.itemsData;
}; };
/**
* Set groups
* @param {vis.DataSet} groups
*/
ItemSet.prototype.setGroups = function setGroups(groups) {
var me = this,
ids;
// unsubscribe from current dataset
if (this.groupsData) {
util.forEach(this.groupListeners, function (callback, event) {
me.groupsData.unsubscribe(event, callback);
});
// remove all drawn groups
ids = this.groupsData.getIds();
this._onRemoveGroups(ids);
}
// replace the dataset
if (!groups) {
this.groupsData = null;
}
else if (groups instanceof DataSet || groups instanceof DataView) {
this.groupsData = groups;
}
else {
throw new TypeError('Data must be an instance of DataSet or DataView');
}
if (this.groupsData) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.groupListeners, function (callback, event) {
me.groupsData.on(event, callback, id);
});
// draw all new groups
ids = this.groupsData.getIds();
this._onAddGroups(ids);
}
this.emit('change');
};
/**
* Get the current groups
* @returns {vis.DataSet | null} groups
*/
ItemSet.prototype.getGroups = function getGroups() {
return this.groupsData;
};
/** /**
* Remove an item by its id * Remove an item by its id
* @param {String | Number} id * @param {String | Number} id
@ -587,12 +656,11 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
// update item // update item
if (!constructor || !(item instanceof constructor)) { if (!constructor || !(item instanceof constructor)) {
// item type has changed, delete the item and recreate it // item type has changed, delete the item and recreate it
me._deleteItem(item);
me._removeItem(item);
item = null; item = null;
} }
else { else {
item.data = itemData; // TODO: create a method item.setData ?
item.repaint();
me._updateItem(item, itemData);
} }
} }
@ -600,14 +668,14 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
// create item // create item
if (constructor) { if (constructor) {
item = new constructor(me, itemData, me.options, itemOptions); item = new constructor(me, itemData, me.options, itemOptions);
item.id = id;
item.id = id; // TODO: not so nice setting id afterwards
me._addItem(item);
} }
else { else {
throw new TypeError('Unknown item type "' + type + '"'); throw new TypeError('Unknown item type "' + type + '"');
} }
} }
me.items[id] = item;
if (type == 'range' && me.visibleItems.indexOf(item) == -1) { if (type == 'range' && me.visibleItems.indexOf(item) == -1) {
me._checkIfVisible(item, me.visibleItems); me._checkIfVisible(item, me.visibleItems);
} }
@ -637,7 +705,7 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
var item = me.items[id]; var item = me.items[id];
if (item) { if (item) {
count++; count++;
me._deleteItem(item);
me._removeItem(item);
} }
}); });
@ -649,13 +717,140 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
} }
}; };
/**
* Handle updated groups
* @param {Number[]} ids
* @private
*/
ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
this._onAddGroups(ids);
};
/**
* Handle changed groups
* @param {Number[]} ids
* @private
*/
ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
var me = this;
ids.forEach(function (id) {
var group = me.groups[id];
if (!group) {
var groupOptions = Object.create(me.options);
util.extend(groupOptions, {
height: null
});
group = new Group(id);
me.groups[id] = group;
// add items with this groupId to the new group
for (var itemId in me.items) {
if (me.items.hasOwnProperty(itemId)) {
var item = me.items[itemId];
if (item.data.group == id) {
group.add(item);
}
}
}
}
});
this._updateGroupIds();
};
/**
* Handle removed groups
* @param {Number[]} ids
* @private
*/
ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
var groups = this.groups;
ids.forEach(function (id) {
var group = groups[id];
if (group) {
/* TODO
group.setItems(); // detach items data
group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
*/
delete groups[id];
}
});
this._updateGroupIds();
};
/**
* Update the groupIds. Requires a repaint afterwards
* @private
*/
ItemSet.prototype._updateGroupIds = function () {
// reorder the groups
this.groupIds = this.groupsData.getIds({
order: this.options.groupOrder
});
/* TODO
// hide the groups now, they will be shown again in the next repaint
// in correct order
var groups = this.groups;
this.groupIds.forEach(function (id) {
groups[id].hide();
});
*/
};
/**
* Add a new item
* @param {Item} item
* @private
*/
ItemSet.prototype._addItem = function _addItem(item) {
this.items[item.id] = item;
// add to group (if any)
if ('group' in item.data) {
var group = this.groups[item.data.group];
if (group) group.add(item);
}
};
/**
* Update an existing item
* @param {Item} item
* @param {Object} itemData
* @private
*/
ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
var oldGroup = item.data.group,
group;
item.data = itemData;
item.repaint();
// update group (if any)
if (oldGroup != item.data.group) {
if (oldGroup) {
group = this.groups[item.data.group];
if (group) group.remove(item);
}
if ('group' in item.data) {
group = this.groups[item.data.group];
if (group) group.add(item);
}
}
};
/** /**
* Delete an item from the ItemSet: remove it from the DOM, from the map * Delete an item from the ItemSet: remove it from the DOM, from the map
* with items, and from the map with visible items, and from the selection * with items, and from the map with visible items, and from the selection
* @param {Item} item * @param {Item} item
* @private * @private
*/ */
ItemSet.prototype._deleteItem = function _deleteItem(item) {
ItemSet.prototype._removeItem = function _removeItem(item) {
// remove from DOM // remove from DOM
item.hide(); item.hide();
@ -669,6 +864,12 @@ ItemSet.prototype._deleteItem = function _deleteItem(item) {
// remove from selection // remove from selection
index = this.selection.indexOf(item.id); index = this.selection.indexOf(item.id);
if (index != -1) this.selection.splice(index, 1); if (index != -1) this.selection.splice(index, 1);
// remove from group (if any)
if ('group' in item.data) {
var group = this.groups[item.data.group];
if (group) group.remove(item);
}
}; };
/** /**
@ -865,6 +1066,18 @@ ItemSet.itemFromTarget = function itemFromTarget (event) {
return null; return null;
}; };
/**
* Find the Group from an event target:
* searches for the attribute 'timeline-group' in the event target's element tree
* @param {Event} event
* @return {Group | null} group
*/
ItemSet.groupFromTarget = function groupFromTarget (event) {
// TODO: implement groupFromTarget
return null;
};
/** /**
* Find the ItemSet from an event target: * Find the ItemSet from an event target:
* searches for the attribute 'timeline-itemset' in the event target's element tree * searches for the attribute 'timeline-itemset' in the event target's element tree

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

@ -8,6 +8,7 @@
* // TODO: describe available options * // TODO: describe available options
*/ */
function Item (parent, data, options, defaultOptions) { function Item (parent, data, options, defaultOptions) {
this.id = null;
this.parent = parent; this.parent = parent;
this.data = data; this.data = data;
this.dom = null; this.dom = null;

Loading…
Cancel
Save