requestAnimationFrame not pausing my background color transisition - javascript

I have a header with a background color of transparent which will change to black when the user scrolls. Since I am using the requestAnimationFrame to accomplish this, the transition should pause when the tab is not active to save resources. I tested this with a function counting to 300 which indeed did pause when the tab was not active and resumed to 300 when active. However, it seems that my header's background color transition does not pause when on a new tab.
I am using mozilla's example which says "This example optimizes the scroll event for requestAnimationFrame." So I think it should work well with my use case (which it does) I would just like some insight as to why my transition doesn't pause when on a different tab to save resources and be as optimal as possible. Thanks!
https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event
"use strict";
// Reference: http://www.html5rocks.com/en/tutorials/speed/animations/
const $ = selector => document.querySelector(selector);
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
// Do something with the scroll position
if ( scrollPos > 0 ) {
$("header").style.backgroundColor = "black";
}
else {
$("header").style.backgroundColor = "transparent";
}
}
document.addEventListener('scroll', function(e) {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});
Here is my JSFiddle https://jsfiddle.net/Boros/zjwyamvq/3/

A different approach would be to use an IntersectionObserver.
If you place a tiny (1px x 1px) element at the top of the page you can set an IntersectionObserver on it which will tell you as soon as it goes out of the viewport or comes back in.
That way you only have to run JS when there is a change from the user having scrolled away from the top - which will happen only once until they scroll back up to the top.
I don't know how much more efficient this is than using the traditional scroll and trying to throttle it, but MDN says:
sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.
The very fact that you aren't coming back to execute some JS every time the user does a scroll must help regardless of whether the browser does additional optimisation.
Here's a trivial example. It adds the 1px div, sets an IntersectionObserver on it and if the div goes out of the viewport that means the user has scrolled and if it is in the viewport the user has scrolled back to the top or the system is at the start position.
Note, if you want to make it slightly less sensitive you can set the check div to have a height of say 20px so if the user scrolls back to pretty near the top the color changes.
const $ = selector => document.querySelector(selector);
const check = (entries) => {
$("header").style.backgroundColor = (entries[0].isIntersecting) ? 'transparent' : 'black';
};
const observer = new IntersectionObserver(check);
observer.observe($("#checker"));
<div id="checker" style="width: 1px; height: 1pc; position: absolute; top: 0; left; 50%;"></div>
<header style="width: 100vw; height: 200vh; transition: 5s;">SCROLL DOWN TO SEE BACKGROUND COLOR CHANGE TO BLACK</header>

Related

scrollBy does not work while user is scrolling (iOS only)

I have a list of items that has an item prepended to it at a regular interval (think a feed of data). The list should update in real-time as new items come in, however if the user scrolls down it should "lock" in place to make the list easier to read. (If a user has scrolled, they want to be able to read the items without them being pushed down every time a new item is added)
It appears that on Chrome (desktop) and Android, the browser implements this automatically. However, on iOS it does not.
As a work around, I figured I could use scrollBy to move the scroll position when a new item is added IF the user has scrolled down. This mostly works, and prevents the list from jumping.
However, if the user is currently in the process of scrolling, scrollBy seems to have no effect and the items jump around.
To reproduce:
Open on iOS: https://codepen.io/Cyral/full/JjbbPaj
Observe that new items are added every second. Scroll down the list a bit and notice how items are no longer moving (great!), however try scrolling slowly (without taking your finger off the screen) and notice how the items start updating again.
It appears that whatever keeps track of the user scroll amount overrides the scrollBy command, resulting in the list jumping around. If you scroll a bit and take your finger off and let it "smooth scroll" you can also see how it jumps a bit. Is there a way to create the desired behavior of the list "pausing" when scrolled away, but updating in realtime once scrolled to the top?
Here is a reference to see the desired behavior on desktop: (It is the same code, except the scrollBy call has been removed as it is not needed) https://codepen.io/Cyral/full/PobboYE
Update: It appears it is only broken on iOS 14. On 12 and 13 it works as it does on desktop, with no hacky scrollBy workaround.
Like this?
const container = document.getElementById("container");
let textNum = 0;
setInterval(() => {
container.innerHTML = `<p>some text ${++textNum}</p><br/>` + container.innerHTML;
//scroll
savedScroll && (container.scrollTop = container.scrollHeight - savedScroll);
}, 700);
let savedScroll = false;
container.addEventListener("scroll", () => {
if (container.scrollTop <= 10 /*if scroll is near top of container*/) {
savedScroll = false;
} else {
savedScroll = container.scrollHeight - container.scrollTop;
}
});
body {
line-height: 5px;
margin: 0px;
}
div {
width: 200px;
height: 190px;
overflow-y: scroll;
border: 1px solid #000000;
}
<div id="container"></div>
It listens for scroll, and if the current scroll is at the top, it disable auto scrolling, else, save current scroll relative to bottom of container. The scroll is set to containerHeight - savedScroll everytime the container is updated.

