Browse Source

Implemented dragging items from one group to another. Implemented detailed configuration of editable actions.

css_transitions
jos 10 years ago
parent
commit
e79c0772ad
13 changed files with 319 additions and 107 deletions
  1. +3
    -0
      HISTORY.md
  2. +144
    -47
      dist/vis.js
  3. +1
    -1
      dist/vis.min.css
  4. +10
    -10
      dist/vis.min.js
  5. +53
    -6
      docs/timeline.html
  6. +17
    -11
      examples/timeline/02_interactive.html
  7. +1
    -1
      examples/timeline/index.html
  8. +20
    -2
      src/timeline/Timeline.js
  9. +58
    -23
      src/timeline/component/ItemSet.js
  10. +2
    -1
      src/timeline/component/RootPanel.js
  11. +1
    -1
      src/timeline/component/item/Item.js
  12. +2
    -2
      src/timeline/component/item/ItemRange.js
  13. +7
    -2
      test/timeline_groups.html

+ 3
- 0
HISTORY.md View File

@ -9,6 +9,9 @@ http://visjs.org
- Large refactoring of the Timeline, simplifying the code. - Large refactoring of the Timeline, simplifying the code.
- Great performance improvements. - Great performance improvements.
- Improved layout of box-items inside groups. - Improved layout of box-items inside groups.
- Items can now be dragged from one group to another.
- Option `editable` can now be used to enable/disable individual manipulation
actions (`add`, `updateTime`, `updateGroup`, `remove`).
- Function `setWindow` now accepts an object with properties `start` and `end`. - Function `setWindow` now accepts an object with properties `start` and `end`.
- Fixed option `autoResize` forcing a repaint of the Timeline with every check - Fixed option `autoResize` forcing a repaint of the Timeline with every check
rather than when the Timeline is actually resized. rather than when the Timeline is actually resized.

+ 144
- 47
dist/vis.js View File

