Draggable div in scrolling container bounces when reaching bottom - javascript

I'm putting together a custom UI control in pure JS which simulates a schedule of events. One event is represented by a draggable div.
Here are the most relevant JS functions:
function onDrag(event)
{
console.log("**** DRAG ****");
if (dragging)
{
console.log("dragging = " + dragging);
//event.preventDefault();
currentY = event.clientY - initialY;
// Check whether we need to scroll up or down
//handleScroll();
// Limit dragging to size of background container
if(currentY < 0)
{
currentY = 0;
}
else if(currentY > background_height - draggable.offsetHeight)
{
currentY = background_height - draggable.offsetHeight;
}
else
{
currentY = currentY;
}
yOffset = currentY;
moveIt(currentY);
}
}
function moveIt(yPos)
{
console.log("moveIt : yPos = " + yPos);
draggable.style.transform = "translate3d( 0px, " + yPos + "px, 0)";
}
JSFiddle here
Everything basically works, but not surprisingly, it doesn't work as cleanly as I had hoped.
When dragging to the bottom of the parent container, the container should auto scroll. In practice, the draggable div follows the mouse until I drag below the container, at which point, the draggable bounces back about 300px upwards and plants itself towards the middle of the container (often partly or completely out of the viewport).
What I want is for the draggable to just park itself at the edge of the parent container, even if I try to drag it 1000px away. If I mouse very slowly, the container scrolls properly and the draggable stops on the edge, but if I'm sloppy and drag past the edge (as I anticipate most users will do), I get the bounce.
Sample console log depicting the bounce:
**** DRAG ****
dragging = true
moveIt : yPos = 712
**** DRAG LEAVE ****
**** DRAG ****
dragging = true
moveIt : yPos = 713
**** DRAG ****
dragging = true
moveIt : yPos = 713
**** DRAG ****
dragging = true
moveIt : yPos = 415
I can switch from HTML5 drag and drop and use a MouseEvent driven style, simply by commenting out the drag and drop listeners and substituting mouse events to avoid this bouncing effect. The fact that things work the way I want with mouse events suggests to me that I'm running into something undocumented with HTML5 DnD. I'm happy to switch to this mouse event driven approach, but then I have to implement scrolling manually and when I try that, the more I drag, the further the draggable moves from the mouse pointer. You can edit the fiddle, commenting out the DnD listeners
I'm happy to have a solution either the DnD or mouse event approaches, but mainly, I want to know what's causing these odd behaviors. Is there something wrong with the way I've structured the page? Is the CSS messed up? Is there some undocumented (but perhaps expected) behavior causing these problems?

Related

How to release object after drag-drop in Javascript

I found this interesting example for a javascript Drag/Drop on https://codepen.io/islempenywis/pen/VXqJVY.
However, there is an issue with it that if you click on the top of the "TODO Item" rectangle, after drag (MouseDown + Drag + MouseUp) it is not possible to drop this item; it sticks to the cursor and travels with it :/
MouseUp is a simple function, nothing fancy :
function onMouseUp(e, item) {
isMouseDown = false;
item.style.backgroundColor = "#F44336";
}
Since I can replicate it with Chrome, Edge and Firefox, I am guessing that this is a code problem but can't find out what that would be.
Please help.
Geo
There is a miscalculation in the onMouseMove event handler:
item.style.top = e.clientY + mouseOffset.y + "px";
When you move the mouse while holding the "TODO" item, the top of the whole element, including its margin, is being placed on the vertical coordinate of the viewport (e.clientY) where you clicked and adjusted to the point in the element where you clicked (+ mouseOffset.y), so the element moves along with the cursor. But it is ignoring its margin. If you pay attention, when you click and move the item, it will move down slightly. Those are 10 pixels of margin. When you click on the top, the element will be placed slightly down the cursor, the cursor will lose it, and it gets bugged. To fix, you have to substract the margin in the calculation.
item.style.top = e.clientY - 10 + mouseOffset.y + "px";

jQuery - how to animate draggable div towards moving cursor during drag event

