TLDR: Here is a JS Fiddle https://jsfiddle.net/qn8jhsaf/10/
I am attempting to create a simple drag and drop UI using mouse events as I didn't like any of the libraries available for the framework I am using. While I have all the events wired up and am getting the application side behavior I want, animating the div moving around isn't working how I would expect.
I am trying to use vanilla JS to insert a cloned div into the dom, absolutlely position it, and then move it around with tranform: translate as the mouse moves around.
So in onMouseDown, I'm doing just that:
const onMouseDown = (e) => {
isDragging = true
const rect = element.getBoundingClientRect()
let node = element.cloneNode(true)
node.style.position = 'absolute'
node.style.zIndex = 1000
node.style.left = rect.x
node.style.top = rect.y
console.log(rect.x)
console.log(node.style.left)
draggableNode = node
document.body.append(node)
}
The curious part happens when I call node.style.left =. It does not throw an error or anything but the value remains stubbornly empty ("") and it does not get translated to the style attribute of the cloned div. As far as I can tell from documentation this is a supported thing. I'm not sure what else to try.
The left and top styles need to be strings.
node.style.left = `${rect.x}px`
node.style.top = `4{rect.y}px`
FWIW, I swear I tried this at some point but ¯\_(ツ)_/¯
Related
I'm working on a hobby project similar to markup.io where you can load in any website and annotate it. I can't figure out how to add an annotation that behaves like it does in markup.io:
Doesn't interrupt the styling or layout of the website you are annotating
The annotation keeps the correct position when scrolling and resizing the window
From what I can see they place an absolute positioned div inside the element that you clicked on. From my understanding by reading the docs that div would position itself based on the closest positioned ancestor. How would you calculate the correct top and left values to position the annotation to where the user clicked? Is there a better way to do this?
I'm using React if that matters.
Things that I have tried:
Append the following bit of html to the element that was clicked:
<div style="width:0px; height:0px; position:relative;">
<div style="width:50px;height:50px;position:absolute; ">this is the annotation </div>
</div>
Problem: This would mess with the page layout because of the relative positioned div that is not ignored by the document flow.
Create fixed overlay over the entire page. Get the css selector of the clicked element. Draw annotation on the fixed overlay at the x,y position of the element.
Problem: Whenever the user would scroll or resize the window the annotation would need to be redrawn at the new position of the element. I used getBoundintClientRect to get the new position and this would cause a reflow and caused the whole website to have severe perfomance issues when dealing with 100+ annotations.
Hopefully someone can point me in the right direction!
The general idea is as follows:
Find the parent of the element that you clicked on
Check if they are positioned (anything other than static)
If it is static search for the closest element that is positioned.
Set the new badge/annotation top and left position to that of the mouse minus the top and left of the element that you're going to append it to (in this case called parent).
Also account for the width and height by subtracting half of each to perfectly center your annotation.
// In my case I put the webpage in an Iframe. If this is your own page
// you can just use document.
iframe.contentWindow.document.addEventListener(
'click',
(e: MouseEvent) => {
// step 1: find the parent.
let parent = e.target.parentElement;
let computedStyle = window.getComputedStyle(parent);
// step 2 & 3: Look up the first positioned element and make this the
// the element that you're going to append your badge/annotation to.
while (
computedStyle.position === 'static' &&
parent.parentElement !== null
) {
parent = parent.parentElement;
computedStyle = window.getComputedStyle(parent);
}
// style the annotation the way you want to
const badge = document.createElement('div');
const { top, left } = parent.getBoundingClientRect();
badge.style.position = 'absolute';
// step 4 and 5 get the mouse position through e.clientX and Y and
// subtract the appropriate value like below to place it exactly at the mouse position
badge.style.top = `${e.clientY - top - 5}px`;
badge.style.left = `${e.clientX - left - 5}px`;
badge.style.backgroundColor = 'red';
badge.style.width = '10px';
badge.style.height = '10px';
badge.style.borderRadius = '50%';
badge.style.zIndex = '9999';
parent.appendChild(badge);
}
);
Basically I just want to get the position of a dragged element. I have read the previous questions like this one: How to get the position of a draggable object. But that is based on jQuery. Is there a way to achieve this with pure javascript?
I have tried something like below but doesn't work:
itemDrag() {
let card = document.querySelector("div.draggable");
let rect = card.getBoundingClientRect();
console.log(rect.top, rect.right, rect.bottom, rect.left);
}
I triggered this with ondrag event. It turns out that although the event is fired multiple times, the rect remains in the original position. Is there a way to handle that?
Assume there is a scroll list, when I insert some new DOM append to the current dom, it works fine.
pullup
But if I insert some new DOM before, the new DOM will be in the viewport, and the old DOM will be push down.
pulldown
Is there a way that I can make pulldown behave like pullup? Without manually set scrollTop after?
In Google Chrome, I cannot reproduce the behavior you describe. Could there be a difference in how browsers manage maintaining scroll positions upon DOM edits?
In Safari, I do see the behavior you describe. I'm not aware of any different injection methods to bypass the issue, and I don't think css-like tricks such as translateY or position: absolute are the way to go...
So, that brings us to the one option you said you didn't want to use... Modifying scrollTop. I don't see many problems with an approach like this though...
function scrollSafeInsertBefore(fragment, el, scrollContainer) {
var scroller = scrollContainer || document.body;
var parent = el.parentElement;
var aboveScroll = el.getBoundingClientRect().top < 0;
if (aboveScroll) {
var st = parent.scrollHeight;
parent.insertBefore(fragment, el);
var dy = parent.scrollHeight - st;
scroller.scrollTop += dy; // Move back the height change
} else {
parent.insertBefore(fragment, el); // Normal injection
}
}
Fiddle: https://jsfiddle.net/pq4t2zbn/ (press the insert button)
I am working on an AngularJs web app and have just started using ngTouch to recognize swipe left and right, I want to use this to open and close my side bar menu. Currently it works and the menu opens and close however, I have the swipe events tied to the entire wrapper like so <div ng-style="body_style" id="wrapper" ng-swipe-right="showMenu()" ng-swipe-left="hideMenu()"> which makes the entire app the swipe area. I would like to be able to set it to just an area in the top left quadrant of the app like so:
This will allow me to set up other swipe areas for additional functionality rather than being limited to just one area.
I know that I can make a div and place it there with defined width and height but the div is going to block content in other areas so that when someone goes to "click" something they would actually be clicking the invisible div for the swipe area. I know there has to be a way around this but I cannot think of one.
tldr: How do I set up an area to capture swipe events that will not effect the interactivity of other elements that may be below it?
In the source for angular-touch, it uses a function called bind. Bind takes an element that should be watched for swipes and watches its events, such as start. So if you wanted to, you could change the source to stop executing the rest of the code if the start is outside of some x or y coordinate that you set.
You could change this code,
pointerTypes = pointerTypes || ['mouse', 'touch'];
element.on(getEvents(pointerTypes, 'start'), function(event) {
startCoords = getCoordinates(event);
active = true;
totalX = 0;
totalY = 0;
lastPos = startCoords;
eventHandlers['start'] && eventHandlers['start'](startCoords, event);
});
To something like this:
pointerTypes = pointerTypes || ['mouse', 'touch'];
element.on(getEvents(pointerTypes, 'start'), function(event) {
startCoords = getCoordinates(event);
if (startCoords.y <= 400 && startCoords.y >= 100 && startCoords.x <=500) {
active = true;
totalX = 0;
totalY = 0;
lastPos = startCoords;
eventHandlers['start'] && eventHandlers['start'](startCoords, event);
}
});
Kind of an ugly hack, but it'll work.
I'm working on a small jQuery plugin that mimics the jQuery UI draggable/droppable behavior with native HTML5 drag and drop events.
A feature I'd want to add is the ability to specify the node which will serve as the drag proxy.
I did a bit of research, and according to MDN, to do this requires the use of setDragImage(), passing an image or an element.
What is the support for setDragImage in different browsers?
I've noticed there's a plugin named jquery.event.drag which takes a different than I expected to this problem.
Would this feature require me to make some kind of workaround like the above plugin, or should this be possible out-of-the-box in most or all browsers using setDragImage?
EDIT
After playing around a bit with this functionality, it would seem that this function is quite limited.
Besides having no support in quite a few browsers, using an arbitrary DOM element as the helper requires it to be in the DOM tree and visible, and so you have the element itself on the body, and a copy of it as the handler. This is mostly unwanted for this sort of plugin.
Further more, rendering is also problematic even when the right terms are met. When trying to create a helper from <span>TEST</span>, the helper itself only showed a white rectangle with the dimensions of the span.
Are these issues that were to be expected according to the specs? Could they be fixed in code or would they require a workaround?
setDragImage is IMO a vital feature for any non trivial drag and drop use case. e.g consider a multi select list where a drag needs to include all the selected items and not just the row that the drag gesture was made on. it's odd that the thing you want to set needs to be visible in the DOM but even worse is that this method is not implemented at all in IE as of version 11.
However, with a bit of effort I was able to get it working reasonably satisfactorily. The custom drag image node can be removed from the DOM in a timeout 0 function. so add it to the DOM in dragstart then use it in set drag image and then remove it. This works perfectly in FF but in chrome the drag image node will flicker before the timeout fires. One way to prevent this is to position it such that the actual browser generated drag image will appear in exactly the same place, this is not as bad as it sounds since you can control the position of the custom drag image relative to the cursor.
I was playing with this recently and was able to get it working on IE as well. the trick there is to get IE to drag the custom drag image node and not the node that dragstart fired on. you can do this with the IE specific dragDrop() method.
The final thing to be aware of is that on windows there is a 300px limit on the width of the custom drag image node this applies to all draggables not just the custom node actually. so the browser applies a heavy radial gradient if the drag image is too big.
http://jsfiddle.net/stevendwood/akScu/21/
$(function() {
(function($) {
var isIE = (typeof document.createElement("span").dragDrop === "function");
$.fn.customDragImage = function(options) {
var offsetX = options.offsetX || 0,
offsetY = options.offsetY || 0;
var createDragImage = function($node, x, y) {
var $img = $(options.createDragImage($node));
$img.css({
"top": Math.max(0, y-offsetY)+"px",
"left": Math.max(0, x-offsetX)+"px",
"position": "absolute",
"pointerEvents": "none"
}).appendTo(document.body);
setTimeout(function() {
$img.remove();
});
return $img[0];
};
if (isIE) {
$(this).on("mousedown", function(e) {
var originalEvent = e.originalEvent,
node = createDragImage($(this), originalEvent.pageX, originalEvent.pageY);
node.dragDrop();
});
}
$(this).on("dragstart", function(e) {
var originalEvent = e.originalEvent,
dt = originalEvent.dataTransfer;
if (typeof dt.setDragImage === "function") {
node = createDragImage($(this), originalEvent.pageX, originalEvent.pageY);
dt.setDragImage(node, offsetX, offsetY);
}
});
return this;
};
}) (jQuery);
$("[draggable='true']").customDragImage({
offsetX: 50,
offsetY: 50,
createDragImage: function($node) {
return $node.clone().html("I'm a custom DOM node/drag image").css("backgroundColor", "orange");
}
}).on("dragstart", function(e) {
e.originalEvent.dataTransfer.setData("Text", "Foo");
});
});