I have the following HTML:
<div class="list" id="list">
<div class="item" id="i1">Item 1</div>
<div class="item" id="i2">Item 2</div>
<div class="item" id="i3">Item 3</div>
</div>
<div class="timeline" id="timeline">
</div>
What I want to be able to do, with jQuery, is:
Be able to drag .items from the #list into the #timeline
.items can be dropped into the timeline as many times as required, eg. there could be 4 of item #i1 in the timeline.
.items in the timeline must not overlap each other
.items can be positioned at any place along the timeline so long as they do not overlap any other items on the timeline
So Ive gone for jQueryUI's Draggable and Droppable, and also gone for the jQueryUI Draggable Collision Plugin.
Here is the jQuery I have started with:
$('#list .item').draggable({
helper: 'clone',
revert: 'invalid',
//the following are for the jquery-ui-dragggable-collision plugin
obstacle: '#timeline .item',
preventCollision: true
});
$('#timeline').droppable({
accept: '.item'
});
My problem is that the jQueryUI Draggable Collision Plugin only works when you are dragging the original Div itself, and not dragging a helper. I need helpers so that I can achieve #2 (adding multiple copies of one item). But I need something like the Collision Plugin so I can achieve #3 (items not overlapping).
Does anybody know of a solution to this problem? Is there another plugin that does collision detection on the helper of a draggable object? Is there another approach I can try to get what I want to achieve?
If you prefer a jsfiddle to that uses the jQueryUI Draggable Collision Plugin as you suggested, here is something to play around with: Link to jsfiddle
The approach uses the original helper in order to make use of collision functionality.
The clone is generated in the start event function (and removed again in the stop event in case the dragging did not result in a successful drop):
$(function(){
var draggableSelector = ".list .item:not(.dropped)";
var init = function() {
$(draggableSelector).each(function(i){
$(this)
.draggable({
//helper: 'clone',
revert: 'invalid',
start: function(event,ui) {
var $clone = ui.helper.clone();
$clone
.removeClass("ui-draggable ui-draggable-dragging")
.insertAfter(ui.helper)
;
$(this).data("clone",$clone);
},
stop: function(event,ui) {
if( $(".ui-draggable-dragging.dropped").length == 0) {
$(this).data("clone").remove();
};
},
//the following are for the jquery-ui-draggable-collision plugin
refreshPositions: true,
obstacle: '.item.dropped',
preventCollision: true,
})
.css("left", ( ($(this).width() + 5) * i) + "px")
;
});
$('.timeline').droppable({
accept: '.item'
,drop: function(event,ui) {
ui.draggable
.addClass("dropped")
;
setTimeout(reinit, 500);
}
});
};
var reinit = function() {
$(".list .item.ui-draggable").draggable("destroy");
init();
}
init();
});
Hope that this will be useful.
Here is an example i wrote for this question showing a simple drag and drop plugin with collision detection.
It allows you to drop items onto a timeline as long as there is space for the item to exist without overlapping.
It is by no means a finished product but hopefully will show that code like this is not incredibly complex to write and trying to hack together massive conflicting plugins are not always the best option. Sometimes its best to start from scratch. Its fun, and a really good way to learn.
/*----------ON DOCUMENT READY----------*/
$(document).ready(function() {
$("#timeline").timeline({
items: ".item"
});
});
/*----------THE TIMELINE PLUGIN----------*/
$.fn.timeline = function(options) {
var defaults = {
items: "div"
}
var options = $.extend(defaults, options)
return this.each(function() {
//-----SETUP-----//
//define all the vars we will need later
var el = $(this);
var items = $(options.items);
var mousedown = false;
var dragging = false;
var activeItem = false;
var placedItems = new Array();
//make everything unselectable so it dosne interfere with dragging
$("html").find("*").css({
"user-select": "none",
"-moz-user-select": "none",
"-webkit-user-select": "none",
"-ms-user-select": "none",
"-o-user-select": "none",
}).attr("unselectable", "true").unbind("onselectstart");
//-----EVENTS-----//
//log when the mouse is down anywhere on the doc
$(document).mousedown(function() {
mousedown = true;
});
//when the mouse is released
$(document).mouseup(function(e) {
//if was dragging an item attempt to place it
if (mousedown && dragging) {
placeItem(e);
}
//log that dragging has stopped
mousedown = false;
dragging = false;
});
//log when the mouse is pressed over an item
items.mousedown(function() {
dragging = true;
//clone the active item and hide it ready for dragging
activeItem = $(this).clone().appendTo("body").hide();
});
//when the mouse movers over the doc
$(document).mousemove(function(e) {
//if mouse was pressed over item attempt to drag
if (mousedown && dragging) {
dragItem(e);
}
});
//-----FUNCTIONS-----//
//drag the item around the screen
function dragItem(e) {
//if no active item done do owt
if (!activeItem) {
return false;
}
//work out where the drag anchor is
var x = e.pageX - (activeItem.height() / 2);
var y = e.pageY - (activeItem.width() / 2);
//save the original position in case we cant place the item
if (!activeItem.origPos) {
activeItem.origPos = {
x: x,
y: y
}
}
//drag the item
activeItem.css({
"position": "absolute",
"top": y,
"left": x,
"z-index": "999",
"opacity": 0.6,
"display": "block"
});
}
//attempt to place the item
function placeItem(e) {
//if no active item dont do owt
if (!activeItem) {
return false;
}
//define som vars needed later on
var onTargetY = false;
var onTargetX = false;
var remove = false;
var collision = false;
//check if item is being relesed withing the timeline bounds
if (e.pageY > el.position().top && e.pageY < el.position().top + el.height()) {
onTargetY = true;
}
if (e.pageX > el.position().left && e.pageX < el.position().left + el.width()) {
onTargetX = true;
}
//if on target attempt to drop on timeline
if (onTargetX && onTargetY) {
//snap to the left or right if dropped at the left or right edges
var maxLeft = el.position().left;
var maxRight = el.position().left + el.width() - activeItem.width();
x = e.pageX - (activeItem.width() / 2);
if (x < maxLeft) {
x = maxLeft;
} else if (x > maxRight) {
x = maxRight;
}
//loop the items already on the timeline and check for collisions
$.each(placedItems, function(i, item) {
var itemMin = item.position().left;
var itemMax = item.position().left + item.width();
if (x + activeItem.width() > itemMin && x < itemMax) {
collision = true;
}
});
y = el.position().top;
}
//if there is a collision or the item is dropped outside the timeline
//set x and y back to original position and set removal flag to true
if (collision || !onTargetX || !onTargetY) {
x = activeItem.origPos.x;
y = activeItem.origPos.y;
remove = true;
//if dropped inside the timeline and no collisions add item to the
//array of items inside the timeline
} else {
placedItems.push(activeItem);
}
//finally either animate the item back to where it started then remove it
//or snap it into the timeline in the space found
activeItem.animate({
top: y,
left: x
}, {
duration: 300,
queue: false,
complete: function() {
//if remove flag set remove the item from the dom
if (remove) {
$(this).remove();
}
//some tidying up
activeItem.css("opacity", 1);
activeItem = false;
}
});
}
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="list" id="list">
<div class="item item1">Item 1</div>
<div class="item item2">Item 2</div>
<div class="item item3">Item 3</div>
</div>
<div class="timeline" id="timeline"></div>
Enjoy :).
Related
I need to scroll menus automatically when I am swiping the content left or right, for an eg.
when I am swiping to Right and coming to 5th panel slide the navigation should jumped to or scrolled to the 5th menu but now its stays or displays in menu 1 or whatever last menu was clicked only.
My expectation is to create like this eg.
https://js.devexpress.com/Demos/WidgetsGallery/Demo/Pivot/Overview/jQuery/iOS/
I am looking to Achieve two scenario, but I am failing and getting every time an error
If I Make the menu(selected menu) left or center side align then it should always be in left or center side align in initial load of the screen and also when I am swipe the slide content the menu should left or center align, sample eg. above link devexpress
When a content is swiped eg. 5th panel slide then the menu should scroll with respect to it
What I have tried is here and also the Jquery code html for reference
$(document).ready(function(){
$.fn.scrollpane = function(options) {
options = $.extend({
direction: "horizontal",
deadzone: 25,
useTransition: false,
desktop: true,
setupCss: true,
onscroll: function(pos, page, duration) {},
onscrollfinish: function(pos, page) {}
}, options);
var isTouch = "ontouchend" in document || !options.desktop,
onTouchstart = isTouch ? "touchstart" : "mousedown",
onTouchmove = isTouch ? "touchmove" : "mousemove",
onTouchend = isTouch ? "touchend" : "mouseup";
return this.each(function() {
// the scroll pane viewport
var outerElem = $(this);
// a large div containing the scrolling content
var innerElem = $("<div></div>");
innerElem.append(outerElem.children().remove());
outerElem.append(innerElem);
// cache these for later
var outerWidth = outerElem.width();
var outerHeight = outerElem.height();
// boolean
var horizontal = (options.direction === "horizontal");
// the number of pixels the user has to drag and release to trigger a page transition
// natural
var deadzone = Math.max(0, options.deadzone);
// the index of the current page. changed after the user completes each scrolling gesture.
// integer
var currentPage = 0;
// width of a page
// integer
var scrollUnit = horizontal ? outerWidth : outerHeight;
// x coordinate on the transform. -ve numbers go to the right,
// so this goes -ve as currentPage goes +ve
// integer (pixels)
var currentPos = 0;
// min and max scroll position:
// integer (pixels)
var scrollMax = 0;
var scrollMin = -scrollUnit * (innerElem.children().length - 1);
// time to settle after touched:
// natural (ms)
var settleTime = 200;
// dragMid and dragEnd are updated each frame of dragging:
// integer (pixels)
var dragStart = 0; // touch position when dragging starts
var dragMid = 0; // touch position on the last touchmove event
var dragEnd = 0; // touch position on this touchmove event
// +1 if dragging in +ve x direction, -1 if dragging in -ve x direction
// U(-1, +1)
var dragDir = 0;
if (options.setupCss) {
outerElem.css({
position: "relative",
overflow: "hidden"
});
// position the pages:
innerElem.children().each(function(index) {
$(this).css({
position: "absolute",
display: "block",
width: outerWidth,
height: outerHeight
}).css(horizontal ? "left" : "top", scrollUnit * index);
});
}
// natural natural boolean -> void
function scrollTo(position, duration, finish) {
var parameters = {};
parameters[(horizontal ? 'marginLeft' : 'marginTop')] = position;
options.onscroll(position, -position / scrollUnit, duration);
if (options.useTransition) {
innerElem.css({
transition: "none",
transform: horizontal ? ("translate3d(" + position + "px, 0, 0)") : ("translate3d(0, " + position + "px, 0)")
});
}
if (finish) {
if (!options.useTransition) {
innerElem.find('li').animate(parameters, duration);
} else {
innerElem.css({
transition: "all " + (duration === 0 ? "0" : duration + "ms")
});
}
setTimeout(function() {
options.onscrollfinish(position, -position / scrollUnit, duration);
});
} else if (!options.useTransition) {
innerElem.find('li').stop().css(parameters);
}
}
// Immediately set the 3D transform on the scroll pane.
// This causes Safari to create OpenGL resources to manage the animation.
// This sometimes causes a brief flicker, so best to do it at page load
// rather than waiting until the user starts to drag.
scrollTo(0, 0, true);
// bind the touch drag events:
outerElem.on(onTouchstart, function(e) {
e = isTouch ? e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] : e;
dragStart = dragEnd = dragMid = horizontal ? e.pageX : e.pageY;
// bind the touch drag event:
$(this).on(onTouchmove, function(e) {
e = isTouch ? e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] : e;
dragEnd = horizontal ? e.pageX : e.pageY;
dragDir = (dragEnd - dragMid) > 0 ? 1 : -1;
currentPos += dragEnd - dragMid;
dragMid = dragEnd;
scrollTo(currentPos, 0, false);
});
// bind the touch end event
}).on(onTouchend, function(e) {
// boolean
var reset = Math.abs(dragEnd - dragStart) < deadzone;
// real
var scrollPage = -1.0 * currentPos / scrollUnit;
// natural
var nextPage = reset ? currentPage : (dragDir < 0 ? Math.ceil(scrollPage) : Math.floor(scrollPage));
// int
var nextPos = Math.max(scrollMin, Math.min(scrollMax, -scrollUnit * nextPage));
currentPos = nextPos;
currentPage = nextPage;
scrollTo(nextPos, settleTime, true);
outerElem.off(onTouchmove);
});
// set up the menu callback:
outerElem.data("showpage", function(page) {
// int
page = page < 0 ? innerElem.children().length + page : page;
currentPos = Math.max(scrollMin, Math.min(scrollMax, -page * scrollUnit));
currentPage = -currentPos / scrollUnit;
scrollTo(currentPos, settleTime, true);
});
});
};
// Once you've initialized a scrollpane with $().scrollpane(),
// you can use this method to cause it to programmatically scroll
// to a particular page. Useful for creating a navigation menu, or
// those little dots on Apple-store-style product galleries.
//
// Pages are indexed from 0 upwards. Negative numbers can be used
// to index pages from the right.
//
// int -> jQuery
$.fn.showpage = function(index) {
var fn = this.data("showpage");
fn(index);
return this;
};
$(document).bind("touchmove", function() {
return false;
});
$(function() {
//$("#hpane").scrollpane();
$("#hpane").scrollpane({
// onscroll: function(pos, page, duration) {
// $("#pos").text(pos);
// $("#page").text(page);
// $("#snapping").text("no");
// },
onscrollfinish: function(pos, page) {
$("#pos").text(pos);
$("#page").text(page);
$("#snapping").text("yes");
$("ul.pager li").removeClass("active")
$("ul.pager li:nth-child("+(page+1)+")").addClass("active");
}
});
$("ul.pager li").click(function() {
var index = $(this).index();
$("ul.pager li").removeClass("active")
$(this).addClass("active");
$("#hpane").showpage(index);
//$("#vpane").showpage(index);
});
// $("input").click(function() {
// alert(this.value);
// });
});
});
I know the jQuery UI API has snap, snapMethod and snapTolerance built in, but these are not going to work in this case.
Here is the problem I face: when dragging within a container, I want the draggable to snap to the edges of the container when within a certain distance. Calculating distances and triggering this is not an issue. It is getting the draggable to snap which I cannot get to work.
I expected something like: $draggable.position().left = 0; might snap it to the left edge of the parent container but it doesn't make any difference.
Here is a fiddle to demonstrate: https://jsfiddle.net/jwxrevL2/1/
JS:
//set draggable
$('.drag').draggable({
drag: function(){ drag($(this)) },
containment: 'parent',
});
//drag
function drag( $draggable ){
var snap_tolerance = 10;
//Draggable
var d_top = $draggable.position().top;
var d_middle = ($draggable.position().top+($draggable.height()/2));
var d_bottom = ($draggable.position().top+$draggable.height());
var d_left = $draggable.position().left;
var d_center = ($draggable.position().left+($draggable.width()/2));
var d_right = ($draggable.position().left+$draggable.width());
//Wrapper
var $wrapper = $('.wrapper');
var w_top = 0;
var w_bottom = $wrapper.height();
var w_left = 0
var w_right = $wrapper.width();
//snap to left
if( d_left <= (w_left+snap_tolerance )){
console.log('snap left');
$draggable.position().left = w_left;
within_snap = true;
}
//snap to right
if( d_right >= (w_right-snap_tolerance)){
console.log('snap right');
$draggable.position().left = (w_right-$draggable.width());
within_snap = true;
}
//snap to top
if( d_top <= (w_top+snap_tolerance )){
console.log('snap top');
$draggable.position().top = w_top;
within_snap = true;
}
//snap to bottom
if( d_bottom >= (w_bottom-snap_tolerance )){
console.log('snap bottom');
$draggable.position().top = (w_bottom-$draggable.height());
within_snap = true;
}
}//end fn drag
I've have managed to get it working. Although I must say I do not fully understand what is going on. These are the changes I've made (updated fiddle):
//set draggable
$('.drag').draggable({
drag: function(e, ui){ drag($(this), ui) },
containment: 'parent',
});
So on the drag event I pass the ui object as well as the jQuery object (I think, please correct me if I am wrong about the ui object) into the drag function.
//snap to left
if( d_left <= (w_left+snap_tolerance )){
console.log('snap left');
ui.position.left = w_left;
within_snap = true;
}
Then by updating the ui objects position.left property I can snap it into position.
Can anyone explain why it is different using ui rather than the jQuery object?
Here's an example.
You need to specify a grid like so:
<script>
$(function() {
$( "#draggable4" ).draggable({ grid: [ 20, 20 ] });
$( "#draggable5" ).draggable({ grid: [ 80, 80 ] });
});
</script>
https://jsfiddle.net/m3r2xra3/
I'm trying to build a curtain slider - much like what is used on the Apple site - http://www.apple.com/30-years/
http://jsfiddle.net/NYEaX/405/
I've created the following code - I need to add listeners to detect the mouse hovering over the far left/far right sides of the page - and then invoke an exponential slide.
var curtainSlider = {
invoke: function(el){
var that = this;
var list = $(el + " ul").find("li");
this.initialListWidth = list.outerWidth(true);
list
.mouseover(function() {
console.log("over");
that.expand(this);
})
.mouseout(function() {
console.log("out");
that.contract(this);
});
},
expand: function(el){
var that = this;
$(el).stop().animate({
width: that.initialListWidth*2
},400, function() {
// Animation complete.
});
},
contract: function(el){
var that = this;
$(el).stop().animate({
width: that.initialListWidth
},400, function() {
// Animation complete.
});
}
}
$(document).ready(function() {
console.log( "ready!" );
curtainSlider.invoke("#curtain");
});
**LATEST CODE - complete integration - http://jsfiddle.net/NYEaX/538/ **
I have stabilized this version of the scroller. - This curtains the images and spectrum fades them on start up. It repositions the a elements so the image is more centrally aligned.
http://jsfiddle.net/NYEaX/432/
I've separated out the code responsible for moving the slider unit, with an acceleration/deceleration. Its this part of the application I wish to focus on now.
http://jsfiddle.net/NYEaX/434/
I've tried to push the pagex variable into the animation part to help manipulate the duration of the animation. How can this be stabilized/improved on. I am finding it hard to reverse engineer the apple 30 year slider.
var curtainSlider = {
bindEvents: function(){
var that = this;
$("body").on("mousemove",function(event) {
if (event.pageX < 50) {
// animate curtain left
console.log("curtain left");
that.scroll("l", event.pageX);
}
if (event.pageX > (window.width - 50)) {
// animate curtain right
console.log("curtain right");
that.scroll("r", window.width - event.pageX);
}
});
},
scroll: function(direction, leveler){
var charge = "-";
if(direction == "r"){
charge = "+";
}
$('#curtainholder #slider').animate({
left: charge+"="+leveler
},400, function() {
// Animation complete.
});
},
invoke: function(el){
var that = this;
this.bindEvents();
}
}
$(document).ready(function() {
curtainSlider.invoke("#curtain");
});
I'm animating a div to left by 0px by clicking on the div colored in red. Below the div , classes are added to li's as the div moves along, but the classes gets added to only certain li's and not all.
Is there any other logic to fix this ?
Fiddle - http://jsfiddle.net/AsfFQ/16/
Below is the image of the issue
Try this jsFiddle example.
var pos;
var timer, selectLi = (function() {
var $block = $('.block'),
$container = $('.container'),
$lis = $('.container ul li'),
liWidth = $lis.width(),
$selectedLi;
return function() {
pos = $block.offset().left - $container.offset().left;
liNum = Math.round(pos / liWidth);
// $selectedLi && $selectedLi.removeClass('selected');
$selectedLi = $($lis.get(liNum));
$('li.eligible').each(function() {
if ($block.offset().left-3 <= $(this).offset().left) $(this).addClass('selected');
});
};
})();
$('.block').click(function() {
timer = setInterval(selectLi, 30);
$(this).animate({
left: 0
}, function() {
clearInterval(timer);
});
});
$('li').each(function() {
$(this).addClass('eligible');
if ($(this).offset().left > $('.block').offset().left) $(this).removeClass('eligible');
});
This sets the eligible list items and then as the bar moves, compares their position to tjat of the bar and if they're in range, they get the class added.
Your little animation needs only a little code.
See jsfiddle example
var $block = $('.block'),
start = $block.offset().left;
$block.one('click').animate({left: 0})
.$('li').filter(function(){return $(this).offset().left<=start})
.repeat(30).filter(function(){return $(this).offset().left>=$block.offset().left})
.addClass('selected').unrepeat();
I'm using this plugin jquery-timing.
This also works when animating 100px on each click, see another fiddle:
var $block = $('.block');
$block.on('click').animate({left: '-=100px'})
.$('li').filter(function(){return $(this).offset().left<=$block.offset().left})
.repeat(30).filter(function(){return $(this).offset().left>=$block.offset().left})
.addClass('selected').unrepeat();
Have fun!
I have a UL-LI e.g.
<ul>
<li id="1">item-1</li>
<li id="2">item-2</li>
<li id="3">item-3</li>
<li id="4">item-4</li>
</ul>
I would like to move one of the items to another position in the list. e.g. item-2 to AFTER item-4.
Normally I can do this by deleting the item and then appending it after another.
But I would like to do this to happen visually with animation. As in, item-2 descends to after item-4.
How can I achieve this?
IDs should not start with numbers...
$('#two').slideUp(500, function () {
$('#four').after(this);
$(this).slideDown(500);
});
Here is a demo: http://jsfiddle.net/jasper/8JFBA/
Or if you always want to add the element to the end:
$('#two').slideUp(500, function () {
$('ul').append(this);
$(this).slideDown(500);
});
Here is a demo: http://jsfiddle.net/jasper/8JFBA/1/
Update
Ok, so if you want the element to slide to it's new location here ya go:
//absolutely position the element and give it a top property so it doesn't go to the top of the container
$('#two').css({ position : 'absolute', top : $('#two').position().top });
//now get the offset to the bottom of the list by getting the top offset and height for the last list-item
var lastOffset = ($(this).children().last().position().top + $(this).children().last().height());
//now animate the element to the new position
$('#two').animate({ top : lastOffset }, 1000, function () {
//when the animation is done, re-add the element to the new position in the list and reset it's position and top values
$(this).appendTo('ul').css({ position : 'relative', top : 0 });
});
And a demo: http://jsfiddle.net/jasper/8JFBA/3/
Update
You can animate not only the element being moved to the end of the list but you can animate the rest of the list items as they move up:
var $LIs = $('ul').children(),
liHeight = 20;
$LIs.on('click', function () {
var index = ($(this).index()),
$LIsAfter = $LIs.filter(':gt(' + index + ')');
console.log(index);
$(this).css({ position : 'absolute', top : $(this).position().top });
$.each($LIsAfter, function (i) {
$(this).css({ position : 'absolute', top : ((i + index + 1) * liHeight) });
});
$(this).stop(true, true).animate({ top : (($LIs.length - 1) * liHeight)}, 1000, function () {
$(this).appendTo('ul').css({ position : 'relative', top : 0 });
});
$.each($LIsAfter, function (i) {
$(this).stop(true, true).animate({ top : ((index + i) * liHeight) }, 1000, function () {
$(this).css({ position : 'relative', top : 0 });
});
});
});
Here is a demo: http://jsfiddle.net/jasper/8JFBA/8/
This isn't quite complete, there is still a bug or two, but it should help get anyone started on the idea.
I tried to implement a smoother transition when you descend and below is my version..
You need to try out the demo to understand how it works.. Select value from the drop down and hit Descend to see the animation.
DEMO
Edit: Updated top position of $from before addClass('active') to start from the exact position and not top: 0px. Thanks to Jasper for finding this issue.
var $from = $('#from');
var $to = $('#to');
$('button').click (function () {
var from = $from.val();
var to = $to.val();
var $li = $('ul li');
var $fromEl = $('#' + from);
var $toEl = $('#' + to);
//only descending
if (from == to || $li.index($fromEl) > $li.index($toEl)) return;
var destX = $toEl.position().top;
$toEl.after('<li id="tmpLi2"></li>');
$('#tmpLi2').animate({height: $fromEl.outerHeight()}, 1000);
//add a blank li for smooth animation
$fromEl
.after('<li id="tmpLi1"> </li>')
.css ('top', $fromEl.position().top)
.addClass ('active' )
.animate({
top: (destX)
},
1000,
function() {
$toEl.after(this);
$('#tmpLi2').remove();
$(this).removeClass('active');
});
$('#tmpLi1').slideUp(function() { $(this).remove()});
});