Scrolling a Div from Anywhere - javascript

When the bottom of my page (a.k.a. #main) comes into view jQuery toggles a class on my sidebar to make it scrollable using overflow-y: scroll — and overflow: hidden when the bottom of the page is out of view.
The desired effect here is to be at the bottom of the page (again in my example that's the #main div), but allow for the sidebar to keep scrolling provided there's more content.
So if you were to keep scrolling down on #main even after you'd reached the bottom, the sidebar would begin to scroll.
The problem right now is the desired scrolling effect only works when the cursor's over #sidebar. I'd like for it to be more natural andcapable of scrolling without the cursor needing to be over the #sidebar.
HTML:
<div id="container">
<div id="header"></div>
<div id="main">
<p>Lorem ipsum dolor sit amet, [...]</p>
</div>
<div id="sidebar">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
<li>Item 9</li>
<li>Item 10</li>
[...]
</ul>
</div>
</div>
Javascript:
$('#sidebar').height( $('#main').height() );
$('#main').waypoint(function() {
$('#sidebar').toggleClass('scrollable');
}, { offset: 'bottom-in-view' });
I've setup a fiddle for my question here: http://jsfiddle.net/ZZqLr/
Follow-Up: By approaching the problem from another angle I managed to achieve the desired effect.
This time around, when the bottom of #main comes into view it becomes fixed to the bottom of the window, while the #sidebar continues to scroll freely. It's a bit of a hack, but is visually identical for my needs.
http://jsfiddle.net/ZZqLr/1/

You can catch the scroll event of a mouse with the following code:
var mouseWheelEvent = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; //FF doesn't recognize mousewheel as of FF3.x
if (document.attachEvent) { //if IE (and Opera depending on user setting)
document.attachEvent("on"+mouseWheelEvent, mouseWheelEventHandler);
} else if (document.addEventListener) { //WC3 browsers
document.addEventListener(mouseWheelEvent, mouseWheelEventHandler, false);
}
After this it is quite easy to see when someone scrolls, even though you have reached the end of the document.
The mouseWheelEventHandler is the function that I passed to handle your mouseWheelEvent for you, it looks like this:
function mouseWheelEventHandler(e)
{
if( !sidebar.hasClass('scrollable') && !sidebar.is(":hover") ) {
return true;
}
var event = window.event || e; //equalize event object
var delta = event.detail ? event.detail*(-120) : event.wheelDelta; //check for detail first so Opera uses that instead of wheelDelta
sidebar.scrollTop(sidebar.scrollTop()-delta);
}
It will only scroll the sidebar if the sidebar has the class scrollable and if you are not already with your mouse on the sidebar, as it would scroll that element anyway and you would and up double scrolling it.
This should work on most browsers according to the following link:
http://www.javascriptkit.com/javatutors/onmousewheel.shtml
And here is the fiddle to play with ofcourse:
http://jsfiddle.net/ZZqLr/5/
EDIT
To complete the answer, we'll add the behaviour of it only scrolling the sidebar up instead of the page when you scroll up again, for this we only have to prevent the scrolling event by event.preventDefault() and add the following code at the end of the mouseWheelEventHandler:
if( sidebar.scrollTop() == 0 ) {
sidebar.removeClass('scrollable');
}
the function then looks like this:
function mouseWheelEventHandler(e)
{
if( !sidebar.hasClass('scrollable') && !sidebar.is(":hover") ) {
return true;
}
var event = window.event || e; //equalize event object
event.preventDefault();
var delta = event.detail ? event.detail*(-120) : event.wheelDelta; //check for detail first so Opera uses that instead of wheelDelta
sidebar.scrollTop(sidebar.scrollTop()-delta);
if( sidebar.scrollTop() == 0 ) {
sidebar.removeClass('scrollable');
}
}
Again you can play with it right here: http://jsfiddle.net/ZZqLr/6/
By the way, the waypoints.js is not even necessary for this, just for fun, one without the waypoints.js by removing the waypoint function and just adding the following in the eventHandle function:
function mouseWheelEventHandler(e)
{
if( !sidebar.hasClass('scrollable') && !sidebar.is(":hover") ) {
if($(window).scrollTop() + $(window).height() == $(document).height()) {
sidebar.addClass('scrollable');
}
return true;
}
var event = window.event || e; //equalize event object
event.preventDefault();
var delta = event.detail ? event.detail*(-120) : event.wheelDelta; //check for detail first so Opera uses that instead of wheelDelta
sidebar.scrollTop(sidebar.scrollTop()-delta);
if( sidebar.scrollTop() == 0 ) {
sidebar.removeClass('scrollable');
}
return true;
}
And for playing with that as well, you might've guessed: http://jsfiddle.net/ZZqLr/7/
And that will be all, I guess :)

Related

Empty list message on jquery-sortable

I'm working with jquery-sortable and I'm having some difficulty modifying the list container (ul) when it's been emptied or loaded empty. For example, If you have two containers:
A collection list to drag from that always contains a few items.
A destination list which loads empty (unless it is being edited and it will contain some list items but can be emptied by dragging them out of there
The empty container (ul) should display a message (i.e. nothing here) whenever it loads empty or it gets emptied on edit.
I tried several approaches with no avail.
SAMPLE HTML FOR EMPTY CONTAINER
<ul id="mediaItemsListDest" class="playlist-items connectedSortable">
<!-- force a message in html -->
<p>Drag and drop an item from the list</p>
</ul>
DIFFERENT JQUERY APPROACHES
if( $("#mediaItemsListDest li").length >= 1 ) {
//remove it when the ul contains an li
$("#mediaItemsListDest p").css('display','none');
}
or
if( $("#mediaItemsListDest li").length === 0 ) {
//no li is found, display a message via jquery but don't add it as a <p> element in the html
$(this).html("Sorry, this is empty");
}
or
if( !$("#mediaItemsListDest").has("li").length ) {
$(this).html("Sorry, this is empty");
}
None of them worked. How else can I hijack this empty or emptied list?
Here's a testing fiddle for you:
DEMO
Thanks in advance.
You need to handle on every list change the error message state so let's say we have the following HTML - example from your demo:
<ol id="mediaItemsListDest" class="simple_with_animation vertical">
<p>Drag and drop an item from the list</p>
<li>Item 1</li>
<li>Item 2</li>
</ol>
Additionally I have extended with a function which is handling the message state, code is placed on initialization part of the application:
function handleMessage() {
let liObjects = $('#mediaItemsListDest li');
let message = $('#mediaItemsListDest p');
console.log('length', liObjects.length);
console.log('message', message);
if (liObjects.length !== 0) {
message.css('display', 'none');
} else {
message.css('display', 'inline');
}
}
handleMessage();
This function needs to be called in onDrop event:
onDrop: function ($item, container, _super) {
// code part removed but you can find in your demo
handleMessage();
}
I did a quick test and it was working fine. I hope this one is helping, let me know if you need more information.

Event listener on parent node is triggering on all children nodes too

I'm writing a draggable class in vanilla js from scratch (this is NOT related to jQuery draggable), and it's working great, only my listeners are triggering on children of the selected node. I've tried e.stopPropagation() inside of the listeners but it doesn't seem to change the outcome.
Here's a snippet of code from the class:
setListeners() {
const targets = document.getElementsByClassName("-jsDraggable");
[...targets].forEach(elem => {
elem.addEventListener("mousemove", e => this.handleMouseMove(e));
elem.addEventListener("mousedown", e => this.handleMouseDown(e));
elem.addEventListener("mouseup", e => this.handleMouseUp(e));
});
}
... and for example:
<ul class="-jsDraggable">
<li>No drag</li>
<li>No drag</li>
<li class="-jsDraggable">Can drag this item</li>
<li>No drag</li>
</ul>
In this scenario I would want both the <ul class="-jsDraggable">...</ul> to be affected and also the single <li class="-jsDraggable">...</li> inside, where dragging from one of the other list items would simply drag the main <ul class="-jsDraggable">...</ul> element.
However, no matter which child node I interact with, regardless of how far down in the DOM it is compared to the node with the class, it triggers the handlers.
Any help would be greatly appreciated!
EDIT: Another example to clarify:
<article class="-jsDraggable">
<header>
<h1>Heading</h1>
</header>
<main>
<p>...</p>
</main>
</article>
No matter where I drag in this <article> element, it should drag the entire group of HTML as one (since the parent of it all has the class). Currently, however, it's acting more like this:
<article class="-jsDraggable">
<header class="-jsDraggable">
<h1 class="-jsDraggable">Heading</h1>
</header>
<main class="-jsDraggable">
<p class="-jsDraggable">...</p>
</main>
</article>
... and every single child is also moving when it should only be moving the parent container that has the class attached.
I solved it, it wasn't working because I was setting the current element as this.elem = e.target instead of this.elem = e.currentTarget. After this, e.stopPropagation() in the mousedown worked as expected.
Thanks for exposing me to e.currentTarget, I wasn't aware of it before reading the help in this thread.
If I understand your question correctly, then one option (amongst others) is to distinguish the origin of the event, and filter event handling via the currentTarget and target fields of the event object like so:
if(event.currentTarget === event.target) {
/* The user is interacting with the actual -jsDraggable element */
}
else {
/* The user is interacting with a descendant of a -jsDraggable element */
}
This in effect means that, if the element that is spawning the event (ie currentTarget) is that same element that the event handler has been bound to (ie target), then we determine that the event is in relation to an actual -jsDraggable element itself (and not a descendant). This approach compliments your current code as shown:
function handleMouseMove(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Mouse move over draggable');
}
}
function handleMouseDown(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Drag start on draggable');
}
}
function handleMouseUp(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Drag end on draggable');
}
}
function setListeners() {
const targets = document.querySelectorAll(".-jsDraggable");
targets.forEach(elem => {
elem.addEventListener("mousemove", e => handleMouseMove(e));
elem.addEventListener("mousedown", e => handleMouseDown(e));
elem.addEventListener("mouseup", e => handleMouseUp(e));
});
}
setListeners();
ul {
padding:1rem;
background:lightgreen;
}
li {
padding:1rem;
background:pink;
}
.-jsDraggable {
background:lightblue;
}
<ul class="-jsDraggable">
<li>No drag</li>
<li>No drag</li>
<li class="-jsDraggable">Can drag this item</li>
<li>No drag</li>
</ul>