@ -3881,7 +3881,8 @@ RootPanel.prototype.getFrame = function getFrame() {
RootPanel.prototype.repaint = function repaint() { RootPanel.prototype.repaint = function repaint() {
// update class name // update class name
var options = this.options; var options = this.options;
var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
var editable = options.editable.updateTime || options.editable.updateGroup;
var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
if (options.className) className += ' ' + util.option.asString(className); if (options.className) className += ' ' + util.option.asString(className);
this.frame.className = className; this.frame.className = className;
@ -4808,7 +4809,17 @@ ItemSet.prototype._create = function _create(){
* Function to let items snap to nice dates when * Function to let items snap to nice dates when
* dragging items. * dragging items.
*/ */
ItemSet.prototype.setOptions = Component.prototype.setOptions;
ItemSet.prototype.setOptions = function setOptions(options) {
Component.prototype.setOptions.call(this, options);
};
/**
* Mark the ItemSet dirty so it will refresh everything with next repaint
*/
ItemSet.prototype.markDirty = function markDirty() {
this.groupIds = [];
this.stackDirty = true;
};
/** /**
* Hide the component from the DOM * Hide the component from the DOM
@ -4943,27 +4954,49 @@ ItemSet.prototype.repaint = function repaint() {
resized = false, resized = false,
frame = this.frame; frame = this.frame;
// TODO: document this feature to specify one margin for both item and axis distance
if (typeof margin === 'number') {
margin = {
item: margin,
axis: margin
};
}
// update className // update className
frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : ''); frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
// reorder the groups (if needed)
resized = this._orderGroups() || resized;
// check whether zoomed (in that case we need to re-stack everything) // check whether zoomed (in that case we need to re-stack everything)
// TODO: would be nicer to get this as a trigger from Range
var visibleInterval = this.range.end - this.range.start; var visibleInterval = this.range.end - this.range.start;
var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth); var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
if (zoomed) this.stackDirty = true;
this.lastVisibleInterval = visibleInterval; this.lastVisibleInterval = visibleInterval;
this.lastWidth = this.width; this.lastWidth = this.width;
// repaint all groups // repaint all groups
var restack = zoomed || this.stackDirty;
var height = 0;
var restack = this.stackDirty,
firstGroup = this._firstGroup(),
firstMargin = {
item: margin.item,
axis: margin.axis
},
nonFirstMargin = {
item: margin.item,
axis: margin.item / 2
},
height = 0,
minHeight = margin.axis + margin.item;
util.forEach(this.groups, function (group) { util.forEach(this.groups, function (group) {
resized = group.repaint(range, margin, restack) || resized;
var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
resized = group.repaint(range, groupMargin, restack) || resized;
height += group.height; height += group.height;
}); });
height = Math.max(height, minHeight);
this.stackDirty = false; this.stackDirty = false;
// reorder the groups (if needed)
resized = this._orderGroups() || resized;
// reposition frame // reposition frame
frame.style.left = asSize(options.left, ''); frame.style.left = asSize(options.left, '');
frame.style.right = asSize(options.right, ''); frame.style.right = asSize(options.right, '');
@ -4993,6 +5026,19 @@ ItemSet.prototype.repaint = function repaint() {
return resized; return resized;
}; };
/**
* Get the first group, aligned with the axis
* @return {Group | null} firstGroup
* @private
*/
ItemSet.prototype._firstGroup = function _firstGroup() {
var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
var firstGroupId = this.groupIds[firstGroupIndex];
var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
return firstGroup || null;
};
/** /**
* Create or delete the group holding all ungrouped items. This group is used when * Create or delete the group holding all ungrouped items. This group is used when
* there are no groups specified. * there are no groups specified.
@ -5379,7 +5425,8 @@ ItemSet.prototype._orderGroups = function () {
// hide all groups, removes them from the DOM // hide all groups, removes them from the DOM
var groups = this.groups; var groups = this.groups;
groupIds.forEach(function (groupId) { groupIds.forEach(function (groupId) {
groups[groupId].hide();
var group = groups[groupId];
group.hide();
}); });
// show the groups again, attach them to the DOM in correct order // show the groups again, attach them to the DOM in correct order
@ -5502,28 +5549,45 @@ ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
* @private * @private
*/ */
ItemSet.prototype._onDragStart = function (event) { ItemSet.prototype._onDragStart = function (event) {
if (!this.options.editable) {
if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
return; return;
} }
var item = ItemSet.itemFromTarget(event), var item = ItemSet.itemFromTarget(event),
me = this;
me = this,
props;
if (item && item.selected) { if (item && item.selected) {
var dragLeftItem = event.target.dragLeftItem; var dragLeftItem = event.target.dragLeftItem;
var dragRightItem = event.target.dragRightItem; var dragRightItem = event.target.dragRightItem;
if (dragLeftItem) { if (dragLeftItem) {
this.touchParams.itemProps = [{
item: dragLeftItem,
start: item.data.start.valueOf()
}];
props = {
item: dragLeftItem
};
if (me.options.editable.updateTime) {
props.start = item.data.start.valueOf();
}
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
}
this.touchParams.itemProps = [props];
} }
else if (dragRightItem) { else if (dragRightItem) {
this.touchParams.itemProps = [{
item: dragRightItem,
end: item.data.end.valueOf()
}];
props = {
item: dragRightItem
};
if (me.options.editable.updateTime) {
props.end = item.data.end.valueOf();
}
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
}
this.touchParams.itemProps = [props];
} }
else { else {
this.touchParams.itemProps = this.getSelection().map(function (id) { this.touchParams.itemProps = this.getSelection().map(function (id) {
@ -5532,11 +5596,12 @@ ItemSet.prototype._onDragStart = function (event) {
item: item item: item
}; };
if ('start' in item.data) {
props.start = item.data.start.valueOf()
if (me.options.editable.updateTime) {
if ('start' in item.data) props.start = item.data.start.valueOf();
if ('end' in item.data) props.end = item.data.end.valueOf();
} }
if ('end' in item.data) {
props.end = item.data.end.valueOf()
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
} }
return props; return props;
@ -5565,16 +5630,29 @@ ItemSet.prototype._onDrag = function (event) {
var start = new Date(props.start + offset); var start = new Date(props.start + offset);
props.item.data.start = snap ? snap(start) : start; props.item.data.start = snap ? snap(start) : start;
} }
if ('end' in props) { if ('end' in props) {
var end = new Date(props.end + offset); var end = new Date(props.end + offset);
props.item.data.end = snap ? snap(end) : end; props.item.data.end = snap ? snap(end) : end;
} }
if ('group' in props) {
// drag from one group to another
var group = ItemSet.groupFromTarget(event);
if (group && group.groupId != props.item.data.group) {
var oldGroup = props.item.parent;
oldGroup.remove(props.item);
oldGroup.order();
group.add(props.item);
group.order();
props.item.data.group = group.groupId;
}
}
}); });
// TODO: implement onMoving handler // TODO: implement onMoving handler
// TODO: implement dragging from one group to another
this.stackDirty = true; // force re-stacking of all items next repaint this.stackDirty = true; // force re-stacking of all items next repaint
this.emit('change'); this.emit('change');
@ -5596,25 +5674,29 @@ ItemSet.prototype._onDragEnd = function (event) {
this.touchParams.itemProps.forEach(function (props) { this.touchParams.itemProps.forEach(function (props) {
var id = props.item.id, var id = props.item.id,
item = me.itemsData.get(id);
itemData = me.itemsData.get(id);
var changed = false; var changed = false;
if ('start' in props.item.data) { if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf()); changed = (props.start != props.item.data.start.valueOf());
item.start = util.convert(props.item.data.start, dataset.convert['start']);
itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
} }
if ('end' in props.item.data) { if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf()); changed = changed || (props.end != props.item.data.end.valueOf());
item.end = util.convert(props.item.data.end, dataset.convert['end']);
itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
}
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 // only apply changes when start or end is actually changed
if (changed) { if (changed) {
me.options.onMove(item, function (item) {
if (item) {
me.options.onMove(itemData, function (itemData) {
if (itemData) {
// apply changes // apply changes
item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(item);
itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
} }
else { else {
// restore original values // restore original values
@ -5817,7 +5899,7 @@ Item.prototype.repositionY = function repositionY() {
* @protected * @protected
*/ */
Item.prototype._repaintDeleteButton = function (anchor) { Item.prototype._repaintDeleteButton = function (anchor) {
if (this.selected && this.options.editable && !this.dom.deleteButton) {
if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
// create and show button // create and show button
var me = this; var me = this;
@ -6476,7 +6558,7 @@ ItemRange.prototype.repositionY = function repositionY() {
* @protected * @protected
*/ */
ItemRange.prototype._repaintDragLeft = function () { ItemRange.prototype._repaintDragLeft = function () {
if (this.selected && this.options.editable && !this.dom.dragLeft) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
// create and show drag area // create and show drag area
var dragLeft = document.createElement('div'); var dragLeft = document.createElement('div');
dragLeft.className = 'drag-left'; dragLeft.className = 'drag-left';
@ -6506,7 +6588,7 @@ ItemRange.prototype._repaintDragLeft = function () {
* @protected * @protected
*/ */
ItemRange.prototype._repaintDragRight = function () { ItemRange.prototype._repaintDragRight = function () {
if (this.selected && this.options.editable && !this.dom.dragRight) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
// create and show drag area // create and show drag area
var dragRight = document.createElement('div'); var dragRight = document.createElement('div');
dragRight.className = 'drag-right'; dragRight.className = 'drag-right';
@ -6703,28 +6785,21 @@ Group.prototype.getLabelWidth = function getLabelWidth() {
/** /**
* Repaint this group * Repaint this group
* @param {{start: number, end: number}} range * @param {{start: number, end: number}} range
* @param {number | {item: number, axis: number}} margin
* @param {{item: number, axis: number}} margin
* @param {boolean} [restack=false] Force restacking of all items * @param {boolean} [restack=false] Force restacking of all items
* @return {boolean} Returns true if the group is resized * @return {boolean} Returns true if the group is resized
*/ */
Group.prototype.repaint = function repaint(range, margin, restack) { Group.prototype.repaint = function repaint(range, margin, restack) {
var resized = false; var resized = false;
if (typeof margin === 'number') {
margin = {
item: margin,
axis: margin
};
}
// update visible items
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
// reposition visible items vertically // reposition visible items vertically
stack.stack(this.visibleItems, margin, restack); stack.stack(this.visibleItems, margin, restack);
this.stackDirty = false; this.stackDirty = false;
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
this.visibleItems[i].repositionY();
var item = this.visibleItems[i];
item.repositionY();
} }
// recalculate the height of the group // recalculate the height of the group
@ -6742,6 +6817,7 @@ Group.prototype.repaint = function repaint(range, margin, restack) {
else { else {
height = margin.axis + margin.item; height = margin.axis + margin.item;
} }
height = Math.max(height, this.props.label.height);
// calculate actual size and position // calculate actual size and position
var foreground = this.dom.foreground; var foreground = this.dom.foreground;
@ -7059,7 +7135,14 @@ function Timeline (container, items, options) {
orientation: 'bottom', orientation: 'bottom',
direction: 'horizontal', // 'horizontal' or 'vertical' direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true, autoResize: true,
editable: false,
editable: {
updateTime: false,
updateGroup: false,
add: false,
remove: false
},
selectable: true, selectable: true,
snap: null, // will be specified after timeaxis is created snap: null, // will be specified after timeaxis is created
@ -7318,6 +7401,17 @@ Emitter(Timeline.prototype);
Timeline.prototype.setOptions = function (options) { Timeline.prototype.setOptions = function (options) {
util.extend(this.options, options); util.extend(this.options, options);
if ('editable' in options) {
var isBoolean = typeof options.editable === 'boolean';
this.options.editable = {
updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
add: isBoolean ? options.editable : (options.editable.add || false),
remove: isBoolean ? options.editable : (options.editable.remove || false)
};
}
// force update of range (apply new min/max etc.) // force update of range (apply new min/max etc.)
// both start and end are optional // both start and end are optional
this.range.setRange(options.start, options.end); this.range.setRange(options.start, options.end);
@ -7333,6 +7427,9 @@ Timeline.prototype.setOptions = function (options) {
} }
} }
// force the itemSet to refresh: options like orientation and margins may be changed
this.itemSet.markDirty();
// validate the callback functions // validate the callback functions
var validateCallback = (function (fn) { var validateCallback = (function (fn) {
if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) { if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
@ -7630,7 +7727,7 @@ Timeline.prototype._onSelectItem = function (event) {
*/ */
Timeline.prototype._onAddItem = function (event) { Timeline.prototype._onAddItem = function (event) {
if (!this.options.selectable) return; if (!this.options.selectable) return;
if (!this.options.editable) return;
if (!this.options.editable.add) return;
var me = this, var me = this,
item = ItemSet.itemFromTarget(event); item = ItemSet.itemFromTarget(event);

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


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


+ 53
- 6
docs/timeline.html View File

@ -347,12 +347,41 @@ var options = {
<tr> <tr>
<td>editable</td> <td>editable</td>
<td>Boolean</td>
<td>Boolean | Object</td>
<td>false</td> <td>false</td>
<td>If true, the items on the timeline can be dragged. Only applicable when option <code>selectable</code> is <code>true</code>. See also the callbacks <code>onAdd</code>, <code>onUpdate</code>, <code>onMove</code>, and <code>onRemove</code>, described in detail in section <a href="#Editing_Items">Editing Items</a>.
<td>If true, the items in the timeline can be manipulated. Only applicable when option <code>selectable</code> is <code>true</code>. See also the callbacks <code>onAdd</code>, <code>onUpdate</code>, <code>onMove</code>, and <code>onRemove</code>. When <code>editable</code> is an object, one can enable or disable individual manipulation actions.
See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.
</td> </td>
</tr> </tr>
<tr>
<td>editable.add</td>
<td>Boolean</td>
<td>false</td>
<td>If true, new items can be created by double tapping an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.remove</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be deleted by first selecting them, and then clicking the delete button on the top right of the item. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.updateGroup</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be dragged from one group to another. Only applicable when the Timeline has groups. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.updateTime</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be dragged to another moment in time. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr> <tr>
<td>end</td> <td>end</td>
<td>Date | Number | String</td> <td>Date | Number | String</td>
@ -428,7 +457,7 @@ var options = {
<td>onAdd</td> <td>onAdd</td>
<td>Function</td> <td>Function</td>
<td>none</td> <td>none</td>
<td>Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.add</code> are set <code>true</code>.
</td> </td>
</tr> </tr>
@ -436,7 +465,7 @@ var options = {
<td>onUpdate</td> <td>onUpdate</td>
<td>Function</td> <td>Function</td>
<td>none</td> <td>none</td>
<td>Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.updateTime</code> or <code>editable.updateGroup</code> are set <code>true</code>.
</td> </td>
</tr> </tr>
@ -444,7 +473,7 @@ var options = {
<td>onMove</td> <td>onMove</td>
<td>Function</td> <td>Function</td>
<td>none</td> <td>none</td>
<td>Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.updateTime</code> or <code>editable.updateGroup</code> are set <code>true</code>.
</td> </td>
</tr> </tr>
@ -452,7 +481,7 @@ var options = {
<td>onRemove</td> <td>onRemove</td>
<td>Function</td> <td>Function</td>
<td>none</td> <td>none</td>
<td>Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.remove</code> are set <code>true</code>.
</td> </td>
</tr> </tr>
@ -792,6 +821,24 @@ timeline.off('select', onSelect);
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 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.
</p> </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>
<pre class="prettyprint lang-js">// enable or disable all manipulation actions
var options = {
editable: true // true or false
};
// enable or disable individual manipulation actions
var options = {
editable: {
add: true, // add new items by double tapping
updateTime: true, // drag items horizontally
updateGroup: true, // drag items from one group to another
remove: true // delete an item by tapping the delete button top right
}
};</pre>
<p> <p>
One can specify callback functions to validate changes made by the user. There are a number of callback functions for this purpose: One can specify callback functions to validate changes made by the user. There are a number of callback functions for this purpose:
</p> </p>

examples/timeline/02_dataset.html → examples/timeline/02_interactive.html View File

@ -1,21 +1,12 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html> <html>
<head> <head>
<title>Timeline | Dataset example</title>
<title>Timeline | Interactive example</title>
<style> <style>
body, html { body, html {
font-family: arial, sans-serif; font-family: arial, sans-serif;
font-size: 11pt; font-size: 11pt;
height: 100%;
margin: 0;
padding: 0;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 100%;
} }
</style> </style>
@ -23,6 +14,9 @@
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> <link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>
<p>Drag items around, create new items, and remove items.</p>
<div id="visualization"></div> <div id="visualization"></div>
<script> <script>
@ -47,8 +41,20 @@
start: '2014-01-10', start: '2014-01-10',
end: '2014-02-10', end: '2014-02-10',
orientation: 'top', orientation: 'top',
height: '100%',
height: '300px',
editable: true, editable: true,
/* alternatively, enable/disable individual actions:
editable: {
add: true,
updateTime: true,
updateGroup: true,
remove: true
},
*/
showCurrentTime: true showCurrentTime: true
}; };

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

@ -13,7 +13,7 @@
<h1>vis.js timeline examples</h1> <h1>vis.js timeline examples</h1>
<p><a href="01_basic.html">01_basic.html</a></p> <p><a href="01_basic.html">01_basic.html</a></p>
<p><a href="02_dataset.html">02_dataset.html</a></p>
<p><a href="02_interactive.html">02_dataset.html</a></p>
<p><a href="03_much_data.html">03_much_data.html</a></p> <p><a href="03_much_data.html">03_much_data.html</a></p>
<p><a href="04_html_data.html">04_html_data.html</a></p> <p><a href="04_html_data.html">04_html_data.html</a></p>
<p><a href="05_groups.html">05_groups.html</a></p> <p><a href="05_groups.html">05_groups.html</a></p>

+ 20
- 2
src/timeline/Timeline.js View File

@ -15,7 +15,14 @@ function Timeline (container, items, options) {
orientation: 'bottom', orientation: 'bottom',
direction: 'horizontal', // 'horizontal' or 'vertical' direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true, autoResize: true,
editable: false,
editable: {
updateTime: false,
updateGroup: false,
add: false,
remove: false
},
selectable: true, selectable: true,
snap: null, // will be specified after timeaxis is created snap: null, // will be specified after timeaxis is created
@ -274,6 +281,17 @@ Emitter(Timeline.prototype);
Timeline.prototype.setOptions = function (options) { Timeline.prototype.setOptions = function (options) {
util.extend(this.options, options); util.extend(this.options, options);
if ('editable' in options) {
var isBoolean = typeof options.editable === 'boolean';
this.options.editable = {
updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
add: isBoolean ? options.editable : (options.editable.add || false),
remove: isBoolean ? options.editable : (options.editable.remove || false)
};
}
// force update of range (apply new min/max etc.) // force update of range (apply new min/max etc.)
// both start and end are optional // both start and end are optional
this.range.setRange(options.start, options.end); this.range.setRange(options.start, options.end);
@ -589,7 +607,7 @@ Timeline.prototype._onSelectItem = function (event) {
*/ */
Timeline.prototype._onAddItem = function (event) { Timeline.prototype._onAddItem = function (event) {
if (!this.options.selectable) return; if (!this.options.selectable) return;
if (!this.options.editable) return;
if (!this.options.editable.add) return;
var me = this, var me = this,
item = ItemSet.itemFromTarget(event); item = ItemSet.itemFromTarget(event);

+ 58
- 23
src/timeline/component/ItemSet.js View File

@ -892,28 +892,45 @@ ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
* @private * @private
*/ */
ItemSet.prototype._onDragStart = function (event) { ItemSet.prototype._onDragStart = function (event) {
if (!this.options.editable) {
if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
return; return;
} }
var item = ItemSet.itemFromTarget(event), var item = ItemSet.itemFromTarget(event),
me = this;
me = this,
props;
if (item && item.selected) { if (item && item.selected) {
var dragLeftItem = event.target.dragLeftItem; var dragLeftItem = event.target.dragLeftItem;
var dragRightItem = event.target.dragRightItem; var dragRightItem = event.target.dragRightItem;
if (dragLeftItem) { if (dragLeftItem) {
this.touchParams.itemProps = [{
item: dragLeftItem,
start: item.data.start.valueOf()
}];
props = {
item: dragLeftItem
};
if (me.options.editable.updateTime) {
props.start = item.data.start.valueOf();
}
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
}
this.touchParams.itemProps = [props];
} }
else if (dragRightItem) { else if (dragRightItem) {
this.touchParams.itemProps = [{
item: dragRightItem,
end: item.data.end.valueOf()
}];
props = {
item: dragRightItem
};
if (me.options.editable.updateTime) {
props.end = item.data.end.valueOf();
}
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
}
this.touchParams.itemProps = [props];
} }
else { else {
this.touchParams.itemProps = this.getSelection().map(function (id) { this.touchParams.itemProps = this.getSelection().map(function (id) {
@ -922,11 +939,12 @@ ItemSet.prototype._onDragStart = function (event) {
item: item item: item
}; };
if ('start' in item.data) {
props.start = item.data.start.valueOf()
if (me.options.editable.updateTime) {
if ('start' in item.data) props.start = item.data.start.valueOf();
if ('end' in item.data) props.end = item.data.end.valueOf();
} }
if ('end' in item.data) {
props.end = item.data.end.valueOf()
if (me.options.editable.updateGroup) {
if ('group' in item.data) props.group = item.data.group;
} }
return props; return props;
@ -955,16 +973,29 @@ ItemSet.prototype._onDrag = function (event) {
var start = new Date(props.start + offset); var start = new Date(props.start + offset);
props.item.data.start = snap ? snap(start) : start; props.item.data.start = snap ? snap(start) : start;
} }
if ('end' in props) { if ('end' in props) {
var end = new Date(props.end + offset); var end = new Date(props.end + offset);
props.item.data.end = snap ? snap(end) : end; props.item.data.end = snap ? snap(end) : end;
} }
if ('group' in props) {
// drag from one group to another
var group = ItemSet.groupFromTarget(event);
if (group && group.groupId != props.item.data.group) {
var oldGroup = props.item.parent;
oldGroup.remove(props.item);
oldGroup.order();
group.add(props.item);
group.order();
props.item.data.group = group.groupId;
}
}
}); });
// TODO: implement onMoving handler // TODO: implement onMoving handler
// TODO: implement dragging from one group to another
this.stackDirty = true; // force re-stacking of all items next repaint this.stackDirty = true; // force re-stacking of all items next repaint
this.emit('change'); this.emit('change');
@ -986,25 +1017,29 @@ ItemSet.prototype._onDragEnd = function (event) {
this.touchParams.itemProps.forEach(function (props) { this.touchParams.itemProps.forEach(function (props) {
var id = props.item.id, var id = props.item.id,
item = me.itemsData.get(id);
itemData = me.itemsData.get(id);
var changed = false; var changed = false;
if ('start' in props.item.data) { if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf()); changed = (props.start != props.item.data.start.valueOf());
item.start = util.convert(props.item.data.start, dataset.convert['start']);
itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
} }
if ('end' in props.item.data) { if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf()); changed = changed || (props.end != props.item.data.end.valueOf());
item.end = util.convert(props.item.data.end, dataset.convert['end']);
itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
}
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 // only apply changes when start or end is actually changed
if (changed) { if (changed) {
me.options.onMove(item, function (item) {
if (item) {
me.options.onMove(itemData, function (itemData) {
if (itemData) {
// apply changes // apply changes
item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(item);
itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
} }
else { else {
// restore original values // restore original values

+ 2
- 1
src/timeline/component/RootPanel.js View File

@ -91,7 +91,8 @@ RootPanel.prototype.getFrame = function getFrame() {
RootPanel.prototype.repaint = function repaint() { RootPanel.prototype.repaint = function repaint() {
// update class name // update class name
var options = this.options; var options = this.options;
var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
var editable = options.editable.updateTime || options.editable.updateGroup;
var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
if (options.className) className += ' ' + util.option.asString(className); if (options.className) className += ' ' + util.option.asString(className);
this.frame.className = className; this.frame.className = className;

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

@ -110,7 +110,7 @@ Item.prototype.repositionY = function repositionY() {
* @protected * @protected
*/ */
Item.prototype._repaintDeleteButton = function (anchor) { Item.prototype._repaintDeleteButton = function (anchor) {
if (this.selected && this.options.editable && !this.dom.deleteButton) {
if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
// create and show button // create and show button
var me = this; var me = this;

+ 2
- 2
src/timeline/component/item/ItemRange.js View File

@ -207,7 +207,7 @@ ItemRange.prototype.repositionY = function repositionY() {
* @protected * @protected
*/ */
ItemRange.prototype._repaintDragLeft = function () { ItemRange.prototype._repaintDragLeft = function () {
if (this.selected && this.options.editable && !this.dom.dragLeft) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
// create and show drag area // create and show drag area
var dragLeft = document.createElement('div'); var dragLeft = document.createElement('div');
dragLeft.className = 'drag-left'; dragLeft.className = 'drag-left';
@ -237,7 +237,7 @@ ItemRange.prototype._repaintDragLeft = function () {
* @protected * @protected
*/ */
ItemRange.prototype._repaintDragRight = function () { ItemRange.prototype._repaintDragRight = function () {
if (this.selected && this.options.editable && !this.dom.dragRight) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
// create and show drag area // create and show drag area
var dragRight = document.createElement('div'); var dragRight = document.createElement('div');
dragRight.className = 'drag-right'; dragRight.className = 'drag-right';

+ 7
- 2
test/timeline_groups.html View File

@ -49,7 +49,7 @@
var itemCount = 20; var itemCount = 20;
// create a data set with groups // create a data set with groups
var names = ['John', 'Alston', 'Lee', 'Grant'];
var names = ['John (0)', 'Alston (1)', 'Lee (2)', 'Grant (3)'];
var groups = new vis.DataSet(); var groups = new vis.DataSet();
for (var g = 0; g < groupCount; g++) { for (var g = 0; g < groupCount; g++) {
groups.add({id: g, content: names[g]}); groups.add({id: g, content: names[g]});
@ -73,7 +73,12 @@
// create visualization // create visualization
var container = document.getElementById('visualization'); var container = document.getElementById('visualization');
var options = { var options = {
editable: true,
editable: {
add: true,
//remove: true,
updateTime: true,
updateGroup: true
},
//height: 200, //height: 200,
groupOrder: 'content' groupOrder: 'content'
}; };

Loading…
Cancel
Save