Related
I'm using a WordPress plugin (open source) that will allow you to add an expandable widget for a WooCommerce product category.
This is the JS:
// mtree.js
// Requires jquery.js and velocity.js (optional but recommended).
// Copy the below function, add to your JS, and simply add a list <ul class=mtree> ... </ul>
;(function ($, window, document, undefined) {
// Only apply if mtree list exists
if($('ul.mtree').length) {
// Settings
var collapsed = true; // Start with collapsed menu (only level 1 items visible)
var close_same_level = true; // Close elements on same level when opening new node.
var duration = mtree_options.duration; // Animation duration should be tweaked according to easing.
var listAnim = true; // Animate separate list items on open/close element (velocity.js only).
var easing = mtree_options.easing_type; // Velocity.js only, defaults to 'swing' with jquery animation.
// Set initial styles
$('.mtree ul').css({'overflow':'hidden', 'height': (collapsed) ? 0 : 'auto', 'display': (collapsed) ? 'none' : 'block' });
// Get node elements, and add classes for styling
var node = $('.mtree li:has(ul)');
node.each(function(index, val) {
$(this).children(':first-child').css('cursor', 'pointer')
$(this).addClass('mtree-node mtree-' + ((collapsed) ? 'closed' : 'open'));
$(this).children('ul').addClass('mtree-level-' + ($(this).parentsUntil($('ul.mtree'), 'ul').length + 1));
});
// Set mtree-active class on list items for last opened element
$('.mtree li > *:first-child').on('click.mtree-active', function(e){
if($(this).parent().hasClass('mtree-closed')) {
$('.mtree-active').not($(this).parent()).removeClass('mtree-active');
$(this).parent().addClass('mtree-active');
} else if($(this).parent().hasClass('mtree-open')){
$(this).parent().removeClass('mtree-active');
} else {
$('.mtree-active').not($(this).parent()).removeClass('mtree-active');
$(this).parent().toggleClass('mtree-active');
}
});
// Set node click elements, preferably <a> but node links can be <span> also
node.children(':first-child').on('click.mtree', function(e){
// element vars
var el = $(this).parent().children('ul').first();
var isOpen = $(this).parent().hasClass('mtree-open');
// close other elements on same level if opening
if((close_same_level || $('.csl').hasClass('active')) && !isOpen) {
var close_items = $(this).closest('ul').children('.mtree-open').not($(this).parent()).children('ul');
// Velocity.js
if($.Velocity) {
close_items.velocity({
height: 0
}, {
duration: duration,
easing: easing,
display: 'none',
delay: 100,
complete: function(){
setNodeClass($(this).parent(), true)
}
});
// jQuery fallback
} else {
close_items.delay(100).slideToggle(duration, function(){
setNodeClass($(this).parent(), true);
});
}
}
// force auto height of element so actual height can be extracted
el.css({'height': 'auto'});
// listAnim: animate child elements when opening
if(!isOpen && $.Velocity && listAnim) el.find(' > li, li.mtree-open > ul > li').css({'opacity':0}).velocity('stop').velocity('list');
// Velocity.js animate element
if($.Velocity) {
el.velocity('stop').velocity({
//translateZ: 0, // optional hardware-acceleration is automatic on mobile
height: isOpen ? [0, el.outerHeight()] : [el.outerHeight(), 0]
},{
queue: false,
duration: duration,
easing: easing,
display: isOpen ? 'none' : 'block',
begin: setNodeClass($(this).parent(), isOpen),
complete: function(){
if(!isOpen) $(this).css('height', 'auto');
}
});
// jQuery fallback animate element
} else {
setNodeClass($(this).parent(), isOpen);
el.slideToggle(duration);
}
// We can't have nodes as links unfortunately
e.preventDefault();
});
// Function for updating node class
function setNodeClass(el, isOpen) {
if(isOpen) {
el.removeClass('mtree-open').addClass('mtree-closed');
} else {
el.removeClass('mtree-closed').addClass('mtree-open');
}
}
// List animation sequence
if($.Velocity && listAnim) {
$.Velocity.Sequences.list = function (element, options, index, size) {
$.Velocity.animate(element, {
opacity: [1,0],
translateY: [0, -(index+1)]
}, {
delay: index*(duration/size/2),
duration: duration,
easing: easing
});
};
}
// Fade in mtree after classes are added.
// Useful if you have set collapsed = true or applied styles that change the structure so the menu doesn't jump between states after the function executes.
if($('.mtree').css('opacity') == 0) {
if($.Velocity) {
$('.mtree').css('opacity', 1).children().css('opacity', 0).velocity('list');
} else {
$('.mtree').show(200);
}
}
}
}(jQuery, this, this.document));
I've added a background image using CSS and :before but the image is not clickable.
Is there a way to add it on the JS so that it can be clicked as well?
I've tried to see where to add some code but actually I'm clueless, should it be between lines 29 and 37?
You can see it in: https://tester.medicalfa.gr/test/katastima/
Ok I fixed it via css, the arrows now appears like they are clickable.
The solution is to remove the code I had added:
ul.mtree.default li.mtree-open:before {
display: inline-block;
margin-left: 200px;
background: url("../images/arrow-down.svg") no-repeat center center;
background-size: 20px 20px;
}
I removed the :before as it adds the arrow behind the text and now appears after the text as it's supposed to.
The code looks like this now:
ul.mtree.default li.mtree-open {
display: block ;
background: url("../images/arrow-down.svg") no-repeat center center;
background-size: 15px 15px;
background-position:top right;
}
I have this input element:
<input type="text" class="textfield" value="" id="subject" name="subject">
Then I have some other elements, like other tag's & <textarea> tag's, etc...
When the user clicks on the <input id="#subject">, the page should scroll to the page's last element, and it should do so with a nice animation (It should be a scroll to bottom and not to top).
The last item of the page is a submit button with #submit:
<input type="submit" class="submit" id="submit" name="submit" value="Ok, Done.">
The animation should not be too fast and should be fluid.
I am running the latest jQuery version. I prefer to not install any plugin but to use the default jQuery features to achieve this.
Assuming you have a button with the id button, try this example:
$("#button").click(function() {
$([document.documentElement, document.body]).animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 2000);
});
I got the code from the article Smoothly scroll to an element without a jQuery plugin. And I have tested it on the example below.
<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script>
$(document).ready(function (){
$("#click").click(function (){
$('html, body').animate({
scrollTop: $("#div1").offset().top
}, 2000);
});
});
</script>
<div id="div1" style="height: 1000px; width 100px">
Test
</div>
<br/>
<div id="div2" style="height: 1000px; width 100px">
Test 2
</div>
<button id="click">Click me</button>
</html>
jQuery .scrollTo(): View - Demo, API, Source
I wrote this lightweight plugin to make page/element scrolling much easier. It's flexible where you could pass in a target element or specified value. Perhaps this could be part of jQuery's next official release, what do you think?
Examples Usage:
$('body').scrollTo('#target'); // Scroll screen to target element
$('body').scrollTo(500); // Scroll screen 500 pixels down
$('#scrollable').scrollTo(100); // Scroll individual element 100 pixels down
Options:
scrollTarget: A element, string, or number which indicates desired scroll position.
offsetTop: A number that defines additional spacing above scroll target.
duration: A string or number determining how long the animation will run.
easing: A string indicating which easing function to use for the transition.
complete: A function to call once the animation is complete.
If you are not much interested in the smooth scroll effect and just interested in scrolling to a particular element, you don't require some jQuery function for this. Javascript has got your case covered:
https://developer.mozilla.org/en-US/docs/Web/API/element.scrollIntoView
So all you need to do is: $("selector").get(0).scrollIntoView();
.get(0) is used because we want to retrieve the JavaScript's DOM element and not the JQuery's DOM element.
UPDATE
now is possible to scroll with animation, passing scroll options (see MDN). You can even control the block position. It seems to have large support, except for Safari
$("selector").get(0).scrollIntoView({behavior: 'smooth'});
This is achievable without jQuery:
document.getElementById("element-id").scrollIntoView();
Using this simple script
if($(window.location.hash).length > 0){
$('html, body').animate({ scrollTop: $(window.location.hash).offset().top}, 1000);
}
Would make in sort that if a hash tag is found in the url, the scrollTo animate to the ID. If not hash tag found, then ignore the script.
jQuery(document).ready(function($) {
$('a[href^="#"]').bind('click.smoothscroll',function (e) {
e.preventDefault();
var target = this.hash,
$target = $(target);
$('html, body').stop().animate( {
'scrollTop': $target.offset().top-40
}, 900, 'swing', function () {
window.location.hash = target;
} );
} );
} );
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul role="tablist">
<li class="active" id="p1">Section 1</li>
<li id="p2">Section 2</li>
<li id="p3">Section 3</li>
</ul>
<div id="pane1"></div>
<div id="pane2"></div>
<div id="pane3"></div>
This is the way I do it.
document.querySelector('scrollHere').scrollIntoView({ behavior: 'smooth' })
Works in any browser.
It can easily be wrapped into a function
function scrollTo(selector) {
document.querySelector(selector).scrollIntoView({ behavior: 'smooth' })
}
Here is a working example
$(".btn").click(function() {
document.getElementById("scrollHere").scrollIntoView( {behavior: "smooth" })
})
.btn {margin-bottom: 500px;}
.middle {display: block; margin-bottom: 500px; color: red;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="btn">Scroll down</button>
<h1 class="middle">You see?</h1>
<div id="scrollHere">Arrived at your destination</div>
Docs
The solution by Steve and Peter works very well.
But in some cases, you may have to convert the value to an integer. Strangely, the returned value from $("...").offset().top is sometimes in float.
Use: parseInt($("....").offset().top)
For example:
$("#button").click(function() {
$('html, body').animate({
scrollTop: parseInt($("#elementtoScrollToID").offset().top)
}, 2000);
});
A compact version of "animate" solution.
$.fn.scrollTo = function (speed) {
if (typeof(speed) === 'undefined')
speed = 1000;
$('html, body').animate({
scrollTop: parseInt($(this).offset().top)
}, speed);
};
Basic usage: $('#your_element').scrollTo();
With this solution you do not need any plugin and there's no setup required besides placing the script before your closing </body> tag.
$("a[href^='#']").on("click", function(e) {
$("html, body").animate({
scrollTop: $($(this).attr("href")).offset().top
}, 1000);
return false;
});
if ($(window.location.hash).length > 1) {
$("html, body").animate({
scrollTop: $(window.location.hash).offset().top
}, 1000);
}
On load, if there is a hash in the address, we scroll to it.
And - whenever you click an a link with an href hash e.g. #top, we scroll to it.
##Edit 2020
If you want a pure JavaScript solution: you could perhaps instead use something like:
var _scrollToElement = function (selector) {
try {
document.querySelector(selector).scrollIntoView({ behavior: 'smooth' });
} catch (e) {
console.warn(e);
}
}
var _scrollToHashesInHrefs = function () {
document.querySelectorAll("a[href^='#']").forEach(function (el) {
el.addEventListener('click', function (e) {
_scrollToElement(el.getAttribute('href'));
return false;
})
})
if (window.location.hash) {
_scrollToElement(window.location.hash);
}
}
_scrollToHashesInHrefs();
If you are only handling scrolling to an input element, you can use focus(). For example, if you wanted to scroll to the first visible input:
$(':input:visible').first().focus();
Or the first visible input in an container with class .error:
$('.error :input:visible').first().focus();
Thanks to Tricia Ball for pointing this out!
Easy way to achieve the scroll of page to target div id
var targetOffset = $('#divID').offset().top;
$('html, body').animate({scrollTop: targetOffset}, 1000);
If you want to scroll within an overflow container (instead of $('html, body') answered above), working also with absolute positioning, this is the way to do :
var elem = $('#myElement'),
container = $('#myScrollableContainer'),
pos = elem.position().top + container.scrollTop() - container.position().top;
container.animate({
scrollTop: pos
}
After finding the way to get my code work, I think I should make thing a bit clear:
For using:
$('html, body').animate({
scrollTop: $("#div1").offset().top
}, 2000);
you need to be on top of the page since $("#div1").offset().top will return different numbers for different positions you scroll to. If you already scrolled out of the top, you need to specify the exact pageY value (see pageY definition here: https://javascript.info/coordinates).
So now, the problem is to calculate the pageY value of one element. Below is an example in case the scroll container is the body:
function getPageY(id) {
let elem = document.getElementById(id);
let box = elem.getBoundingClientRect();
var body = document.getElementsByTagName("BODY")[0];
return box.top + body.scrollTop; // for window scroll: box.top + window.scrollY;
}
The above function returns the same number even if you scrolled somewhere. Now, to scroll back to that element:
$("html, body").animate({ scrollTop: getPageY('div1') }, "slow");
Animations:
// slide to top of the page
$('.up').click(function () {
$("html, body").animate({
scrollTop: 0
}, 600);
return false;
});
// slide page to anchor
$('.menutop b').click(function(){
//event.preventDefault();
$('html, body').animate({
scrollTop: $( $(this).attr('href') ).offset().top
}, 600);
return false;
});
// Scroll to class, div
$("#button").click(function() {
$('html, body').animate({
scrollTop: $("#target-element").offset().top
}, 1000);
});
// div background animate
$(window).scroll(function () {
var x = $(this).scrollTop();
// freezze div background
$('.banner0').css('background-position', '0px ' + x +'px');
// from left to right
$('.banner0').css('background-position', x+'px ' +'0px');
// from right to left
$('.banner0').css('background-position', -x+'px ' +'0px');
// from bottom to top
$('#skills').css('background-position', '0px ' + -x + 'px');
// move background from top to bottom
$('.skills1').css('background-position', '0% ' + parseInt(-x / 1) + 'px' + ', 0% ' + parseInt(-x / 1) + 'px, center top');
// Show hide mtop menu
if ( x > 100 ) {
$( ".menu" ).addClass( 'menushow' );
$( ".menu" ).fadeIn("slow");
$( ".menu" ).animate({opacity: 0.75}, 500);
} else {
$( ".menu" ).removeClass( 'menushow' );
$( ".menu" ).animate({opacity: 1}, 500);
}
});
// progres bar animation simple
$('.bar1').each(function(i) {
var width = $(this).data('width');
$(this).animate({'width' : width + '%' }, 900, function(){
// Animation complete
});
});
In most cases, it would be best to use a plugin. Seriously. I'm going to tout mine here. Of course there are others, too. But please check if they really avoid the pitfalls for which you'd want a plugin in the first place - not all of them do.
I have written about the reasons for using a plugin elsewhere. In a nutshell, the one liner underpinning most answers here
$('html, body').animate( { scrollTop: $target.offset().top }, duration );
is bad UX.
The animation doesn't respond to user actions. It carries on even if the user clicks, taps, or tries to scroll.
If the starting point of the animation is close to the target element, the animation is painfully slow.
If the target element is placed near the bottom of the page, it can't be scrolled to the top of the window. The scroll animation stops abruptly then, in mid motion.
To handle these issues (and a bunch of others), you can use a plugin of mine, jQuery.scrollable. The call then becomes
$( window ).scrollTo( targetPosition );
and that's it. Of course, there are more options.
With regard to the target position, $target.offset().top does the job in most cases. But please be aware that the returned value doesn't take a border on the html element into account (see this demo). If you need the target position to be accurate under any circumstances, it is better to use
targetPosition = $( window ).scrollTop() + $target[0].getBoundingClientRect().top;
That works even if a border on the html element is set.
This is my approach abstracting the ID's and href's, using a generic class selector
$(function() {
// Generic selector to be used anywhere
$(".js-scroll-to").click(function(e) {
// Get the href dynamically
var destination = $(this).attr('href');
// Prevent href=“#” link from changing the URL hash (optional)
e.preventDefault();
// Animate scroll to destination
$('html, body').animate({
scrollTop: $(destination).offset().top
}, 500);
});
});
<!-- example of a fixed nav menu -->
<ul class="nav">
<li>
Item 1
</li>
<li>
Item 2
</li>
<li>
Item 3
</li>
</ul>
Very simple and easy to use custom jQuery plugin. Just add the attribute scroll= to your clickable element and set its value to the selector you want to scroll to.
Like so: <a scroll="#product">Click me</a>. It can be used on any element.
(function($){
$.fn.animateScroll = function(){
console.log($('[scroll]'));
$('[scroll]').click(function(){
selector = $($(this).attr('scroll'));
console.log(selector);
console.log(selector.offset().top);
$('html body').animate(
{scrollTop: (selector.offset().top)}, //- $(window).scrollTop()
1000
);
});
}
})(jQuery);
// RUN
jQuery(document).ready(function($) {
$().animateScroll();
});
// IN HTML EXAMPLE
// RUN ONCLICK ON OBJECT WITH ATTRIBUTE SCROLL=".SELECTOR"
// <a scroll="#product">Click To Scroll</a>
$('html, body').animate(...) does not work for me in the iPhone, Android, Chrome, or Safari browsers.
I had to target the root content element of the page.
$('#cotnent').animate(...)
Here is what I have ended up with:
if (navigator.userAgent.match(/(iPod|iPhone|iPad|Android)/)) {
$('#content').animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 'slow');
}
else{
$('html, body').animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 'slow');
}
All body content wired up with a #content div
<html>
....
<body>
<div id="content">
...
</div>
</body>
</html>
$('html, body').animate({scrollTop:
Math.min(
$(to).offset().top-margintop, //margintop is the margin above the target
$('body')[0].scrollHeight-$('body').height()) //if the target is at the bottom
}, 2000);
To show the full element (if it's possible with the current window size):
var element = $("#some_element");
var elementHeight = element.height();
var windowHeight = $(window).height();
var offset = Math.min(elementHeight, windowHeight) + element.offset().top;
$('html, body').animate({ scrollTop: offset }, 500);
var scrollTo = function($parent, $element) {
var topDiff = $element.position().top - $parent.position().top;
$parent.animate({
scrollTop : topDiff
}, 100);
};
This is Atharva's answer from: https://developer.mozilla.org/en-US/docs/Web/API/element.scrollIntoView.
Just wanted to add if your document is in an iframe, you can choose an element in the parent frame to scroll into view:
$('#element-in-parent-frame', window.parent.document).get(0).scrollIntoView();
I wrote a general purpose function that scrolls to either a jQuery object, a CSS selector, or a numeric value.
Example usage:
// scroll to "#target-element":
$.scrollTo("#target-element");
// scroll to 80 pixels above first element with class ".invalid":
$.scrollTo(".invalid", -80);
// scroll a container with id "#my-container" to 300 pixels from its top:
$.scrollTo(300, 0, "slow", "#my-container");
The function's code:
/**
* Scrolls the container to the target position minus the offset
*
* #param target - the destination to scroll to, can be a jQuery object
* jQuery selector, or numeric position
* #param offset - the offset in pixels from the target position, e.g.
* pass -80 to scroll to 80 pixels above the target
* #param speed - the scroll speed in milliseconds, or one of the
* strings "fast" or "slow". default: 500
* #param container - a jQuery object or selector for the container to
* be scrolled. default: "html, body"
*/
jQuery.scrollTo = function (target, offset, speed, container) {
if (isNaN(target)) {
if (!(target instanceof jQuery))
target = $(target);
target = parseInt(target.offset().top);
}
container = container || "html, body";
if (!(container instanceof jQuery))
container = $(container);
speed = speed || 500;
offset = offset || 0;
container.animate({
scrollTop: target + offset
}, speed);
};
When the user clicks on that input with #subject, the page should
scroll to the last element of the page with a nice animation. It
should be a scroll to bottom and not to top.
The last item of the page is a submit button with #submit
$('#subject').click(function()
{
$('#submit').focus();
$('#subject').focus();
});
This will first scroll down to #submit then restore the cursor back to the input that was clicked, which mimics a scroll down, and works on most browsers. It also doesn't require jQuery as it can be written in pure JavaScript.
Can this fashion of using focus function mimic animation in a better way, through chaining focus calls. I haven't tested this theory, but it would look something like this:
<style>
#F > *
{
width: 100%;
}
</style>
<form id="F" >
<div id="child_1"> .. </div>
<div id="child_2"> .. </div>
..
<div id="child_K"> .. </div>
</form>
<script>
$('#child_N').click(function()
{
$('#child_N').focus();
$('#child_N+1').focus();
..
$('#child_K').focus();
$('#child_N').focus();
});
</script>
I set up a module scroll-element npm install scroll-element. It works like this:
import { scrollToElement, scrollWindowToElement } from 'scroll-element'
/* scroll the window to your target element, duration and offset optional */
let targetElement = document.getElementById('my-item')
scrollWindowToElement(targetElement)
/* scroll the overflow container element to your target element, duration and offset optional */
let containerElement = document.getElementById('my-container')
let targetElement = document.getElementById('my-item')
scrollToElement(containerElement, targetElement)
Written with help from the following SO posts:
offset-top-of-an-element-without-jquery
scrolltop-animation-without-jquery
Here is the code:
export const scrollToElement = function(containerElement, targetElement, duration, offset) {
if (duration == null) { duration = 1000 }
if (offset == null) { offset = 0 }
let targetOffsetTop = getElementOffset(targetElement).top
let containerOffsetTop = getElementOffset(containerElement).top
let scrollTarget = targetOffsetTop + ( containerElement.scrollTop - containerOffsetTop)
scrollTarget += offset
scroll(containerElement, scrollTarget, duration)
}
export const scrollWindowToElement = function(targetElement, duration, offset) {
if (duration == null) { duration = 1000 }
if (offset == null) { offset = 0 }
let scrollTarget = getElementOffset(targetElement).top
scrollTarget += offset
scrollWindow(scrollTarget, duration)
}
function scroll(containerElement, scrollTarget, duration) {
let scrollStep = scrollTarget / (duration / 15)
let interval = setInterval(() => {
if ( containerElement.scrollTop < scrollTarget ) {
containerElement.scrollTop += scrollStep
} else {
clearInterval(interval)
}
},15)
}
function scrollWindow(scrollTarget, duration) {
let scrollStep = scrollTarget / (duration / 15)
let interval = setInterval(() => {
if ( window.scrollY < scrollTarget ) {
window.scrollBy( 0, scrollStep )
} else {
clearInterval(interval)
}
},15)
}
function getElementOffset(element) {
let de = document.documentElement
let box = element.getBoundingClientRect()
let top = box.top + window.pageYOffset - de.clientTop
let left = box.left + window.pageXOffset - de.clientLeft
return { top: top, left: left }
}
Updated answer as of 2019:
$('body').animate({
scrollTop: $('#subject').offset().top - $('body').offset().top + $('body').scrollTop()
}, 'fast');
ONELINER
subject.onclick = e=> window.scroll({ top: submit.offsetTop, behavior: 'smooth'});
subject.onclick = e=> window.scroll({top: submit.offsetTop, behavior: 'smooth'});
.box,.foot{display: flex;background:#fdf;padding:500px 0} .foot{padding:250px}
<input type="text" class="textfield" value="click here" id="subject" name="subject">
<div class="box">
Some content
<textarea></textarea>
</div>
<input type="submit" class="submit" id="submit" name="submit" value="Ok, Done.">
<div class="foot">Some footer</div>
For what it's worth, this is how I managed to achieve such behavior for a general element which can be inside a DIV with scrolling. In our case we don't scroll the full body, but just particular elements with overflow: auto; within a larger layout.
It creates a fake input of the height of the target element, and then puts a focus to it, and the browser will take care about the rest no matter how deep within the scrollable hierarchy you are. Works like a charm.
var $scrollTo = $('#someId'),
inputElem = $('<input type="text"></input>');
$scrollTo.prepend(inputElem);
inputElem.css({
position: 'absolute',
width: '1px',
height: $scrollTo.height()
});
inputElem.focus();
inputElem.remove();
This worked for me:
var targetOffset = $('#elementToScrollTo').offset().top;
$('#DivParent').animate({scrollTop: targetOffset}, 2500);
I have this input element:
<input type="text" class="textfield" value="" id="subject" name="subject">
Then I have some other elements, like other tag's & <textarea> tag's, etc...
When the user clicks on the <input id="#subject">, the page should scroll to the page's last element, and it should do so with a nice animation (It should be a scroll to bottom and not to top).
The last item of the page is a submit button with #submit:
<input type="submit" class="submit" id="submit" name="submit" value="Ok, Done.">
The animation should not be too fast and should be fluid.
I am running the latest jQuery version. I prefer to not install any plugin but to use the default jQuery features to achieve this.
Assuming you have a button with the id button, try this example:
$("#button").click(function() {
$([document.documentElement, document.body]).animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 2000);
});
I got the code from the article Smoothly scroll to an element without a jQuery plugin. And I have tested it on the example below.
<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script>
$(document).ready(function (){
$("#click").click(function (){
$('html, body').animate({
scrollTop: $("#div1").offset().top
}, 2000);
});
});
</script>
<div id="div1" style="height: 1000px; width 100px">
Test
</div>
<br/>
<div id="div2" style="height: 1000px; width 100px">
Test 2
</div>
<button id="click">Click me</button>
</html>
jQuery .scrollTo(): View - Demo, API, Source
I wrote this lightweight plugin to make page/element scrolling much easier. It's flexible where you could pass in a target element or specified value. Perhaps this could be part of jQuery's next official release, what do you think?
Examples Usage:
$('body').scrollTo('#target'); // Scroll screen to target element
$('body').scrollTo(500); // Scroll screen 500 pixels down
$('#scrollable').scrollTo(100); // Scroll individual element 100 pixels down
Options:
scrollTarget: A element, string, or number which indicates desired scroll position.
offsetTop: A number that defines additional spacing above scroll target.
duration: A string or number determining how long the animation will run.
easing: A string indicating which easing function to use for the transition.
complete: A function to call once the animation is complete.
If you are not much interested in the smooth scroll effect and just interested in scrolling to a particular element, you don't require some jQuery function for this. Javascript has got your case covered:
https://developer.mozilla.org/en-US/docs/Web/API/element.scrollIntoView
So all you need to do is: $("selector").get(0).scrollIntoView();
.get(0) is used because we want to retrieve the JavaScript's DOM element and not the JQuery's DOM element.
UPDATE
now is possible to scroll with animation, passing scroll options (see MDN). You can even control the block position. It seems to have large support, except for Safari
$("selector").get(0).scrollIntoView({behavior: 'smooth'});
This is achievable without jQuery:
document.getElementById("element-id").scrollIntoView();
Using this simple script
if($(window.location.hash).length > 0){
$('html, body').animate({ scrollTop: $(window.location.hash).offset().top}, 1000);
}
Would make in sort that if a hash tag is found in the url, the scrollTo animate to the ID. If not hash tag found, then ignore the script.
jQuery(document).ready(function($) {
$('a[href^="#"]').bind('click.smoothscroll',function (e) {
e.preventDefault();
var target = this.hash,
$target = $(target);
$('html, body').stop().animate( {
'scrollTop': $target.offset().top-40
}, 900, 'swing', function () {
window.location.hash = target;
} );
} );
} );
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul role="tablist">
<li class="active" id="p1">Section 1</li>
<li id="p2">Section 2</li>
<li id="p3">Section 3</li>
</ul>
<div id="pane1"></div>
<div id="pane2"></div>
<div id="pane3"></div>
This is the way I do it.
document.querySelector('scrollHere').scrollIntoView({ behavior: 'smooth' })
Works in any browser.
It can easily be wrapped into a function
function scrollTo(selector) {
document.querySelector(selector).scrollIntoView({ behavior: 'smooth' })
}
Here is a working example
$(".btn").click(function() {
document.getElementById("scrollHere").scrollIntoView( {behavior: "smooth" })
})
.btn {margin-bottom: 500px;}
.middle {display: block; margin-bottom: 500px; color: red;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="btn">Scroll down</button>
<h1 class="middle">You see?</h1>
<div id="scrollHere">Arrived at your destination</div>
Docs
The solution by Steve and Peter works very well.
But in some cases, you may have to convert the value to an integer. Strangely, the returned value from $("...").offset().top is sometimes in float.
Use: parseInt($("....").offset().top)
For example:
$("#button").click(function() {
$('html, body').animate({
scrollTop: parseInt($("#elementtoScrollToID").offset().top)
}, 2000);
});
A compact version of "animate" solution.
$.fn.scrollTo = function (speed) {
if (typeof(speed) === 'undefined')
speed = 1000;
$('html, body').animate({
scrollTop: parseInt($(this).offset().top)
}, speed);
};
Basic usage: $('#your_element').scrollTo();
With this solution you do not need any plugin and there's no setup required besides placing the script before your closing </body> tag.
$("a[href^='#']").on("click", function(e) {
$("html, body").animate({
scrollTop: $($(this).attr("href")).offset().top
}, 1000);
return false;
});
if ($(window.location.hash).length > 1) {
$("html, body").animate({
scrollTop: $(window.location.hash).offset().top
}, 1000);
}
On load, if there is a hash in the address, we scroll to it.
And - whenever you click an a link with an href hash e.g. #top, we scroll to it.
##Edit 2020
If you want a pure JavaScript solution: you could perhaps instead use something like:
var _scrollToElement = function (selector) {
try {
document.querySelector(selector).scrollIntoView({ behavior: 'smooth' });
} catch (e) {
console.warn(e);
}
}
var _scrollToHashesInHrefs = function () {
document.querySelectorAll("a[href^='#']").forEach(function (el) {
el.addEventListener('click', function (e) {
_scrollToElement(el.getAttribute('href'));
return false;
})
})
if (window.location.hash) {
_scrollToElement(window.location.hash);
}
}
_scrollToHashesInHrefs();
If you are only handling scrolling to an input element, you can use focus(). For example, if you wanted to scroll to the first visible input:
$(':input:visible').first().focus();
Or the first visible input in an container with class .error:
$('.error :input:visible').first().focus();
Thanks to Tricia Ball for pointing this out!
Easy way to achieve the scroll of page to target div id
var targetOffset = $('#divID').offset().top;
$('html, body').animate({scrollTop: targetOffset}, 1000);
If you want to scroll within an overflow container (instead of $('html, body') answered above), working also with absolute positioning, this is the way to do :
var elem = $('#myElement'),
container = $('#myScrollableContainer'),
pos = elem.position().top + container.scrollTop() - container.position().top;
container.animate({
scrollTop: pos
}
After finding the way to get my code work, I think I should make thing a bit clear:
For using:
$('html, body').animate({
scrollTop: $("#div1").offset().top
}, 2000);
you need to be on top of the page since $("#div1").offset().top will return different numbers for different positions you scroll to. If you already scrolled out of the top, you need to specify the exact pageY value (see pageY definition here: https://javascript.info/coordinates).
So now, the problem is to calculate the pageY value of one element. Below is an example in case the scroll container is the body:
function getPageY(id) {
let elem = document.getElementById(id);
let box = elem.getBoundingClientRect();
var body = document.getElementsByTagName("BODY")[0];
return box.top + body.scrollTop; // for window scroll: box.top + window.scrollY;
}
The above function returns the same number even if you scrolled somewhere. Now, to scroll back to that element:
$("html, body").animate({ scrollTop: getPageY('div1') }, "slow");
Animations:
// slide to top of the page
$('.up').click(function () {
$("html, body").animate({
scrollTop: 0
}, 600);
return false;
});
// slide page to anchor
$('.menutop b').click(function(){
//event.preventDefault();
$('html, body').animate({
scrollTop: $( $(this).attr('href') ).offset().top
}, 600);
return false;
});
// Scroll to class, div
$("#button").click(function() {
$('html, body').animate({
scrollTop: $("#target-element").offset().top
}, 1000);
});
// div background animate
$(window).scroll(function () {
var x = $(this).scrollTop();
// freezze div background
$('.banner0').css('background-position', '0px ' + x +'px');
// from left to right
$('.banner0').css('background-position', x+'px ' +'0px');
// from right to left
$('.banner0').css('background-position', -x+'px ' +'0px');
// from bottom to top
$('#skills').css('background-position', '0px ' + -x + 'px');
// move background from top to bottom
$('.skills1').css('background-position', '0% ' + parseInt(-x / 1) + 'px' + ', 0% ' + parseInt(-x / 1) + 'px, center top');
// Show hide mtop menu
if ( x > 100 ) {
$( ".menu" ).addClass( 'menushow' );
$( ".menu" ).fadeIn("slow");
$( ".menu" ).animate({opacity: 0.75}, 500);
} else {
$( ".menu" ).removeClass( 'menushow' );
$( ".menu" ).animate({opacity: 1}, 500);
}
});
// progres bar animation simple
$('.bar1').each(function(i) {
var width = $(this).data('width');
$(this).animate({'width' : width + '%' }, 900, function(){
// Animation complete
});
});
In most cases, it would be best to use a plugin. Seriously. I'm going to tout mine here. Of course there are others, too. But please check if they really avoid the pitfalls for which you'd want a plugin in the first place - not all of them do.
I have written about the reasons for using a plugin elsewhere. In a nutshell, the one liner underpinning most answers here
$('html, body').animate( { scrollTop: $target.offset().top }, duration );
is bad UX.
The animation doesn't respond to user actions. It carries on even if the user clicks, taps, or tries to scroll.
If the starting point of the animation is close to the target element, the animation is painfully slow.
If the target element is placed near the bottom of the page, it can't be scrolled to the top of the window. The scroll animation stops abruptly then, in mid motion.
To handle these issues (and a bunch of others), you can use a plugin of mine, jQuery.scrollable. The call then becomes
$( window ).scrollTo( targetPosition );
and that's it. Of course, there are more options.
With regard to the target position, $target.offset().top does the job in most cases. But please be aware that the returned value doesn't take a border on the html element into account (see this demo). If you need the target position to be accurate under any circumstances, it is better to use
targetPosition = $( window ).scrollTop() + $target[0].getBoundingClientRect().top;
That works even if a border on the html element is set.
This is my approach abstracting the ID's and href's, using a generic class selector
$(function() {
// Generic selector to be used anywhere
$(".js-scroll-to").click(function(e) {
// Get the href dynamically
var destination = $(this).attr('href');
// Prevent href=“#” link from changing the URL hash (optional)
e.preventDefault();
// Animate scroll to destination
$('html, body').animate({
scrollTop: $(destination).offset().top
}, 500);
});
});
<!-- example of a fixed nav menu -->
<ul class="nav">
<li>
Item 1
</li>
<li>
Item 2
</li>
<li>
Item 3
</li>
</ul>
Very simple and easy to use custom jQuery plugin. Just add the attribute scroll= to your clickable element and set its value to the selector you want to scroll to.
Like so: <a scroll="#product">Click me</a>. It can be used on any element.
(function($){
$.fn.animateScroll = function(){
console.log($('[scroll]'));
$('[scroll]').click(function(){
selector = $($(this).attr('scroll'));
console.log(selector);
console.log(selector.offset().top);
$('html body').animate(
{scrollTop: (selector.offset().top)}, //- $(window).scrollTop()
1000
);
});
}
})(jQuery);
// RUN
jQuery(document).ready(function($) {
$().animateScroll();
});
// IN HTML EXAMPLE
// RUN ONCLICK ON OBJECT WITH ATTRIBUTE SCROLL=".SELECTOR"
// <a scroll="#product">Click To Scroll</a>
$('html, body').animate(...) does not work for me in the iPhone, Android, Chrome, or Safari browsers.
I had to target the root content element of the page.
$('#cotnent').animate(...)
Here is what I have ended up with:
if (navigator.userAgent.match(/(iPod|iPhone|iPad|Android)/)) {
$('#content').animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 'slow');
}
else{
$('html, body').animate({
scrollTop: $("#elementtoScrollToID").offset().top
}, 'slow');
}
All body content wired up with a #content div
<html>
....
<body>
<div id="content">
...
</div>
</body>
</html>
$('html, body').animate({scrollTop:
Math.min(
$(to).offset().top-margintop, //margintop is the margin above the target
$('body')[0].scrollHeight-$('body').height()) //if the target is at the bottom
}, 2000);
To show the full element (if it's possible with the current window size):
var element = $("#some_element");
var elementHeight = element.height();
var windowHeight = $(window).height();
var offset = Math.min(elementHeight, windowHeight) + element.offset().top;
$('html, body').animate({ scrollTop: offset }, 500);
var scrollTo = function($parent, $element) {
var topDiff = $element.position().top - $parent.position().top;
$parent.animate({
scrollTop : topDiff
}, 100);
};
This is Atharva's answer from: https://developer.mozilla.org/en-US/docs/Web/API/element.scrollIntoView.
Just wanted to add if your document is in an iframe, you can choose an element in the parent frame to scroll into view:
$('#element-in-parent-frame', window.parent.document).get(0).scrollIntoView();
I wrote a general purpose function that scrolls to either a jQuery object, a CSS selector, or a numeric value.
Example usage:
// scroll to "#target-element":
$.scrollTo("#target-element");
// scroll to 80 pixels above first element with class ".invalid":
$.scrollTo(".invalid", -80);
// scroll a container with id "#my-container" to 300 pixels from its top:
$.scrollTo(300, 0, "slow", "#my-container");
The function's code:
/**
* Scrolls the container to the target position minus the offset
*
* #param target - the destination to scroll to, can be a jQuery object
* jQuery selector, or numeric position
* #param offset - the offset in pixels from the target position, e.g.
* pass -80 to scroll to 80 pixels above the target
* #param speed - the scroll speed in milliseconds, or one of the
* strings "fast" or "slow". default: 500
* #param container - a jQuery object or selector for the container to
* be scrolled. default: "html, body"
*/
jQuery.scrollTo = function (target, offset, speed, container) {
if (isNaN(target)) {
if (!(target instanceof jQuery))
target = $(target);
target = parseInt(target.offset().top);
}
container = container || "html, body";
if (!(container instanceof jQuery))
container = $(container);
speed = speed || 500;
offset = offset || 0;
container.animate({
scrollTop: target + offset
}, speed);
};
When the user clicks on that input with #subject, the page should
scroll to the last element of the page with a nice animation. It
should be a scroll to bottom and not to top.
The last item of the page is a submit button with #submit
$('#subject').click(function()
{
$('#submit').focus();
$('#subject').focus();
});
This will first scroll down to #submit then restore the cursor back to the input that was clicked, which mimics a scroll down, and works on most browsers. It also doesn't require jQuery as it can be written in pure JavaScript.
Can this fashion of using focus function mimic animation in a better way, through chaining focus calls. I haven't tested this theory, but it would look something like this:
<style>
#F > *
{
width: 100%;
}
</style>
<form id="F" >
<div id="child_1"> .. </div>
<div id="child_2"> .. </div>
..
<div id="child_K"> .. </div>
</form>
<script>
$('#child_N').click(function()
{
$('#child_N').focus();
$('#child_N+1').focus();
..
$('#child_K').focus();
$('#child_N').focus();
});
</script>
I set up a module scroll-element npm install scroll-element. It works like this:
import { scrollToElement, scrollWindowToElement } from 'scroll-element'
/* scroll the window to your target element, duration and offset optional */
let targetElement = document.getElementById('my-item')
scrollWindowToElement(targetElement)
/* scroll the overflow container element to your target element, duration and offset optional */
let containerElement = document.getElementById('my-container')
let targetElement = document.getElementById('my-item')
scrollToElement(containerElement, targetElement)
Written with help from the following SO posts:
offset-top-of-an-element-without-jquery
scrolltop-animation-without-jquery
Here is the code:
export const scrollToElement = function(containerElement, targetElement, duration, offset) {
if (duration == null) { duration = 1000 }
if (offset == null) { offset = 0 }
let targetOffsetTop = getElementOffset(targetElement).top
let containerOffsetTop = getElementOffset(containerElement).top
let scrollTarget = targetOffsetTop + ( containerElement.scrollTop - containerOffsetTop)
scrollTarget += offset
scroll(containerElement, scrollTarget, duration)
}
export const scrollWindowToElement = function(targetElement, duration, offset) {
if (duration == null) { duration = 1000 }
if (offset == null) { offset = 0 }
let scrollTarget = getElementOffset(targetElement).top
scrollTarget += offset
scrollWindow(scrollTarget, duration)
}
function scroll(containerElement, scrollTarget, duration) {
let scrollStep = scrollTarget / (duration / 15)
let interval = setInterval(() => {
if ( containerElement.scrollTop < scrollTarget ) {
containerElement.scrollTop += scrollStep
} else {
clearInterval(interval)
}
},15)
}
function scrollWindow(scrollTarget, duration) {
let scrollStep = scrollTarget / (duration / 15)
let interval = setInterval(() => {
if ( window.scrollY < scrollTarget ) {
window.scrollBy( 0, scrollStep )
} else {
clearInterval(interval)
}
},15)
}
function getElementOffset(element) {
let de = document.documentElement
let box = element.getBoundingClientRect()
let top = box.top + window.pageYOffset - de.clientTop
let left = box.left + window.pageXOffset - de.clientLeft
return { top: top, left: left }
}
Updated answer as of 2019:
$('body').animate({
scrollTop: $('#subject').offset().top - $('body').offset().top + $('body').scrollTop()
}, 'fast');
ONELINER
subject.onclick = e=> window.scroll({ top: submit.offsetTop, behavior: 'smooth'});
subject.onclick = e=> window.scroll({top: submit.offsetTop, behavior: 'smooth'});
.box,.foot{display: flex;background:#fdf;padding:500px 0} .foot{padding:250px}
<input type="text" class="textfield" value="click here" id="subject" name="subject">
<div class="box">
Some content
<textarea></textarea>
</div>
<input type="submit" class="submit" id="submit" name="submit" value="Ok, Done.">
<div class="foot">Some footer</div>
For what it's worth, this is how I managed to achieve such behavior for a general element which can be inside a DIV with scrolling. In our case we don't scroll the full body, but just particular elements with overflow: auto; within a larger layout.
It creates a fake input of the height of the target element, and then puts a focus to it, and the browser will take care about the rest no matter how deep within the scrollable hierarchy you are. Works like a charm.
var $scrollTo = $('#someId'),
inputElem = $('<input type="text"></input>');
$scrollTo.prepend(inputElem);
inputElem.css({
position: 'absolute',
width: '1px',
height: $scrollTo.height()
});
inputElem.focus();
inputElem.remove();
This worked for me:
var targetOffset = $('#elementToScrollTo').offset().top;
$('#DivParent').animate({scrollTop: targetOffset}, 2500);
I'm making a vertical parallax scrolling site with ScrollMagic which includes a navigation menu at the top to link within the site.
The menu itself works correctly when no parallax animation is applied to the scroll but when the parallax is added (ie the 2nd section moves up over the intro section), it seems unable to take the reduction in overall height into account when moving to the section, so it overshoots.
Here is some code:
var site = {
smController : {},
init : function () {
site.setupScroll();
site.setupMainNavigation();
site.setupAnimation();
},
setupScroll : function () {
// init the smController
var controller = new ScrollMagic({
globalSceneOptions: {
triggerHook: "onLeave"
}
});
site.smController = controller;
},
setupMainNavigation : function () {
$('.menuclick').on('click', function (event) {
event.preventDefault();
var anchor = $(this),
sectionId = $(anchor.attr('href'));
site.scrollToSection(sectionId);
});
},
/**
* uses tweenlite and scrolltoplugin from greensock
* #param {string} sectionId id of section to scroll to
* #return {void}
*/
scrollToSection : function (sectionId) {
var scrollYPos = $(sectionId).offset().top;
TweenLite.to(window, 0.5, { scrollTo:{ y: scrollYPos } });
},
setupAnimation : function () {
// parallax animation - move marginTop back by 100%
var tween = new TimelineMax()
.to('#section1', 2, { marginTop: '-100%', ease:Linear.easeNone });
var controller = site.smController,
scene = new ScrollScene({ duration: 500 })
.setTween(tween)
.addTo(controller);
// show indicators (requires debug extension)
scene.addIndicators();
}
};
$(document).ready(function () {
site.init();
});
Does anyone have a strategy to deal with moving (parallax) sections like this please?
Thanks
In ScrollMagic 1.1 you can now provide custom scroll functions AND scroll to the beginning of a specific scene.
Read more here:
http://janpaepke.github.io/ScrollMagic/docs/ScrollMagic.html#scrollTo
I would also strongly suggest not to use animated elements as scroll targets, because their position might be different before and after initiating scroll.
If you have elements that influence the DOM height, try to take them out of the DOM flow.
You can do this for example by adding an element as a placeholder and setting your element as positioned absolutely.
hope this helps.
J
I did something like this, using a similar setup to the demo page
new ScrollMagic.Scene(settings)
.setPin(slides[i])
.on('enter', function () {
var $trigger = $(this.triggerElement()),
$nextSlide = $trigger.parent().next().find('.slide');
/*
* If there's a next slide,
* update the href of the button
* to target the next slide
* otherwise, we're at the end;
* toggle the button state so it targets
* the top of the page
*/
if ($nextSlide.length) {
$('.btn-scroll').attr('href', '#' + $nextSlide.attr('id'));
} else {
$('.btn-scroll').attr('href', '#').addClass('up');
}
})
.on('leave', function (event) {
var $trigger = $(this.triggerElement()),
$firstSlide = $('.slide:first');
/*
* If we're going back up and we pass
* the first slide, update the button
* so it targets the first slide
*/
if (event.scrollDirection === 'REVERSE' && ($trigger.offset().top === $firstSlide.offset().top)) {
$('.btn-scroll').attr('href', originalTarget).removeClass('up');
}
})
.addTo(controller);
It just needs an anchor link with the href set to the first slide.
and something like this to handle the scroll:
var scrollToContent = function (target, speed) {
if (target === '#') {
target = $('body');
} else {
target = $(target);
}
speed = typeof speed !== 'undefined' ? speed : 'slow';
$('html, body').animate({
scrollTop: target.offset().top
}, speed);
}
I'm trying to create an accordion widget in jquery similar to jquery's accordion plugin, with the difference that I want the handles to appear below their respective content instead of above. My accordion works by decreasing the height of the open content section while at the same time increasing the height of the clicked content section. I've posted an example here. My problem is that the animations aren't started at exactly the same time, and there is a noticeable "jump" due to the slight delay before the second animation is started.
Scriptaculous has a function called Effect.Parallel that allows you to create an array of animation effects and execute them in parallel. Unfortunately, I can't seem to find something similar with jquery.
Is there a way I can run precise parallel animations on separate divs in jquery?
Edit: I'm as much interested in alternative methods of coding this accordion widget. So if there is any other method people think would work I'm open to that.
One more answer, hopefully my last one...
Unfortunately, John Resig's syncAnimate method is not quite up to snuff for the accordion-type animation I want to do. While it works great on Firefox, I couldn't get it working smoothly on IE or Safari.
With that said, I decided to bite the bullet and write my own animation engine that does simple parallel animations. The class-code uses jquery functions but is not a jquery plugin. Also, I've only set it up to do size/position animations, which is all I need.
ParallelAnimations = function(animations, opts){
this.init(animations, opts);
};
$.extend(ParallelAnimations.prototype, {
options: {
duration: 250
},
rules: {},
init: function(animations, opts){
// Overwrite the default options
$.extend(this.options, opts);
// Create a set of rules to follow in our animation
for(var i in animations){
this.rules[i] = {
element: animations[i].element,
changes: new Array()
};
for(var style in animations[i].styles){
// Calculate the start and end point values for the given style change
var from = this.parse_style_value(animations[i].element, style, "");
var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);
this.rules[i].changes.push({
from: from,
to: to,
style: style
});
}
}
this.start()
},
/*
* Does some parsing of the given and real style values
* Allows for pixel and percentage-based animations
*/
parse_style_value: function(element, style, given_value){
var real_value = element.css(style);
if(given_value.indexOf("px") != -1){
return {
amount: given_value.substring(0, (given_value.length - 2)),
unit: "px"
};
}
if(real_value == "auto"){
return {
amount: 0,
unit: "px"
};
}
if(given_value.indexOf("%") != -1){
var fraction = given_value.substring(0, given_value.length - 1) / 100;
return {
amount: (real_value.substring(0, real_value.length - 2) * fraction),
unit: "px"
};
}
if(!given_value){
return {
amount: real_value.substring(0, real_value.length - 2),
unit: "px"
};
}
},
/*
* Start the animation
*/
start: function(){
var self = this;
var start_time = new Date().getTime();
var freq = (1 / this.options.duration);
var interval = setInterval(function(){
var elapsed_time = new Date().getTime() - start_time;
if(elapsed_time < self.options.duration){
var f = elapsed_time * freq;
for(var i in self.rules){
for(var j in self.rules[i].changes){
self.step(self.rules[i].element, self.rules[i].changes[j], f);
}
}
}
else{
clearInterval(interval);
for(var i in self.rules){
for(var j in self.rules[i].changes)
self.step(self.rules[i].element, self.rules[i].changes[j], 1);
}
}
}, 10);
},
/*
* Perform an animation step
* Only works with position-based animations
*/
step: function(element, change, fraction){
var new_value;
switch(change.style){
case 'height':
case 'width':
case 'top':
case 'bottom':
case 'left':
case 'right':
case 'marginTop':
case 'marginBottom':
case 'marginLeft':
case 'marginRight':
new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
break;
}
if(new_value)
element.css(change.style, new_value);
}
});
Then the original Accordion class only needs to be modified in the animate method to make use of the new call.
Accordion = function(container_id, options){
this.init(container_id, options);
}
$.extend(Accordion.prototype, {
container_id: '',
options: {},
active_tab: 0,
animating: false,
button_position: 'below',
duration: 250,
height: 100,
handle_class: ".handle",
section_class: ".section",
init: function(container_id, options){
var self = this;
this.container_id = container_id;
this.button_position = this.get_button_position();
// The height of each section, use the height specified in the stylesheet if possible
this.height = $(this.container_id + " " + this.section_class).css("height");
if(options && options.duration) this.duration = options.duration;
if(options && options.active_tab) this.active_tab = options.active_tab;
// Set the first section to have a height and be "open"
// All the rest of the sections should have 0px height
$(this.container_id).children(this.section_class).eq(this.active_tab)
.addClass("open")
.css("height", this.height)
.siblings(this.section_class)
.css("height", "0px");
// figure out the state of the handles
this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));
// Set up an event handler to animate each section
$(this.container_id + " " + this.handle_class).mouseover(function(){
if(self.animating)
return;
self.animate($(this));
});
},
/*
* Determines whether handles are above or below their associated section
*/
get_button_position: function(){
return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
},
/*
* Animate the accordion from one node to another
*/
animate: function(handle){
var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());
var open_section = handle.siblings().andSelf().filter(".open");
if(active_section.hasClass("open"))
return;
this.animating = true;
// figure out the state of the handles
this.do_handle_logic(handle);
// Close the open section
var arr = new Array();
arr.push({
element: open_section,
styles: {
"height": "0px"
}
});
arr.push({
element: active_section,
styles: {
"height": this.height
}
});
new ParallelAnimations(arr, {duration: this.duration});
var self = this;
window.setTimeout(function(){
open_section.removeClass("open");
active_section.addClass("open");
self.animating = false;
}, this.duration);
},
/*
* Update the current class or "state" of each handle
*/
do_handle_logic: function(handle){
var all_handles = handle.siblings(".handle").andSelf();
var above_handles = handle.prevAll(this.handle_class);
var below_handles = handle.nextAll(this.handle_class);
// Remove all obsolete handles
all_handles
.removeClass("handle_on_above")
.removeClass("handle_on_below")
.removeClass("handle_off_below")
.removeClass("handle_off_above");
// Apply the "on" state to the current handle
if(this.button_position == 'below'){
handle
.addClass("handle_on_below");
}
else{
handle
.addClass("handle_on_above");
}
// Apply the off above/below state to the rest of the handles
above_handles
.addClass("handle_off_above");
below_handles
.addClass("handle_off_below");
}
});
The HTML is still called the same way:
<html>
<head>
<title>Parallel Accordion Animation</title>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.js"></script>
<script type="text/javascript">
$(document).ready(function(){
new Accordion("#accordion");
});
</script>
<style type="text/css">
#accordion{
position: relative;
}
#accordion .handle{
width: 260px;
height: 30px;
background-color: orange;
}
#accordion .section{
width: 260px;
height: 445px;
background-color: #a9a9a9;
overflow: hidden;
position: relative;
}
</style>
</head>
<body>
<div id="accordion">
<div class="section"><!-- --></div>
<div class="handle">handle 1</div>
<div class="section"><!-- --></div>
<div class="handle">handle 2</div>
<div class="section"><!-- --></div>
<div class="handle">handle 3</div>
<div class="section"><!-- --></div>
<div class="handle">handle 4</div>
<div class="section"><!-- --></div>
<div class="handle">handle 5</div>
</div>
</body>
</html>
There are a few things I may add in the future:
- Queued Animations
- Animations for other types of styles (colors,etc)
John Resig posted a synchronized animation sample (no instructions, click a colored box). It might take some work to figure out how to apply it to your control, but it could be a good place to start.
This does not solve running animations in parallel however it reproduces your expected behavior without the jitter. I placed section inside of handle to reduce the number of animations. You could use andSelf() to make the code smaller but it would be harder to read. You will need to make some style tweaks.
<html>
<head>
<title>Accordion Test</title>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("#accordion .handle").click(function(){
var open = $(this).parent().children(".section, .open");
var active = $(this);
if (!active.hasClass("open"))
{
if (active.hasClass("up"))
{
console.log("up");
active.animate({top:"+=100"}).removeClass("up");
active.nextAll(".handle").andSelf().filter(".up").animate({top:"+=100"}).removeClass("up");
$(".section", active).slideUp();
$(".section", active.nextAll()).slideUp();
$(".section", active.prev()).slideDown();
}
else
{
active.prevAll(".handle").not(".up").animate({top:"-=100"}).addClass("up");
$(".section", active.prev()).slideDown();
}
open.removeClass("open");
active.addClass("open");
}
});
});
</script>
<style type="text/css">
#accordion{
width: 200px;
position:relative;
}
#accordion .section{
width: 196px;
margin-left: 2px;
height: 100px;
background-color: #b9b9b9;
display:none;
}
#accordion .handle{
width: 200px;
height: 30px;
background-color: #d9d9d9;
border: 1px solid black;
cursor: pointer;
cursor: hand;
position: absolute;
}
#accordion .handle .header {
height: 30px;
}
</style>
</head>
<body>
<div id="accordion">
<div id="s1" class="section open" style="display:block">This is section 1</div>
<div class="handle open" style="top:100;">
<div class="header">handle 1</div>
<div class="section">This is section 2</div>
</div>
<div class="handle" style="top:130;">
<div class="header">handle 2</div>
<div class="section">This is section 3</div>
</div>
<div class="handle" style="top:160;">
<div class="header">handle 3</div>
<div class="section">This is section 4</div>
</div>
<div class="handle" style="top:190;">
<div class="header">handle 4</div>
<div class="section">This is section 5</div>
</div>
<div class="handle" style="top:220;">
<div class="content">handle 5</div>
</div>
</div>
</body>
</html>
Thanks Adam Plumb for a really great solution to parallel animations. I had a small problem with it though and that was that it somehow saved roles from earlier animations i fixed that by setting the rules to {} before adding them in the init function. It can probably be done in a better way though. I also added a callback function that is called when the animation have finished.
ParallelAnimations = function(animations, opts){
this.init(animations, opts);
};
$.extend(ParallelAnimations.prototype, {
options: {
duration: 250,
callback: null
},
rules: {},
init: function(animations, opts){
// Overwrite the default options
$.extend(this.options, opts);
// Create a set of rules to follow in our animation
this.rules = {}; // Empty the rules.
for(var i in animations){
this.rules[i] = {
element: animations[i].element,
changes: new Array()
};
for(var style in animations[i].styles){
// Calculate the start and end point values for the given style change
var from = this.parse_style_value(animations[i].element, style, "");
var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);
this.rules[i].changes.push({
from: from,
to: to,
style: style
});
}
}
this.start()
},
/*
* Does some parsing of the given and real style values
* Allows for pixel and percentage-based animations
*/
parse_style_value: function(element, style, given_value){
var real_value = element.css(style);
if(given_value.indexOf("px") != -1){
return {
amount: given_value.substring(0, (given_value.length - 2)),
unit: "px"
};
}
if(real_value == "auto"){
return {
amount: 0,
unit: "px"
};
}
if(given_value.indexOf("%") != -1){
var fraction = given_value.substring(0, given_value.length - 1) / 100;
return {
amount: (real_value.substring(0, real_value.length - 2) * fraction),
unit: "px"
};
}
if(!given_value){
return {
amount: real_value.substring(0, real_value.length - 2),
unit: "px"
};
}
},
/*
* Start the animation
*/
start: function(){
var self = this;
var start_time = new Date().getTime();
var freq = (1 / this.options.duration);
var interval = setInterval(function(){
var elapsed_time = new Date().getTime() - start_time;
if(elapsed_time < self.options.duration){
var f = elapsed_time * freq;
for(var i in self.rules){
for(var j in self.rules[i].changes){
self.step(self.rules[i].element, self.rules[i].changes[j], f);
}
}
}
else{
clearInterval(interval);
for(var i in self.rules){
for(var j in self.rules[i].changes)
self.step(self.rules[i].element, self.rules[i].changes[j], 1);
}
if(self.options.callback != null) {
self.options.callback(); // Do Callback
}
}
}, 10);
},
/*
* Perform an animation step
* Only works with position-based animations
*/
step: function(element, change, fraction){
var new_value;
switch(change.style){
case 'height':
case 'width':
case 'top':
case 'bottom':
case 'left':
case 'right':
case 'marginTop':
case 'marginBottom':
case 'marginLeft':
case 'marginRight':
new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
break;
}
if(new_value)
element.css(change.style, new_value);
}
});
I think your problem isn't timing but fractional division of a pixel. If you try this code it looks smooth for handle 1 and 2 but not others in Firefox 3 but still looks jumpy in chrome.
active
.animate({ height: "100px" })
.siblings(".section")
.animate({ height: "0px" });
Have you thought about making the position of the elements static or absolute? If your only moving the position of two elements you don't have to worry about the other ones jumping. Give me a second and I'll try to make an example.
Update: I'm no longer using John Resig's syncAnimate plugin. See my later answer for the final solution
I just wanted to supply the final working solution that I'm employing on my project. It uses the syncAnimate plugin that John Resig wrote (posted by Corbin March).
This code will:
Read and use the section height from CSS
Allow you to set the animation duration, and default active section through an options object.
Automatically detect handle position relative to section and adjusts accordingly. So you move the handles above or below a section in the markup and not have to change the js code.
HTML
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.js"></script>
<script type="text/javascript">
$(document).ready(function(){
new Accordion("#accordion", {active_tab: 0});
});
</script>
<style type="text/css">
#accordion .handle{
width: 260px;
height: 30px;
background-color: orange;
}
#accordion .section{
width: 260px;
height: 445px;
background-color: #a9a9a9;
overflow: hidden;
position: relative;
}
</style>
<div id="accordion">
<div class="section">Section Code</div>
<div class="handle">handle 1</div>
<div class="section">Section Code</div>
<div class="handle">handle 2</div>
<div class="section">Section Code</div>
<div class="handle">handle 3</div>
<div class="section">Section Code</div>
<div class="handle">handle 4</div>
<div class="section">Section Code</div>
<div class="handle">handle 5</div>
</div>
ui.js
Accordion = function(container_id, options){
this.init(container_id, options);
}
$.extend(Accordion.prototype, {
container_id: '',
options: {},
active_tab: 0,
animating: false,
button_position: 'below',
duration: 250,
height: 100,
handle_class: ".handle",
section_class: ".section",
init: function(container_id, options){
var self = this;
this.container_id = container_id;
this.button_position = this.get_button_position();
// The height of each section, use the height specified in the stylesheet if possible
this.height = $(this.container_id + " " + this.section_class).css("height");
if(options && options.duration) this.duration = options.duration;
if(options && options.active_tab) this.active_tab = options.active_tab;
// Set the first section to have a height and be "open"
// All the rest of the sections should have 0px height
$(this.container_id).children(this.section_class).eq(this.active_tab)
.addClass("open")
.css("height", this.height)
.siblings(this.section_class)
.css("height", "0px");
// figure out the state of the handles
this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));
// Set up an event handler to animate each section
$(this.container_id + " " + this.handle_class).mouseover(function(){
if(self.animating)
return;
self.animate($(this));
});
},
/*
* Determines whether handles are above or below their associated section
*/
get_button_position: function(){
return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
},
/*
* Animate the accordion from one node to another
*/
animate: function(handle){
var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());
var open_section = handle.siblings().andSelf().filter(".open");
if(active_section.hasClass("open"))
return;
this.animating = true;
// figure out the state of the handles
this.do_handle_logic(handle);
// Close the open section
open_section
.syncAnimate(active_section, {"height": "0px"}, {queue:false, duration:this.duration}, '')
.removeClass("open");
// Open the new section
active_section
.syncAnimate(open_section, {"height": this.height}, {queue:false, duration:this.duration}, '')
.addClass("open");
var self = this;
window.setTimeout(function(){
self.animating = false;
}, this.duration);
},
/*
* Update the current class or "state" of each handle
*/
do_handle_logic: function(handle){
var all_handles = handle.siblings(".handle").andSelf();
var above_handles = handle.prevAll(this.handle_class);
var below_handles = handle.nextAll(this.handle_class);
// Remove all obsolete handles
all_handles
.removeClass("handle_on_above")
.removeClass("handle_on_below")
.removeClass("handle_off_below")
.removeClass("handle_off_above");
// Apply the "on" state to the current handle
if(this.button_position == 'below'){
handle
.addClass("handle_on_below");
}
else{
handle
.addClass("handle_on_above");
}
// Apply the off above/below state to the rest of the handles
above_handles
.addClass("handle_off_above");
below_handles
.addClass("handle_off_below");
}
});
You can't do a parallel effect in jquery with proper queue and scope. Scriptaculous got it right with queue and scope where jQuery on the other hand has .queue and .animate that are basically useless combined. The only thing jQuery is good for out of the box is pushing some style attributes around on the dom whereas Scriptaculous covers the whole spectrum of what's possible with effects.
You need to use Scriptaculous and John Resig should rethink jQuery.fx, he should have a look at scripty2.com while he's at it.