Preventing textarea scroll behaviour in chrome after newline added - javascript

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>

Related

getBoundingClientRect during throttled scroll event handling

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())

scrollTop not working automatically

I know there are a couple of questions to scrollTop already out there but I haven't really seen anything resembling my problem.
Using jquery 1.7.2 on an IE9 we have a page with three Tabs (JqueryUI).
The Data is connected and that resulted in us only having the current tab on the page. Changing tabs will remove the unseen one and reload the one we jump into.
The Scroll-Positions are stored correctly in variables on the base page but trying to set that position in the document-ready-function does not work.
An alert shows the correct number, so the function is actually called but the scrollbar does not move.
Calling the same function with a button on the page afterwards however works perfectly.
The document-ready-function on the tab's jsp is quite simple:
<script type="text/javascript">
jQuery(document).ready(function(){
setAhaScrollbar();
});
</script>
and the called function is quite simple as well:
function setAhaScrollbar() {
var scrollWert = $('#scrollbarAnhaengeartikel').val();
alert(scrollWert);
$('#anhaengeGridScrollable').scrollTop(scrollWert);
}
Called from document-ready it does nothing. Called from a button later on it works fine.
The div where the scroll position is supposed to be set is defined with overflow: auto and a fixed height
crollTop( value )
Description: Set the current vertical position of the scroll bar for each of the set of matched elements.
.scrollTop( value )
value
Type: Number
An integer indicating the new position to set the scroll bar to.
More Information
As the documentationsaid value should be number.
Try
var scrollWert = Number($('#scrollbarAnhaengeartikel').val());
or
var scrollWert = parseInt($('#scrollbarAnhaengeartikel').val());
Apparently it was primarily a timing problem. Maybe there were still things going on when document ready fired.
Changing that function to
jQuery(document).ready(function(){
setTimeout("setAhaScrollbar()", 500);
});
did the trick so my problem is solved.

Hashchange history breaks iScroll

I have created the following application using iScroll: http://preview.na-software.co.uk/Demo/FutureLearning4/#/section-0
As the user flicks left and right or clicks the arrows in the bottom corners, the application moves the content sections it updates the history by changing the hash so that the user can move back and forth to other sections and bookmark them etc.
However! If you access a hash like: http://preview.na-software.co.uk/Demo/FutureLearning4/#/section-2 and then navigate a few sections and then use the back buttons two issues happen:
1.) It scrolls to the first screen (even though currentSection is correct, and iScroll has been told the correct section).
2.) If you click the back or forward button multiple times, you stop the animation and cause it to become confused and stick in between two sections.
Looking into the code, and seeing that the correct indexes and elements are being passed to iScroll on hashchange, and console logging out the offsets, I've discovered the issue is cause because the offsets are incorrectly set... however just doing refresh() won't fix the issue, as it will then reset the position.
Can anyone see where the problem is or see a way to fix this?
I should note that this bug ONLY happens if you come into the application on a URL that isn't section 0 and then scroll around the application. This is because the offsets will be created correctly by your interactions. But if you come into a URL like section 3, then the offsets will be incorrect and so the hashchanges don't work correctly, if that makes sense.
The hashchange method looks like:
// handle hashchange events
$(window).hashchange( function(){
// read the hash to find out what the new section number is
var nums = location.href.match(/(section)-\d+/g).map(
function(x){ return +x.replace(/\D/g,"") }
);
// set currentSection
currentSection = nums[0];
// if the hashchange was called by user scrolling
if(hashCalledByScroll){
// no need to anything as they have already updated hash and scrolled
hashCalledByScroll = false;
} else {
// find the section to scrollTo
sectionToScrollTo = $('#horizontal > .sections > .section').eq(currentSection).attr('id');
// tell iscroll to scroll to the section
horizontal.scrollToElement( '#' + sectionToScrollTo, null, null, true );
}
// hide the menu on hashchange
hideMenu();
});
Testing your site, I noticed the following: Whenever I access the site via section-3 and then enter the url for section-2, the navigation would instead send me to section-0.
I believe this is the same behaviour as you are experiencing in 1).
So I investigated and came to the following analysis:
In the function horizontal.scrollToElement( '#' + sectionToScrollTo, null, null, true )
iScroll retrieves the utils.offset(el) [iScroll.js#772] for the given el-ement. This offset tells it, where the element to scroll to is.
iScroll goes through the element and all of its offsetParents to add up their offsets. This is where things are breaking: <div class="sections"> has a negative offset to its parent, which imho it should not have.
This, in turn, messes up the scrollTo-coordinates.
To see what I am talking about: document.querySelector('.sections').offsetLeft
This has all just been analysis. My approach to fix this would be to avoid scrollToElement() and instead use scrollTo():
...
} else {
// find the section to scrollTo
sectionToScrollTo = $('#horizontal > .sections > .section').eq(currentSection).attr('id');
// tell iscroll to scroll to the section
var posLeft = -$('#' + sectionToScrollTo)[0].offsetLeft;
var posTop = -$('#' + sectionToScrollTo)[0].offsetTop;
horizontal.scrollTo(posLeft, posTop, 1000);
}
// hide the menu on hashchange
hideMenu();
});
Thus, just calculate the location of the section you want to go to yourself.
About 2) I am not sure if there is much one can do about it. Jumping around quickly breaks a lot of carousels. Maybe a delayed callback to scrollEnd, verifying the validity of the current state.
Another thing I noticed is that you can accidentally stop the transition. Try to click, hold and release the cursor midway a transition - you need to be quick.
Hope this helps.
Found not best solution and it doesn't solve main problem, but it works.
$(window).hashchange(function () {
if (hashCalledByScroll) {
hashCalledByScroll = false;
} else {
var hpage = window.location.hash;
var hpage = hpage.replace('#/section-', ''); //get number of target page
var cpage = currentSection; //number of current page
var count = parseInt(hpage) - parseInt(cpage); //difference
while (count > 0) { //if difference positive: go forward count-times
horizontal.next();
count--;
}
while (count < 0) { //if difference negative: go backward count-times
horizontal.prev();
count++;
}
}
hideMenu();
});
FIDDLE

