I'm waist deep in my own React virtualization implementation and one of the minor issues that has been annoying me is that if I middle click on an item in my list and start scrolling, once that element is removed from the DOM the scrolling halts. My first theory was that the element was gaining focus and that preventing that would solve the issue, but what I've tried hasn't been working and I'm not even sure that's the issue.
How can I prevent this from happening?
See this fiddle for a basic demonstration:
https://jsfiddle.net/v169xkym/2/
And the relevant bit of code that handles virtualization:
$('#container').scroll(function(e) {
$('#container').children().each(function(i) {
if ($('.item:eq(' + i + ')').length > 0) {
if ($('.item:eq(' + i + ')').offset().top < 0) {
$('.item:eq(' + i + ')').remove();
$('#topPadding').height($('#topPadding').height() + 45);
}
}
});
});
Basically, I'm using the standard method of removing the element and upping the padding. In my React implementation this is handled different but here you get a basic functional representation.
you can get around this by not having the disappearing element register mouse events.
this can be done with CSS3 :
div.item {
pointer-events : none;
}
(Not entirely sure why, but my guess is that once the element disappears, the origin of the event is missing, so browsers simply stop doing what they were doing.)
Jsfiddle here
Maybe a bit late to the party. A workaround I am using on a virtual scroller is to detect when there is a scroll event, and when there has been no new events for a time, I consider the scroll is complete.
let scrollTimer = null;
let isScrolling = false;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimer);
isScrolling = true;
scrollTimer = setTimeout(()=>{
isScrolling = false;
},500);
}, false);
I then grab a reference to the element that is hovered at the time isScrolling becomes true (using mouseOver) and prevent this element being unloaded until isScrolling is false. It is a bit of a juggle, but works. I am hoping I can find something simpler as it only seems to be a Chrome problem.
Update: It seems to be a known bug, about to be fixed related to pointer-events: none on something that overlays a virtual scroller (reproduction by someone https://codepen.io/flachware/pen/WNMzKav). I have no idea why my work around above works, but nice to know it wont be needed come Chrome 103. https://bugs.chromium.org/p/chromium/issues/detail?id=1330045&q=chrome%20scroll&can=2&sort=-opened
Related
I built a basic picture carousel a while back, and I'm finally getting around to transferring it from MooTools over to jQuery so I can drop MooTools. I've got the script completely functional, but for whatever reason when the carousel slides in one direction, you can see a "pop" where it resets itself.
I've tried playing around with the order it handles everything, but no matter what it seems to always desync for just a fraction of a section.
Here's a copy of my code: https://jsfiddle.net/Chaosxmk/pf6dzchm/
The offending section of code is this:
styles['position'] = 'absolute';
styles[self.params.axis] = -32768;
$(self.list[0]).css(styles).hide();
$(self.list[0]).appendTo(self.carousel);
$(self.list[conf.mi]).css(self.params.axis, (100-conf.pr)+'%');
styles = {};
styles['position'] = 'relative';
styles[self.params.axis] = 'auto';
$(self.list[conf.mi]).css(styles);
Issue is that $.fadeOut() sets display:none on the element, which causes some strange rendering issues in your setTimeout() callback. Works better if you use $.fadeTo() instead:
if (self.params.direction) {
// Go forward
self.carousel.css(self.params.axis, '-'+conf.pr+'%');
$(self.list[0]).fadeTo(400, 0);
$(self.list[conf.mi]).css(self.params.axis, '100%').fadeTo(400, 1);
} else {
// Go backward
self.carousel.css(self.params.axis, conf.pr+'%');
$(self.list[conf.mi-1]).fadeTo(400, 0);
self.list.last().css(self.params.axis, '-'+conf.pr+'%').fadeTo(400, 1);
}
For simplicity I used a 400ms duration, but you can set this to whatever you need.
JSFiddle
This is a general question about a problem I run into often, where I need something to happen at a certain screen width, or scrollTop position, so I end up triggering events for the entire scroll or resize event. This seems really unnecessary and hurts performance. I am wondering what steps I can take to limit calling code written inside scroll or resize events so that I am only triggering these events when I need them.
In the example below I just want the background color to change at a certain scrollTop offset, but since its wrapped in a scroll event, it gets trigged for every pixel.
I know there are things like lodash, but wouldn't I have the same problem of a throttle running just as often on scroll? Any general approach help would be greatly appreciated.
$(window).on('scroll', function() {
var scrollPosition = $(window).scrollTop();
if (scrollPosition > 500) {
$('.container').css('background-color', 'blue');
} else {
$('.container').css('background-color', 'red');
}
});
.container {
background-color: red;
height: 2000px;
width: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
</div>
You should really have a look at Intersection Observer (IO), this was created to solve problems like you described.
With IO you tell the browsers which elements to watch and the browser will then execute a callback function once they come into view (or leave the view) or intersect with each other.
First you have to set the options for your observer:
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Here for example I specified that everytime the observed element is fully visible in viewport I want to execute a callback function. Obviously you can set the parameters to your liking.
Second you have to specify which elements you want to observe:
let target = document.querySelectorAll('.container');
observer.observe(target);
Here I say I want to watch all elements on the page with the class container.
Last I have define the callback function which will be triggered everytime one container element is visible.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element
});
};
With this approach you don't have to worry about performance issues of scroll events.
Since you can theoretically build all of this with listening to scroll events too you can also use this official polyfill from w3c to support older browsers.
You can also stop observing an element if you don't want to observe it anymore.
Lastly have a look at this demo, it shows you can easily change the background-color of an element depending on how much of the element is visible.
You definitely should use throttle or debounce for scroll or resize handlers. It can be lodash or your own implementation. Cancelled handler triggering costs almost nothing in term of performance, so don't even bother about that.
You can use a library like Waypoint.js to accomplish a similar feature. You'll just need to add an element at the event position and it should work quite efficient. Otherwise there aren't that many other ways except the ones that Huangism already mentioned. As you also asked about resizing events it may be better to use CSS media rules because these are quite performant and easy to use:
#media only screen and (max-width: 600px) {
.container {
background-color: blue;
}
}
As mentioned in comments, you just need to throttle the action. Here is a sample code of how scrolling throttling would work
The expensive part of the scroll/resize event is the part where you are doing something, like when you getting the scrolltop and comparing it then running something. By throttling, your executable code don't actually run until the threshold is reached which saves you big time on performance
Resizing would be the same except you do it for the resize function of course
scrollTimer = setTimeout(function() {
// your execution code goes here
}, 50);
var scrollInit = false;
var scrollTimer = null;
if (!scrollInit) {
var waiting = false;
$(window).on("scroll", function(event) {
if (waiting) {
return false;
}
clearTimeout(scrollTimer);
waiting = true;
scrollTimer = setTimeout(function() {
console.log("scroll event triggered");
}, 50);
});
}
scrollInit = true;
div {
background: grey;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div></div>
I am currently switching the menu of my site from pure JavaScript to jQuery. My menu has a rollout / rollin effect.
The menu has an outer wrapper which has an onmouseout event set. If this fires, the relatedTarget is checked whether it's a child of the outer wrapper. If not, the rollin shall happen.
What happens right now is, that if the mouse is moved from the menu's inner wrapper (this is to center the actual menu) to the menu's outer wrapper, the onmouseout fires. There seems to be a tiny part which doesn't belong to the menuOuterWrapper.
The site isn't online right now, so I've prepared a Fiddle here. You will see the problem if you move your mouse from the gray area above the handle to the left or right dark area. The menu will roll in and then immediately out again. The rollin shall only occur when the mouse is moved out of the outer wrapper, i.e. under the dark gray area (or the light gray handle area). To see the dark gray areas, you might have to increase the width of the result block. [EDIT: I reduced the width of inner to 600px, so the dark side areas should be visible by default now.]
SO tells me that I shall include code when linking to JSFiddle. I don't want to break the rules but I'll be honest: I'm clueless where the problem comes from. My best idea is that I made a mistake in my isChildOf implementation, so I'll give you this:
jQuery.fn.isChildOf = function (parentId) {
if ($(this).parents("#" + parentId).length > 0) {
return true;
} else {
return false;
}
};
$('#outer').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
Although this is a minimal example, I did nearly the same with pure JS, where it worked fine. So I guess it's something in the jQuery part. Since these are my first steps with jQuery, it is even more likely.
Every help you can provide is highly appreciated :)
[UPDATE]
I got it working now. The problem was that I didn't check for the relatedTarget to be "outer" itself. So when the mouse leaves the content div and enters the outer div, mouseout fires and of course, outer is no child of itself. So I amended it to
$('#outer').on('mouseout', function(event) {
if (!(event.relatedTarget.id == "outer") &&
!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
and that fixed the problem.
if i understood your question right.
This might help
$('#inner').on('mouseover', function() {
mouseIsOverMenu = true;
setTimeout(menu_rollout, 500);
});
$('#inner').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
What i did is i have changed the id of #outer to #inner.
This is a dirty hack, but your problem seems to be with the mouseout function applying too frequently, and what functionality you really want is capturing the mouse leaving the bottom of the menu/content.
Here's some code that will do just that.
$('#outer').on('mouseout', function(event) {
if(event.clientY >= document.getElementById('outer').offsetHeight){
mouseIsOverMenu = false;
menu_rollin();
}
});
here's the associated jsFiddle
This may come as a huge surprise to some people but I am having an issue with the IE browser when I am using the $(window).scroll method.
My goal:
I would like to have the menu located on the left retain it's position until the scroll reaches > y value. It will then fix itself to the top of the page until the scroll returns to a < y value.
My error:
Everything seems just fine in Chrome and Firefox but when I go to Internet Explorer it would seem the browser is moving #scroller every time the scroll value changes, this is causing a moving/flickering event.
If someone could point me to a resource or give me a workaround for this I would be very grateful!
Here is a fiddle:
http://jsfiddle.net/CampbeII/nLK7j/
Here is a link to the site in dev:
http://squ4reone.com/domains/ottawakaraoke/Squ4reone/responsive/index.php
My script:
$(window).scroll(function () {
var navigation = $(window).scrollTop();
if (navigation > 400) {
$('#scroller').css('top',navigation - 220);
} else {
$('#scroller').css('top',183);
$('#scroller').css('position','relative');
}
});
You might want to take a look at the jQuery Waypoints plugin, it lets you do sticky elements like this and a lot more.
If you want to stick with your current method, like the other answers have indicated you should toggle fixed positioning instead of updating the .top attribute in every scroll event. However, I would also introduce a flag to track whether or not it is currently stuck, this way you are only updating the position and top attributes when it actually make the transition instead of every scroll event. Interacting with the DOM is computationally expensive, this will take a lot of load off of the layout engine and should make things even smoother.
http://jsfiddle.net/WYNcj/6/
$(function () {
var stuck = false,
stickAt = $('#scroller').offset().top;
$(window).scroll(function () {
var scrollTop = $(window).scrollTop();
if (!stuck && scrollTop > stickAt) {
$('#scroller').css('top', 0);
$('#scroller').css('position','fixed');
stuck = true;
} else if (stuck && scrollTop < stickAt) {
$('#scroller').css('top', stickAt);
$('#scroller').css('position','absolute');
stuck = false;
}
});
});
Update
Switching the #scroller from relative to fixed removes it from the normal flow of the page, this can have unintended consequences for the layout as it re-flows without the missing block. If you change #scroller to use an absolute position it will be removed from the normal flow and will no longer cause these side-effects. I've updated the above example and the linked jsfiddle to reflect the changes to the JS/CSS.
I also changed the way that stickAt is calculated as well, it uses .offset() to find the exact position of the top of #scoller instead of relying on the CSS top value.
Instead of setting the top distance at each scroll event, please consider only switching between a fixed position and an absolute or relative position.All browsers will appreciate and Especially IE.
So you still listen to scroll but you now keep a state flag out of the scroll handler and simply evaluate if it has to switch between display types.
That is so much more optimized and IE likes it.
I can get flickers in Chrome as well if I scroll very quickly. Instead of updating the top position on scroll, instead used the fixed position for your element once the page has scrolled below the threshold. Take a look at the updated fiddle: http://jsfiddle.net/nLK7j/2/
Is there a way in javascript to bind an event handler to a horizontal scroll as opposed to the generic scroll event which is fired when the user scrolls horizontally and vertically? I want to trigger an event only when the user scrolls horizontally.
I searched around for an answer to this question, but couldn't seem to find anything.
Thanks!
P.S. My apologies if I'm using some terminology incorrectly. I'm fairly new to javascript.
UPDATE
Thanks so much for all your answers! In summary, it looks like you are all saying that this isn't supported in javascript, but I that I can accomplish the functionality with something like this (using jQuery) (jsFiddle):
var oldScrollTop = $(window).scrollTop();
$(window).bind('scroll', function () {
if (oldScrollTop == $(window).scrollTop())
//scrolled horizontally
else {
//scrolled vertically
oldScrollTop = $(window).scrollTop();
}
});
That's all I needed to know. Thanks again!
Answering from my phone, so unable to provide code at the moment.
What you'll need to do is subscribe to the scroll event. There isn't a specific one for vertical/horizontal.
Next, you'll need to get some measurements about the current display area. You'll need to measure the window.clientHeight and window.clientWidth.
Next, get window.top and window.left. This will tell you where position of the viewport is, ie if it's greater than 0 then scroll bars have been used.
It's pretty simple math from here to get what you need. If no one else has provided a code example in the next few hours I'll try to do so.
Edit:
A bit further information.
You must capture the scroll event. You also need to store the initial window.top and window.left properties somewhere. Whenever the scroll event happens, do a simple check to see if the current top/left values differ from the stores value.
At this point, if either are different you can trigger your own custom events to indicate vertical or horizontal scrolling. If you are using jQuery, this is very easy. If you are writing js without library assistance, it's easy too but a little more involved.
Do some searches for event dispatching in js.
You can then write any other code you want to subscribe to your custom events without needing to tie them together with method calls.
I wrote a jQuery plugin for you that lets you attach functions to the scrollh event.
See it in action at jsfiddle.net.
/* Enable "scrollh" event jQuery plugin */
(function ($) {
$.fn.enableHScroll = function() {
function handler(el) {
var lastPos = el
.on('scroll', function() {
var newPos = $(this).scrollLeft();
if (newPos !== lastPos) {
$(this).trigger('scrollh', newPos - lastPos);
lastPos = newPos;
}
})
.scrollLeft();
}
return this.each(function() {
var el = $(this);
if (!el.data('hScrollEnabled')) {
el.data('hScrollEnabled', true);
handler(el);
}
});
}
}(jQuery));
It's this easy to use:
$('#container')
.enableHScroll()
.on('scrollh', function(obj, offset) {
$('#info').val(offset);
});
Please note that scroll events come very fast. Even if you click in the scrollbar to jump to a new position, many scroll events are generated. You may want to adjust this code to wait a short time and accumulate all the changes in position during that time before firing the hscroll event.
You can use the same scroll event, but within your handler use the scrollLeft function to see if the scrollbar moved horizontally from the last time the event was fired. If the scrollbar did not move then just return from your handler. Otherwise update your variable to the new position and take action.
You can check if the the x value of the page changes and ignore your y value.
If the x value changes: There is your horizontal scroll.
With page-load, store the initial scrollbar positions for both in two variables (presumably both will be 0). Next, whenever a scroll event occurs, find the scrollleft and scrolltop properties. If the scrollleft property's value is different and scrolltop's value is same as compared to their earlier values, that's a horizontal scroll. Then set the values of the variables to the new scroll values.
No, there is no special event for scroll horizontal (it is for global scroll), but you can try to check the position of content by property .scrollLeft and if it's different from the previous value it means that the user scrolled content horizontally.