I am building a news feed with slides that show a summary and a read more button that when clicked reveals the rest of the content.
Here is a jsFiddle: http://jsfiddle.net/pbunz5ue/1/
When Read More is clicked the story opens and the scrolling stops, then the user clicks Less, its hides the story and continues scrolling.
When Read More is clicked I need the scroll of the div to be aligned so that the opened story starts at the top of the div. My slider does this perfectly the first round the stories make, once they are displayed a second time from the button up this no longer works.
Can someone explain why ??
Here is my code:
JS:
$(document).ready(function() {
//User clicks Read More, add 'open' class to news item
$('.news-read-more').on('click', function() {
blockedSlider = true;
clearInterval(myTimer);
$('.news-list').children('li').each(function() {
$(this).removeClass('open');
});
$(this).parent().toggleClass('open');
var n = $(this).parent();
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top);
});
//User clicks Less, remove 'open' class from news item
$('.news-read-less').on('click', function() {
if (blockedSlider == true) {
blockedSlider = false;
$(this).parent().removeClass('open');
myTimer = setInterval(slideLoop, 2000)
}
});
var myTimer = setInterval(slideLoop, 2000)
var blockedSlider = false;
function slideLoop() {
// Work out width of current slider size
var widthPx = $('.news-list-item').css('height');
var width = widthPx.substring(0, widthPx.length - 2);
// Work out current left
var left = $('.news-list').css('top');
left = left.substring(0, left.length - 2);
if (left <= -(width * 2)) {
var neg = '-' + widthPx;
$('.news-list').css('top', neg);
var slide = $('.news-list li:first');
$('.news-list').children('li:first').remove();
$('.news-list ').append(slide);
//User clicks Read More, add 'open' class to news item
$('.news-read-more').on('click', function() {
blockedSlider = true;
clearInterval(myTimer);
$('.news-list').children('li').each(function() {
$(this).removeClass('open');
});
$(this).parent().toggleClass('open');
var n = $(this).parent();
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top - 360);
});
//User clicks Less, remove 'open' class from news item
$('.news-read-less').on('click', function() {
if (blockedSlider == true) {
blockedSlider = false;
$(this).parent().removeClass('open');
myTimer = setInterval(slideLoop, 2000)
}
});
var move = "-=" + widthPx;
$('.news-list').animate({ top: move }, "slow", "swing");
}
else {
var move = "-=" + widthPx;
$('.news-list').animate({ top: move }, "slow", "swing");
}
}
});
The problem is caused by negative "top" position of .news-list that busts the position of the opened element.
-> I update your fiddle: http://jsfiddle.net/pbunz5ue/3/ <-
What I've changed
the main error:
when you calculate the position you've not considered the negative top offset of the news container .news-list when it slide up:
I've changed this two lines:
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top);
in:
var pos = n.position().top + parseInt($(".news-list").css("top")) || n.position().top;
$('.news-slider-wrapper').scrollTop(pos);
If $(".news-list") has top position I add It to the calculation of position (if the result is auto there is a fallback that prevent NaN result || n.position().top)
You had duplicated the handler $('.news-read-more').on('click') and $('.news-read-less').on('click'); I deleted those within the function because they seems useless.
EDIT:: .on click issue
Try this two way to solve your problem:
1.
If you have some problem when new item was added into your list try to bind the event to the "items" container .news-list and filter the selector every time it will fire:
$('.news-read-more').on('click', function() { //...
$('.news-read-less').on('click', function() { //...
becomes:
$('.news-list').on('click','.news-read-more', function() { //...
$('.news-list').on('click','.news-read-less', function() { //...
the container .news-list never change during your animation, and DOM manipulation.
2.
An other way is to change this rows, in your code:
var slide = $('.news-list li:first');
$('.news-list').children('li:first').remove();
$('.news-list ').append(slide);
becomes:
var slide = $('.news-list li:first');
$('.news-list ').append(slide);
or:
$('.news-list ').append('.news-list li:first');
if you .remove the element, (I think) you unbind all event because you remove the element from DOM, but you not need to remove you need to move it from top to bottom. This way prevent the unbind of click event.
I hope to had understand the problem :).
sorry for may bad english
Related
Hello there I've been trying to find a fix for the many scroll events firing on one scroll. This is the only thing close to working for me so far. I want to smoothscroll between two divs (#boxes and #header) I want to use the scroll bar to trigger this smooth scroll and not a button. Any suggestions on how to only take one scroll event? I also used solutions based from prev stackoverflow questions. I used my own locator instead of offsets because thats also unreliable
$(window).scroll(function () {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(function () {
if (locator == 0) {
id = $("#boxes");
locator = 1;
} else if (locator = 1) {
id = $("#header");
locator = 0;
}
// target element
var $id = $(id);
if ($id.length === 0) {
return;
}
// top position relative to the document
var pos = $id.offset().top;
// animated top scrolling
$('html, body').animate({scrollTop: pos}, 1500, function () {
$('html, body').clearQueue();
$('html, body').stop();
});
}, 2);
});
So, to be clear, you want any minor scroll event to scroll between one item and the other? Note that when a user scrolls, there is a "momentum" that the browser implements, and you'll be battling with that.
Regardless: You don't need to wrap this in a setTimeout. Right now, your javascript is creating a new setTimeout function that is being fired every 2ms. Scroll events occur with every pixel of movement in the scroll, so if you scroll 100px, you're going to be firing 100 times every 2ms. (That's 50,000 times).
Instead, have a a variable (isScrolling) track the state, so, if you're in the middle of scrolling, the function won't fire.
var isScrolling = false;
var locator = 0;
$(window).scroll(function () {
if (isScrolling) return false;
if (locator == 0) {
id = $("#boxes");
locator = 1;
} else if (locator = 1) {
id = $("#header");
locator = 0;
}
// target element
var $id = $(id);
if ($id.length === 0) {
return;
}
// top position relative to the document
var pos = $id.offset().top;
// animated top scrolling
isScrolling = true;
$('html, body').animate({scrollTop: pos}, 1500, function () {
$('html, body').clearQueue();
$('html, body').stop();
isScrolling = false;
});
});
Here's a JSbin: http://jsbin.com/jugefup/edit?html,css,js,output
Here is a code that should open and close my site's menu. The menu is divided to divs and each one is timed to enter the screen after the other.
<script type="text/javascript">
var s=0;
function menuOpen() {
if (s==0){
document.getElementById("menu_icon").src = "x.png";
document.getElementById("nav_menu").style.zIndex = "3";
$('.box-wrapper').each(function(index, element) {
setTimeout(function(){
element.classList.remove('loading');
}, index * 100);
});
s++;
} else {
document.getElementById("menu_icon").src = "menu_icon.png";
$('.box-wrapper').each(function(index, element) {
setTimeout(function(){
element.classList.add('loading');
}, index * 100);
});
s=0;
// how to make this part run after the menu has finished folding?
setTimeout(function(){
document.getElementById("nav_menu").style.zIndex = "1";
}, 1500);
}
}
</script>
The content of the page is at z-index 2. The menu when folded is at 1 and when open at 3.
Is there a way to run the command moving the menu to z-index 1 after the menu has finished folding completely?
Currently what I did was to time the animation (1600ms) and use setTimeout. But this timing will change if I'll add more lines to my menu or if someone is clicking rapidly on the menu icon.
I'm rather new to JS and Jquery so go easy on me (:
Thanks of your help.
Below you can find the code and link to jsfiddle. Unfortunetly jsfiddle blocks the animate method for unknown reason so I don't debug, but even if it code will not work :))) I hope you will cathch the idea. And also some explanation.
Firstly our items are hidden. There are two functions displayMenu and hideMenu, they are similar, but display - run animations from the top invisible, and hide - start hide items from the bottom visible. To prevent mess I use two flags and two classes, first openFlag it is say what animations should be played now. Our hide and display functions are recursive and after they end current animation(hide or show) they check openFlag, and play next hide/show or start another chain of hide/show functions. It is the most difficult to understand part) But important that with it you can click as many times as you want and all would be fine and would be never broken by clicks.
Two classes we use as animation-chain can change behaviour and we need the way to choose items that alredy visible or hidden, so this why after each animation we set only one of this classes and remove another.
Now there is one problem if all animation are ended when we click button we should start new chain of animations, but if chain has been already started we need just to switch openFlag, and when current animation stops, it will change the behaviour. So this is the reason for btnFlag it is 1 if no active chain-animations at this moment.
After the last execution of element of animation-chain it will call callback arg, that you should pass, also at this moment will set btnFlag to 0, that means that animation-chain stopped. The openFlag as you remember changed at moment og click.
function end() {
console.log("here you can set zIndex");
}
var openFlag = 0; //become 1 after we start open elems
var btnFlag = 1;
$(document).ready(function() {
$('.toggleMenu').click(function() {
if (!$('.menuBlocks').hasClass('visible')) {
if (openFlag == 0) {
openFlag = 1;
if (btnFlag) {
var items = $('.invisibleItem');
var amount = items.length;
displayMenu(0, amount, items, end);
}
}
} else {
openFlag = 0;
if (btnFlag) {
var items = $('.visibleItem');
var amount = items.length;
hideMenu(amount - 1, items, end);
}
}
});
});
function displayMenu(i, amount, items, callback) {
if (i < amount && openFlag) {
items[i].animate({
"width": "100px"
}, 1000, function() {
items[i].removeClass('invisibleItem');
items[i].addClass('visibleItem');
displayMenu(i + 1, amount, items);
});
} else if (!openFlag) {
var items = $('.visibleItem');
var amount = items.length;
hideMenu(amount - 1, items, makeToggleVisible);
} else {
btnFlag = 1; //all aniimations ended
callback();
}
}
function hideMenu(i, items, callback) {
if (i < 0 && openFlag) {
items[i].animate({
"width": "0px"
}, 1000, function() {
items[i].removeClass('visibleItem');
items[i].addClass('invisibleItem');
hideMenu(i - 1, amount, items);
});
} else if (!openFlag) {
var items = $('.invisibleItem');
var amount = items.length;
displayMenu(0, amount, items, makeToggleVisible);
} else {
btnFlag = 1; //all animations are ended
callback();
}
}
https://jsfiddle.net/ShinShil/nrtyshv5/4/
Ok fixed it.
I moved everything to jquery. Used animate and promise.
This is what came out at the end. It is a side menu that will open it's li elements one-by-one.
var s=0;
var navMenu = document.getElementById("nav_menu");
var navBtn = document.getElementById("btn");
$(document).ready(function(){
$("button").click(function(){
if (s==0) {
navMenu.style.zIndex = "4";
navBtn.classList.add('close');
$('ul').each(function() {
$(this).children().each(function(i) {
$(this).delay(i * 100).animate({left:0});
});
});
$( "li" ).promise().done(function() {
navMenu.style.zIndex = "4";
});
s++;
}
else {
navBtn.classList.remove('close');
$('ul').each(function() {
$(this).children().each(function(i) {
$(this).delay(i * 100).animate({left:"100%"});
});
});
s=0;
$( "li" ).promise().done(function() {
navMenu.style.zIndex = "1";
});
}
});
});
and with CSS transitions.
var s=0;
function menuOpen() {
if (s==0){
document.getElementById("menu_icon").src = "x.png";
document.getElementById("nav_menu").style.zIndex = "3";
$('.box-wrapper').each(function(index, element) {
setTimeout(function(){
element.classList.remove('loading');
}, index * 100);
});
s++;
$("#last").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){
document.getElementById("nav_menu").style.zIndex = "3";
});
} else {
document.getElementById("menu_icon").src = "menu_icon.png";
$('.box-wrapper').each(function(index, element) {
setTimeout(function(){
element.classList.add('loading');
}, index * 100);
});
s=0;
$("#last").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){
document.getElementById("nav_menu").style.zIndex = "1";
$("#nav_menu").scrollTop(0);
});
}
}
I want to make the sticky-nav to act similar(scroll is off when the menu is expanded) to this website's nav(http://amandagerhardsen.com/#cloudbusting/4) when expanded.
How do I do it?
var Boxlayout = (function () {
var $el = $('#sticky-nav'),
$sections = $el.children('section'),
// work panels
$workPanelsContainer = $('#bl-panel-work-items'),
// close work panel trigger
$closeWorkItem = $workPanelsContainer.find('nav > span.hidemenu'),
transEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'msTransition': 'MSTransitionEnd',
'transition': 'transitionend'
},
// transition end event name
transEndEventName = transEndEventNames[Modernizr.prefixed('transition')],
// support css transitions
supportTransitions = Modernizr.csstransitions;
function init() {
initEvents();
}
function initEvents() {
$sections.each(function () {
var $section = $(this);
// expand the clicked section and scale down the others
$section.on('click', function () {
if (!$section.data('open')) {
$section.data('open', true).addClass('bl-expand bl-expand-top');
$el.addClass('bl-expand-item');
}
}).find('span.hidemenu').on('click', function () {
// close the expanded section and scale up the others
$section.data('open', false).removeClass('bl-expand').on(transEndEventName, function (event) {
if (!$(event.target).is('section')) return false;
$(this).off(transEndEventName).removeClass('bl-expand-top');
});
if (!supportTransitions) {
$section.removeClass('bl-expand-top');
}
$el.removeClass('bl-expand-item');
return false;
});
});
// clicking on a work item: the current section scales down and the respective work panel slides up
$workItems.on('click', function (event) {
// scale down main section
$sectionWork.addClass('bl-scale-down');
// show panel for this work item
$workPanelsContainer.addClass('bl-panel-items-show');
var $panel = $workPanelsContainer.find("[data-panel='" + $(this).data('panel') + "']");
currentWorkPanel = $panel.index();
$panel.addClass('bl-show-work');
return false;
});
// navigating the work items: current work panel scales down and the next work panel slides up
$nextWorkItem.on('click', function (event) {
if (isAnimating) {
return false;
}
isAnimating = true;
var $currentPanel = $workPanels.eq(currentWorkPanel);
currentWorkPanel = currentWorkPanel < totalWorkPanels - 1 ? currentWorkPanel + 1 : 0;
var $nextPanel = $workPanels.eq(currentWorkPanel);
$currentPanel.removeClass('bl-show-work').addClass('bl-hide-current-work').on(transEndEventName, function (event) {
if (!$(event.target).is('div')) return false;
$(this).off(transEndEventName).removeClass('bl-hide-current-work');
isAnimating = false;
});
if (!supportTransitions) {
$currentPanel.removeClass('bl-hide-current-work');
isAnimating = false;
}
$nextPanel.addClass('bl-show-work');
return false;
});
// clicking the work panels close button: the current work panel slides down and the section scales up again
$closeWorkItem.on('click', function (event) {
// scale up main section
$sectionWork.removeClass('bl-scale-down');
$workPanelsContainer.removeClass('bl-panel-items-show');
$workPanels.eq(currentWorkPanel).removeClass('bl-show-work');
return false;
});
}
return {
init: init
};
})();
Here is a fiddle: http://jsfiddle.net/77P2e/
Be careful to unlock scrolling again when done, or this could be very annoying for the user!
Setup code
var $window = $(window), previousScrollTop = 0, scrollLock = false;
$window.scroll(function(event) {
if(scrollLock) {
$window.scrollTop(previousScrollTop);
}
previousScrollTop = $window.scrollTop();
});
To lock scroll position:
scrollLock = true;
And to unlock again...
scrollLock = false;
As an example use, you could lock the window scroll position when the mouse enters the navigation area, and unlock it again when the mouse leaves:
$("nav")
.mouseenter(function(){ scrollLock = true; })
.mouseleave(function(){ scrollLock = false; });
In my opinion the accepted answer is not what should be achieved, as the window.scroll() function will be still running (endlessly), even if the 'event' has occured.
The window.scroll() function is an event handler. So use on() to bind the event and off() to unbind it (after the 'event' has occured).
$(window).on('scroll', function() { // bind event handler
var offset = $(window).scrollTop();
console.log("page Y-Offset: ", offset); // just to see it working
if(offset >= 100) $(window).off('scroll'); // unbind the event handler when the condition is met
});
The Javascript solution is a little janky for me, on mobile. It's like it scrolls a little bit and then snaps back into place.
However, I figured out a way to do it much more cleanly, without any jank, just by changing CSS's overflow property on the part you don't want to scroll. Here's the code in d3 but the concept should be pretty clear:
var body = d3.select('body');
var preventScroll = function () {
body.style('overflow', 'hidden');
},
allowScroll = function () {
body.style('overflow', 'scroll');
};
d3.select('#sticky-nav')
.on('touchmove', preventScroll)
.on('touchstart', preventScroll)
.on('touchend', allowScroll)
.on('touchcancel', allowScroll);
As I was using jquery animation,
if ($(window).scrollTop() >= $('.btn').offset().top + $('.btn').outerHeight() - window.innerHeight)
{
$(".tab").stop();
}
I did this and it worked.
.btn is the button. That .tab div would stop if it scrolls to that position.
If you're using jquery animation you can try using the stop() function on the animated object.
Creating an accordion - on the slide - the elements underneath the element that is sliding seem to move down a px and then back up, creating a juddering effect.
$(document).ready(function() {
//Promos banners rotation and accordion
$(function(){
var $accordionList = $('.accordion').find('li');
var numberOfItems = $accordionList.length;
var currentItem = 0;
// Set first item to active
$accordionList.eq(currentItem).addClass('active').find('.content').slideToggle(800, function() {});
// Loops through promos
var infiniateLoop = setInterval(function() {
if(currentItem == numberOfItems - 1){
currentItem = 0;
}
else {
currentItem++;
}
// Remove active class, if is has it, and close content
$accordionList.parent().find('li.active').removeClass('active')
.find('.content').slideToggle(800, function() {
});
// Add active class and open content
$accordionList.eq(currentItem).addClass('active').find('.content').slideToggle(800, function() {
});
}, 4000 );
// Click to show promo
$accordionList.on('click', function () {
// Stop rotation
clearInterval(infiniateLoop);
var $accordionHead = $(this);
// Remove active class, if is has it, and close content
if($accordionHead.hasClass('active')) {
// Do nothing
}
else {
$accordionHead.parent().find('li.active').removeClass('active')
.find('.content').slideToggle(800, function() {
});
// Add active class and open content
$accordionHead.addClass('active').find('.content').slideToggle(800, function() {
});
};
});
});
});
Fiddle here demonstrating the problem
I've seen some suggestions that you fix the height of the content div - but the site is responsive so that won't work.
Ya, I've had this problem before to. My favorite fix is to just make my own .slideToggle()
div = $('div');
height = div.height();
width = div.width();
$('div').click( function() {
if ($(this).hasClass('hidden')) {
$(this).animate({height: "0", width: "0"}, 200).hide().addClass('hidden');
} else {
$(this).animate({height: height, width: width}, 200).show().removeClass('hidden');
}
});
You could even wrap it in a prototype function if you wanted to.
I'm putting together a jQuery plugin. The plugin takes panels, and auto-sizes their height. So I start off with something like this:
<div class="panel">Test 1</div>
<div class="panel">Test 2</div>
<div class="panel">Test 3</div>
The code for that looks something like:
sizePanels: function(){
panels.each(function(){
$(this).height(docHeight);
});
},
There is a down button, that when clicked, will take the user to the next $(".panel):
nextPanel: function(){
$.scrollTo($(".panel:eq(" + panelIndex + ")"), 250, { easing: "swing" });
}
With that, I'm keeping track of the panel index that their on:
if (panelIndex < (panelCount - 1) ) {
panelIndex += 1;
}
I'm trying to figure out a way to track if they happen to scroll manually, and pass one of the elements, to then increase the "panelIndex", so that the button doesn't move them up instead of down because it was never incremented properly due to the user using the scroll bar instead of the button. This is what I have so far:
$(window).scroll(function(){
panels.each(function(index){
if ($(window).scrollTop() > $(this).scrollTop()) {
panelIndex = index;
// console.log(index);
}
});
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The code excessively checks and feels somewhat overboard. is there a better way to do it?
An easy optimization is to only calculate the scrollTop once and to exit the each loop when a match is found. You can exit a $.each loop by returning false.
$(window).scroll(function(){
var scrollTop = $(window).scrollTop();
panels.each(function(index){
if (scrollTop > $(this).scrollTop()) {
panelIndex = index;
} else {
return false;
}
});
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The next way that I would suggest optimizing this is to pre-calculate the scrollTop of each panel on page load (and when the viewport is resized). If you store these values in an array, then you can loop through them very quickly.
Here is some rough code to illustrate the idea:
var panelTops = [];
findPanelTops(); // calculate on load
$(window).on("resize", findPanelTops); // calculate on resize
function findPanelTops() {
panelTops = [];
panels.each(function(index) {
panelTops.push($(this).scrollTop());
});
}
$(window).scroll(function(){
var scrollTop = $(window).scrollTop();
for (var i = 0; i < panelTops.length; i++) {
if (scrollTop > panelTops[i]) {
panelIndex = i;
} else {
break;
}
};
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The scroll event can fire a lot and very quickly, so you want to keep the amount of computation as minimal as possible. One way to get around all of this is to implement a scrollend handler. This will only fire when the scroll event has appeared to have stopped.
Here is some basic code for doing that. It will fire when the scroll event has stopped for more than 500ms:
var scrollTimeout = null;
function onScroll() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(onScrollEnd, 500);
}
function onScrollEnd() {
// Scrolling has stopped
// Do stuff ...
scrollTimeout = null;
}
$(window).on("scroll", onScroll);