Browse Source

Drag object in to item (#3506)

* Make items redraws return queues

* Parallel initial items redraw

* Seperate read and write actions in items

* Parallel all items redraws

* Remove comments

* Fix linting comments

* Fix redraws on actions

* Seperate group read and write

* Add objects that can be dropped to items

* Move example

* Fix group get/set group properties

* Add documentation

* Remove console.log

* return Group.js changes

* Fix review comments

* Fix example

* Update drag&drop.html
jittering-top
Yotam Berkowitz 6 years ago
committed by GitHub
parent
commit
3bc3d164df
5 changed files with 163 additions and 84 deletions
  1. +9
    -0
      docs/timeline/index.html
  2. +71
    -19
      examples/timeline/other/drag&drop.html
  3. +8
    -3
      lib/timeline/Core.js
  4. +74
    -62
      lib/timeline/component/ItemSet.js
  5. +1
    -0
      lib/timeline/optionsTimeline.js

+ 9
- 0
docs/timeline/index.html View File

@ -913,6 +913,14 @@ function (option, path) {
</td>
</tr>
<tr>
<td>onDropObjectOnItem</td>
<td>function</td>
<td>none</td>
<td>Callback function triggered when an object containing <code>target:'item'</code> in its drag data is dropped in to a timeline item.
</td>
</tr>
<tr>
<td>onUpdate</td>
<td>function</td>
@ -1823,6 +1831,7 @@ var items = new vis.DataSet([
<ul>
<li><code>onAdd(item, callback)</code> Fired when a new item is about to be added. If not implemented, the item will be added with default text contents.</li>
<li><code>onUpdate(item, callback)</code> Fired when an item is about to be updated. This function typically has to show a dialog where the user change the item. If not implemented, nothing happens.</li>
<li><code>onDropObjectOnItem(objectData, item)</code> Fired when an object is dropped in to an existing timeline item.</li>
<li><code>onMove(item, callback)</code> Fired when an item has been moved. If not implemented, the move action will be accepted.</li>
<li><code>onMoving(item, callback)</code> Fired repeatedly while an item is being moved (dragged). Can be used to adjust the items start, end, and/or group to allowed regions.</li>
<li><code>onRemove(item, callback)</code> Fired when an item is about to be deleted. If not implemented, the item will be always removed.</li>

+ 71
- 19
examples/timeline/other/drag&drop.html View File

@ -27,6 +27,27 @@
font-size: inherit;
cursor: move;
}
li.object-item {
list-style: none;
width: 150px;
color: #1A1A1A;
background-color: #D5DDF6;
border: 1px solid #97B0F8;
border-radius: 2px;
margin-bottom: 5px;
padding: 5px 12px;
}
li.object-item:before {
content: "≣";
font-family: Arial, sans-serif;
display: inline-block;
font-size: inherit;
cursor: move;
}
.items-panel {
display: flex;
justify-content: space-around;
}
</style>
</head>
@ -37,23 +58,33 @@
<p>For this to work, you will have to define your own <code>'dragstart'</code> eventListener on each item in your list of items (make sure that any new item added to the list is attached to this eventListener 'dragstart' handler). This 'dragstart' handler must set <code>dataTransfer</code> - notice you can set the item's information as you want this way.</p>
<div id="mytimeline" ></div>
<div>
<h3>Items:</h3>
<ul class="items">
<li draggable="true" class="item">
item 1 - box
</li>
<li draggable="true" class="item">
item 2 - point
</li>
<li draggable="true" class="item">
item 3 - range
</li>
<li draggable="true" class="item">
item 3 - range - fixed times - <br>
(start: now, end: now + 10 min)
<div class='items-panel'>
<div class='side'>
<h3>Items:</h3>
<ul class="items">
<li draggable="true" class="item">
item 1 - box
</li>
<li draggable="true" class="item">
item 2 - point
</li>
<li draggable="true" class="item">
item 3 - range
</li>
<li draggable="true" class="item">
item 3 - range - fixed times - <br>
(start: now, end: now + 10 min)
</li>
</ul>
</div>
<div class='side'>
<h3>Object with "target:'item'":</h3>
<li draggable="true" class="object-item">
object with target:'item'
</li>
</ul>
</div>
</div>
<script>
@ -99,7 +130,11 @@
start: new Date(),
end: new Date(1000*60*60*24 + (new Date()).valueOf()),
editable: true,
orientation: 'top'
orientation: 'top',
onDropObjectOnItem: function(objectData, item, callback) {
if (!item) { return; }
alert('dropped object with content: "' + objectData.content + '" to item: "' + item.content + '"');
}
};
// create a Timeline
@ -107,7 +142,7 @@
timeline1 = new vis.Timeline(container, items, groups, options);
function handleDragStart(event) {
dragSrcEl = event.target;
var dragSrcEl = event.target;
event.dataTransfer.effectAllowed = 'move';
var itemType = event.target.innerHTML.split('-')[1].trim();
@ -122,17 +157,34 @@
item.start = new Date();
item.end = new Date(1000*60*10 + (new Date()).valueOf());
}
event.dataTransfer.setData("text", JSON.stringify(item));
}
function handleObjectItemDragStart(event) {
var dragSrcEl = event.target;
event.dataTransfer.effectAllowed = 'move';
var objectItem = {
content: 'objectItemData',
target: 'item'
};
event.dataTransfer.setData("text", JSON.stringify(objectItem));
}
var items = document.querySelectorAll('.items .item');
var objectItems = document.querySelectorAll('.object-item');
for (var i = items.length - 1; i >= 0; i--) {
var item = items[i];
item.addEventListener('dragstart', handleDragStart.bind(this), false);
}
for (var i = objectItems.length - 1; i >= 0; i--) {
var objectItem = objectItems[i];
objectItem.addEventListener('dragstart', handleObjectItemDragStart.bind(this), false);
}
</script>
</body>

+ 8
- 3
lib/timeline/Core.js View File

@ -291,10 +291,10 @@ Core.prototype._create = function (container) {
// prevent redirect to blank page - Firefox
if(event.preventDefault) { event.preventDefault(); }
if(event.stopPropagation) { event.stopPropagation(); }
// return when dropping non-vis items
// return when dropping non-vis items
try {
var itemData = JSON.parse(event.dataTransfer.getData("text"))
if (!itemData.content) return
if (!itemData || !itemData.content) return
} catch (err) {
return false;
}
@ -303,8 +303,13 @@ Core.prototype._create = function (container) {
event.center = {
x: event.clientX,
y: event.clientY
};
if (itemData.target !== 'item') {
me.itemSet._onAddItem(event);
} else {
me.itemSet._onDropObjectOnItem(event);
}
me.itemSet._onAddItem(event);
me.emit('drop', me.getEventProperties(event))
return false;
}

+ 74
- 62
lib/timeline/component/ItemSet.js View File

@ -61,10 +61,14 @@ function ItemSet(body, options) {
order: false,
add: false,
remove: false
},
},
snap: TimeStep.snap,
// Only called when `objectData.target === 'item'.
onDropObjectOnItem: function(objectData, item, callback) {
callback(item)
},
onAdd: function (item, callback) {
callback(item);
},
@ -444,7 +448,7 @@ ItemSet.prototype.setOptions = function(options) {
this.options[name] = fn;
}
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving', 'onAddGroup', 'onMoveGroup', 'onRemoveGroup'].forEach(addCallback);
['onDropObjectOnItem', 'onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving', 'onAddGroup', 'onMoveGroup', 'onRemoveGroup'].forEach(addCallback);
// force the itemSet to refresh: options like orientation and margins may be changed
this.markDirty();
@ -2060,7 +2064,19 @@ ItemSet.prototype._onUpdateItem = function (item) {
};
/**
* Handle creation of an item on double tap
* Handle drop event of data on item
* Only called when `objectData.target === 'item'.
* @param {Event} event The event
* @private
*/
ItemSet.prototype._onDropObjectOnItem = function(event) {
var item = this.itemFromTarget(event);
var objectData = JSON.parse(event.dataTransfer.getData("text"));
this.options.onDropObjectOnItem(objectData, item)
}
/**
* Handle creation of an item on double tap or drop of a drag event
* @param {Event} event The event
* @private
*/
@ -2070,69 +2086,65 @@ ItemSet.prototype._onAddItem = function (event) {
var me = this;
var snap = this.options.snap || null;
var item = this.itemFromTarget(event);
if (!item) {
var xAbs;
var x;
// add item
if (this.options.rtl) {
xAbs = util.getAbsoluteRight(this.dom.frame);
x = xAbs - event.center.x;
} else {
xAbs = util.getAbsoluteLeft(this.dom.frame);
x = event.center.x - xAbs;
var xAbs;
var x;
// add item
if (this.options.rtl) {
xAbs = util.getAbsoluteRight(this.dom.frame);
x = xAbs - event.center.x;
} else {
xAbs = util.getAbsoluteLeft(this.dom.frame);
x = event.center.x - xAbs;
}
// var xAbs = util.getAbsoluteLeft(this.dom.frame);
// var x = event.center.x - xAbs;
var start = this.body.util.toTime(x);
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
var end;
var newItemData;
if (event.type == 'drop') {
newItemData = JSON.parse(event.dataTransfer.getData("text"));
newItemData.content = newItemData.content ? newItemData.content : 'new item';
newItemData.start = newItemData.start ? newItemData.start : (snap ? snap(start, scale, step) : start);
newItemData.type = newItemData.type || 'box';
newItemData[this.itemsData._fieldId] = newItemData.id || util.randomUUID();
if (newItemData.type == 'range' && !newItemData.end) {
end = this.body.util.toTime(x + this.props.width / 5);
newItemData.end = snap ? snap(end, scale, step) : end;
}
// var xAbs = util.getAbsoluteLeft(this.dom.frame);
// var x = event.center.x - xAbs;
var start = this.body.util.toTime(x);
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
var end;
var newItemData;
if (event.type == 'drop') {
newItemData = JSON.parse(event.dataTransfer.getData("text"));
newItemData.content = newItemData.content ? newItemData.content : 'new item';
newItemData.start = newItemData.start ? newItemData.start : (snap ? snap(start, scale, step) : start);
newItemData.type = newItemData.type || 'box';
newItemData[this.itemsData._fieldId] = newItemData.id || util.randomUUID();
if (newItemData.type == 'range' && !newItemData.end) {
end = this.body.util.toTime(x + this.props.width / 5);
newItemData.end = snap ? snap(end, scale, step) : end;
}
} else {
newItemData = {
start: snap ? snap(start, scale, step) : start,
content: 'new item'
};
newItemData[this.itemsData._fieldId] = util.randomUUID();
// when default type is a range, add a default end date to the new item
if (this.options.type === 'range') {
end = this.body.util.toTime(x + this.props.width / 5);
newItemData.end = snap ? snap(end, scale, step) : end;
}
} else {
newItemData = {
start: snap ? snap(start, scale, step) : start,
content: 'new item'
};
newItemData[this.itemsData._fieldId] = util.randomUUID();
// when default type is a range, add a default end date to the new item
if (this.options.type === 'range') {
end = this.body.util.toTime(x + this.props.width / 5);
newItemData.end = snap ? snap(end, scale, step) : end;
}
}
var group = this.groupFromTarget(event);
if (group) {
newItemData.group = group.groupId;
}
var group = this.groupFromTarget(event);
if (group) {
newItemData.group = group.groupId;
}
// execute async handler to customize (or cancel) adding an item
newItemData = this._cloneItemData(newItemData); // convert start and end to the correct type
this.options.onAdd(newItemData, function (item) {
if (item) {
me.itemsData.getDataSet().add(item);
if (event.type == 'drop') {
me.setSelection([item.id]);
}
// TODO: need to trigger a redraw?
// execute async handler to customize (or cancel) adding an item
newItemData = this._cloneItemData(newItemData); // convert start and end to the correct type
this.options.onAdd(newItemData, function (item) {
if (item) {
me.itemsData.getDataSet().add(item);
if (event.type == 'drop') {
me.setSelection([item.id]);
}
});
}
// TODO: need to trigger a redraw?
}
});
};
/**

+ 1
- 0
lib/timeline/optionsTimeline.js View File

@ -118,6 +118,7 @@ let allOptions = {
multiselect: { 'boolean': bool},
multiselectPerGroup: { 'boolean': bool},
onAdd: {'function': 'function'},
onDropObjectOnItem: {'function': 'function'},
onUpdate: {'function': 'function'},
onMove: {'function': 'function'},
onMoving: {'function': 'function'},

Loading…
Cancel
Save