I am using jquery UI draggable and want to have a draggable div animate during a drag event. Imagine that when stationary, the square div in the example is attached to a magnet. Wen you click on it and start dragging, it will not drag until a certain distance threshold is reached (distance = distance between center of div and current mouse position). After that threshold is reached, the div will animate toward the mouse and should then proceed to a normal drag event.
My problem is that when the mouse is dragged past the distance threshold at a fast enough speed, the div will start flashing because it is switching between showing the animated div and the one that is being dragged. Another problem that occurs is that if you drag the mouse fast enough and then stop, the div animates to the last recorded mouse position, but by the time the calculation is made, the mouse might already be at a different position, so the mouse is at one spot and the div is at another. Any ideas on how to make the div animate towards the mouse and then continue the drag event in one smooth transition? (also, i want the animation do be long enough so that you can see the div moving. if duration is set to 1, it works fine but I want it to be more visually appealing and smooth) Here is a demo: http://jsfiddle.net/b84wn2nf/
Here is some of the code found in the demo:
$(".dragSquare").draggable({
drag: function (event, ui) {
if($(this).hasClass("stuck")){
var mx = event.pageX;
var my = event.pageY;
ui.position.left = $(this).position().left;
ui.position.top = $(this).position().top;
var d = Math.floor(Math.sqrt(Math.pow(mx - ($(this).offset().left + ($(this).width() / 2)), 2) + Math.pow(my - ($(this).offset().top + ($(this).height() / 2)), 2)));
console.log(d)
if (d > 200) {
var animateToX = event.pageX - $(this).width() / 2;
var animateToY = event.pageY - $(this).height() / 2;
$(this).stop(true, true).animate({
left: animateToX + 'px',
top: animateToY + 'px'
}, {
/*easing: 'easeOutBounce,'*/
duration: 500,
start: function () {},
complete: function () {
$(this).removeClass("stuck");
}
});
}
}
} });
Okay, So I know I posted this a long time ago, and since then, I started to use the Snap.js SVG library instead of jQuery, which makes it much easier to cancel a drag, but before the switch, I solved the problem the only way I could: by modifying the source code.
In jquery-ui.js, locate the jQuery UI Draggable section, and then scroll down to _mouseDrag method.
What you need to do in your own code is set a global variable to tell jQuery if you want the drag behavior to be overridden. I used 'cancelDrag' as the variable name. So when its set to false, dragging behaves normally.
In _mouseDrag, you will see the following code:
this.helper[0].style.left = this.position.left + "px";
this.helper[0].style.top = this.position.top + "px";
What you need to do is wrap it in a conditional statement that depends on your boolean variable, so it looks like this:
if(!cancelDrag){
this.helper[0].style.left = this.position.left + "px";
this.helper[0].style.top = this.position.top + "px";
}
Basically, if cancelDrag is set to true, jQuery drag handler will not change the position of your element. Its not ideal, and should probably not be used, but it works. Make sure that if you modify this file, you are not using the minified source.

Move row from bottom of table to top on touchmove

I'm using touch events to handle swiping a table containing roughly 350 rows. The number of rows is variable as they are pulled from a Google Spreadsheet. The table auto-scrolls continuously, which means that when a rows scrolls off at the top or bottom, I have to append or prepend it again as the case may be.
The user can touch the screen to swipe the table up or down (note that I'm not talking about mobile devices here, but rather a touch-screen monitor). I've provided an implementation for the touchmove event when swiping down, though swiping up would be similar:
function handleMove(event) {
var touch, newY, delta,
$firstItem = $(".item:first"),
$lastItem = $(".item:last");
if (!isDown) {
return;
}
if (event.originalEvent.targetTouches.length == 1) {
touch = event.originalEvent.targetTouches[0];
newY = touch.clientY;
}
delta = lastY - newY;
if (delta < 0) { //Swiping down
//Move last row from the end of the table to the beginning if first row is fully visible.
if (($firstItem.position().top + $firstItem.outerHeight(true)) >= $page.position().top) {
$page.css("margin-top", parseInt($page.css("margin-top")) - $lastItem.outerHeight(true) + "px").prepend($lastItem);
}
$page.css("margin-top", parseInt($page.css("margin-top")) - delta + "px");
}
lastY = newY;
}
The problem is that the more rows that are in the table, the more sluggish the code becomes for moving the row from the bottom to the top and adjusting the margin, causing a jittery swipe.
I'm guessing this is an expensive operation to perform. I read up on browser reflow, but I didn't see many optimizations that I could make to my code. Even tried introducing a DocumentFragment to no avail.
Any ideas on things I can try? Or am I going to have to live with this?
Thx.

disable mousedown bind event on scrollbar

on the http://www.associationtsunami.org/ site if i make a mousedown on the document the cube rotates depending on the direction the user moves the mouse.
the code is:
key code ...
).bind('mousedown touchstart', function (evt) {
delete mouse.last;
if ($(evt.target).is('a, iframe')) {
return true;
}
evt.originalEvent.touches ? evt = evt.originalEvent.touches[0] : null;
mouse.start.x = evt.pageX;
mouse.start.y = evt.pageY;
$(document).bind('mousemove touchmove', function (event) {
dragging = 1;
// Only perform rotation if one touch or mouse (e.g. still scale with pinch and zoom)
if (!touch || !(event.originalEvent && event.originalEvent.touches.length > 1)) {
event.preventDefault();
// Get touch co-ords
event.originalEvent.touches ? event = event.originalEvent.touches[0] : null;
$('.viewport').trigger('move-viewport', { x: event.pageX, y: event.pageY });
}
});
$(document).bind('mouseup touchend', function () {
dragging = 0;
$(document).unbind('mousemove touchmove');
});
});
full code https://github.com/AssociationTsunami/tsunami/blob/gh-pages/js/cube.js#L72
i would like to disable this event if a user makes the mousedown on a scrollbar - for example on the 'ONSONPARLA' page there is a TAB with ACCORDIONS, if you open any of the accordion content you get a scrollbar on the edge or within the accordion and if you try to move the scrollbar, this also moves the cube.
what is the correct way to overwrite this in the cube.js so that the cube does not turn if the event is on a scrollbar element?
It can't be done in such manner.
But there is an alternative solution. Use some custom scroll bar plugin to replace classic scroll bar. You will be able to prevent events on him. I understand this is not an excellent solution, but according to you web page you like to take a chance. :)
Here you can find few good plugins.
Good plugin example is here.

