I'm trying to make a drag-and-drop engine in JavaScript. Right now, I'm adding a bounds feature which will trap the .drag object inside its parent element. However, to do this, I need to understand how positioning works in html, and I don't. Can anyone thoroughly explain it?
Javascript Engine:
// JavaScript Document
var posX;
var posY;
var element;
var currentPos;
document.addEventListener("mousedown", drag, false);
function drag(event) {
if(~event.target.className.search(/drag/)) {
element = event.target;
element.style.zIndex="100";
currentPos = findPos(element);
posX = event.clientX -currentPos.x;
posY = event.clientY -currentPos.y;
if(~event.target.className.search(/bound/))
document.addEventListener("mousemove", boundMovement, false);
else
document.addEventListener("mousemove", freeMovement, false);
}
}
function freeMovement(event) { // This functions works
if (typeof(element.mouseup) == "undefined")
document.addEventListener("mouseup", drop, false);
//Prevents redundantly adding the same event handler repeatedly
element.style.left = event.clientX - posX + "px";
element.style.top = event.clientY - posY + "px";
}
function boundMovement(event) { // This function doesn't work
if (typeof(element.mouseup) == "undefined")
document.addEventListener("mouseup", drop, false);
//Prevents redundantly adding the same event handler repeatedly
// Below logic is false- I wish to understand why =]
currentPos = findPos(element.offsetParent);
if((event.clientX - posX) <= currentPos.x)
element.style.left = event.clientX - posX + "px";
if((event.clientY - posY) <= currentPos.y)
element.style.top = event.clientY - posY + "px";
}
function drop() {
element.style.zIndex="1";
document.removeEventListener("mousemove", boundMovement, false);
document.removeEventListener("mousemove", freeMovement, false);
document.removeEventListener("mouseup", drop, false);
//alert("DEBUG_DROP");
}
function findPos(obj) { // Donated by `lwburk` on StackOverflow
var curleft = curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
}
Here is the CSS I am using:
#charset "utf-8";
/* CSS Document */
.drag {
position: absolute;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.bound {
/* Class to signify that the drag_object can not leave the parent element */
;
}
.square {
width: 100px;
height: 100px;
background: red;
cursor:move;
}
p {
padding: 0px;
margin: 0px;
outline-style: dotted;
outline-color: #000;
outline-width: 1px;
}
Some example HTML:
<p class="drag bound square">Thing One</p>
<p class="drag square">Thing Two</p>
Please note I am including the JavaScript so that if I have questions on how things are applied relative to what I've written. Also, thank you all for reading and helping. StackOverflow has been an exceptional resource in learning how to code in JavaScript.
EDIT:
1) I should say that I am coding the engine to help me learn the language. This is my first week of JavaScript, and I would like to be able to code in the language before I use a library.
2) For example I would really like for someone to explain how offsets are working here. I would like to know how instead of using position:absolute to make my JavaScript engine, I can use position:relative so that elements can stack on top of each other ect.
I've posted a solution at http://jsfiddle.net/vJ6r6/.
First of all, I nested the items to be dragged inside the bounding box:
<div class="bound">Thing One
<div class="drag square">Thing Two</div>
<div class="drag square">Thing Three</div>
</div>
Also, I turned them into div's because p's can't be nested. (Don't forget to change the style declaration as well.)
Then, I set styles on the bounding box:
<style>
.bound {
margin: 100px;
width: 400px;
height: 400px;
position: relative;
}
</style>
The key property is position: relative, which causes the items inside it to be positioned relative to it, rather than to the page. Note that because I'm using relative positioning, this example works best when you want to keep the items in a particular container.
My changes to the JavaScript were more radical, so here's the whole thing:
<script>
var dragInfo;
function down(event) {
if (~event.target.className.search(/drag/)) {
document.addEventListener("mouseup", drop, false);
var t = event.target;
t.style.zIndex = 100;
dragInfo = {
element: t,
// record the bounds
maxX: t.parentNode.offsetWidth - t.offsetWidth,
maxY: t.parentNode.offsetHeight - t.offsetHeight,
// we don't need findPos, because it's no longer relative to the page
posX: event.clientX - t.offsetLeft,
posY: event.clientY - t.offsetTop
};
document.addEventListener("mousemove", freeMovement, false);
}
}
function freeMovement(event) {
// the min and max calculations keep the X and Y within the bounds
dragInfo.element.style.left = Math.max(0, Math.min(event.clientX - dragInfo.posX, dragInfo.maxX)) + "px";
dragInfo.element.style.top = Math.max(0, Math.min(event.clientY - dragInfo.posY, dragInfo.maxY)) + "px";
}
function drop() {
dragInfo.element.style.zIndex = 1;
document.removeEventListener("mousemove", freeMovement, false);
document.removeEventListener("mouseup", drop, false);
}
document.addEventListener("mousedown", down, false);
</script>
Note that this line:
dragInfo.element.style.left = Math.max(0, Math.min(event.clientX - dragInfo.posX, dragInfo.maxX)) + 'px';
Is equivalent to this:
var x = event.clientX - dragInfo.posX;
if (x < 0) x = 0;
if (x > dragInfo.maxX) x = dragInfo.maxX;
dragInfo.element.style.left = x + 'px';
I think the problem (with your example at least) is that when you call the findPos function for the bounded element in boundMovement, you are passing its parent, which has no parent of its own. So inside the findPos function this specific line
if (obj.offsetParent) {
// ...
is returning undefined because it doesn't have a parent. Try removing that if statement and try it again.
Have you seen this jQuery demo http://jqueryui.com/demos/draggable/#events
using jQuery makes JavaScript much easier, and will also ensure that your code works cross browser.
You may find this helpful
A list Apart's css-positioning 101
What exactly is the problem you are having?
Absolute removes the element from normal "flow of the document" and allows you to position it where you want.
Relative position is a lot like static but allows you to absolute position elements within it.
Static follows the normal document flow. (stacking)
By the way, I recommend you use a library or framework like JQuery or Prototype instead of building this by hand. It will save you hours of frustration and testing in different browsers.
Edit:
Since you are hellbent doing this the hard way...I hope I can be of some help.
I recomend you download the the firebug addon for Firefox if that is what you use or get familiar Javascript Console under tools in Chrome.
HTML
<p class="drag bound square">Thing One</p>
<p class="drag square">Thing Two</p>
There is no parent/child relationship here did you mean for the document to be the boundry?
freeMovement does not call findPos on the elements offSetParent where BoundMovement does.
This is retuning undefined since the document does not have that property.
Try wrapping these two paragraphs in something other than the body tag.
<div id='container'>
<p class="drag bound square">Thing One</p>
<p class="drag square">Thing Two</p>
</div>
Related
I am currently working on an online presentation software. For the sake of this question imagine it as powerpoint or keynote.
I want to be able to add elements to the slide and then drag them around (live), getting the new position, updating the database.
However I want to do this without any use of external libraries or frameworks, including jQuery.
Can anyone point me in a direction for my research? My current ideas to implement this are pretty messy. Especially the live-dragging is what's giving me headaches.
Thanks!
UPDATE!
the elements look something like this:
<div class="textelement"
data-id="528fc9026803fa9d4b03e506"
data-role="Textelement"
style=" left: 50px;
top: 50px;
z-index: 0;
width: 72px;
height: 72px;">
<div class="textnode">slide: 0 textelement: 0</div>
</div>
While HTML5 does provide native drag and drop, this isn't what you asked for. Check out this simple tutorial to accomplish dragging in vanilla JS: http://luke.breuer.com/tutorial/javascript-drag-and-drop-tutorial.aspx
There is great vanilla JS snippet available, but with one problem - when element start dragged on clickable element, it "clicks" on mouseup: see it on http://codepen.io/ekurtovic/pen/LVpvmX
<div class="draggable">
Dont click me, just drag
</div>
<script>
// external js: draggabilly.pkgd.js
var draggie = new Draggabilly('.draggable');
</script>
here is the "plugin": draggabilly
And, here is my independent solution, working by :class: of the element:
(function (document) {
// Enable ECMAScript 5 strict mode within this function:
'use strict';
// Obtain a node list of all elements that have class="draggable":
var draggable = document.getElementsByClassName('draggable'),
draggableCount = draggable.length, // cache the length
i; // iterator placeholder
// This function initializes the drag of an element where an
// event ("mousedown") has occurred:
function startDrag(evt) {
that.preventDefault();
// The element's position is based on its top left corner,
// but the mouse coordinates are inside of it, so we need
// to calculate the positioning difference:
var diffX = evt.clientX - this.offsetLeft,
diffY = evt.clientY - this.offsetTop,
that = this; // "this" refers to the current element,
// let's keep it in cache for later use.
// moveAlong places the current element (referenced by "that")
// according to the current cursor position:
function moveAlong(evt) {
evt.preventDefault();
var left = parseInt(evt.clientX - diffX);
var top = parseInt(evt.clientY - diffY);
// check for screen boundaries
if (top < 0) { top = 0; }
if (left < 0) { left = 0; }
if (top > window.innerHeight-1)
{ top = window.innerHeight-1; }
if (left > window.innerWidth-1)
{ left = window.innerWidth-1; }
// set new position
that.style.left = left + 'px';
that.style.top = top + 'px';
}
// stopDrag removes event listeners from the element,
// thus stopping the drag:
function stopDrag() {
document.removeEventListener('mousemove', moveAlong);
document.removeEventListener('mouseup', stopDrag);
}
document.addEventListener('mouseup', stopDrag);
document.addEventListener('mousemove', moveAlong);
return false;
}
// Now that all the variables and functions are created,
// we can go on and make the elements draggable by assigning
// a "startDrag" function to a "mousedown" event that occurs
// on those elements:
if (draggableCount > 0) for (i = 0; i < draggableCount; i += 1) {
draggable[i].addEventListener('mousedown', startDrag);
}
}(document));
The HTML5 current specification removed the <frameset> tag.
There is a nice feature of <frameset> which is not easy to reproduce without it:
In a frameset, you can change the position of the line separating the frames with the mouse.
How would I provide the same functionality with with using DIVs in JavaScript?
I've come across the following which demonstrates the behavior I'm looking for. However I would like to avoid using JQuery, even though using jQuery should be the preferred way.
After looking at your example fiddle, I can say that this is actually quite easy without jQuery.
All the functions there are just simple innerHTML and style manipulation, and event subscription.
A direct rewrite of that code without jQuery would be:
var i = 0;
document.getElementById("dragbar").onmousedown = function (e) {
e.preventDefault();
document.getElementById("mousestatus").innerHTML = "mousedown" + i++;
window.onmousemove = function (e) {
document.getElementById("position").innerHTML = e.pageX + ', ' + e.pageY;
document.getElementById("sidebar").style.width = e.pageX + 2 + "px";
document.getElementById("main").style.left = e.pageX + 2 + "px";
};
console.log("leaving mouseDown");
};
window.onmouseup = function (e) {
document.getElementById("clickevent").innerHTML = 'in another mouseUp event' + i++;
window.onmousemove = null;
};
So here is the same fiddle with pure JS.
EDIT: As #BenjaminGruenbaum pointed out, overriding the on* properties on a DOM element is not the same as specifying a new event handler.
Overriding properties like onmouseup, onload, onclick on DOM elements is the "old" way, and therefore it was supported in even the stone age of JS. My code above was written like that.
Nowadays the standard way of adding and removing event handlers are addEventListener and removeEventListener. They are not supported in old IE (but this can be worked around).
It let's you attach unlimited number of listeners to the same event and they will not interfere with each other.
So the same functionality can be achieved by:
var i = 0;
function dragBarMouseDown(e) {
e.preventDefault();
document.getElementById("mousestatus").innerHTML = "mousedown" + i++;
window.addEventListener("mousemove", windowMouseMove, false);
console.log("leaving mouseDown");
}
function windowMouseMove(e) {
document.getElementById("position").innerHTML = e.pageX + ', ' + e.pageY;
document.getElementById("sidebar").style.width = e.pageX + 2 + "px";
document.getElementById("main").style.left = e.pageX + 2 + "px";
}
function windowMouseUp(e) {
document.getElementById("clickevent").innerHTML = 'in another mouseUp event' + i++;
window.removeEventListener("mousemove", windowMouseMove, false);
}
document.getElementById("dragbar").addEventListener("mousedown", dragBarMouseDown, false);
window.addEventListener("mouseup", windowMouseUp, false);
Fiddle.
Note that in this case my functions are not anonymous, so a self executing function for scoping would make sense here, if you are not already in function scope.
Here is a horizontal version of #SoonDead's pure and simple answer with a bottom shelf and a horizontal divider.
The result should look like this:
fiddle here
var i = 0;
function dragBarMouseDown(e) {
e.preventDefault();
document.getElementById("mousestatus").innerHTML = "mousedown" + i++;
window.addEventListener("mousemove", windowMouseMove, false);
console.log("leaving mouseDown");
}
function windowMouseMove(e) {
document.getElementById("position").innerHTML = e.pageX + ', ' + e.pageY;
//document.getElementById("main").style.height = e.pageY + 2 + "px";
document.getElementById("dragbar").style.top = e.pageY + 2 + "px";
document.getElementById("bottomshelf").style.top = e.pageY + 17 + "px";
}
function windowMouseUp(e) {
document.getElementById("clickevent").innerHTML = 'in another mouseUp event' + i++;
window.removeEventListener("mousemove", windowMouseMove, false);
}
document.getElementById("dragbar").addEventListener("mousedown", dragBarMouseDown, false);
window.addEventListener("mouseup", windowMouseUp, false);
body, html {
width:100%;
height:100%;
padding:0;
margin:0;
}
#header {
background-color: wheat;
width:100%;
height: 50px;
}
#main {
background-color: BurlyWood;
float: top;
position: absolute;
top:50px;
width:100%;
bottom: 38px;
overflow-y: hidden;
}
#dragbar {
background-color:grey;
width:100%;
float: top;
top:120px;
bottom:0px;
height: 15px;
cursor: row-resize;
position:absolute;
}
#bottomshelf {
background-color: IndianRed;
width:100%;
float: top;
position: absolute;
top:135px;
bottom: 38px;
}
#footer {
background-color: PaleGoldenRod;
width:100%;
height: 38px;
bottom:0;
position:absolute;
}
<div id="header">header <span id="mousestatus"></span>
<span id="clickevent"></span>
</div>
<div id="main">main area:
The bottom shelf will slide over this.
</div>
<div id="dragbar">drag me up or down</div>
<div id="bottomshelf">
<span id="position"></span>
bottom shelf</div>
<div id="footer">footer</div>
I don't have enough reputation to add comment to "SoonDead"s solutions, so I have to do this. :-( The solutions are great and worked out for me, except for IE8
1) The line
e.preventDefault();
has two issues for IE8
the argument "e"vent is not defined.
preventDefault method is not defined for e
So the above line is replaced with:
e = e || window.event;
e.preventDefault ? e.preventDefault() : e.returnValue=false;
The above stopped the errors (and I stopped here since I don't really care about IE8 but do not want error boxes popping up for the hapless user). So yes, in my app IE8 users cannot resize.
However, I did chase it down a little, and found these issues:
Code flow did not enter the onmousemove function
The e.pageX will have to be replaced by e.clientX or e.screenX (depending on your case) since I could not see pageX as a property in the IE8 debugger.
Here are some options discussed on SO.
My personal recommendation would be jQuery Resizable.
As BenjaminGruenbaum said in a comment, jQuery is JavaScript. If you don't want to load the full jQuery library, you'll need to find which parts of the jQuery library you need and pull out the source JavaScript to use. It's certainly doable.
I have two columns in my HTML page.
<div id="content">
<div id="left"></div>
<div id="right"></div>
</div>
Each of them occupies half of the page
#content {
height: 100%;
}
#left, #right {
float: left;
width: 50%;
height: 100%;
overflow: auto;
}
I'd like the boundary between left and right halves to be adjustable by the user. That is, the user can move the boundary to the left or to the right as he/she browses the page. Is it possible to do that somehow?
Yes, but it requires JavaScript. To apply it, you could of course just set the width of each of the sides:
var leftPercent = 50;
function updateDivision() {
document.getElementById('left').style.width = leftPercent + '%';
document.getElementById('right').style.width = (100 - leftPercent) + '%';
}
Now you can adjust the division with, say leftPercent = 50; updateDivision(), but the user isn't going to do that. There are multiple different ways you could present this to the user. Probably the best-suited way would be a little line in the middle they could drag. For this, you could use a little CSS for the positioning:
#content {
position: relative;
}
#divider {
position: absolute;
/* left to be set by JavaScript */
width: 1px;
top: 0;
bottom: 0;
background: black;
cursor: col-resize;
/* feel free to customize this, of course */
}
And then make sure you've got a div with an id of divider in content and update updateDivision to also update the left of divider:
document.getElementById('left').style.left = leftPercent + '%';
Then you just need a little logic to handle the dragging. (Here, I've put all of the elements into appropriately-named variables):
divider.addEventListener('mousedown', function(e) {
e.preventDefault();
var lastX = e.pageX;
document.documentElement.addEventListener('mousemove', moveHandler, true);
document.documentElement.addEventListener('mouseup', upHandler, true);
function moveHandler(e) {
e.preventDefault();
e.stopPropagation();
var deltaX = e.pageX - lastX;
lastX = e.pageX;
leftPercent += deltaX / parseFloat(document.defaultView.getComputedStyle(content).width) * 100;
updateDivision();
}
function upHandler(e) {
e.preventDefault();
e.stopPropagation();
document.documentElement.removeEventListener('mousemove', moveHandler, true);
document.documentElement.removeEventListener('mouseup', upHandler, true);
}
}, false);
You should be able to read it to see how it works, but in short: It listens for when someone presses on the divider. When they do, it'll attach listeners to the page for when they move their mouse. When they do, it updates the variable and calls updateDivision to update the styles. When eventually it gets a mouseup, it stops listening on the page.
As a further improvement, you could make every element have an appropriate cursor style while dragging so your cursor doesn't flash while dragging it.
Try it out.
There's nothing in the divisions so nothing will happen. It's like writing:
<h1></h1>
And changing the CSS for h1 and expecting something to be there
Usually I prefer to write my own solutions for trivial problems because generally plugins add a lot of unneeded functionality and increase your project in size. Size makes a page slower and a 30k difference (compared to jquery draggable) in a 100k pageviews / day website makes a big difference in the bill. I already use jquery and I think that's all I need for now, so please, don't tell me to use another plugin or framework to drag things around.
Whit that in mind I wrote the following code, to allow a box to be draggable around. The code works just fine (any tip about the code itself will be great appreciate), but I got a small little glitch.
When I drag the box to the browser right edge limit, a horizontal scroll bar appears, the window width gets bigger because of the box. The desirable behavior is to see no horizontal scroll bar, but allow to put part of the box outside the window area, like a windows window do.
Any tips?
CSS:
.draggable {
position: absolute;
cursor: move;
border: 1px solid black;
}
Javascript:
$(document).ready(function() {
var x = 0;
var y = 0;
$("#d").live("mousedown", function() {
var element = $(this);
$(document).mousemove(function(e) {
var x_movement = 0;
var y_movement = 0;
if (x == e.pageX || x == 0) {
x = e.pageX;
} else {
x_movement = e.pageX - x;
x = e.pageX;
}
if (y == e.pageY || y == 0) {
y = e.pageY;
} else {
y_movement = e.pageY - y;
y = e.pageY;
}
var left = parseFloat(element.css("left")) + x_movement;
element.css("left", left);
var top = parseFloat(element.css("top")) + y_movement;
element.css("top", top);
return false;
});
});
$(document).mouseup(function() {
x = 0;
y = 0;
$(document).unbind("mousemove");
});
});
HTML:
<div id="d" style="width: 100px; left: 0px; height: 100px; top: 0px;" class="draggable">a</div>
For a simple solution, you could just add some CSS to the draggable object's container to prevent the scrollbars.
body { overflow: hidden; }
jsFiddle: http://jsfiddle.net/F894P/
Instead of this :
$("#d").live("mousedown", function () {
// your code here
}); // live
try this :
$("body").on("mousedown","#d", function(){
// your code here
$("#parent_container").css({"overflow-x":"hidden"});
// or $("body").css({"overflow-x":"hidden"});
}); // on
Where #parent_container is where your draggable object is.
You should be using jQuery 1.7+
As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers. Users of older versions of jQuery should use .delegate() in preference to .live().
Setup
I'm making an HTML page that replaces the cursor with a div element. The Javascript is below. The div element is simply <div id="cursor"/>.
function fixCursor()
{
var cPos = getCursorPosition();
cursor.style="top:" + (cPos.y) + "; left:" + (cPos.x) + "; position:fixed; width:16px; height:16px; background-color:#DDDDDD; box-shadow: 2px 2px 4px rgba(0,0,0,.5); border:2px solid #111111; border-radius:0 100% 100% 100%;";
return;
}
function getCursorPosition(e) {
e = e || window.event;
var cursor = {x:0, y:0};
if (e.pageX || e.pageY) {
cursor.x = e.pageX;
cursor.y = e.pageY;
}
else if (e.screenX || e.ecreenY) {
cursor.x = e.screenX;
cursor.y = e.screenY;
}
else if (e.x || e.y) {
cursor.x = e.x;
cursor.y = e.y;
}
else if (e.clientX || e.clientY) {
var de = document.documentElement;
var b = document.body;
cursor.x = e.clientX;
cursor.y = e.clientY;
}
return cursor;
}
Problem
This only works on Opera, and shows signs of working on IE, but doesn't show the cursor. On Firefox and Chrome, nothing appears. I haven't tried Safari, as I uninstalled it a while ago, but in my experience, its rendering works alot like Chrome, anyway.
In your code, getCursorPosition takes an event object, e. In fixCursor, you are not passing anything in. You should probably make fixCursor take an event object as well and pass it through to getCursorPosition. Then, in your event handler where you're presumably calling fixCursor, pass in the event object passed into your event handler.
Also, you cannot set style equal to a string. You can, however, set style.cssText.
You can add custom cursors with the CSS cursor property, all major browsers but Opera support it to one extent or another. If you want it to work in IE you'll need to use a .cur or .ani cursor file, the other browsers support at minimum .cur, .png, .jpg and .gif.
Alternatively this answer points to a jQuery plugin that might be easier to use than implementing it yourself.
Personally I've never found a situation where I had to use a custom cursor, I usually just use a .png cursor in my stylesheet and leave a sensible default value for the browsers that don't support .png cursors.
Are you planning to do this onmousemove? That's a lot of overhead. In any case, I see a few problems, but I don't know if fixing them alone will get you a cross-browser solution.
First, let's assume you're triggering this function onmousemove, so you have this in your script:
document.onmousemove = fixCursor;
You have to have fixCursor() pass an event object to getCursorPosition() like this:
function fixCursor(e)
{
var cPos = getCursorPosition(e);
...
And you have to explicitly set each style attribute:
cursor.style.cursor = 'none'; // Didn't see you set this
cursor.style.top = cPos.y;
cursor.style.left = cPos.x;
cursor.style.position = "fixed";
A quick test showed me that this worked in Firefox but not IE. Setting position to "absolute" got closer in IE, but not in a useful way.
EDIT: Oh, and it appears you're not properly referencing the "cursor" div in your fixCursor() function. Use
var cursor = document.getElementById('cursor');