Started using RxJs. Can't find a way around this problem. I have a draggable control:
startDrag = rx.Observable.fromEvent(myElem,'mousedown')
now, because the control is too small mousemove and mouseup events should be at document level (otherwise it won't stop dragging unless cursor exactly on the element)
endDrag = rx.Observable.fromEvent document,'mouseup'
position = startDrag.flatMap ->
rx.Observable.fromEvent document,'mousemove'
.map (x)-> x.clientX
.takeUntil endDrag
now how do I "catch" the right moment when is not being dragged anymore (mouseup).
you see the problem with subscribing to endDrag? It will fire every time clicked anywhere, and not just myElem
How do I check all 3 properties at once? It should take only those document.mouseups that happened exactly after startDrag and position
Upd: I mean the problem is not with moving the element. That part is easy - subscribe to position, change element's css.
My problem is - I need to detect the moment of mouseup and know the exact element that's been dragged (there are multiple elements on the page). How to do that I have no idea.
I have adapted the drag and drop example provided at the RxJS repo to behave as you need.
Notable changes:
mouseUp listens at document.
The targeted element is added to the return from select.
Drag movements are handled inside of map and map returns the element that was targeted in the mouseDown event.
Call last after takeUntil(mouseUp) so subscribe will only be reached when the drag process ends (once per drag).
Working example:
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 mousedrag = mousedown.selectMany(function(md) {
// calculate offsets when mouse down
var startX = md.offsetX;
var startY = md.offsetY;
// Calculate delta with mousemove until mouseup
return mousemove.select(function(mm) {
if (mm.preventDefault) mm.preventDefault();
else event.returnValue = false;
return {
// Include the targeted element
elem: mm.target,
pos: {
left: mm.clientX - startX,
top: mm.clientY - startY
}
};
})
.map(function(data) {
// Update position
dragTarget.style.top = data.pos.top + 'px';
dragTarget.style.left = data.pos.left + 'px';
// Just return the element
return data.elem;
})
.takeUntil(mouseup)
.last();
});
// Here we receive the element when the drag is finished
subscription = mousedrag.subscribe(function(elem) {
alert('Drag ended on #' + elem.id);
});
}
main();
#dragTarget {
position: absolute;
width: 20px;
height: 20px;
background: #0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.7/rx.all.min.js"></script>
<div id="dragTarget"></div>
Related
I have a complex UI component with custom Drag & Drop behaviour and custom event code of several hundred lines, that basically works via regular MouseEvents.
I need to add additional functionality so that a user can Drag & Drop items from inside the component to outside the component. The "outside" works via regular HTML5 Drag & Drop events, meaning there is an element that already listens to the drop event, etc. Now, i just need to trigger the whole Drag & Drop chain, so that the drop zone element notices the event and during the drag operation i have the regular look drag image.
Simply setting my draggable item to draggable="true", does not work as the dragstart event is not thrown, it is prevented by our custom event code inside the component with event.preventDefault(). So my idea was to manually trigger dragstart events whenever the user wants to drag the item outside of the boundary of the component.
My current status is, that my onMoveStart() triggers the dragstart event, once it realizes the user wants to drag the item outside of the component. The onMove() triggers regular drag events while moving the mouse, the onMoveEnd() triggers the corresponding dragend / drop events. A somewhat similar and very boiled down version can be seen here: Fiddle.
Problem: A dragstart is properly triggered when dragging an item outside of the component, a drag event is properly triggered while regular mouse moving, the dragend event also works. The dropzone element however never receives the drop event. Additionally, there is no dragging image during the drag operation, which makes this look not working to the user. The datatransfer that i manually setup always has effectAllowed and dropOperation set to none even though i manually set them to proper values.
How can i get this working? How to implement the Drag & Drop behaviour manually? How to set the effectAllowed and dropEffect properties on DataTransfer manually so that i have a proper dragging effect without them being always overwritten to none?
Based on the code you provided, I produced something that built around it. From your post and my understanding, I believe there are 2 problems or maybe 3 that you are trying to solve
I start with dragging image. So I just clone the element that we want it to appear as dragging, set it with proper attributes
function draggingImage(el) {
let image = draggable.cloneNode(true);
image.setAttribute("id", "drag-image");
image.style.width = el.offsetWidth + 'px'
image.style.height = el.offsetHeight + 'px'
image.style.position = 'fixed'
image.style.left = el.getBoundingClientRect().left + 'px';
image.style.top = el.getBoundingClientRect().top + 'px';
image.style.background = getComputedStyle(draggable).backgroundColor
image.style.opacity = .5
let div = document.body.appendChild(image);
}
as for now, the dragging image is static, as expected, and we want it to move as our mouse move thus the dragging effect
let image = document.getElementById('drag-image')
image.style.left = image.getBoundingClientRect().left + e.movementX + 'px'
image.style.top = image.getBoundingClientRect().top + e.movementY + 'px'
while our mousemove with wasdragging = true, it's a good time to detect what's underneath our cursor, are we within the dropzone or outside
function getDropzone(e, left, top, id) {
// let rect1 = document.getElementById(id).getBoundingClientRect();
let rect1 = drop.getBoundingClientRect()
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
we can add the above function within onDrag function. The getDropzone function return true or false, and that's all needed. From there we can execute our logic or styling accordingly.
I also add global variable called as data, then a function that work almost similar to dataTransfer concept, naively.
let data = {}
function manualSetData(variable, v) {
data[variable] = v
}
with this, we can just get the data that we set with this function in ondragend
let getData = data[variable]
so this is DnD from scratch in a nutshell
const draggable = document.querySelector("#draggable");
const drop = document.querySelector("#drop");
const condition = true;
let wasDragging = false;
let allowedDrop = false
let data = {}
draggable.addEventListener("mousedown", onMoveStart)
document.addEventListener("mousemove", onMove)
document.addEventListener("mouseup", onMoveEnd)
function onMoveStart(e) {
event.preventDefault();
event.stopPropagation();
let el = e.target
// Other logic and custom event handling ...
if (condition) {
onDragStart(el);
}
}
function onMove(e) {
// Other logic ...
if (wasDragging) {
onDrag(e);
}
}
function onMoveEnd(event) {
// Other logic ...
if (wasDragging) {
onDragEnd(event.target);
}
}
function onDragStart(el) {
draggingImage(el)
wasDragging = true;
manualSetData('helloMom', el)
}
function draggingImage(el) {
let image = draggable.cloneNode(true);
image.setAttribute("id", "drag-image");
image.style.width = el.offsetWidth + 'px'
image.style.height = el.offsetHeight + 'px'
image.style.position = 'fixed'
image.style.left = el.getBoundingClientRect().left + 'px';
image.style.top = el.getBoundingClientRect().top + 'px';
image.style.background = getComputedStyle(draggable).backgroundColor
image.style.opacity = .5
let div = document.body.appendChild(image);
}
function manualSetData(variable, v) {
data[variable] = v
}
function onDrag(e) {
let image = document.getElementById('drag-image')
image.style.left = image.getBoundingClientRect().left + e.movementX + 'px'
image.style.top = image.getBoundingClientRect().top + e.movementY + 'px'
let left = e.pageX;
let top = e.pageY;
let overlap = getDropzone(e, left, top)
drop.style.backgroundColor = allowedDrop ? 'green' : 'blue'
if (overlap) {
document.body.style.cursor = 'copy';
allowedDrop = true
} else {
document.body.style.cursor = 'no-drop';
allowedDrop = false
}
}
function getDropzone(e, left, top, id) {
// let rect1 = document.getElementById(id).getBoundingClientRect();
let rect1 = drop.getBoundingClientRect()
// to detect the overlap of mouse into the dropzone, as alternative of mouseover
var overlap = !(rect1.right < left ||
rect1.left > left ||
rect1.bottom < top ||
rect1.top > top)
return overlap
}
function onDragEnd(element) {
let image = document.getElementById('drag-image')
image.remove()
document.body.style.cursor = 'default';
if (allowedDrop) {
let getData = data['helloMom']
drop.appendChild(getData)
}
wasDragging = false;
}
#draggable {
background-color: red;
}
#drop {
background-color: blue;
}
<div id="draggable" draggable="true">
fancy content
</div>
<div id="drop">
drop area
</div>
I'm currently building a menu that is also draggable and I'm using the following on each individual tab:
(mousedown)="dragging = false"
(mousemove)="checkMouseMove($event)"
(mouseup)="changeRoute('forms')"
Change Route looks like this:
changeRoute(routeName: string) {
// manual reset for dragging.
if (this.dragging) {
return;
}
Past this is just my routing and switch statement's that correctly will change route and apply styling etc.
Previously inside the mousemove event I just had dragging = true but the problem with this is that even the slightest movement will mean that a click does not occur when it's likely to be intended to.
My first instinct was that I need to add a buffer to the amount of space it will need to move to be called a drag but given the output events I'm not sure how to achieve this.
checkMouseMove(event) {
console.log(event);
}
This event provides me with the following output:
How can I use these events in conjunction with my checkMouseMove / Component to only change dragging after a reasonable amount of movement?
You can save the last mouse position and calculate the distance the cursor moved.
Based on this distance you can filter small and unwanted drags.
var lastEvent;
function checkMouseMove (event){
var dx = 0;
var dy = 0;
if (lastEvent) {
dx = event.clientX - lastEvent.clientX;
dy = event.clientY - lastEvent.clientY;
}
var d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
if (d > 5) { /* handle mouse move here */ }
lastEvent = event;
}
You can also use any other non euclidean heuristics like Manhattan or Chebyshev.
I need to manually construct/fire a mousedown event in a way that can be handled by any relevant event handlers (either in straight JS, jQuery, or interact.js), just as a natural mousedown event would. However, the event does not seem to trigger anything the way I expect it to.
I am trying to make some irregularly shaped images draggable using the interact.js library. The images are simply rectangular img elements with transparent portions. On each element, I have defined 2 interact.js event listeners:
Checking if the click was inside the image area, disabling drag if not (fires on "down" event)
Handling the drag (fires on "drag" event)
However, if the img elements are overlapping, and the user clicks in the transparent area of the top element but on the filled area of the lower element, the lower element should be the target of the drag. After trying several things (see below), I have settled on the solution of: re-ordering the z-indexes of the elements at the same time that I disable drag in step 1, then re-firing the mousedown event on all lower elements. I'm using the "native" event type (not jQuery or interact.js) in hopes that it will literally just replicate the original mousedown event.
// code to re-assign "zIndex"s
function demote(element, interactible, event){
// block dragging on element
interactible.draggable(false);
// get all images lower than the target
var z = element.css("zIndex");
var images = $("img").filter(function() {
return Number($(this).css("zIndex")) < z;
});
// push the target to the back
element.css("zIndex",1);
// re-process all lower events
$(images).each( function () {
// move element up
$(this).css("zIndex",Number($(this).css("zIndex"))+1);
// re-fire event if element began below current target
elem = document.getElementById($(this).attr('id'));
// Create the event.
e = new MouseEvent("mousedown", {clientX: event.pageX, clientY: event.pageY});
var cancelled = !elem.dispatchEvent(e);
});
}
Unfortunately, this does not work, as the mousedown event does not register with any of the handlers. Why?
I have all the (relevant) code at this JSFiddle: https://jsfiddle.net/tfollo/xr27938a/10/
Note that this JSFiddle does not seem to work as smoothly as it does in a normal browser window, but I think it demonstrates the intended functionality.
Other things I have tried:
Lots of people have proposed different schemes to handle similar problems (i.e. forwarding events to lower layers, using pointer-events: none, etc), but none seem to work to trigger the interact.js handler and start a drag interaction on the right element. I also tried using interaction.start (provided by interact.js) but it seems buggy--there is at least one open issue on the topic and when I tried to start a new drag interaction on a target of my choice I got lots of errors from within the library's code.
I'm not against revisiting any of these solutions per se, but I would also really like to know why manually firing a mousedown event won't work.
The idea is to listen down event on parent element and to choose manually drag target. Also I didn't use z-index for choosing which image to drag, because z-index doesn't work with position:static. Instead of that I just gave priorities to images, it's all up to you. https://jsfiddle.net/yevt/6wb5oxx3/3/
var dragTarget;
function dragMoveListener (event) {
var target = event.target,
// keep the dragged position in the data-x/data-y attributes
x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
// translate the element
target.style.webkitTransform =
target.style.transform =
'translate(' + x + 'px, ' + y + 'px)';
// update the posiion attributes
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
function setDragTarget(event) {
//choose element to drag
dragTarget = $('#parent').find('img')
.filter(function(i, el) {
var clickCandicateRect = el.getBoundingClientRect();
return insideBoundingRect(event.x, event.y, clickCandicateRect);
}).sort(byPriority).get(0);
}
function insideBoundingRect(x, y, rect) {
return (rect.left <= x) && (rect.top <= y) && (rect.right >= x) && (rect.bottom >= y);
}
function byPriority (a, b) {
return $(b).data('priority') - $(a).data('priority');
}
function startDrag(event) {
var interaction = event.interaction;
var target = dragTarget;
if (!interaction.interacting()) {
interaction.start({ name: 'drag' }, event.interactable, target);
}
}
//Give priority as you wish
$('#face1').data('priority', 2);
$('#face2').data('priority', 1);
interact('#parent').draggable({
//use manualStart to determine which element to drag
manualStart: true,
onmove: dragMoveListener,
restrict: {
restriction: "parent",
endOnly: true,
elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
},
})
.on('down', setDragTarget)
.on('move', startDrag);
Have you tried to set the css property pointer-events: none on the higher levels (it could also be set via javascript)?
I have an AngularJS component that should react to either a single click or a drag (resizing an area).
I started to use RxJS (ReactiveX) in my application, so I try to find a solution using it. The Angular side of the request is minor...
To simplify the problem (and to train myself), I made a slider directive, based on the rx.angular.js drag'n'drop example: http://plnkr.co/edit/UqdyB2
See the Slide.js file (the other code is for other experiments). The code of this logic is:
function(scope, element, attributes)
{
var thumb = element.children(0);
var sliderPosition = element[0].getBoundingClientRect().left;
var sliderWidth = element[0].getBoundingClientRect().width;
var thumbPosition = thumb[0].getBoundingClientRect().left;
var thumbWidth = thumb[0].getBoundingClientRect().width;
// Based on drag'n'drop example of rx-angular.js
// Get the three major events
var mousedown = rx.Observable.fromEvent(thumb, 'mousedown');
var mousemove = rx.Observable.fromEvent(element, 'mousemove');
var mouseup = rx.Observable.fromEvent($document, 'mouseup');
// I would like to be able to detect a single click vs. click and drag.
// I would say if we get mouseup shortly after mousedown, it is a single click;
// mousedown.delay(200).takeUntil(mouseup)
// .subscribe(function() { console.log('Simple click'); }, undefined, function() { console.log('Simple click completed'); });
var locatedMouseDown = mousedown.map(function (event)
{
event.preventDefault();
// console.log('Click', event.clientX - sliderPosition);
// calculate offsets when mouse down
var initialThumbPosition = thumb[0].getBoundingClientRect().left - sliderPosition;
return { from: initialThumbPosition, offset: event.clientX - sliderPosition };
});
// Combine mouse down with mouse move until mouse up
var mousedrag = locatedMouseDown.flatMap(function (clickInfo)
{
return mousemove.map(function (event)
{
var move = event.clientX - sliderPosition - clickInfo.offset;
// console.log('Move', clickInfo);
// calculate offsets from mouse down to mouse moves
return clickInfo.from + move;
}).takeUntil(mouseup);
});
mousedrag
.map(function (position)
{
if (position < 0)
return 0;
if (position > sliderWidth - thumbWidth)
return sliderWidth - thumbWidth;
return position;
})
.subscribe(function (position)
{
// console.log('Drag', position);
// Update position
thumb.css({ left: position + 'px' });
});
}
That's mostly D'n'D constrained horizontally and to a given range.
Now, I would like to listen to mousedown, and if mouse up happens within a short while (say 200 ms, to adjust), I see it as a click and I do a specific treatment (eg. resetting the position to zero).
I tried with delay().takeUntil(mouseup), as seen in another SO answer, without success. Perhaps a switch() might be needed, too (to avoid going the drag route).
Any idea? Thanks in advance.
You can use timeout (timeoutWith if you are using ReactiveX/RxJS)
var click$ = mousedown.flatMap(function (md) {
return mouseup.timeoutWith(200, Observable.empty());
});
If the mouseup doesn't occur before the timeout it will just propagate an empty Observable instead. If it does then the downstream observer will receive an event.
Isn't the trick with delay(Xms).takeUntil(mouseup) doing the opposite of what you want? I mean, you want to detect when the mouseup event happens before the countdown, while the aformentioned trick detect when the mouseup event happens after.
I would try something around those lines (untested for now, but hopefully it will orient you in some positive direction):
var click$ = mousedown.flatMap(function ( mouseDownEv ) {
return merge(
Rx.just(mouseDownEv).delay(Xms).map(function ( x ) {return {event : 'noclick'};}),
mouseup.map(function ( mouseUpEv ) {return {event : mouseUpEv};})
).first();
});
The idea is to race the mouseup event against a dummy emission happening after your delay, and see who wins. So if click$ emits 'noclick' then you can consider that no click happened.
Hopefully that works, i will test soon but if you do before me, let me know.
I am using html5 'draggable' attribute to drag 2 elements within a container and svg line to connect the two.
Once connected, Dragging the first Div should redraw the connecting svg line (i do on dragover event by calling 'handleDragOver' function). But If you drag the first div faster, drop event is not triggered and the div maintains it's original place while the line gets drawn.
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
//Some code doing DOM computation and manipulation
}
return false;
//e.dataTransfer.dropEffect = 'move';
}
How can I ensure that drop event gets triggered every time.
Please note:
I can't use any framework, just plain javascript
I can't not redraw line while dragging.
drag functionality works fine when I'm not doing any computation/redrawing in 'handleDragOver'
Here is the code: http://jsfiddle.net/bhuwanbhasker/3yx9ds4m/1/
Please take a look on the updated fiddle: http://jsfiddle.net/sejoker/d4vs9fgs/1/
following changes:
line manipulation moved from dragover's handler to handleDrop, redraw line once during drag'n'drop operation
calling moveLine in setTimeout seems to improve usability (optional)
minor changes in moveLine function to connect draggable elements properly (optional)
function handleDrop(e) {
console.log('enter Drop');
if (e.stopPropagation) {
e.stopPropagation(); // stops the browser from redirecting.
}
var offset = e.dataTransfer.getData('Text').split(',');
dragSrcEl.style.left = (e.clientX + parseInt(offset[0], 10)) + 'px';
dragSrcEl.style.top = (e.clientY + parseInt(offset[1], 10)) + 'px';
setTimeout(function(){
var elem = svgLine.aLine;
if (elem !== null) {
var x = e.clientX - svgLine.left,
y = e.clientY - svgLine.top;
moveLine(x, y, elem, offset[2]);
}
}, 50)