jQuery hover not triggered when element is programatically moved under the mouse

I have an image with a hover effect (higher opacity when mouse is over it). It works as desired when the mouse moves in and out.
However, the image itself is moving (I'm periodically changing the css attribute top). When the mouse does not move and the image moves under the mouse cursor, no related events are triggered. That means, the hover functions are not called. I also tried using the mouseenter and mouseleave events instead, but they don't work either.
What would be a good approach to get the desired behavior (hover effect whenever the mouse is over the image, regardless of why it got there)?
You won't be able to trigger mouse events if the mouse isn't moving, though you will be able to check where the mouse is when the image is moving. What you need to do is track the mouse position in a global variable, and check to see if that position is inside your image when it moves.
jQuery has a nice article about how to do it using their library: http://docs.jquery.com/Tutorials:Mouse_Position
To find the position of your image you can use the jQuery position function: http://api.jquery.com/position/
With that position you can create a bounds using the height/width of your image. On your image move check to see if that global mouse position is inside your image bounds and you should be good to go.
This is how I would write the code(completely untested btw):
var mousex = 0;
var mousey = 0;
jQuery(document).ready(function(){
$(document).mousemove(function(e){
mousex = e.pageX;
mousey = e.pageY;
});
})
img.move(function(){
...move code...
var p = $(this).position();
if(mousex >= p.left && mousex <= p.left + $(this).width
&& mousey <= p.top && mousey >= p.top + $(this).height)
{
...opacity code...
}
});
You could manually test to see if the mouse is in the image when you move the image then fire the desired event.
Mouse position using jQuery outside of events will show you how to keep track of the mouse position. Then just find the offset of the image and see if it's inside the image.
In addition to wajiw's and ryan's answers, you should trigger the mouseenter and mouseleave events as you detect that the mouse is over/not over the image, so that whatever code you bound to .hover() is still executed:
$(".my-image").trigger("mouseenter");
$(".my-image").trigger("mouseleave");
#wajiw has posted a great solution, but unfortunately it's plagued with typos meaning it won't work out of the box until you fix it.
Here is a class you can use which is tested and works which will allow you to test if an object is under the mouse.
Class definition
// keeps track of recent mouse position and provides functionality to check if mouse is over an object
// useful for when nodes appear underneath the mouse without mouse movement and we need to trigger hover
// see http://stackoverflow.com/questions/4403518
function MouseTracker($) {
var mouseX, mouseY;
$(document).mousemove(function(e) {
mouseX = e.pageX;
mouseY = e.pageY;
});
return {
isOver: function(node) {
var p = $(node).offset();
if (mouseX >= p.left && mouseX <= p.left + $(node).width()
&& mouseY >= p.top && mouseY <= p.top + $(node).height())
{
return true;
}
return false;
}
}
}
Usage example
var mouseTracker = new MouseTracker(jQuery);
if (mouseTracker.isOver($('#my-object-in-question'))) {
$('#my-object-in-question').trigger("mouseenter");
}
Hope that helps.
I could make this into a jQuery plugin very easily if anyone wants it, just drop me a line and I'll go ahead.
Matt

Categories