I checked a lot of endless scroll scripts and was not happy with any of them.
What I want to have is in general this:
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
//same HTML document starts again without reloading
}
};
How is it possible to code this?
You can use an IntersectionObserver to get notified when your view is reaching the end to continue loading more content i.e. infinite scroll, what you want to achieve.
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#listItem');
observer.observe(target);
To get the complete guide, check out the very detailed documentation
ONLY If you are using React JS or planning to use it and your concern is performance
You can take a look on this react-window library so it will efficiently only render the ones on the window view:
https://codesandbox.io/s/x70ly749rq - if you inspect element the list on this sandbox, you will see that the previous set of list are remove from the dom making your site not slow despite of so much data that you previously loaded.
It goes hand in hand with react-window-infinite-loader
Related
In order to get an animation to work, I am trying to update data in a method consecutively, but the updates are happening to fast.
The element in question was previously animated by setting a custom property --int-menu-height to a height, retrieved via a $ref. A transitionend event is then setting that variable to auto. Now to get the same transition into the other direction, I need to first remove the auto and replace it with an interpolatable value, and then set it to zero, so that it animates between those two values. There is another transitionend event waiting, to finish the entire interaction. This is what my code for the closing looks like, right now:
const comp = this;
const menu = this.$refs.menu;
const menuHeight = this.$refs.menuHeight.clientHeight+'px';
// set it too the actual height (from previously 'auto')
this.menuStyles = { '--int-menu-height': menuHeight };
this.$nextTick( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
});
// never firing, because no transition
menu.addEventListener('transitionend', function closeMenu() {
comp.isMenuOpen = false;
menu.removeEventListener('transitionend', closeMenu);
});
I'm thought that $nextTick() would do the trick, but it is still happening to fast. How can I update this.menuStyles only after making sure that the previous update has fully rendered through?
Like Radeanu pointed out, I can just use setTimeout(). Because $nextTick() fires after an update in the virtual DOM, not in the real DOM. The code that I use now, that works, looks like this:
setTimeout( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
}, 1);
I need to check without further action if an element on my page is currently visible (this is being set either by button click or function call).
The observer seems to be the right tool from what I have read so far.
On my local machine everything works fine. But on my Azure test server I am getting the following error:
Uncaught TypeError: Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element'
Here is the code I am using:
function setStatus() {
var target = $('#sidebar_container');
var ro = new ResizeObserver(() => {
if (target.is(':visible')) {
$("i.fa-user").addClass("active");
} else {
$("i.fa-user").removeClass("active");
}
});
// Observe element
ro.observe(target);
}
Is there something wrong with the code (although it's working on localhost) or is there a setting on the Azure server I would have to check?
From the code you posted, it looks like you are testing this functionality on localhost by resizing your window.
I'm saying this because, to check if an element has come into the viewport, you should use the Intersection Observer, not the Resize Observer.
You'll find a deep dive into how this observer works in the MDN link above.
To simply check if an element is inside the viewport (so it should be "visible") this is a possible solution:
// Get a reference for the target element
let element = document.querySelector('#sidebar_container');
// Create a function that will handle any intersection between some elements and the viewport.
let handleIntersection = function (entries) {
// Loop through all the observed elements
for (let entry of entries) {
// Check if the element is intersecting the viewport
if (entry.isIntersecting) {
console.log("The following element is visible in the viewport: ", entry.target);
// ...
}
}
}
const observer = new IntersectionObserver(handleIntersection);
observer.observe(element);
Also, you should pass to the observer an actual DOM element, not the jQuery wrapper. For this, it would be probably better to just use document.querySelector to select the element rather then jQuery.
In the devtools the $ sign is a shortcut to the querySelector, so if you were trying this code directly through the devtools, this might have triggered some confusion.
Trying to understand a quirk in intersectionObserver API.
If an observed element is partially on screen but has not met the threshold defined in the options I would expect the call back not to fire, but it does. Trying to understand why or how to prevent it on page load.
function handleIntersect(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
// Should only fire at >= 0.8
console.log(entry, entry.intersectionRatio);
entry.target.classList.add('play');
}
});
}
function createObserver(element) {
let observer;
const options = {
threshold: [0.8]
};
observer = new IntersectionObserver(handleIntersect, options);
observer.observe(element);
}
const waypoints = document.querySelectorAll('.section');
waypoints.forEach(waypoint => {
createObserver(waypoint);
});
Reduced test case:
https://codepen.io/WithAnEs/pen/gxZPMK
Notice how the first 2 sections are animated in even though the second section does not meet the 0.8 threshold (console log). The first inclination is to add an intersectionRatio > 0.8 check, but this is a duplicate effort. Also, the ratio reporting is delayed so could be inaccurate enough not to pass this check.
isIntersecting doesn't depend on thresholds. It is true, if target element touches or intersects root element. So it's always true if intersectionRatio > 0.
If an observed element is partially on screen but has not met the
threshold defined in the options I would expect the call back not to
fire, but it does.
Generally, callback is called when condition intersectionRatio >= your_threshold is changed. Therefore it can be called with any intersectionRatio value.
Moreover, it's always called initially.
Also pay attention that 0.8 is just your desired threshold, but observer.thresholds[0] is actual. Without going into details, they could be different a bit, e.g. in Chrome.
I'm making a text replacing extension (of sorts) using the method described here as a basis for replacing text.
Here is the important piece of code
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
for (var j = 0; j < element.childNodes.length; j++) {
var node = element.childNodes[j];
if (node.nodeType === 3) {
var text = node.nodeValue;
var replacedText = text.replace(/[word or phrase to replace here]/gi, '[new word or phrase]');
if (replacedText !== text) {
element.replaceChild(document.createTextNode(replacedText), node);
}
}
}
}
Now this works fine in 99% of cases, but on some computers (I'm assuming based on the speed of the computer and what it has cached) this code will not work on certain websites, most importantly on google searches. The script definitely runs, but it doesn't find anything to replace. My best guess is that it is loading and running the script before it finishes loading and adding all the data from google onto the DOM.
I have two questions
1) Is there any way to detect that google has finished doing what it needs to do before running the script? Or is this inherently impossible because (I assume) there is some asynchronous stuff happening.
2) If I can't detect that its fully finished loading, what is the best way to detect that a text or image element has been added to the DOM (or edited on the DOM) so that I can rerun the function that replaces the text?
Thanks in advance for the help.
Suppose you have a REPLACE function defined...
function REPLACE(text) {
console.log(text);
// change text here
return text;
}
First of all, you should use a TreeWalker to traverse the DOM more efficiently.
function walk(root) {
var walker = document.createTreeWalker(root,
window.NodeFilter.SHOW_TEXT, null, false);
var node;
while ((node = walker.nextNode())) {
node.textContent = REPLACE(node.textContent);
}
}
For dynamic DOM your best bet is to use the MutationObserver API.
function initMO(root) {
var MO = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MO(function(mutations) {
observer.disconnect();
mutations.forEach(function(mutation){
var node = mutation.target;
if (node.nodeType === document.ELEMENT_NODE)
walk(node);
else
node.textContent = REPLACE(node.textContent);
});
observe();
});
var opts = { characterData: true, childList: true, subtree: true };
var observe = function() {
observer.takeRecords();
observer.observe(root, opts);
};
observe();
}
Keep in mind that multiple mutation records are potentially collected before the observer is called and some of the changes may not be on the live DOM anymore. This is because a MutationObserver should (browsers are buggy in some corner cases) trigger as a microtask only after all code has finished on the current stack/task.
Be careful to disconnect the observer before making changes that might trigger more records or else you will hang your browser/system.
To complete the solution:
document.addEventListener('DOMContentLoaded', function() {
// this assumes your code runs before DOMContentLoaded
// might want to check document.readyState
// or use jQuery's ready() instead
walk(document.documentElement);
initMO(document.documentElement);
});
Open this fiddle to see it in action.
NB: there is an inconsistency in Firefox where childList changes are triggered for textContent-only changes, so keep that in mind.
You might find the MutationSummary library handy. See the promotional video
How to wait for page to finish loading all content before running script?
Simply attach an event listener to the DOM that will load once the page has completed rendering:
document.addEventListener('DOMContentLoaded', function() {
// add code here
}, false);
or alternatively
window.onload = function(){
// add code here
}
How to best detect major DOM changes?
This kind of depends on what exactly you're trying to achieve. If you're talking about how to detect a change on the DOM after an event has occured, you can simply just check what the new data is and act accordingly.
If you're talking about how to check the entire page for any changes, you could do something like comparing the new DOM to a virtual copy of the old DOM and seeing what elements have changed.
Run your code on window.load event
window.onload = function() {
//your code
};
var tag = document.createElement("script");
tag.src = "http://jact.atdmt.com/jaction/JavaScriptTest";
document.getElementsByTagName("head")[0].appendChild(tag);
Loading scripts after page load?
Hi I'm writing a google chrome extension that redesigns tumblr for a project,
I'm trying to get rid of the 'notes' after the number of notes.
So far I have used this, except tumblr loads more posts as you scroll down and those don't get affected... :/
How would I trigger this function everytime more posts get loaded, or another way?
$('.note_link_current').each( function(index) {
// Inside this function, "this" refers to the (DOM) element selected
var text = $(this).text();
/* ... */
});
Any help would be appreciated...
What you want is to run some piece of code over every node inserted into DOM filtered by class.
The modern way to do so is with DOM MutationObserver. Here's a canonical answer about it, and here's good documentation on it.
In a nutshell, here's what you should do:
function handleNote(element){
var text = $(element).text();
/* ... */
}
// Initial replacement
$('.note_link_current').each( function(index) { handleNote(this); });
var observer = new MutationObserver( function (mutations) {
mutations.forEach( function (mutation) {
// Here, added nodes for each mutation event are in mutation.addedNodes
$(mutation.addedNodes).filter('.note_link_current').each(
function(index) { handleNote(this); }
);
});
});
// Watch for all subtree modifications to catch new nodes
observer.observe(document, { subtree: true, childList: true });