Make Parent DOM Elements unreadable

I'm making my IFrame to be fullscreen, I want to make DOM Elements in Parent disable [on tabbing] using javascript. Any Idea?
thanks in advance
I think the scenario is very similar to content pop-overs (lightboxes), where you want it to:
Move the keyboard focus to the top of the iframe.
Keep the keyboard focus in the iframe.
To move the keyboard focus into the iframe use a proper link (with href) and trigger:
$("#myIframe").attr("tabindex", "-1").css("outline", "0");
$("#myIframe").focus(); // separate line to ensure the tabindex is applied first.
To keep the focus in the iframe, find the first and last elements and loop them around:
(function(){
var firstLink = $("#myIframe a:first").get(0);
var lastLink = $("#myIframe a:last").get(0);
$(firstLink).keydown(function(e) {
// if you shift-tab on first link, go to last
if(e.shiftKey && e.keyCode == 9) {
e.preventDefault();
$(lastLink).focus();
}
});
$(lastLink).keydown(function(e) {
// if you press tab without shift, loop to first link.
if (!e.shiftKey && e.keyCode == 9) {
e.preventDefault();
$(firstLink).focus();
}
});
})(); // end tabloop anonymous function
JavaScript/jQuery isn't my strong point, so you might need to adjust this. For example, if the first/last focusable element is a form control that wouldn't work.
Also, it is worth knowing that screen readers do not necessarily use tab to progress through the page, they 'arrow' through (browse mode) and do not necessarily trigger focus.
In order to keep them within the iframe you effectively need to 'hide' everything else.
If you have the main content and iframe on the same level this is straightforward, you would start with:
<div class="mainContent">...</div>
<iframe id="myIframe">...</iframe>
When the page loads use:
$("#myIframe").attr("aria-hidden", "true");
When the iframe becomes the focus:
$("#myIframe").attr("aria-hidden", "false");
$("div.mainContent").attr("aria-hidden", "true");
All the techniques for this (in the context of a lightbox) are in a gist here: https://gist.github.com/alastc/7340946
NB: The whole concept of full-screening an iframe sounds a bit dubious, if you provided some context there may be a better solution?

Limiting click detection on the page [javascript]

I'm trying to limit the user's ability to click on an object to a certain time limit. I looked around and found that apparently, setTimeout() is the correct function to use for this type of thing. I've applied the function to my code, but its not working. I'm thinking/know now that the problem is that the setTimeout in my code isn't limiting the actual click event, which I need to do. Here is a snippet of my click code:
function clickRun(event) {
var $objectVersion = correspondingObject(event.target.id);
if (isAnyVisible() == false) { // none open
$objectVersion.makeVisible();
} else if (isAnyVisible() && $objectVersion.isVisible()) { //click already open div
$objectVersion.makeInvisible();
} else if (isAnyVisible() && $objectVersion.isVisible()==false) { //different div open
searchAndDestroy();
$objectVersion.delay(600).makeVisible();
};
};
$('.ChartLink').click(function(event) {
setTimeout(clickRun(event),5000);
});
I've also created a JSFiddle to represent what I'm talking about: http://jsfiddle.net/FHC7s/
Is there a way to achieve limiting the actual click detection on the page?
I think the easiest way to do it is to keep track of the time of the previous click and if the current click is too soon after that, then don't do anything:
onClick = function(){
if(new Date().getTime() - lastCheck < MIN_CLICK_SPACING) return;
}
Have a look at this JSFiddle, I've set it up so you can have the button disable itself for time duration after detecting a click. Just make sure to remember how your closures are operating with your setTimeouts.
Your code contains an error... your line should be
setTimeout(function(){clickRun(event)},5000);
but even then I don't think that's exactly what you're looking for; that code will "delay" the click by 5 seconds, not actually prevent more clicks. If your true intent is to ignore all clicks after a certain amount of time, then I would go with mowwalker's answer; there's no way to stop the clicks, but you can check to see if you should honor them or not.

Categories