I am rolling out a custom drag drop implementation which is a simplified version of the Dragula library. Here are my fiddles:
With Dragula: http://jsfiddle.net/8m4jrfwn/
With my custom drag drop implementation: http://jsfiddle.net/7wLtojs4/1/
The issue I am seeing is that my custom drag and drop implementation is a bit to free-form. I would like for the user to only be able to drop on the top or bottom of another div with the class box. How can I make this possible? Code here:
function handleMouseDown(e) {
window.dragging = {};
dragging.pageX = e.pageX;
dragging.pageY = e.pageY;
dragging.elem = this;
dragging.offset = $(this).offset();
$('body')
.on('mouseup', handleMouseUp)
.on('mousemove', handleDragging);
}
function handleDragging(e) {
let left = dragging.offset.left + (e.pageX - dragging.pageX);
let top = dragging.offset.top + (e.pageY - dragging.pageY);
$(dragging.elem)
.offset({top: top, left: left});
}
function handleMouseUp() {
$('body')
.off('mousemove', handleDragging)
.off('mouseup', handleMouseUp);
}
let boxElement = '.item';
$(boxElement).mousedown(handleMouseDown);
I does not see any logic here about what will happen on mouseup event. Seems you need detect on mouseup new order of elements and then exchange their positions.
For this you should know positions of all of them. On mouseup you can reorder them based on new positions.
In my realization(https://jsfiddle.net/jaromudr/c99oueg5/) I used next logic:
onMove(draggable) {
const sortedDraggables = this.getSortedDraggables()
const pinnedPositions = sortedDraggables.map((draggable) => draggable.pinnedPosition)
const currentIndex = sortedDraggables.indexOf(draggable)
const targetIndex = indexOfNearestPoint(pinnedPositions, draggable.position, this.options.radius, getYDifference)
if (targetIndex !== -1 && currentIndex !== targetIndex) {
arrayMove(sortedDraggables, currentIndex, targetIndex)
this.bubling(sortedDraggables, draggable)
}
}
bubling(sortedDraggables, currentDraggable) {
const currentPosition = this.startPosition.clone()
sortedDraggables.forEach((draggable) => {
if (!draggable.pinnedPosition.compare(currentPosition)) {
if (draggable === currentDraggable && !currentDraggable.nativeDragAndDrop) {
draggable.pinnedPosition = currentPosition.clone()
} else {
draggable.pinPosition(currentPosition, (draggable === currentDraggable) ? 0 : this.options.timeExcange)
}
}
currentPosition.y = currentPosition.y + draggable.getSize().y + this.verticalGap
})
}
where on move we detect if position in list was changed and move other draggables to their new positions.
On drag end we just move currentDraggable to pinnedPosition
Related
I have an angular project wherein the three representation is using threeJs. I wish to add a context menu on right-click to the scene. This is what I have so far. I am also receiving an error called 'menu does not exist'.
window.addEventListener('mousedown', (e: MouseEvent) => { this.rightClickContextMenu(e); }, false);
if (document.addEventListener) {
document.addEventListener('contextmenu', function (e) {
e.preventDefault();
}, false);
} else {
(<any>document).attachEvent('oncontextmenu', function () {
window.event.returnValue = false;
});
}
public rightClickContextMenu(e: MouseEvent): void {
var rightclick;
if (!event) var event = window.event;
if ((<any>event).which) rightclick = ((<any>event).which == 3);
else if ((<any>event).button) rightclick = ((<any>event).button == 2);
if (!rightclick) return;
const x = e.clientX - this.rect.left;
const y = e.clientY - this.rect.top;
var intersect;
let scenePointer = new th.Vector2();
scenePointer.x = (x / this.canvas.clientWidth) * 2 - 1;
scenePointer.y = (y / this.canvas.clientHeight) * - 2 + 1;
// Determine the component in-focus using raycasting
this.raycaster.setFromCamera(scenePointer, this.camera);
var intersects = this.raycaster.intersectObjects(this.scene.children);
if (intersects.length) {
intersect = intersects[0].object;
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "";
}
else {
intersect = undefined;
}
}
So far, on left-click, the color of the mesh changes but nothing happens on right click. I was wondering how do I add on a context menu to the same on right click. Modifications to my code to highlight the correct one would be much appreciated from you guys. I apologise, I am very new to threejs in case this question sounds trivial.
Stop event propagation on the control where u want this feature and then capture the event and diplay
I have an issue which is related to greensock animation but also may be a more general js issue in that I need to transfer a mousedown event to a second draggable instance after killing the first draggable instance.
the codepen hopefully will illustrate what i am trying to do.. code is underneath.
Codepen URL: http://codepen.io/anon/pen/ypdGn
var detachdraggable;
var rotatedraggable;
function makeRotateDraggable() {
// create rotate draggable for the cards..
rotatedraggable = Draggable.create(".dragclone", {
type:"rotation",
throwProps:true,
dragResistance : 0,
edgeResistance : 1,
bounds:{minRotation:-60, maxRotation:60},
onDragStart : function() {
var $el = $(this.target);
var cardStartAngle = $el.data('startangle');
},
onDrag: function() {
currentCardAngle = this.rotation;
var $el = $(this.target);
var cardStartAngle = $el.data('startangle');
cardDirection = ( currentCardAngle > cardStartAngle ) ? "up":"down";
cardTravelDegrees = Math.abs(currentCardAngle - cardStartAngle);
this.vars.type = "x";
},
onDragEnd: function() {
},
onClick: function() {
return false;
}
});
$('.dragclone').mousedown(function() {
$('.dragclone').css('z-index','10');
rotatedraggable[0].enable();
});
$('.dragclone').mouseout(function() {
detachdraggable[0].disable();
$('.dragclone').trigger('mousedown');
});
$('.dragclone').trigger('mouseout');
}
// first setup the x,y draggable..
detachdraggable = Draggable.create('.dragclone', {
type: "x,y",
edgeResistance:1,
throwProps:true,
onPress:function() {
startX = this.x;
startY = this.y;
},
onDrag:function() {
var xChange = this.x - startX,
yChange = this.y - startY,
ratio = Math.abs(xChange / yChange),
direction = [];
// NB:: you can adjust these ratio thresholds to make things
// more or less sensitive to diagonal movement.
if (ratio > 0.25) {
direction.push((xChange < 0) ? "left" : "right");
}
if (ratio < 4) {
direction.push((yChange < 0) ? "up" : "down");
}
if(direction[0] == "left") {
// TweenMax.to( $('.cloned'), .0001, {right:-xChange + 135});
}
// moving up so lets switch cards !!
if(direction[0] == "up") {
if(Math.abs(yChange) > 20) {
makeRotateDraggable();
}
}
},
onDragEnd:function() {
// driftDragCardBack();
// if(!cardPopping) { driftClonedCardBack(); }
},
onThrowComplete: function() {
}
});
I am battling to switch between 2 draggables setup on same element, am wondering if this is possible at all. basically the codepen has an example of what I want to do, but it isnt seamless. draggable is setup for element of type x,y, what i want to do is when the drag direction is up kill the type x,y draggable and switch to a type: rotation draggable so that the card moves around on an axis. you can see mine does that, but only if you release and then click again - is there any way to make this seamless, so it just switches mid-drag ?
thanks,
Justin
See http://codepen.io/anon/pen/klanu
I've never used greensock but just had a look at their document:
https://greensock.com/docs/#/HTML5/Drag/Draggable/startDrag/
First of all you had a couple of issues within your code. You don't need to create rotatedraggable inside a function, just create it, it's not enabled anyways. I also moved
$('.dragclone').mousedown(function() {
$('.dragclone').css('z-index','10');
detachdraggable[0].enable();
});
outside the function.
In your 2nd draggable, you were calling createRotation to create the rotation but like I said you can create it at the start. When the div is moved to the top, I just disabled the current drag and enabled the 1st one and called dragStart on it. e is what's passed to drag of the 2nd.
if(Math.abs(yChange) > 20) {
detachdraggable[0].disable();
rotatedraggable[0].enable();
rotatedraggable[0].startDrag(e);
}
Given: the reactive extensions drag and drop example , how would you subscribe to just a drop event?
I have modified the code to subscribe to a 'completed' callback, but it does not complete.
(function (global) {
function main () {
var dragTarget = document.getElementById('dragTarget');
var $dragTarget = $(dragTarget);
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown
.filter(function(md){
//console.log(md.offsetX + ", " + md.offsetY);
return md.offsetX <= 100
||
md.offsetY <= 100;
})
.flatMap(function (md) {
// calculate offsets when mouse down
var startX = md.offsetX, startY = md.offsetY;
// Calculate delta with mousemove until mouseup
return mousemove.map(function (mm) {
mm.preventDefault();
return {
left: mm.clientX - startX,
top: mm.clientY - startY
};
}).takeUntil(mouseup);
});
// Update position
var subscription = mousedrag.subscribe(
function (pos) {
dragTarget.style.top = pos.top + 'px';
dragTarget.style.left = pos.left + 'px';
},
function(errorToIgnore) {},
function() { alert('drop');});
}
main();
}(window));
I have read that hot observables, such as those that have been created from mouse events, never 'complete'. Is this correct? How can I otherwise get a callback on 'drop'?
Something like this should do the trick.
(function (global) {
function main () {
var dragTarget = document.getElementById('dragTarget');
// Get the three major events
var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var drop = mousedown
.selectMany(
Rx.Observable
.concat(
[
mousemove.take(1).ignoreElements(),
mouseup.take(1)
]
)
);
}
main();
}(window));
Edit:
If you think of an observable as an asynchronous function which yields multiple values, and then possibly completes or errors, you'll immediately recognize that there can only be one completion event.
When you start composing multiple function, the outer-most function still only completes once, even if that function contains multiple functions inside of it. So even though the total number of "completions" is 3, the outer-most function still only completes once.
Basically, that means if the outer-most function is suppose to return a value each time a drag completes, you need a way of actually doing that. You need to translate the drag completion into an "onNext" event for the outer-most observable.
ANY which way you can do that is going to get you what you want. Maybe that's the only kind of events that the outer-most function returns, or maybe it also returns drag starts and moves, but so long as it returns the drag completions, you'll end up with what you need (even if you have to filter it later).
The example I've given above is just one way to return the drag drops in the outer-most observable.
OP's link to the official example was down, it's here:
https://github.com/Reactive-Extensions/RxJS/blob/master/examples/dragndrop/dragndrop.js
There are two solutions to the original question, using either the primitive mouse events expanding upon the official example, or the native HTML5 drag and drop events.
Composing from 'mouseup', 'mousedown' and 'mousemove'
First we use mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1)) to get the 'drag start' stream. switchMapTo (see doc) uses a combination of flattening (mapping 'mousedown1' to each of the first emit of 'mouse moving until mouse up', aka 'dragging') and switch (see doc), giving us the latest mousedown1 emit followed by dragging, i.e. not just any time you click your mouse on the box.
Another 'mousedown' stream mousedown2 is used to compose a mousedrag stream so we can constantly render the box while dragging. mousedown1 above is prioritised over mousedown2. See https://stackoverflow.com/a/35964479/232288 for more information.
Once we have our 'drag start' stream we can get the 'drag stop' stream via: mousedragstart.mergeMapTo(mouseup.take(1)). mergeMapTo (see doc) maps 'mousedragstart' to each of the first emit of 'mouseup', giving us the latest 'mousedragstart' followed by 'mouseup', which is essentially 'drag stop'.
The demo below works with RxJS v5:
const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');
const mouseup = fromEvent(target, 'mouseup');
const mousemove = fromEvent(document, 'mousemove');
const [mousedown1, mousedown2] = prioritisingClone(fromEvent(target, 'mousedown'));
const mousedrag = mousedown2.mergeMap((e) => {
const startX = e.clientX + window.scrollX;
const startY = e.clientY + window.scrollY;
const startLeft = parseInt(e.target.style.left, 10) || 0;
const startTop = parseInt(e.target.style.top, 10) || 0;
return mousemove.map((e2) => {
e2.preventDefault();
return {
left: startLeft + e2.clientX - startX,
top: startTop + e2.clientY - startY
};
}).takeUntil(mouseup);
});
// map the latest mouse down emit to the first mouse drag emit, i.e. emits after pressing down and
// then dragging.
const mousedragstart = mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1));
// map the mouse drag start stream to first emit of a mouse up stream, i.e. emits after dragging and
// then releasing mouse button.
const mousedragstop = mousedragstart.mergeMapTo(mouseup.take(1));
mousedrag.subscribe((pos) => {
target.style.top = pos.top + 'px';
target.style.left = pos.left + 'px';
});
mousedragstart.subscribe(() => {
console.log('Dragging started');
events.innerText = 'Dragging started';
});
mousedragstop.subscribe(() => {
console.log('Dragging stopped');
events.innerText = 'Dragging stopped';
});
function prioritisingClone(stream$) {
const first = new Rx.Subject();
const second = stream$.do(x => first.next(x)).share();
return [
Rx.Observable.using(
() => second.subscribe(() => {}),
() => first
),
second,
];
}
.box {
position: relative;
width: 150px;
height: 150px;
background: seagreen;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box"></div>
<h3 id="events"></h3>
Composing from HTML5 native 'dragstart', 'dragover' and 'drop'
The native events make things a little easier. Just make sure the dragover observer calls e.preventDefault() so the body element can become a valid drop zone. See more info.
We use switchMap (see doc) in a similar fashion as how switchMapTo is used above: flatten and map the latest 'dragstart' to the latest 'drop' to get our 'drag then drop' stream.
The difference is only when the user drops the div do we update the position.
const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');
const dragstart = fromEvent(target, 'dragstart');
const dragover = fromEvent(document.body, 'dragover');
const drop = fromEvent(document.body, 'drop');
const dragthendrop = dragstart.switchMap((e) => {
const startX = e.clientX + window.scrollX;
const startY = e.clientY + window.scrollY;
const startLeft = parseInt(e.target.style.left, 10) || 0;
const startTop = parseInt(e.target.style.top, 10) || 0;
// set dataTransfer for Firefox
e.dataTransfer.setData('text/html', null);
console.log('Dragging started');
events.innerText = 'Dragging started';
return drop
.take(1)
.map((e2) => {
return {
left: startLeft + e2.clientX - startX,
top: startTop + e2.clientY - startY
};
});
});
dragover.subscribe((e) => {
// make it accepting drop events
e.preventDefault();
});
dragthendrop.subscribe((pos) => {
target.style.top = `${pos.top}px`;
target.style.left = `${pos.left}px`;
console.log('Dragging stopped');
events.innerText = 'Dragging stopped';
});
.box {
position: relative;
width: 150px;
height: 150px;
background: seagreen;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box" draggable="true"></div>
<h3 id="events"></h3>
This question already has answers here:
Moveable/draggable <div>
(9 answers)
Closed 5 years ago.
I want to create a movable/draggable div in native javascript without using jquery and libraries. Is there a tutorial or anythign?
OK, here's my personal code that I use for lightweight deployments (projects where using a library is either not allowed or overkill for some reason). First thing first, I always use this convenience function so that I can pass either an id or the actual dom element:
function get (el) {
if (typeof el == 'string') return document.getElementById(el);
return el;
}
As a bonus, get() is shorter to type than document.getElementById() and my code ends up shorter.
Second realize that what most libraries are doing is cross-browser compatibility. If all browsers behave the same the code is fairly trivial. So lets write some cross-browser functions to get mouse position:
function mouseX (e) {
if (e.pageX) {
return e.pageX;
}
if (e.clientX) {
return e.clientX + (document.documentElement.scrollLeft ?
document.documentElement.scrollLeft :
document.body.scrollLeft);
}
return null;
}
function mouseY (e) {
if (e.pageY) {
return e.pageY;
}
if (e.clientY) {
return e.clientY + (document.documentElement.scrollTop ?
document.documentElement.scrollTop :
document.body.scrollTop);
}
return null;
}
OK, the two functions above are identical. There're certainly better ways to write them but I'm keeping it (relatively) simple for now.
Now we can write the drag and drop code. The thing I like about this code is that everything's captured in a single closure so there are no global variables or helper functions littering the browser. Also, the code separates the drag handle from the object being dragged. This is useful for creating dialog boxes etc. But if not needed, you can always assign them the same object. Anyway, here's the code:
function dragable (clickEl,dragEl) {
var p = get(clickEl);
var t = get(dragEl);
var drag = false;
offsetX = 0;
offsetY = 0;
var mousemoveTemp = null;
if (t) {
var move = function (x,y) {
t.style.left = (parseInt(t.style.left)+x) + "px";
t.style.top = (parseInt(t.style.top) +y) + "px";
}
var mouseMoveHandler = function (e) {
e = e || window.event;
if(!drag){return true};
var x = mouseX(e);
var y = mouseY(e);
if (x != offsetX || y != offsetY) {
move(x-offsetX,y-offsetY);
offsetX = x;
offsetY = y;
}
return false;
}
var start_drag = function (e) {
e = e || window.event;
offsetX=mouseX(e);
offsetY=mouseY(e);
drag=true; // basically we're using this to detect dragging
// save any previous mousemove event handler:
if (document.body.onmousemove) {
mousemoveTemp = document.body.onmousemove;
}
document.body.onmousemove = mouseMoveHandler;
return false;
}
var stop_drag = function () {
drag=false;
// restore previous mousemove event handler if necessary:
if (mousemoveTemp) {
document.body.onmousemove = mousemoveTemp;
mousemoveTemp = null;
}
return false;
}
p.onmousedown = start_drag;
p.onmouseup = stop_drag;
}
}
There is a reason for the slightly convoluted offsetX/offsetY calculations. If you notice, it's just taking the difference between mouse positions and adding them back to the position of the div being dragged. Why not just use the mouse positions? Well, if you do that the div will jump to the mouse pointer when you click on it. Which is a behavior I did not want.
You can try this
HTML
<div id="one" style="height:50px; width:50px; border:1px solid #ccc; background:red;">
</div>
Js Script for draggable div
window.onload = function(){
draggable('one');
};
var dragObj = null;
function draggable(id)
{
var obj = document.getElementById(id);
obj.style.position = "absolute";
obj.onmousedown = function(){
dragObj = obj;
}
}
document.onmouseup = function(e){
dragObj = null;
};
document.onmousemove = function(e){
var x = e.pageX;
var y = e.pageY;
if(dragObj == null)
return;
dragObj.style.left = x +"px";
dragObj.style.top= y +"px";
};
Check this Demo
This code corrects the position of the mouse (so the dragged object doesn't jump when you start dragging) and works with touch screens/phones as well
var dragObj = null; //object to be moved
var xOffset = 0; //used to prevent dragged object jumping to mouse location
var yOffset = 0;
window.onload = function()
{
document.getElementById("menuBar").addEventListener("mousedown", startDrag, true);
document.getElementById("menuBar").addEventListener("touchstart", startDrag, true);
document.onmouseup = stopDrag;
document.ontouchend = stopDrag;
}
function startDrag(e)
/*sets offset parameters and starts listening for mouse-move*/
{
e.preventDefault();
e.stopPropagation();
dragObj = e.target;
dragObj.style.position = "absolute";
var rect = dragObj.getBoundingClientRect();
if(e.type=="mousedown")
{
xOffset = e.clientX - rect.left; //clientX and getBoundingClientRect() both use viewable area adjusted when scrolling aka 'viewport'
yOffset = e.clientY - rect.top;
window.addEventListener('mousemove', dragObject, true);
}
else if(e.type=="touchstart")
{
xOffset = e.targetTouches[0].clientX - rect.left; //clientX and getBoundingClientRect() both use viewable area adjusted when scrolling aka 'viewport'
yOffset = e.targetTouches[0].clientY - rect.top;
window.addEventListener('touchmove', dragObject, true);
}
}
function dragObject(e)
/*Drag object*/
{
e.preventDefault();
e.stopPropagation();
if(dragObj == null) return; // if there is no object being dragged then do nothing
else if(e.type=="mousemove")
{
dragObj.style.left = e.clientX-xOffset +"px"; // adjust location of dragged object so doesn't jump to mouse position
dragObj.style.top = e.clientY-yOffset +"px";
}
else if(e.type=="touchmove")
{
dragObj.style.left = e.targetTouches[0].clientX-xOffset +"px"; // adjust location of dragged object so doesn't jump to mouse position
dragObj.style.top = e.targetTouches[0].clientY-yOffset +"px";
}
}
function stopDrag(e)
/*End dragging*/
{
if(dragObj)
{
dragObj = null;
window.removeEventListener('mousemove', dragObject, true);
window.removeEventListener('touchmove', dragObject, true);
}
}
div{height:400px; width:400px; border:1px solid #ccc; background:blue; cursor: pointer;}
<div id="menuBar" >A</div>
<div draggable=true ondragstart="event.dataTransfer.setData('text/plain', '12345')">
drag me
</div>
<div ondragover="return false;" ondrop="this.innerHTML=event.dataTransfer.getData('text/plain')">
drop on me
</div>
I have a draggable which on the stop event, I want move to a certain boundary IF it wasnt dragged onto anything else. Problem: the drop event handled by droppable occurs AFTER the stop event from the draggable handler. The stop handler for draggables gives the location that the draggable was dragged to, but I dont know how to determine what element is at that location. Any ideas?
You could probably just loop through the droppable elements and check their offset position. Combined with the width/height of each element, you can calculate which element the draggable was over when the stop event is fired. Something like this (this is untested, off the top of my head.)
function stopCallback (x, y) {
var $droppables = $(".droppable"),
containedDraggableAtStop = function($item, left, top) {
var itemL = $item.offset().left,
itemT = $item.offset().top,
itemR = itemL + $item.width,
itemB = itemT + $item.height;
if (top > itemT && top < itemB && left > itemL && left < itemR) {
return true;
}
return false;
},
numDroppables = $droppables.length,
i;
// x, y are the coords passed from the stop function
for (i = 0; i < numDroppables; i++) {
if (containedDraggableAtStop($droppables.eq(i), x, y)) {
return $droppables.eq(i);
}
}
return -1;
}