diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 9c4fda08..4fc29cab 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -368,17 +368,17 @@ Timeline.prototype.setItems = function(items) { if (!items) { newDataSet = null; } - else if (items instanceof DataSet) { + else if (items instanceof DataSet || items instanceof DataView) { newDataSet = items; } - if (!(items instanceof DataSet)) { - newDataSet = new DataSet({ + else { + // turn an array into a dataset + newDataSet = new DataSet(items, { convert: { start: 'Date', end: 'Date' } }); - newDataSet.add(items); } // set items @@ -432,11 +432,24 @@ Timeline.prototype.setItems = function(items) { /** * 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); }; /** diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index 282bc97d..b7fc6d2d 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -1,48 +1,16 @@ /** * @constructor Group - * @param {Panel} groupPanel - * @param {Panel} labelPanel - * @param {Panel} backgroundPanel - * @param {Panel} axisPanel * @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.itemSet = null; // ItemSet - this.options = options || {}; - this.options.top = 0; - - this.props = { - label: { - width: 0, - height: 0 - } - }; - 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(); } -Group.prototype = new Component(); - -// TODO: comment -Group.prototype.setOptions = Component.prototype.setOptions; - /** * Create DOM elements for the group * @private @@ -56,158 +24,31 @@ Group.prototype._create = function() { inner.className = 'inner'; label.appendChild(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; -}; diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index e14e471b..d07073c1 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -27,11 +27,12 @@ function ItemSet(backgroundPanel, axisPanel, options) { this.hammer = null; 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) { 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.orderedItems = { byStart: [], byEnd: [] }; -// this.systemLoaded = false; + + this.groups = {}; // Group object for every group + this.groupIds = []; + this.visibleItems = []; // visible, ordered items 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.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; } 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) { // unsubscribe from old dataset - util.forEach(this.listeners, function (callback, event) { + util.forEach(this.itemListeners, function (callback, event) { oldItemsData.unsubscribe(event, callback); }); @@ -525,7 +541,7 @@ ItemSet.prototype.setItems = function setItems(items) { if (this.itemsData) { // subscribe to new dataset var id = this.id; - util.forEach(this.listeners, function (callback, event) { + util.forEach(this.itemListeners, function (callback, event) { 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} */ ItemSet.prototype.getItems = function getItems() { 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 * @param {String | Number} id @@ -587,12 +656,11 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { // update item if (!constructor || !(item instanceof constructor)) { // item type has changed, delete the item and recreate it - me._deleteItem(item); + me._removeItem(item); item = null; } 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 if (constructor) { 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 { throw new TypeError('Unknown item type "' + type + '"'); } } - me.items[id] = item; if (type == 'range' && me.visibleItems.indexOf(item) == -1) { me._checkIfVisible(item, me.visibleItems); } @@ -637,7 +705,7 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { var item = me.items[id]; if (item) { 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 * with items, and from the map with visible items, and from the selection * @param {Item} item * @private */ -ItemSet.prototype._deleteItem = function _deleteItem(item) { +ItemSet.prototype._removeItem = function _removeItem(item) { // remove from DOM item.hide(); @@ -669,6 +864,12 @@ ItemSet.prototype._deleteItem = function _deleteItem(item) { // remove from selection index = this.selection.indexOf(item.id); 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; }; +/** + * 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: * searches for the attribute 'timeline-itemset' in the event target's element tree diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 0ca1b757..b0d4b074 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -8,6 +8,7 @@ * // TODO: describe available options */ function Item (parent, data, options, defaultOptions) { + this.id = null; this.parent = parent; this.data = data; this.dom = null;