I've been learning Ruby on Rails and Javascript and trying to learn to animate things as I go.
I have a series of bootstrap cards generated by an Articles controller and shown by an article partial in an index view, where the list of cards (representing posts/messages) proceeds down the page. I want these cards to fade into view as they are scrolled into.
I start the card opacity at 0 with css:
$bg_color: #23282e;
$corner_radius: 5px;
.card {
border-radius: $corner_radius;
opacity: 0;
}
and fade it to 1 with javascript:
function fadeCardsIn() {
const window_top = $(window).scrollTop()
const window_bottom = $(window).scrollTop() + $(window).innerHeight()
console.log(`Viewport: ${window_top} to ${window_bottom}.`)
const cards = $(".card")
let count = 0
cards.each(function() {
count += 1
const card = $(this)
const card_top = card.offset().top;
const card_bottom = card.offset().top + card.outerHeight()
// If our card is in the range of the window viewport.
if ((window_bottom > card_top) && (window_top < card_bottom)) {
card.fadeTo(1000, 1, "swing")
console.log(`Showing card ${count}.`)
} else {
card.fadeTo(1000, 0, "swing")
}
})
}
$(document).ready(fadeCardsIn)
$(window).scroll(fadeCardsIn)
I've taken the bits that should allow things to function without all the rails backend and without the partial, with several cards copied/pasted in the html to simulate what I'm working with in the rendered view:
https://jsfiddle.net/n9kemt3L/5/
On load, the javascript function works as expected with the first few cards in the viewport fading in. However on scroll, based on the console log, the correct cards are "showing" and should be animating/fading in, but they either never fade in, or fade in / fade out very late (sometimes upwards of 30+ seconds later of not scrolling).
I'm assuming calling the fadeTo function multiple times on the same cards by scrolling is causing an issue, possibly calling fadeTo many times before the first fadeTo has even completed, but I'm not sure.
How should I go about beginning to solve this? Any suggestions to nudge me in the right direction are very welcome.
If the fadeTo excess calls are breaking the animations, then I imagine I might need to add some sort of state to know when each card is being animated into/out of, but I don't know where I should start with that if that were the case, as each card is generated by rails and served via pagination, and I'm just iterating over each card on the final rendered with jQuery's each() function.
On another note, I haven't been able to get fadeIn and fadeOut to function at all in this setup, hence me making use of fadeTo instead.
Is there a better way to go about this other than calling this sort of function on scroll?
Edit: I managed to get the functionality I was aiming for by simply removing the else statement.
cards.each(function() {
count += 1
const card = $(this)
const card_top = card.offset().top;
const card_bottom = card.offset().top + card.outerHeight()
// If our card is in the range of the window viewport.
if ((window_bottom > card_top) && (window_top < card_bottom)) {
console.log(`Showing card ${count}.`)
card.fadeTo(500, 1, "swing",function(){console.log(`Loaded card ${count}`)})
But I am still wondering if there's a better way to do this, as this each loop does fire off every single time I scroll.
Definitely feel like you're on the right path.
When I make things fade in from the top, bottom, left or right I usually do not explicitly give them an initial opacity of 0 in their own class.
Rather I create an animation class that has an an initial opacity of 0 and then assign it to them.
Like so...
HTML
<div class='card'>im a card example </div>
CSS
.card {
}
#keyframes fromRight {
0% {
transform: translateX(50px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
Javascript/jQuery
$(document).ready(function() {
$(window).scroll(function() {
let position = $(this).scrollTop();
console.log(position)
// specify your position by inspecting the page
if (position >= 1100) {
$('.card').addClass('fromRight');
}
});
})
Related
I have a custom icon element that is only displayed when its specific row in the table is hovered over, but when I scroll down without moving my mouse it doesn't update the hover and maintains the button on the screen and over my table's header. How can I make sure this doesn't happen?
export const StyleTr = styled.tr`
z-index: ${({ theme }) => theme.zIndex.userMenu};
&:hover {
background-color: ${({ theme, isData }) =>
isData ? theme.colors.primary.lighter : theme.colors.white};
div {
visibility: visible;
}
svg[icon] {
display: initial;
}
}
`;
I was just working on something similar to this for a web scraper recently.
Something like this should work:
function checkIfIconInViewport() {
// define current viewport (maximum browser compatability use both calls)
const viewportHeight =
window.innerHeight || document.documentElement.clientHeight;
//Get our Icon
let icon = document.getElementById('icon');
let iPos = icon.getBoundingClientRect();
//Show if any part of icon is visible:
if (viewportHeight - iPos.top > 0 && iPos.bottom > 0) {
icon.style.visibility = visibile;
}
else { icon.style.visibility = hidden; }
//Show only if all of icon is visible:
if (iPos.bottom > 0 && iPos.top >= 0) {
{
icon.style.visibility = visibile;
}
else { icon.style.visibility = hidden; }
//Add && iPos.bottom <= viewportHeight to the if check above for very large elements.
{
//Run function everytime that the window is scrolled.
document.addEventListener('scroll', checkIfIconInViewport);
Basically, every time a scroll event happens, we just check to see if the top & bottom of our element (the icon in your case) are within the bounds of the viewport.
Negative values, or values greater than the viewport's height mean that the respective portion of the element is outside the viewport's boundary.
Hopefully this helps! If you are dealing with a large quantity of objects, it may make sense to bundle the objects you are tracking together into an array and check each of them in a single function call to avoid saving function definitions for each individual object.
Edit: I just realized that I misunderstood your issue a bit. I think you can get by with just the bottom part of the code, and when a scroll event happens, set the icon's visibility to hidden. Assuming you want to hide it whenever the user scrolls?
Have you tried getting the scroll position of the DOM, then disabling (removing) the element once a certain scroll position is reached?
I am trying to apply the gradient effect, for example, if my mouse passes a grid cell it will light at first but the more I move my mouse towards that particular cell it will gradually get lighter. Here is my project https://repl.it/#antgotfan/etch-a-sketch and here is a working example (not mine btw), https://codepen.io/beumsk/pen/dVWPOW (just click on the gradient and will see the effect)
I am trying to use "this" and event.target to solve this problem but it is not working. I also tried to use fadeIn with jQuery but it is not my desired effect.
function incrementOpacity() {
let opacity = 0.1;
$(".cell").hover((event) => {
$(event.target).css({"opacity": `${opacity += 0.1}`,
"backgroundColor": "#f5f5f5"})
});
}
In the function, the opacity will increase forever but will stay white.
The issue is setting let opacity = 0.1; outside of the .hover event. The value should be based off of the cell being hovered, so fetch it inside the event via event.target.style.opacity. Also, make sure it is parsed correctly as a float to allow correct incrementing (parseFloat()):
function incrementOpacity() {
$(".cell").hover((event) => {
let opacity = parseFloat(event.target.style.opacity);
$(event.target).css({"opacity": `${opacity + 0.1}`, "backgroundColor": "#f5f5f5"})
});
}
Here is the updated Repl.it for reference: https://repl.it/repls/GreenDigitalWireframe
Couple of optional changes; added check for maximum opacity. Anything over 1 is redundant. Also consider using mouseenter for function so opacity is only increased when hovered on, and not hovered off:
function incrementOpacity() {
$(".cell").mouseenter((event) => {
let opacity = parseFloat(event.target.style.opacity);
if(opacity <= 0.9){
$(event.target).css({"opacity": `${opacity + 0.1}`, "backgroundColor": "#f5f5f5"})
}
});
}
Edit: For clarification, the reason using let opacity = 0.1 in conjunction with ${opacity += 0.1} doesn't work is that in every instance of .hover, the value opacity was being incremented by 0.1, but never reset. Essentially, after 9 (or 10) iterations, opacity was at a value greater than 1, so each cell was "filled" instantly.
2nd Edit: To prevent issue with assigning multiple handles for the .hover (or .mouseenter) function, add event unbinding before setting:
function incrementOpacity() {
$(".cell").off("mouseenter");
$(".cell").mouseenter((event) => { ... });
}
...
function getRandomColors() {
$(".cell").off("mouseenter");
$(".cell").mouseenter((event) => { ... });
}
Also, make sure the bound event is consistent between the two functions, either .hover or .mouseenter.
Im creating a fixed header where on load, the logo is flat white. On scroll, it changes to the full color logo.
However, when scrolling back to the top, it stays the same colored logo instead of going back to white.
Here's the code (and a pen)
$(function() {
$(window).scroll(function() {
var navlogo = $('.nav-logo-before');
var scroll = $(window).scrollTop();
if (scroll >= 1) {
navlogo.removeClass('.nav-logo-before').addClass('nav-logo-after');
} else {
navlogo.removeClass('.nav-logo-after').addClass('nav-logo-before');
}
});
});
http://codepen.io/bradpaulp/pen/gmXOjG
There's a couple of things here:
1) You start with a .nav-logo-before class but when the logo becomes black you remove that class and then try to get the same element using a class selector that doesn't exist anymore
2) removeClass('.nav-logo-before') is different than removeClass('nev-logo-before), notice the "." in the first selector.
3) You get the element using the $('.selector')in every scroll event, this can be a performance issue, it's better to cache them on page load and then use the element stored in memory
4) It's not a good practice to listen to scroll events as this can be too performance demanding, it's usually better to use the requestAnimationFrame and then check if the scroll position has changed. Using the scroll event it could happen that you scroll up really fast and the scroll event doesn't happen at 0, so your logo won't change. With requestAnimationFrame this can't happen
$(function() {
var navlogo = $('.nav-logo');
var $window = $(window);
var oldScroll = 0;
function loop() {
var scroll = $window.scrollTop();
if (oldScroll != scroll) {
oldScroll = scroll;
if (scroll >= 1) {
navlogo.removeClass('nav-logo-before').addClass('nav-logo-after');
} else {
navlogo.removeClass('nav-logo-after').addClass('nav-logo-before');
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
});
body {
background-color: rgba(0,0,0,.2);
}
.space {
padding: 300px;
}
.nav-logo-before {
content: url(https://image.ibb.co/kYANyv/logo_test_before.png)
}
.nav-logo-after {
content: url(https://image.ibb.co/jYzFJv/logo_test_after.png)
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<img class="nav-logo nav-logo-before">
</div>
<div class="space">
</div>
Dont need to add the dot . in front of the class name in removeClass and addClass:
Use this:
navlogo.removeClass('nav-logo-before')
Secondly, you are removing the class that you are using to get the element in the first place.
I have an updated codepen, see if this suits your needs: http://codepen.io/anon/pen/ZeaYRO
You are removing the class nav-logo-before, so the second time the function runs, it can't find any element with nav-logo-before.
Just give a second class to your navlogo element and use that on line 3.
Like this:
var navlogo = $('.second-class');
working example:
http://codepen.io/anon/pen/ryYajx
You are getting the navlogo variable using
var navlogo = $('.nav-logo-before');
but then you change the class to be 'nav-logo-after', so next time the function gets called you won't be able to select the logo using jquery as it won't have the '.nav-logo-before'class anymore.
You could add an id to the logo and use that to select it, for example.
Apart from that, removeClass('.nav-logo-before') should be removeClass('nav-logo-before') without the dot before the class name.
The problem is that you removes nav-logo-before and then you want to select element with such class but it doesn't exist.
I've rafactored you code to avert it.
Another problem is that you uses dot in removeClass('.before') while it should be removeClass('before') - without dot
$(function() {
var navlogo = $('.nav-logo');
$(window).scroll(function() {
var scroll = $(window).scrollTop();
if (scroll >= 1) {
navlogo.removeClass('before').addClass('after');
} else {
navlogo.removeClass('after').addClass('before');
}
});
});
body {
background-color: rgba(0,0,0,.2);
}
.space {
padding: 300px;
}
.before {
content: url(https://image.ibb.co/kYANyv/logo_test_before.png)
}
.after {
content: url(https://image.ibb.co/jYzFJv/logo_test_after.png)
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<img class="nav-logo before">
</div>
<div class="space">
</div>
Pleasantries
I've been playing around with this idea for a couple of days but can't seem to get a good grasp of it. I feel I'm almost there, but could use some help. I'm probably going to slap myself right in the head when I get an answer.
Actual Problem
I have a series of <articles> in my <section>, they are generated with php (and TWIG). The <article> tags have an image and a paragraph within them. On the page, only the image is visible. Once the user clicks on the image, the article expands horizontally and the paragraph is revealed. The article also animates left, thus taking up the entire width of the section and leaving all other articles hidden behind it.
I have accomplished this portion of the effect without problem. The real issue is getting the article back to where it originally was. Within the article is a "Close" <button>. Once the button is clicked, the effect needs to be reversed (ie. The article returns to original size, only showing the image, and returns to its original position.)
Current Theory
I think I need to retrieve the offset().left information from each article per section, and make sure it's associated with its respective article, so that the article knows where to go once the "Close" button is clicked. I'm of course open to different interpretations.
I've been trying to use the $.each, each(), $.map, map() and toArray() functions to know avail.
Actual Code
/*CSS*/
section > article.window {
width:170px;
height:200px;
padding:0;
margin:4px 0 0 4px;
position:relative;
float:left;
overflow:hidden;
}
section > article.window:nth-child(1) {margin-left:0;}
<!--HTML-->
<article class="window">
<img alt="Title-1" />
<p><!-- I'm a paragraph filled with text --></p>
<button class="sClose">Close</button>
</article>
<article class="window">
<!-- Ditto + 2 more -->
</article>
Failed Attempt Example
function winSlide() {
var aO = $(this).parent().offset()
var aOL = aO.left
var dO = $(this).offset()
var dOL = dO.left
var dOT = dO.top
var adTravel = dOL-aOL
$(this).addClass('windowOP');
$(this).children('div').animate({left:-(adTravel-3)+'px', width:'740px'},250)
$(this).children('div').append('<button class="sClose">Close</button>');
$(this).unbind('click', winSlide);
}
$('.window').on('click', winSlide)
$('.window').on('click', 'button.sClose', function() {
var wW = $(this).parents('.window').width()
var aO = $(this).parents('section').offset()
var aOL = aO.left
var pOL = $(this).parents('.window').offset().left
var apTravel = pOL - aOL
$(this).parent('div').animate({left:'+='+apTravel+'px'},250).delay(250, function() {$(this).animate({width:wW+'px'},250); $('.window').removeClass('windowOP');})
$('.window').bind('click', winSlide)
})
Before you go scratching your head, I have to make a note that this attempt involved an extra div within the article. The idea was to have the article's overflow set to visible (.addclass('windowOP')) with the div moving around freely. This method actually did work... almost. The animation would fail after it fired off a second time. Also for some reason when closing the first article, the left margin was property was ignored.
ie.
First time a window is clicked: Performs open animation flawlessly
First time window's close button is clicked: Performs close animation flawlessly, returns original position
Second time SAME window is clicked: Animation fails, but opens to correct size
Second time window's close button is clicked (if visible): Nothing happens
Thank you for your patience. If you need anymore information, just ask.
EDIT
Added a jsfiddle after tinkering with Flambino's code.
http://jsfiddle.net/6RV88/66/
The articles that are not clicked need to remain where they are. Having problems achieving that now.
If you want to go for storing the offsets, you can use jQuery's .data method to store data "on" the elements and retrieve it later:
// Store offset before any animations
// (using .each here, but it could also be done in a click handler,
// before starting the animation)
$(".window").each(function () {
$(this).data("closedOffset", $(this).position());
});
// Retrieve the offsets later
$('.window').on('click', 'button.sClose', function() {
var originalOffset = $(this).data("originalOffset");
// ...
});
Here's a (very) simple jsfiddle example
Update: And here's a more fleshed-out one
Big thanks to Flambino
I was able to create the effect desired. You can see it here: http://jsfiddle.net/gck2Y/ or you can look below to see the code and some explanations.
Rather than having each article's offset be remembered, I used margins on the clicked article's siblings. It's not exactly pretty, but it works exceptionally well.
<!-- HTML -->
<section>
<article>Click!</article>
<article>Me Too</article>
<article>Me Three</article>
<article>I Aswell</article>
</section>
/* CSS */
section {
position: relative;
width: 404px;
border: 1px solid #000;
height: 100px;
overflow:hidden
}
article {
height:100px;
width:100px;
position: relative;
float:left;
background: green;
border-right:1px solid orange;
}
.expanded {z-index:2;}
//Javascript
var element = $("article");
element.on("click", function () {
if( !$(this).hasClass("expanded") ) {
$(this).addClass("expanded");
$(this).data("originalOffset", $(this).offset().left);
element.data("originalSize", {
width: element.width(),
height: element.height()
});
var aOffset = $(this).data("originalOffset");
var aOuterWidth = $(this).outerWidth();
if(!$(this).is('article:first-child')){
$(this).prev().css('margin-right',aOuterWidth)
} else {
$(this).next().css('margin-left',aOuterWidth)
}
$(this).css({'position':'absolute','left':aOffset});
$(this).animate({
left: 0,
width: "100%"
}, 500);
} else {
var offset = $(this).data("originalOffset");
var size = $(this).data("originalSize");
$(this).animate({
left: offset + "px",
width: size.width + "px"
}, 500, function () {
$(this).removeClass("expanded");
$(this).prev().css('margin-right','0')
$(this).next().css('margin-left','0')
element.css({'position':'relative','left':0});
});
}
});
I can't get the actually LazyLoad plugin to work for me so I am trying to write my own with. Currently I have a list of images loading inside of a DIV. They are pulled by a PHP query to the mysql database. The DIV scroll is set to auto. The code I am using is:
<div id="b1" style="overflow:auto;">
<?PHP $result = mysql_query("SELECT * FROM images");
while($row = mysql_fetch_assoc($result)) {
echo "<img src='$row[photo]' style='display:none'> <br>";
}
</div>
<script type="text/javascript">
function imgCheck() {
var position = $("img").offset().top;
var scrollCheck = $("#b1").scrollTop() + $("#b1").height();
if ( scrollCheck > position) {
$("img").fadeIn("fast");
}
$("#b1").scroll( function() { imgCheck() } );
</script>
Although this is not quite working for me. Could anyone help me out or shoot out some suggestions?
A couple of things:
As the others have already said, your code has syntax errors - both with the PHP and the Javascript.
If you use display: none, the elements will not take up any height, thus causing the entire thing to become unscrollable and fail.
The first few elements should be visible without the user having to start scrolling
Taking these into consideration, we can try writing this this way:
// Cache the containing element and it's height
var b1 = $('#b1'),
h = b1.height();
// Insert 20 img's - simulating server-side code
for(var i = 0; i < 20; i++){
$('<img />', {
src: 'http://placehold.it/100x100',
alt: '',
class: 'hidden',
width: 100,
height: 100
// visibility: hidden to retain it's size
}).css('visibility', 'hidden').appendTo(b1);
}
b1.scroll(function(){
// Loop through only hidden images
$('img.hidden').each(function(){
// $(this).position().top calculates the offset to the parent
// So scrolling is already taken care of here
if(h > $(this).position().top){
// Remove class, set visibility back to visible,
// then hide and fade in image
$(this).css('visibility', 'visible')
.hide()
.removeClass('hidden')
.fadeIn(300);
} else {
// No need to check the rest - everything below this image
// will always evaluate to false - so we exit out of the each loop
return false;
}
});
// Trigger once to show the first few images
}).trigger('scroll');
See a demo of this here: http://jsfiddle.net/yijiang/eXSXm/2
If all of the images are hidden, then there will never be a 'scroll' even called as the element will never scroll.
What exactly are you trying to achieve? If it is to have new images that weren't previously visible, but now may be, become visible then you will have to do something like;
<div id="b1" style="overflow:auto;">
<?php $result = mysql_query("SELECT * FROM images");
while($row = mysql_fetch_assoc($result)) {
echo "<img src='$row[photo]' style='visibility:hidden'> <br>";
} ?>
</div>
<script type="text/javascript">
function imgCheck() {
var scrollCheck = $("#b1").scrollTop() + $("#b1").height();
$("#b1 img").each(function() {
var position = $(this).offset().top;
if (scrollCheck > position) {
$(this).fadeIn("fast");
}
});
}
$(document).ready(imgCheck);
$("#b1").scroll(imgCheck);
</script>
Note that I haven't tested the above, and I can imagine that it will result in all images being shown immediately, since all of their 'top's will be 0 due to them all being hidden and not having their position effected by previous images in the DOM.
Edit
I've changed the code above so the img's have visibility:hidden, which should give them a height and take up space in the DOM
I've been testing it, it seems to work with some modifications:
http://jsfiddle.net/antiflu/zEHtu/
Some things I had to change:
I added the closing } for the imgCheck() function, you forgot it
Some items need to be visible from the beginning on, otherwise the scrollbar never appears and imgCheck() is never called.
OK the problem with the above was that the images don't fade in separately upon scroll. I got it to work though with some modifications:
http://jsfiddle.net/antiflu/GdzmQ/
What I changed:
I changed the display: none to opacity: 0 so that any invisible picture has at least an empty placeholder of the same size, so that the scroll bar will be visible.
I then fade in using animate to opacity: 1
I used jQuery each() to iterate over the images and check for each of them if they should or should not be faded in (instead of checking for all, like before).
I wrapped the images in DIV's. I don't think it's necessary, but it doesn't harm either.
I tagged each image with an id, so that I can single them out for the fadein.
There are still some esthetic issues but this should help you on your way.