Is it possible to get list of visible elements in scrollable container? When using a scroll, the number of elements visible on the screen obviously changes - for this reason, add particular class to the last two visible elements is extremely difficult.
Do you have any ideas?
<div >
<div *ngFor="let item of items">
{{ item?.Name }}
</div>
</div>
CodePudding user response:
Listening for scroll
events on the document and then determining the position of the bounding rectangle relative to the viewport will let you determine if and where an element is visible:
Simply replace div#question
with the locator of the element you are interested in.
document.addEventListener('scroll', function(e) {
var rect = document.querySelector('div#question').getBoundingClientRect();
if (rect.top > window.innerHeight) {
console.log("Element below viewport");
} else if (rect.top > 0 && rect.top < window.innerHeight && rect.bottom > window.innerHeight) {
console.log("Element partly visible at bottom");
} else if (rect.top > 0 && rect.top < window.innerHeight && rect.bottom < window.innerHeight) {
console.log("Element fully visible");
} else if (rect.top < 0 && rect.bottom > 0) {
console.log("Element partly visible at top");
} else if (rect.bottom < 0) {
console.log("Element above viewport");
}
});
CodePudding user response:
Here's the most performant way I know of to do it in native JavaScript. The concept is the same for Angular, but you'll need to make some changes and will want to make others. I'll cover that after the example.
You may want to view the example in full screen.
(Note that more than two items could be highlighted if part of a third is partially visible. Once you read about the threshold
option below, you can play around with it to tweak this behavior).
const list = document.querySelector('.scrollable-container');
const options = {
root: list,
rootMargin: '-150px 0px 0px 0px',
threshold: 0
};
function onIntersectionChange(entries) {
entries.forEach(entry => {
if (entry.isIntersecting)
entry.target.classList.add('highlighted');
else
entry.target.classList.remove('highlighted');
});
}
const observer = new IntersectionObserver(onIntersectionChange, options);
{
const listItems = list.children;
for (let i = 0; i < listItems.length; i ) {
observer.observe(listItems[i]);
}
}
.scrollable-container {
height: 200px;
border: 1px solid black;
overflow: auto;
}
.item {
padding: 10px 0;
}
.highlighted {
background-color: blue;
color: white;
}
<div >
<div >Item 1</div>
<div >Item 2</div>
<div >Item 3</div>
<div >Item 4</div>
<div >Item 5</div>
<div >Item 6</div>
<div >Item 7</div>
<div >Item 8</div>
<div >Item 9</div>
<div >Item 10</div>
<div >Item 11</div>
<div >Item 12</div>
<div >Item 13</div>
<div >Item 14</div>
<div >Item 15</div>
<div >Item 16</div>
<div >Item 17</div>
<div >Item 18</div>
<div >Item 19</div>
<div >Item 20</div>
</div>
I'm using the Intersection Observer API to listen for intersections between the items in the list and the bottom portion of the list container element. Every time one of the observed items changes from "intersecting" to "not intersecting", the API calls my handler method.
To set this up, I've provided an object called options
to the observer with the following options set:
root
: the list container.rootMargin
: an offset from the actual bounding box of theroot
. Think of this like shrinking the actual size of the targetroot
down by some margin. Here I've decreased the size by 100px from the top. If an element goes above this imaginary line in the list container, it will no longer be considered "intersecting".threshold
: how much of an element (on a scale from 0 to 1.0) must be inside theroot
to be considered "intersecting". While 0 is the default, I've included it for clarity. Here 0 means that if any pixels at all enterroot
, the element is considered "intersecting" until the last pixels leave. If it were set to 1.0, then all the pixels in the element would have to be inroot
before it was considered "intersecting" and it would be considered "not intersecting" as soon as one pixel leftroot
.
Next I've created the callback that will be executed every time any of the observed elements changes from "intersecting" to "not intersecting" and vice-versa. entries
is the collection of elements whose status changed. If the element is now intersecting root
, we add the appropriate CSS class. If it is no longer intersecting root
, we remove the class.
Finally, I create the observer and register each item in the list as an entry that should be observed. The registration is inside a function block to keep listItems
available only while we need it. It will fall out of scope once the elements are registered.
About Angular, there is a change you must make:
- Because you're using
ngFor
, you cannot register items until the template is rendered. That means your component will need to implementAfterViewInit
and register items with the observer insidengAfterViewInit()
.
There are also changes you may want to make:
- Angular has access to directives like
@ViewChild
that make selecting items from the template safer than using nativequerySelector
/getElementById
approaches, particularly as your template is rerendered. - Angular uses TypeScript, so you'll want to type your variables for ease of testing, error checking, etc. Because the Intersection Observer API is native JavaScript, there are existing TypeScript types for it; you don't need to write your own.
- Angular is set up to work well with proper rxjs observables and you will likely improve performance even more by working with them to e.g. debounce when the handler function is called, observe when specific items change intersection status, etc. There are a few good blog posts about using the Intersection Observer API with Angular that cover these kinds of topics.
Here's a StackBlitz with a least-effort port into Angular (meaning only the needed change was made).