Keep scroll position when adding element to the top

I'm trying to create a calendar that can be infinitely scrolled to the left and to the right. It has to load new content dynamically as the user scrolls forward (easy) or backward (the problem is here).
When I add content to the end of the page, it works fine - the scrollbar adjusts to the new container.scrollWidth. But when I have to add content to the start, the whole calendar moves to the right in a huge 400px jump, because the container.scrollLeft property hasn't changed and there's now a new element at the start.
I'm trying to mitigate this by increasing the container.scrollLeft by 400px - width of the newly created element. This approach works, but only when scrolling with mousewheel (shift+mousewheel to scroll sideways) or mobile touchscreen.
If I use my mouse to drag the scrollbar, it kind of glitches out - my guess is it keeps trying to scroll to the old scrollLeft position and disregards that I increased it.
Could you please suggest a way to achieve this behavior for all ways of scrolling?
It would also be great if you could just point me to a site that uses this technique so I could investigate myself.
Here's my semi-working example:
function Container() {
const rectangleWidth = 400;
const container = React.useRef(null);
const [leftRectangles, setLeftRectangles] = React.useState([0]);
const [rightRectangles, setRightRectangles] = React.useState([1, 2, 3, 4]);
// When we just rendered a new rectangle in the left of our container,
// move the scroll position back
React.useEffect(() => {
container.current.scrollLeft += rectangleWidth;
}, [leftRectangles]);
const loadLeftRectangle = () => {
const newAddress = leftRectangles[0] - 1;
setLeftRectangles([newAddress, ...leftRectangles]);
};
const loadRightRectangle = () => {
const newAddress = rightRectangles[rightRectangles.length - 1] + 1;
setRightRectangles([...rightRectangles, newAddress]);
};
const handleScroll = (e) => {
// When there is only 100px of content left, load new content
const loadingOffset = 100;
if (e.target.scrollLeft < loadingOffset) {
loadLeftRectangle(e.target);
} else if (e.target.scrollLeft > e.target.scrollWidth - e.target.clientWidth - loadingOffset) {
loadRightRectangle(e.target);
}
};
return (
<div
className="container"
onScroll={handleScroll}
ref={container}
>
{leftRectangles.map((address) => (
<div className="rectangle" key={address}>
{address}
</div>
))}
{rightRectangles.map((address) => (
<div className="rectangle" key={address}>
{address}
</div>
))}
</div>
);
}
ReactDOM.render(<Container />, document.querySelector("#app"))
.container {
display: flex;
overflow-x: scroll;
}
.rectangle {
border: 1px solid #000;
box-sizing: border-box;
flex-shrink: 0;
height: 165px;
width: 400px;
font-size: 50px;
line-height: 165px;
text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I think this is a case where one should not use native browser scroll areas. If you think about it, scrollbars have no meaning if they continue to get longer infinitely in both directions. Or their only meaning is "area which has been viewed". A scrollbar is not a good metaphor for an infinite area, and therefore a scrollbox is a poor way to do what you want to do here.
If you think of it another way, you are only showing a set of months which fit within a known screen width. The way I would approach this would be to absolute position each calendar inside a container and calculate their positions in a render loop based on where in a virtual view the user is shuttling. This also would allow you to remove calendars once they go too far off screen and create/buffer them offscreen for display before the user scrolls to their position. It would prevent you from having an arbitrarily wide object which would eventually slow down rendering.
One approach to this would be to simply number each month since 1970 and treat your view as having a fractional position viewX along this line. The month for x should have a position of viewX - x. When the user is dragging you move viewX inversely and reposition the buffered elements. When the user stops dragging, you take the last velocity and slow it until viewX - x is an integer.
This would avoid most cross-browser issues and offset problems. It only requires a render loop while the display is in motion.
Use the read-write scrollLeft to get the scroll position prior to dynamically adding content then use the .scrollLeft method to reposition the scroll position back to where you want it. You might need fire off a dialog displaying an indeterminant progress indicator ( or simply display text "working..." ) which displays during the process to prevent janking.
The trick for cross browser functionality is that dialog element which is well known to be challenging regarding consistency across device types so I would recommend using a UI library for your dialog or build your own but keep it super simple. That progress indicator will be the fix for screen jank.
Another feature to consider regarding janking would be CSS transitions where the addition of content (e.g. a block element) would gradually fade/animate in to the viewport.

JavaScript function to manipulate CSS custom properties not working on $(window).resize()

I have a fixed header with :target in-page anchors, and need to adjust the property values dynamically via JavaScript or JQuery so as to maintain the relevant :target's position directly under the header when the window is resized, while adapting to the changes in both the previous section's .container height and the .header_container height that occur with resizing.
The simplest solution seems to be a ::before pseudo-element for the :target pseudo-class, and to then utilize CSS custom properties to dynamically modify the style properties.
I have no trouble correctly positioning the :target with the below function when the page is loaded (or reloaded), or correctly position the first :target on $(window).resize(), however it's failing to do the same for the remaining targets on $(window).resize().
Fiddles
Simplified Code: https://jsfiddle.net/chayanyc/g6p3549s/
Responsive Design (Simplified): https://jsfiddle.net/chayanyc/wuk92dns/
Code Snippets
CSS:
.header_container {height: 98px; margin: 0; padding: 0; position: fixed; top: 0; display: block; z-index: 100;}
.main {margin-top: 98px; width: 100%;}
:target::before {height: var(--target_position1); margin-top: var(--target_position2); content: ""; display: block; visibility: hidden;}
JavaScript:
var headerHeight;
function setTarget() {
headerHeight = document.getElementById('header').offsetHeight;
headerHeight1 = headerHeight + "px";
document.documentElement.style.setProperty('--target_position1', headerHeight1);
document.documentElement.style.setProperty('--target_position2', '-' + headerHeight1);
}
$(window).resize(function () {
setTarget();
});
$(document).ready(function () {
setTarget();
});
There is no complete solution to this Problem,
because you want the target element stay on place on document resize, but if the user do a scroll on his page, it is not possible to know where staying on the same first word of the first line on display.
So here, i just replace on the same target on top when user resize his document, even if he had done a scroll just before.
no need of this CSS part (remove it)
:target::before {margin: 0; content: ""; dis.....
and change your jQuery to:
$(document).ready(function () {
// global info for menu -> target elememt
var InfoTarget = { ID: null, tempo:300 }
$('a').click(function(evt){
InfoTarget.ID = $(this).attr('href') // possible target elm
// check if InfoTarget.ID exist on page
let nbElements = 0
try { nbElements = $(InfoTarget.ID).length }
catch(err) { nbElements = 0 }
if ( nbElements != 1 ) {
InfoTarget.ID = null // not target element found
}
else {
evt.preventDefault() // disable auto scroll to target element
$('html').animate({
scrollTop: ($(InfoTarget.ID).offset().top - $('#header').outerHeight(true))
}, InfoTarget.tempo );
}
});
$(window).resize(function (){
if (InfoTarget.ID) { // if InfoTarget.ID exist <=> InfoTarget.ID != null
$('html').animate({
scrollTop: ($(InfoTarget.ID).offset().top - $('#header').outerHeight(true))
}, InfoTarget.tempo);
}
});
});
My code speaks for itself, but here is a complete explanation:
the principle is simple: the function target css activates on a click on a link <a href="#..."> to trigger a scroll of the page towards the element having for id = to that contained in the initial href.
therefore this code intercepts any click on a link on the page and must first determine whether it is a link to an anchor or not.
To determine if this is a link to an anchor on the page, it simply tests whether an element of the page has this value as this ID, (// check if InfoTarget.ID exists on page).
As this kind of test can also generate an error, this test is placed in a try / catch.
If the result is indeed an anchor, then the action of the click is canceled, (with evt.preventDefault()) which prevents the browser from triggering its automatic scroll to the link;
the reference link is kept in an object variable (global)
var InfoTarget = {ID: null, tempo: 300}
seen on: InfoTarget.ID = $(this).attr('href') // possible target elm
the rest is simple, you have to scroll down to the anchor.
Depending on the width of the page and the previous elements, browsers continuously recalculate the position of each tag present on a page and jQuery can be retrieved this offset position by $(element).offset().Top
as there is a menu bar on your page that masks the top of the page, you must deduct its height from the position in scroll (= $ ('# header'). outerHeight (true))
a scroll = 0 will force a move to the top of the page
a scroll = $(element).offset().top places the element at the top of the page
to which we must deduct the height of the #header
the complete formula is
scrollTop: ( $(InfoTarget.ID).offset().top - $('#header').outerHeight(true) )
this command is placed in a jQuery.animate, for a visually smoother move, and uses the InfoTarget.tempo value as the duration for this animation.
During a resize of the page, and to the extent that a link having a target has been previously clicked (therefore always active) then the same type of scroll is triggered.
The different jQuery methods used are all explained in the jQuery doc (for example: https://api.jquery.com/outerHeight/ )
New Solution -- Lundi 14 oct 2019 / 01:00 (in the night of sunday / monday)
this script must be placed after all the html elements of the body
// scroll to target upon window.location.hash
$(window).on('hashchange', function() {
$('.TargetMark').removeClass('TargetMark')
$(window.location.hash).addClass('TargetMark')
setTimeout( scrollTop2, 220 ) // scroll to target after browser auto scrolling conflit
})
function scrollTop2() {
if ($('.TargetMark').length===1) { // if target exist
$('html').animate({
scrollTop: ($('.TargetMark').offset().top - $('#header').outerHeight(true))
}, 100);
}
}
In this version the target element is added a class (TargetMark) allowing to find it when window resize
ending part
$(document).ready(function () {
//...
// ---------------------------> no call to scrollTop();
//...
});
$(window).resize(function () {
//...
scrollTop2();
//...
});
about toggleMenu conflict:
function toggleMenu() {
$('.navbar-toggle').on('click', function () {
if ($("#js-menu").is(".expand")) {
$("#js-menu").toggleClass("expand");
$("#submenu").removeClass("active_sub").addClass("inactive_sub");
} else {
$("#js-menu").toggleClass("expand");
$("#submenu").removeClass("inactive_sub").addClass("active_sub");
}
resetTarget();
setTimeout( scrollTop2, 220 ) // scroll to target after browser auto scrolling conflit
});
}
I spent a lot of my time on your question, I studied differents approaches and the different automatisms put at work by the navigators themselves and which is necessary to fight to get the result you'r looking for. I came to the conclusion that the problem first came from the architecture of your page.
The fact that the menu ("#header") covers the page ("#main") is a major flaw that prevents to have an effective JS code for your question.
The call on the hash of an anchor triggers one or more scrolls of the page, the resize of the page also entails a scroll calculation because by changing size on the screen, it also changes the size of the page. page (reducing the size of the screen by half makes the page size double), it is the same by changing the size of the font, it also changes the size in page.
Whenever the page size changes, the browser must recalculate a lot of things and some of these mechanisms can trigger one or more scrolls.
What you are asking here is to recalculate a page positioning according to an element of which we can not be certain that it is completely established because this process is executed in parallel with other processes of the browser which can change useful values.
Plus the fact that some of the browser processes also work to scroll the page and that it can be the last done!
So the fact that there is an overlap between the menu and the page add more complexity and makes the possibility of a solution impossible.
Change your layout and 3/4 of your problem will be fixed.
Resize is firing, offset height is not changing. Setting the same value over and over again, yields no change. You might check this:
see the value change
I used the logo for output:
$('.logo').text(headerHeight + ' -' + i++);
You want to scroll down to the one div selected by target without having it to be overlapped by your nav?
.. then extend the areas. see here
add positive margin-top and negative padding-top.
.... to compensate for any nav size changes, use media queries to change your css vars.

Google Translate Bar moving content down

Is there a way to stop Google Translate Bar from moving my content down? I have a static background image, and a header image that corresponds with the background image, so when the Google Translate Bar is fixed to the top of my screen, it moves my top content down and out of the background image.
Is there a way to make it just statically over my content or fixed in such a way it won't move my content down?
Or Can I detect is Translation is taking place, then move my background accordingly? I tried to use this but it doesn't revert back if I remove the Translation Bar:
document.addEventListener('DOMSubtreeModified', function (e) {
if(e.target.tagName === 'HTML' && window.google) {
if(e.target.className.match('translated')) {
document.body.style.backgroundPosition="0px 40px";
} else {
document.body.style.backgroundPosition="0px 0px";
}
}
}, true);
It's a bit difficult without a code example, but the easiest solution would be to set position: fixed; and top: 0 on the translate bar, however, this means it will always remain at the top of the page once you scroll down.
If the translate bar is near the top of your document, which it sounds like it is, you can set the position to absolute instead, keeping the top: 0 declaration. This should make it appear at the top of the closest positioned ancestor, i.e. an element with position set to relative, absolute, fixed, or sticky. If this doesn't exist, it'll be positioned according to the root tag, i.e. <html> in a well-formed document. Here, you could set position: relative on your <body>, for example.
Both fixed and sticky takes the element entirely out of the document flow, so they will do exactly what you're requesting here: appear on top of other content.
The addEventListener('DOMSubtreeModified') is outdated. What you want is to use DOM MutationObserver Events to apply the change. This DOM API is available on all major browser since 2012 I think.
I use this on to lower the google translator bar, so maybe moving the bar down also solves your problem. If not, just change the callback function and variables for your need.
The google translator creates an iframe element like this:
<iframe id=":1.container" class="goog-te-banner-frame skiptranslate" frameborder="0" src="javascript:''" style="visibility: visible; top: calc(100% - 40px);">...</iframe>
So the MutationObserver code to move that element down is as follow:
//Observer for Google translator bar creation and action to move to bottom
// Select the nodetree that will be observed for mutations
var nodetree = document.getElementsByTagName("body")[0];
// Select the target node atributes (CSS selector)
var targetNode = "iframe.goog-te-banner-frame";
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: true };
// Callback function to execute when mutations of DOM tree are observed
var lowerGoogleTranslateBar = function(mutations_on_DOMtree) {
for(var mutation of mutations_on_DOMtree) {
if (mutation.type == 'childList') {
console.log(mutation);
if (document.querySelector(targetNode) != null) {
//40px is the height of the bar
document.querySelector(targetNode).style.setProperty("top", "calc(100% - 40px)");
//after action is done, disconnect the observer from the nodetree
observerGoogleTranslator.disconnect();
}
}
}
};
// Create an observer instance linked to the callback function
var observerGoogleTranslator = new MutationObserver(lowerGoogleTranslateBar);
// Start observing the target node for configured mutations
observerGoogleTranslator.observe(nodetree, config);
You can learn more about this here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

change css on scroll event w/ requestAnimation Frame

I want to change the background color of in-viewport elements (using overflow: scroll)
So here was my first attempt:
http://jsfiddle.net/2YeZG/
As you see, there is a brief flicker of the previous color before the new color is painted. Others have had similar problems.
Following the HTML5 rocks instructions, I tried to introduce requestAnimationFrame to fix this problem to no avail:
http://jsfiddle.net/RETbF/
What am I doing wrong here?
Here is a simpler example showing the same problem: http://jsfiddle.net/HJ9ng/
Filed bug with Chromium here: http://code.google.com/p/chromium/issues/detail?id=151880
if it is only the background color, well why don't you just change the parent background color to red and once it scroll just change it to pink?
I change your CSS to that
#dad
{
overflow-y: scroll;
overflow-x: hidden;
width: 100px;
height: 600px;
background-color:red;
}​
I remove some of you Jquery and change it to this
dad.bind('scroll', function() {
dad.css('background-color', 'pink');
});
And I remove this line
iChild.css('backgroundColor', 'red');
But is the Red color it is important that won't work for sure http://jsfiddle.net/2YeZG/5/
I like Manuel's Solution.
But even though I don't get what you're exactly trying to do, I want to point out a few things.
In your fiddle code, I saw that you included Paul Irish's Shim for requestAnimationFrame.
But you never use it.
(It's basically a reliable setTimeOut, nothing else) it's from frame based animations.)
So since you just want to change some CSS properties, I don't see why you would need it. Even if you want transitions, you should rely on CSS transitions.
Other than that your code could look something like
dad.bind('scroll', function() {
dad.css('background-color', 'pink');
eachElemNameHere.css('background-color','randomColor');
});
Also you should ideally not use something like that if you can help it. You should just add and remove class names and add all these properties in your CSS. Makes it work faster.
Also, again I don't quite get it, but you could use the jQuery function to find out each elements' position from the top to have better control.
Your problem seems to be that you only change the background color of the elements which have already been scrolled into view. Your code expects that the browser waits for your code to handle the scroll event before the browser redraws its view. This is most probably not a guarantee given by the HTML spec. That's why it flickers.
What you should do instead is to change the elements which are going to be scrolled into view. This is related to off screen rendering or double buffering as it is called in computer games programming. You build your scene off screen and copy the finished scene to the visible frame buffer.
I modified your first JSFiddle to include a multiplier for the height of the scroll area: http://jsfiddle.net/2YeZG/13/.
dad.bind('scroll', function() {
// new: query multiplier from input field (for demonstration only) and print message
var multiplier = +($("#multiplier")[0].value);
$("#message")[0].innerHTML=(multiplier*100)-100 + "% of screen rendering";
// your original code
var newScrollY = newScrollY = dad.scrollTop();
var isForward = newScrollY > oldScrollY;
var minVal = bSearch(bots, newScrollY, true);
// new: expand covered height by the given multiplier
// multiplier = 1 is similar to your code
// multiplier = 2 would be complete off screen rendering
var newScrollYHt = newScrollY + multiplier * dadHeight;
// your original code (continued)
var maxVal;
for (maxVal = minVal; maxVal < botsLen; maxVal++) {
var nxtTopSide = tops[maxVal];
if (nxtTopSide >= newScrollYHt) {
break;
}
}
maxVal = Math.min(maxVal, botsLen);
$(dadKids.slice(minVal, maxVal)).css('background', 'pink');
});
Your code had a multiplier of 1, meaning that you update the elements which are currently visible (100% of scroll area height). If you set the multiplier to 2, you get complete off screen updates for all your elements. The browser updates enough elements to the new background color so that even a 100% scroll would show updated elements. Since the browser seldom scrolls 100% of the area in one step (depends of the operating system and the scroll method!), it may be sufficient to reduce the multiplier to e.g. 1.5 (meaning 50% off screen rendering). On my machine (Google Chrome, Mac OS X with touch pad) I cannot produce any flicker if the multiplier is 1.7 or above.
BTW: If you do something more complicated than just changing the background color, you should not do it again and again. Instead you should check whether the element has already been updated and perform the change only afterwards.

Categories