getBoundingClientRect during throttled scroll event handling - javascript

I faced an issue during my project developing, it's related to a difference between getBoundingClientRect values with and without preventive break points during debugging. Trying to minimize repro, I got following.
const scrollHandler = () => {
setTimeout(() => {
const top = document.getElementById('test').getBoundingClientRect().top;
console.log(top);
});
};
document.getElementById('viewport').addEventListener('scroll', scrollHandler);
where the viewport is just scrollable div with fixed height. The content of the viewport is big enough to be able to fire at least one scroll event. I also created Plunker demo. All magic happens inside of setTimeout callback.
Now the steps. Success scenario.
Set break point at the beginning of the setTimeout callback.
Fire scroll event.
Make "step over" to obtain top value.
Execute document.getElementById('test').getBoundingClientRect().top in the console.
The results of 3 and 4 are the same.
Failed scenario.
Set break point at the end of the setTimeout callback.
Fire scroll event.
Get the value of top variable (no action is needed).
Execute document.getElementById('test').getBoundingClientRect().top in the console.
The results of 3 and 4 are not the same.
To avoid any misunderstanding with this repro, I recorded a short demo movie with the above steps.
In my project I'm doing calculations in a similar environment (throttled scroll events) and getting wrong results that don't match expectations. But when I debug it, I'm getting different picture; a preventive break point fixes calculations.
Is it a bug or known issue? Or did I miss something? should I refuse to use getBoundingClientRect in such a situation?

I'm not sure what it is that you are looking for but assume your animation on scroll isn't working as expected.
There is a tip on throttling scroll here that is incorrect. Let's say you throttle to every 30 milliseconds. If page stops scrolling 25 milliseconds after last animation then you're stuck with a scroll position 25 milliseconds before it stopped scrolling.
Maybe a better way to do it is this (can test in the console on this page):
var ol = document.querySelectorAll("ol")[2];
window.onscroll = ((lastExecuted)=>{
const expensiveHandler = e => {
lastExecuted=Date.now();
//do some animation maybe with requestAnimationFrame
// it may prevent overloading but could make animation
// glitchy if system is busy
console.log("1:",ol.getBoundingClientRect().top);
};
var timerID
return e=>{
if(Date.now()-lastExecuted>19){
//only animate once every 20 milliseconds
clearTimeout(timerID);
expensiveHandler(e);
}else{
//make sure the last scroll event does the right animation
// delay of 20 milliseconds
console.log("nope")
clearTimeout(timerID);//only the last is needed
timerID=setTimeout(e=>expensiveHandler(e),20);
}
}
})(Date.now())

Related

Preventing textarea scroll behaviour in chrome after newline added