How do I simulate hover with Javascript on keydown?

First of, I'd like to use only native JavaScript to complete this task.
Let's say I am to make a custom dropdown, and the HTML code looks kind of like this.
<div class="dropdown">
<span class="dropdown-label" style="display:block">Select a thing</span>
<ul class="dropdownItemContainer">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
</ul>
</div>
In the CSS file I have something close to this:
ul.dropdownItemContainer li:hover {
background-color: #FF0000;
}
Yeah, there's really no dropdownish behavior, but it's not the point of discussion actually. The problem is that I couldn't think of a decent way to enable keyboard control for this dropdown. The desired outcome is the following: I press the down key, and the first option is highlighted; I press it again, and the second option is highlighted and so on.
The only option that I see at this point (just started studying JS) is to fetch all of the ul's children, stick'em into an array and assign the tags a background color through JS methods in a proper way whenever the down key is pressed.
On the other hand, I still have the :hover behavior described in the CSS for mouse countrol. Is there a smart way of simulating hovers?
I would go with a simple assignment of a class on your li-elements and steer it with a keydown handler. The following code is not meant to be complete but give you something you can work with.
var active = document.querySelector(".hover") || document.querySelector(".dropdownItemContainer li");
document.addEventListener("keydown",handler);
document.addEventListener("mouseover",handler);
function handler(e){
console.log(e.which);
active.classList.remove("hover");
if (e.which == 40){
active = active.nextElementSibling || active;
}else if (e.which == 38){
active = active.previousElementSibling || active;
}else{
active = e.target;
}
active.classList.add("hover");
}
You can see a working example here
You may want to use a library instead of coding this from scratch.
http://vebersol.net/demos/jquery-custom-forms/
http://www.dreamcss.com/2009/05/15-jquery-plugins-to-enhance-your-html.html
I would suggest removing the hover attribute from css.
And add only a hovered class which is applied on keypresses and on mouseover
This could look like this in Code
var dropDown = document.getElementsByClassName("dropdownItemContainer")[0]
document.addEventListener("keydown",function (e) {
if(e.keyCode == 38 || e.keyCode == 40 ) {
var key = e.keyCode
var hovered = dropDown.getElementsByClassName("hovered")
if(hovered.length != 0 ) {
cur = hovered[0]
cur.className = ""
cur = cur[(key==38?"previous":"next")+"ElementSibling"] || dropDown.children[key==38?dropDown.children.length-1:0]
} else {
cur = dropDown.children[key==38?dropDown.children.length-1:0]
}
cur.className="hovered"
}
});
dropDown.addEventListener("mouseover",function (e) {
for( var i = 0,j; j = dropDown.getElementsByClassName("hovered")[i];i++)
j.className = "";
e.srcElement.className = "hovered";
});
Heres an example on JSFiddle
Reality you didn't need any js for dropdown but You can use JavaScript Event for simulating it. You can use event like hover, focus, onclick
In JS You Can use This For Set Event
document.getElementById('id').addEventListener('focus',function(e){
//place code that want ran at event happened
}
In JQuery you can use bind, click ,...
$('#id')bind('focus',function(e){
//place code that want ran at event happened
}
List of Event
http://www.quirksmode.org/dom/events/index.html

sequential effects on list-items in jQuery

here's the snippet of my code :
<ul>
<li>Home</li>
<li>Links</li>
<li>Contact</li>
</ul>
I use css to style them horizontally (menu-like) and what I would like to do is to animate all the list-items of the <ul> element. I top them when the dom is ready and animate them to the bottom to attract users' eyes when the entire page is loaded.
here's the jquery code:
$(function(){
$("ul li").css('top', '-40px'); //items are in relative position
$(window).bind("load", items_animate, false);
});
function items_animate(){
... //I'd like to animate each <li> of the <ul> changing 'top' to '0px' but not simultaneously, I want to declare a DELAY between each animation (<li>'s get down in a row)
}
I know how to sequence effects with queue() or calling functions one by one but on only one element, I'm lost in this case..
EDIT : for those who are interested, here's the code to accomplish this sequence, thanks to Joseph
var animationDelay = 600;
var offset = 200;
function blah(meh) {
setTimeout(function(){
$(meh).animate({
opacity: "0"
}, animationDelay);
},$(meh).index() * offset)
}
$("li").each(function(){
blah(this);
})
Demo
Here is another way (using opacity for clarity) that animates the list items in series with a delay in between.
<ul>
<li>Home</li>
<li>Links</li>
<li>Contact</li>
</ul>
var animationDelay = 600;
var offset = 200;
function blah(meh) {
setTimeout(function(){
$(meh).animate({
opacity: "0"
}, animationDelay);
},$(meh).index() * offset)
}
$("li").each(function(){
blah(this);
})
*pardon the less than original names... it's late :P
function slide_down_recursive(e,duration,callback)
{
$(e).animate(
{
top: '0px'
}, duration, 'linear',function()
{
if($(e).next().length == 0)
{
if(typeof(callback) == 'function')
{
callback();
}
return;
}
else
{
// Apply recursion for every sibling.
slide_down_recursive($(e).next(),duration,callback);
}
});
} // End slide_down_recursive
slide_down_recursive($('li:first-child'),500);
Here is a demo: http://jsfiddle.net/rpvyZ/
Try something like this:
$(function() {
function animateSequentially(element, properties, duration) {
element.animate(properties, duration, function() {
animateSequentially(element.next(), properties, duration);
});
}
animateSequentially($("ul > li:first-child"), {top: '0'}, 1000);
});
Edit: If you'd like them to animate sequentially but not wait for the previous one, you can try this:
$(function() {
$("ul > li").each(function(index, item) {
setTimeout(function() {
$(item).animate({top: '0'}, 500);
}, index*175);
});
});
Try the one that waits here, or the one that doesn't wait here.
Use .animates callback function to animate the next element.
$('li:eq(0)').animate({
top: "0px"
}, 5000, function() {
$('li:eq(1)').animate({
top: "0px"
}, 5000, function() {
...
});
});
as of this request, I wrote a jQuery plugin to walk sequencially through a list of (any) elements and applying css changes.
You can checkout the Plugin here:
https://github.com/ieservices/frontend-components/tree/master/jQuery/Plugins/jquery.list-effects
There I made it quite easy to apply those effects by defining the list and the effect options as a JavaScript object. For the first version I created the possiblity to define the delay of the changes between the elements as well as the options to define a starting index to define on which element the changes should be applied.
With the plugin you can do something like this:
<div id="myList">
<h4>This is my list</h4>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
By applying css stylesheet changes by rotating through the list elements:
jQuery('#myList ul li').listEffect(
{delay: '2000', attribute: 'color', value: '#ccc'}
);
Also I created and a demo in the repo, which is available here:
https://github.com/ieservices/frontend-components/blob/master/jQuery/Plugins/jquery.list-effects/demo/list-effects-demo-simple.html
So, far it can't do much, but what do you guys think of that Plugin?

Control allowed drop locations for jQuery sortable

I have a sortable list where the user can dynamically attach items to each other. In the following example "Item 3" is attached to "Item 2". If two items are attached I want to prevent the user from dropping items between the two items ie in the example the user should not be allowed to drop an item between "Item 2" and "Item 3".
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
<li class="attached">Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
Is there a callback which allows me to control which drop locations are allowed? Alternatively is there a different plugin similar to jQuery sortable which can provide this functionality?
You can optionally pass sortable an items option which lets you specify which items you want to be sortable. Note that with this method you will not be able to move items two and three around.
Html
<ul id="list">
<li>Item 1</li>
<li class="attached">Item 2</li>
<li class="attached">Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
JavaScript
$("#sortable").sortable({ items: ':not(.attached)'});
Hope that gets you on the right track. Here's a working demo: http://jsfiddle.net/SPPVc/
The jQuery sortable widget does not provide the capability for controlling the allowed drop zone behaviour. The problem can however be solved hackish-ly by subclassing the the widget:
$.widget("ui.custom_list", $.ui.sortable, {
_mouseDrag: function(event) {
// copy this code from the source code of jquery.ui.sortable.js
//Rearrange
for (var i = this.items.length - 1; i >= 0; i--) {
//Cache variables and intersection, continue if no intersection
var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item);
if (!intersection) continue;
if(itemElement != this.currentItem[0] //cannot intersect with itself
&& this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before
&& !$.ui.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked
&& (this.options.type == 'semi-dynamic' ? !$.ui.contains(this.element[0], itemElement) : true)
// add this line
&& this._allowDropping(itemElement, (intersection == 1 ? "down" : "up"))
//&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
) {
this.direction = intersection == 1 ? "down" : "up";
// Rest of the function
},
_allowDropping: function(itemElement, direction) {
if(this.options.allowDropping) {
return this.options.allowDropping(itemElement, direction);
}
return true;
}
});
The _mouseDrag function is mostly copied from the sortable source. The only adjustment is the line:
&& this._allowDropping(itemElement, (intersection == 1 ? "down" : "up"))
The allowed drop zone behaviour can then be customized by providing a function for the allowDropping parameter:
$("ul").custom_list({
allowDropping: function(element, direction) {
// element refers to the item that would be moved but not the one being dragged
if(direction == "up") {
...
} else {
...
}
}
})

Categories