Recently my version of chrome has been doing something strange (74.0.3729.131 on ubuntu 18.04) more and more often. I have a small editor script which has a textarea which displays code. The textarea has a fixed size and a vertical scroll bar. Beyond that nothing fancy.
Usually, when I insert a newline (normal behaviour of textarea), the scroll bar doesn't move. Now for some reason about 80% of the times it scrolls the textarea down till the position of the caret is at the top of the textarea. Strangely if I delete and enter the newline in the same position, it usually does not scroll.
I'm not sure if this is some new issue in Chrome. I usen't have this issue with previous versions with the identical editor.
Here is a codepen which demonstrates the issue, scroll to some line, press enter and the textarea should scroll down. Try this a few times to see the unpredictable behaviour (adding the code just to be able to add the link, as you can see it's just a textarea).
https://codepen.io/anon/pen/rgKqMb
<textarea style="width:90%;height:300px"></textarea>
The only solution that occurs to me to avoid this is to stop the normal behaviour of the enter key and add the newline to the text. Any other ideas/insights very much welcome.
It's almost the end of 2020, Chrome version 86 and this issue still exists? What's more, I am surprised I have not found more information (complaints) on this matter (this post is the only thing I've found which speaks of this issue specifically.) I have observed that this behavior occurs not only in typing, but pasting any text containing a newline. I also observed that if I execute an undo action after this occurs, another random scroll happens, taking me even farther up the page, and nowhere near where the caret is.
I experimented and examined this behavior at much length, and was not able to find any repeatable circumstances which might give a clue as to how to predict when this would occur. It truly just seems "random". Nonetheless, I've had to work around this issue for an NWJS editor app I'm creating (NWJS uses Chrome for UI.)
This is what seems to be working for me:
First all, let me start simple in order to introduce the principle. We attach an "input" listener and "scroll" listener to the textarea. This works because, from my observation anyway, the "input"[1] listener gets fired before the random scroll action occurs.
The scroll listener records each scrolling action and saves it in a global prevScrollPos. It also checks for a global flag scrollCorrection.
The "input" listener sets the scrollCorrection flag everytime text is input into the textarea. Remember, this has happened before the random scroll occurs.
So the next scroll to occur, which may be the nuisance random action, the scroll listener will clear scrollCorrection, then scroll the textarea to the previous scroll position, ie, scrolling it back to where it was before the "random" scroll. But the issue is unpredictable, what if there is no random scroll and the next scroll to occur is intentional? That is not a big deal. It just means that if the user scrolls manually, the first scroll event is basically nullified, but then after that (with scrollCorrection cleared) everything will scroll normally. Since during normal scrolling, events are spit out so rapidly, it is unlikely there will be any noticeable effect.
Here is the code:
let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
function onScroll(evt) {
if (scrollCorrection) {
// Reset this right off so it doesn't get retriggered by the corrction.
scrollCorrection = false;
textarea.scrollTop = prevScrollPos;
}
prevScrollPos = textarea.scrollTop;
}
function onInput(evt) {
scrollCorrection = true;
}
window.addEventListener("load", () => {
textarea = document.getElementById("example_textarea");
textarea.addEventListener("scroll", onScroll);
textarea.addEventListener("input", onInput);
})
Now let's expand on it:
There is another consideration. What if the typing or pasting action puts the end of the typed or pasted text (and thus the caret) outside the view of the textarea viewport? When normal scrolling is in play, most browsers will scroll the page[2] so the caret will remain in view. However now that we've taken over scrolling action, we'll need to implement that ourselves.
In the psuedo-code below, on input to the textarea, besides setting scrollCorrection, we call a function which will:
determine the xy position of caret relative to textarea viewport
determine if it is scrolled out of view
if so:
determine the amount to scroll to bring it in view
determine if the random scroll has already occurred by testing the state of scrollCorrection
if it hasn't, set flag scrollCorrection2 containing the amount to scroll
if it has, explicitly do the additional scrolling to bring it back into view
Finding the xy position of the caret in a textarea is not a trivial matter and is outside the scope of this answer, but there are plenty of methods to be found in searching the web. Most involve replicating the textarea contents in a non-form element, eg div block, with similar font, font-size, text wrapping etc, then using getBoundingClientRect on the resultant containing block and such. In my situation, I was already doing most of this for my editor, so it wasn't much of an additional expense. But I've included some psuedo-code to show how this can be implemented in the scroll correction mechanism. setCaretCorrection basically does steps 1 - 7 above.
let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
let caretCorrection = 0;
function onScroll(evt) {
if (scrollCorrection) {
// Reset this right off so it doesn't get retriggered by the correction.
scrollCorrection = false;
textarea.scrollTop = prevScrollPos + caretCorrection;
caretCorrection = 0;
}
prevScrollPos = textarea.scrollTop;
}
function onTextareaInput() {
scrollCorrection = true;
setCaretCorrection();
}
function setCaretCorrection(evt) {
let caretPos = textarea.selectionStart;
let scrollingNeeded;
let amountToScroll;
/* ... Some code to determine xy position of caret relative to
textarea viewport, if it is scrolled out of view, and if
so, how much to scroll to bring it in view. ... */
if (scrollingNeeded) {
if (scrollCorrection) {
// scrollCorrection is true meaning random scroll has not occurred yet,
// so flag the scroll listener to add additional correction. This method
// won't cause a flicker which could happen if we scrollBy() explicitly.
caretCorrection = amountToScroll;
} else {
// Random scroll has already occurred and been corrected, so we are
// forced to do the additional "out of viewport" correction explicitly.
// Note, in my situation I never saw this condition happen.
textarea.scrollBy(0, amountToScroll);
}
}
}
One could go further and use the experimental event, "beforeinput"[3], to optimize this a little bit so fewer unnecessary calls to setCaretCorrection are made. If one examines event.data from "beforeinput" event, in certain cases it will report the data to be input. If it does not, then it outputs null. Unfortunately, when a newline is typed, event.data is null. However it will report newlines if they are pasted. So at least one can see if event.data contains a string, and if the string does not contain newlines, skip the whole correction action. (Also, see [1] below.)
[1] I also don't see any reason you couldn't do in the "beforeinput"[3] listener, what what we're doing in the "input" listener. That may also give more insurance that we set scrollCorrection before the random scroll occurs. Although note that "beforeinput" is experimental.
[2] I suspect it is broken implementation of this feature which is causing this issue.
[3] https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event (Available on Chrome and all major browsers except Firefox according to this link.)
You can try avoiding the events on the textarea with css and js, then force the scroll to it's current position:
css:
textarea {
overflow:auto;
resize:none;
width:90%;
height:300px;
}
js:
You'll need to insert the first answer from this question at A
function preventMoving(e) {
var key = (e.keyCode ? e.keyCode : e.which);
if(key == 13) {
e.preventDefault();
// A
}
}
Then on your HTML:
<textarea onkeyup="preventMoving(event);"></textarea>

Scrolling to the bottom of a page that changes height

I want to scroll to the bottom of a webpage using JS and, looking online, most people say to use
window.scrollTo(0,document.body.scrollHeight);
However the page I want to scroll down changes its height as you reach the bottom (it continuously loads more and more of the page) so its height is not fixed. The eventual height also varies (thus I cannot just scroll to the final height of the page) so I need some way of scrolling until it determines it has reached the bottom (it cannot scroll any further).
If you have infinite scroll, you actually need to incorporate that in your design. E.g. scrollToBottom should look like this:
function scrollToBottom(lastOffset) {
const offset = document.body.scrollHeight;
// check if no more scrolling required.
if (lastOffset === offset) {
return;
}
// if it is, go to bottom.
window.scrollTo(0, offset);
// Now, trigger your `loadMore` logic, whatever it might be, then call scrollToBottom again.
triggerLoadMore().then(() => scrollToBottom(offset));
// or triggerLoadMore(() => scrollToBottom(offset));
}
// call this somewhere after your initial load.
Now, triggerLoadMore, erm, triggers your loadMore logic. I included two versions in the above code, one being promise-based, the commented one being callback-based. It should call back your function (scrollToBottom) when it has loaded more and put it on the screen. How you do that, depends on what you use, and is possibly another question.
Now, there's a third option here, that your trigger is something that you cannot call directly, because it's tied to window.scroll event or some such logic. In that case, you need to write this bit of logic at the end of such function flow - load more, render on screen, scroll. Which then triggers the cycle again.
#Zlatko answer is the best performance. However, you can use this one in every context :
window.addEventListener( 'resize', function(){
window.scrollTo(0,document.body.scrollHeight);
});
Sounds like you need to reach bottom of the page, wait for load and scroll to bottom again. You can't reach the end of the page where you cannot know the end.
function scroll_and_wait(){
return new Promise(
function(resolve, reject){
setTimeout(function(){
resolve(window.scrollTo(0,document.body.scrollHeight));
}, 500);})}
async function scroller(n){while(n>0){await scroll_and_wait();n=n-1;}}
scroller(5)
Calling scroller function will scroll to to bottom of the page as many times as you specify with 500ms pause for loading.

How to make the cursor change in d3.js when I want?

This is a question related to Basic Javascript loading message while js processing completes
My main problem is that cursor not is changed before my two functions drawlegend() and display() are called, but changes after everthing has finnished.
With the code as below where the restore of the cursor temporary commented out, I get the hourglass, but after everything has finnished.
How to get my cursor to change to an hourglass before my slow functions are called?
examplefunc()
{
mini.append("text")
.text(series[i].name)
.attr("x",30)
.attr("y",(15+(15*i)))
.attr("stroke",(d3.rgb(192,192,192)))
.attr("fill",(d3.rgb(192,192,192)))
.attr("stroke-width",0)
.style("font-size","12px")
.attr("text-anchor","start")
.attr("id","legend")
.on('mouseup', legendclick);
}
//===== legend clicked
function legendclick()
{
//--- get mouse pos
var origin = d3.mouse(this);
//--- get channel
var ch=Math.floor((origin[1]-4)/15);
//--- toggle active state
if (series[ch].active==true)
series[ch].active=false;
else
series[ch].active=true;
setTimeout(setcursor("wait"),5);
drawlegend();
display();
//setTimeout(setcursor("default"),5); // temp removed to see any result at all
}
//===== set cursor
function setcursor(cursor)
{
d3.select("body").style("cursor", cursor);
}
It is known that executing things in javascript, hangs your application. This means that only the eventual output is displayed on your screen. Thus, when you change the cursor to "wait" and after execution to "cursor", the javascript hasn't changed it, because the ui thread was busy calculating the things in the functions "drawlegend" and "display". However, I think when you execute the "drawlegend" and "display" asynchronous like
setTimeout(function () {
drawLegend();
display();
setcursor("default");
}, 0);
then things should go like you want to.
Let me know if this works for you.
Extra info: on this slideshare (especially slide 5) is explained what your problem is.

Flickering mousenter mouseleave with delay, button list

I have made a list(<p>) with buttons. When I move my mouse over them it's a 1,2 sec delay before my textbox are marked with yellow to show where I can write. When I move my mouse away they turn normal(white).
My problem is when I quickly hover my mouse over the buttons back and forth a lot of the textboxes gets marked.
I had hoped the 1,2 sec delay would have worked then but it doesn't. But it works if I move my mouse slowly in and out of the button.
Here is a fiddle to it: http://jsfiddle.net/Pota/Fj6E6/
Here is my JavaScript code
$(function () {
$("p.pRespRoleId").mouseenter(function () {
var timeOut = 1200;
$this = $(this);
$this.data("delay", setTimeout(function () {
mouseInRespRoleId();
}, timeOut)
);
})
.mouseleave(function () {
$this = $(this);
if ($this.next(mouseOutRespRoleId()).is(":visible")) {
clearTimeout($this.data("delay"));
mouseOutRespRoleId();
}
else {
$this.next("p.pRespRoleId").show();
}
});
});
and
function mouseInRespRole()
{
var txtInRespRole = document.getElementById("<%=txtRespRoleName.ClientID %>");
txtInRespRole.style.background = "#FFFF00";
if (document.getElementById('txtRespRoleName').value == '')
{
document.getElementById('txtRespRoleName').innerHTML = txtInRespRole;
return false;
}
}
function mouseOutRespRole()
{
var txtOutRespRole = document.getElementById("<%=txtRespRoleName.ClientID %>");
txtOutRespRole.style.background = "white";
if (document.getElementById('txtRespRoleName').value == '')
{
document.getElementById('txtRespRoleName').innerHTML = txtOutRespRole;
return true;
}
}
Your jsFiddle is surely confusing to me (I am not sure what you are trying to achieve - there is a tangible possibility that you are overcomplicating things). I hope I got your requirement right...
Anyway, I believe your logic was right, but there were some flaws in the implementation. So, here is a modified (and partially corrected) version of your jsFiddle, which does what (I believe) you were trying to achieve.
Your use of '$this.next(mouseOutRespRoleId()).is(":visible")' was sure the most confusing, so I removed it completely. (In case it was fulfilling some other, not obvious purpose, you'll have to provide a more detailed description.)
The main problem was that $this.next(mouseOutRespRoleId()).is(":visible") was never evaluating to true, thus never clearing the timer that called mouseInRespRoleId().
EDIT:
I updated my jsFiddle illustration so that it takes care of IE9's strange behaviour (a.k.a. bug (?)). It should work without flickering now.
Short explanation of the problem:
Aparantly, in IE9 the mouse-events generation has some "timing issues", so that when entering (mouseOver) and leaving (mouseOut) a component multiple times rapidly, sometimes the mouse-events order gets messed up. E.g.:
The following event sequence (i.e. actual events):
mouseOver -> mouseOut -> mouseOver
May produce the following (obviously wrong) javascript-event sequence (i.e. events triggered by JS-engine in IE9):
mouseOver -> mouseOver(!) -> mouseOut(!)
So, I added an extra clearTimeout($this.data("delay")) in the "mouseentered" handler-function, in order to clear any pending scheduled executions of "mouseInRespRoleId".
It does not work perfectly on IE9 (and probably previous versions of IE - not tested), but it is as good as it can get (afaik).
(NOTE: It still works as intended on other (non-buggy) browsers.)

How can I reveal content and maintain its visibility when mousing to a child element?

I'm asking a question very similar to this one—dare I say identical?
An example is currently in the bottom navigation on this page.
I'm looking to display the name and link of the next and previous page when a user hovers over their respective icons. I'm pretty sure my solution will entail binding or timers, neither of which I'm seeming to understand very well at the moment.
Currently, I have:
$(document).ready(function() {
var dropdown = $('span.hide_me');
var navigator = $('a.paginate_link');
dropdown.hide();
$(navigator).hover(function(){
$(this).siblings(dropdown).fadeIn();
}, function(){
setTimeout(function(){
dropdown.fadeOut();
}, 3000);
});
});
with its respective HTML (some ExpressionEngine code included—apologies):
<p class="older_entry">Older<span class="hide_me">Older entry:
<br />
{title}</span></p>
{/exp:weblog:next_entry}
<p class="blog_home">Blog Main<span class="hide_me">Back to the blog</span></p>
{exp:weblog:prev_entry weblog="blog"}
<p class="newer_entry">Newer<span class="hide_me">Newer entry:
<br />
{title}</span></p>
This is behaving pretty strangely at the moment. Sometimes it waits three seconds, sometimes it waits one second, sometimes it doesn't fade out altogether.
Essentially, I'm looking to fade in 'span.hide_me' on hover of the icons ('a.paginate_link'), and I'd like it to remain visible when users mouse over the span.
Think anyone could help walk me through this process and understand exactly how the timers and clearing of the timers is working?
Thanks so much, Stack Overflow. You guys have been incredible as I walk down this road of learning to make the internet.
If you just want to get it working, you can try to use a tooltip plugin like this one.
If you want to understand how this should be done: first, get rid of the timeout, and make it work without it. The difference (from the user's point of view) is very small, and it simplifies stuff (developing and debugging). After you get it working like you want, put the timeout back in.
Now, the problem is you don't really want to hide the shown element on the navigator mouse-out event. You want to hide it in its own mouse out event. So I think you can just pass the first argument to the navigator hover function, and add another hover to dropdowns, that will have an empty function as a first argument, and the hiding code in its second argument.
EDIT (according to your response to stefpet's answer)
I understand that you DO want the dropdown to disappear if the mouse moves out of the navigator, UNLESS its moved to the dropdown itself. This complicates a little, but here is how it can be done: on both types of items mouse-out event, you set a timer that calls a function that hides the dropdown. lets say the timer is 1 second. on both kind of item mouse-in even, you clear this timer (see the w3school page on timing for syntax, etc). plus, in the navigator's mouse-in you have to show the dropdown.
Another issue with the timer in your code is that it will always execute when mouse-out. Due to the 3 seconds delay you might very well trigger it again when mouse-over but since the timer still exist it will fade out despite you actually have the mouse over the element.
Moving the mouse back and forth quickly will trigger multiple timers.
Try to get it to work without the timer first, then (if really needed) add the additional complexity with the delay (which you must keep track of and remove/reset depending on state).
Here was the final working code, for anyone who comes across this again. Feel free to let me know if I could have improved it in any ways:
$(document).ready(function() {
var dropdown = $('span.hide_me');
var navigator = $('a.paginate_link');
dropdown.hide();
$(navigator).hover(function(){
clearTimeout(emptyTimer);
$(this).siblings(dropdown).fadeIn();
}, function(){
emptyTimer = setTimeout(function(){
dropdown.fadeOut();
}, 500);
});
$(dropdown).hover(function(){
clearTimeout(emptyTimer);
}, function(){
emptyTimer = setTimeout(function(){
dropdown.fadeOut();
}, 500);
});
});

Categories