Intersection Observer and removing the observed element - javascript

I'm using intersection observer in a marquee, where I am observing for when the first child leaves the viewport, removing that first child, shifting the new (identical) first child by its element's width to the right (with marginLeft), and then appending another identical child element to the end of the parent div's children.
This works once, but intersection observer does not fire a second time, even though the new first child is identical to the deleted one.
I was wondering whether mutation observer would be more useful here, but not sure how to go about it!
let firstChild = document.querySelector('.marquee').firstElementChild;
let target = firstChild;
let options = {
root: null,
threshold: 0,
};
function onChange(changes) {
changes.forEach(change => {
if (change.intersectionRatio === 0) {
firstChild.remove();
console.log('Header is outside viewport');
for ( i = 0; i < 100000; i++ ) {
if ( rectParent.childElementCount < 3 ) {
let child = document.createElement('p');
child.className = "global-chat";
child.innerHTML = "Something written here";
let docFrag2 = document.createDocumentFragment().appendChild(child.cloneNode(true));
rectParent.appendChild(docFrag2);
document.querySelector('.marquee').firstElementChild.style.marginLeft = `${boxWidth}` * `${i+1}` + "px";
}
}
}
});
}
let observer = new IntersectionObserver(onChange, options);
observer.observe(target);```

The Problem
you define the IntersectionObserver class and instantiate it in the observer. at this moment, the firstChild variable gets the proper element, but in the next change, your firstChild didn't get an update!
The solution
You can update your firstChild element in the onChange function to ensure get the proper element before changes.
for example:
function onChange(changes) {
changes.forEach(change => {
if (change.intersectionRatio === 0) {
const firstChild = document.querySelector('.marquee').firstElementChild;
firstChild.remove();
// rest of the codes ...
}
});
}

Related

I need help using recursion to navigate every element in the DOM

I need to use recursion to navigate every element in the DOM, and for every body element, determine if it is an element node. If it is, I need to add a child node to it. I need to use JavaScript for this assignment. Here is what I have so far in my JavaScript file:
window.addEventListener("load", function() {
var highlightButton = document.getElementById("highlight");
highlightButton.addEventListener('click', search);
function search(node) {
if (node.nodeType === 1) {
var spanEl = document.createElement("span");
spanEl.className = "hoverNode";
node.appendChild(spanEl);
spanEl.innerHTML = spanEl.parentNode.tagName;
}
}
})
I understand how to append a child node, but I don't know how to traverse the entire DOM and append the child node to every element.
Given "every body element" actually means "every element in the body", you can start with an element and get all its child elements. Loop over the child elements and if any is type 1 and has a child nodes, you call the function again with the element.
If it doesn't have children, go to the next child, etc. The following is just an example of recursing over all the nodes and picking out just the type 1s. You can modify it to do whatever.
// Call with a document or HTML element
function checkBodyElements(node) {
// Recursive function
function traverseBody(node) {
if (node.childNodes.length) {
// Loop over every child node
node.childNodes.forEach(child => {
// If it's a type 1, call the function recursively
if (child.nodeType == 1) {
console.log(child.tagName, child.nodeType)
traverseBody(child);
}
});
}
}
// Get the body element
let body = node.querySelector('body');
// If a body element was found, traverse its children
if (body) {
traverseBody(body);
}
}
window.onload = checkBodyElements(document);
<div>
<div>
<p><span></span>
</p>
</div>
<div>
<p><span></span>
</p>
</div>
</div>
Is there any specific reason that implies the creation of a recursive function, other than to support older browsers, like IE6 and 7?
If no, you could simply use document.body.querySelectorAll('*') to select every element node in the DOM while ignoring the ones outside the body element. An example would be:
window.addEventListener('load', function () {
var highlightButton = document.getElementById("highlight");
function search () {
document.body.querySelectorAll('*').forEach(function (el) {
var spanEl = document.createElement('span');
spanEl.innerHTML = el.tagName;
spanEl.className = 'hoverNode';
el.appendChild(spanEl);
});
}
highlightButton.addEventListener('click', search);
});
If yes, then an option would be:
window.addEventListener('load', function () {
var highlightButton = document.getElementById("highlight");
// traverse downwards from rootEl while excluding Comment elements on IE6 7 & 8
function traverseDOMFrom (rootEl, iterator) {
if (!rootEl || rootEl.nodeType !== 1 || typeof iterator !== 'function') {
return;
}
if (rootEl.children && rootEl.children.length > 0) {
Array.prototype.slice.call(rootEl.children).forEach(function (el) {
traverseDOMFrom(el, iterator);
});
}
iterator(rootEl);
}
function search () {
traverseDOMFrom(document.body, function (el) {
var spanEl = document.createElement('span');
spanEl.innerHTML = el.tagName;
spanEl.className = 'hoverNode';
el.appendChild(spanEl);
});
}
highlightButton.addEventListener('click', search);
});
Notice that in both cases, a polyfill for Array.prototype.forEach() and for EventTarget.addEventListener() would be necessary if you want to support those features on IE6, 7 and 8! Otherwise you could also achieve the same results by iterating the element's array with a custom for loop instead, as for this .addEventListener, a simple .onload event handler could be used if there's no need to support multiple listeners.

when element exists without using a setInterval

I have 2 dynamic sub-modals that appear/expand within a parent modal. (parent modals or pre-expanded modal doesn't need the below update).
Within two sub modals, that become expanded from parent modal, I am simply trying to add/invoke a 'back button' and 'close button' in the .both_submodals__heading of those two modals; I am achieving this with the below solution, but I hate the fact I am using a set interval, this also creates console errors when the setInterval is checking for the relevant .both_submodals__heading until it's found.
on(footWrap, 'click', function (event) {
let visible = coordinateWidget.visible;
coordinateWidget.visible = !visible;
var checkExists = setInterval(function() {
const coordWid = document.getElementsByClassName('both_submodals__heading')[0];
if (typeof(coordWid) != 'undefined' && coordWid != null) {
const closeBlock = document.getElementById('closeCoord');
const headerTitleTxt = document.getElementsByClassName('esri-widget__heading')[0];
headerTitleTxt.insertAdjacentHTML('afterend', '<div id="closeCoord">X</div>');
on(closeBlock, 'click', function (event) {
let visible = coordinateWidget.visible;
coordinateWidget.visible = !visible;
});
}
}, 100);
});
Via the comment about mutation observers; I found the below solution to be an alternative... but looks like a ton of code.
(function(win) {
'use strict';
var listeners = [],
doc = win.document,
MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
observer;
function ready(selector, fn) {
// Store the selector and callback to be monitored
listeners.push({
selector: selector,
fn: fn
});
if (!observer) {
// Watch for changes in the document
observer = new MutationObserver(check);
observer.observe(doc.documentElement, {
childList: true,
subtree: true
});
}
// Check if the element is currently in the DOM
check();
}
function check() {
// Check the DOM for elements matching a stored selector
for (var i = 0, len = listeners.length, listener, elements; i < len; i++) {
listener = listeners[i];
// Query for elements matching the specified selector
elements = doc.querySelectorAll(listener.selector);
for (var j = 0, jLen = elements.length, element; j < jLen; j++) {
element = elements[j];
// Make sure the callback isn't invoked with the
// same element more than once
if (!element.ready) {
element.ready = true;
// Invoke the callback with the element
listener.fn.call(element, element);
}
}
}
}
// Expose `ready`
win.ready = ready;
})(this);

Same intersection observer for multiple HTML elements

I am trying to make it so that as some text items stop overlapping a dark background, they will individually change color one by one as the user scrolls. All of the text items are position: fixed
EDIT: The MDN docs say (emphasis mine):
The Intersection Observer API provides a way to asynchronously observe
changes in the intersection of a target element with an ancestor
element
I think this means there is no way to solve my problem because the elements I want to monitor for overlap are not children of the root I am specifying in the options object.
Is there any way to detect overlap if the overlapping element is not a child of the other element?
if ("IntersectionObserver" in window) {
const options = {
root: document.getElementById("flow-landing"),
rootMargin: "0px",
threshold: 0,
};
var callback = function (entries, observer) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.color = "white";
} else {
entry.target.style.color = null;
}
});
};
const observer = new IntersectionObserver(callback, options);
var targets = [
Array.from(document.querySelectorAll(".social-item")),
Array.from(document.querySelectorAll(".additional-item")),
].flat();
targets.forEach((target) => observer.observe(target));
}
There aren't any console errors but the code isn't doing anything.
Modifying Ruslan's answer a little because in his answer, multiple Intersection Observer objects are being created.
It is possible to observe multiple elements using the same observer by calling .observe() on multiple elements.
let observerOptions = {
rootMargin: '0px',
threshold: 0.5
}
var observer = new IntersectionObserver(observerCallback, observerOptions);
function observerCallback(entries, observer) {
entries.forEach(entry => {
if(entry.isIntersecting) {
//do something
}
});
};
let target = '.targetSelector';
document.querySelectorAll(target).forEach((i) => {
if (i) {
observer.observe(i);
}
});
you can do something like that, at least it helps me:
document.querySelectorAll('.social-item').forEach((i) => {
if (i) {
const observer = new IntersectionObserver((entries) => {
observerCallback(entries, observer, i)
},
{threshold: 1});
observer.observe(i);
}
})
const observerCallback = (entries, observer, header) => {
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
entry.target.style.color = "white";
}
else {
entry.target.style.color = null;
}
});
};
You could use the offsetTop and offsetHeight properties instead of the IntersectionObserver API.
For example, when the user scrolls, you can check to see if the offsetTop of element 2 is greater than the offsetTop and less than the offsetHeight of element 1.
WARNING: use debounce because the scroll event's handler function will be called once for every pixel the user scrolls, just imagine the performance nightmare that would occur if user scrolled 600-1000 pixels.
The LoDash documentation, describes it's debounce function as:
"[a function that] creates a debounced function that delays invoking func (handler) until after wait (time) milliseconds have elapsed since the last time the debounced function was invoked."
If you aren't using LoDash, here is a Vanilla JavaScript debounce function:
function debounce(handler, time) {
var timeout;
return function() {
var self = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
return handler.apply(self, args);
}, time);
};
}
Here is the code that'll allow you to "do stuff" if element 2 "intersects" element 1.
let element_1 = document.querySelector("#my-element-1");
let element_2 = document.querySelector("#my-element-2");
window.addEventListener("scroll", debounce(() => {
if(element_2.offsetTop > element_1.offsetTop && element_2.offsetTop < element_1.offsetHeight) {
console.log("The elements are intersecting.");
}
}, 100));
In case that looks complex or hard to read, here is the same code, broken up into smaller chunks:
let element_1 = document.querySelector("#my-element-1");
let element_2 = document.querySelector("#my-element-2");
window.addEventListener("scroll", debounce(() => {
let x = element_2.offsetTop > element_1.offsetTop;
let y = element_2.offsetTop < element_1.offsetHeight;
if(x && y) {
console.log("The elements are intersecting.");
}
}, 250));
Notes & information:
you could use the >= and <= operators instead of the > and < operators
a wait time that's too long could make the effect look unnatural and forced.
Good luck.

Remove all content between two comment tags using JavaScript

How can I remove all content between two tags that they aren't in standard tags;
<!--googleoff: index-->
some codes and content here...
<!--googleon: index-->
This is an ads that showing in one site and I'd like to block and remove theme in browser by User JS
Those are comment nodes, not tags. The best thing would probably be to identify the parent, then loop through the children; see comments:
// Assuming a single parent
let parent = document.querySelector(".stuff");
if (parent) {
// Uncomment if you want to see nodes before the change
// showNodes("before", parent);
let removing = false;
let child = parent.firstChild;
let next = null;
// While we still have child elements to process...
while (child) {
// If we're already removing, remember that
let removeThis = removing;
// Before we remove anything, identify the next child to visit
next = child.nextSibling;
// Is this a comment node?
if (child.nodeType === Node.COMMENT_NODE) {
if (child.nodeValue.includes("googleoff: index")) {
// It's the node that tells us to start removing:
// Turn on our flag and also remove this node
removing = true;
removeThis = true;
} else if (child.nodeValue.includes("googleon: index")) {
// It's the node that tells us to stop removing:
// Turn off our flag, but do remove this node
removing = false;
removeThis = true;
}
}
if (removeThis) {
// This is either stuff in-between the two comment nodes
// or one of the comment nodes; either way, remove it
parent.removeChild(child);
}
// Move on to next child
child = next;
}
// Uncomment if you want to see nodes before the change
// showNodes("after", parent);
}
Live Example:
// Brief delay so you can see it happen
setTimeout(() => {
// Assuming a single parent
let parent = document.querySelector(".stuff");
if (parent) {
// Uncomment if you want to see nodes before the change
// showNodes("before", parent);
let removing = false;
let child = parent.firstChild;
let next = null;
// While we still have child elements to process...
while (child) {
// If we're already removing, remember that
let removeThis = removing;
// Before we remove anything, identify the next child to visit
next = child.nextSibling;
// Is this a comment node?
if (child.nodeType === Node.COMMENT_NODE) {
if (child.nodeValue.includes("googleoff: index")) {
// It's the node that tells us to start removing:
// Turn on our flag and also remove this node
removing = true;
removeThis = true;
} else if (child.nodeValue.includes("googleon: index")) {
// It's the node that tells us to stop removing:
// Turn off our flag, but do remove this node
removing = false;
removeThis = true;
}
}
if (removeThis) {
// This is either stuff in-between the two comment nodes
// or one of the comment nodes; either way, remove it
parent.removeChild(child);
}
// Move on to next child
child = next;
}
// Uncomment if you want to see nodes before the change
// showNodes("after", parent);
}
}, 800);
function showNodes(label, parent) {
console.log(label);
for (let child = parent.firstChild; child; child = child.nextSibling) {
console.log(child.nodeType, child.nodeValue);
}
}
<div>
Just some content that isn't related
</div>
<div class="stuff">
This is the parent element
<!--googleoff: index-->
some codes and content here...
<!--googleon: index-->
</div>
If this stuff appears in more than one place, obviously wrap that in a loop.
If you can't identify the parent, you'll have to walk the DOM all the way through, which is a bit more work (recursive function), but not that bad.

jQuery convert DOM element to different type

I need to convert a DOM element to a different type (as in HTML tag name, a to p in this case), but still retain all the original elements attributes. Whether they are valid for the new type or not doesn't matter in this case.
Any suggestions on how to do this?
I've looked at just creating a new element and copying the attributes across, but this isn't without it's own complications. In Firefox, DOMElement.attributes helpfully only includes attributes with a value, but in IE it reports all possible attributes for that element. The attributes property itself is read-only, so no way to copy that.
Sans-jQuery solution:
function makeNewElementFromElement( tag, elem ) {
var newElem = document.createElement(tag),
i, prop,
attr = elem.attributes,
attrLen = attr.length;
// Copy children
elem = elem.cloneNode(true);
while (elem.firstChild) {
newElem.appendChild(elem.firstChild);
}
// Copy DOM properties
for (i in elem) {
try {
prop = elem[i];
if (prop && i !== 'outerHTML' && (typeof prop === 'string' || typeof prop === 'number')) {
newElem[i] = elem[i];
}
} catch(e) { /* some props throw getter errors */ }
}
// Copy attributes
for (i = 0; i < attrLen; i++) {
newElem.setAttribute(attr[i].nodeName, attr[i].nodeValue);
}
// Copy inline CSS
newElem.style.cssText = elem.style.cssText;
return newElem;
}
E.g.
makeNewElementFromElement('a', someDivElement); // Create anchor from div
while not a complete solution, the logic would basically be:
Save your existing element:
var oldElement = $(your selector here);
create a new element and insert it just before or after your oldElement
copy the attributes
oldElement.attr().each(function(){
copy old
});
better yet, here is an example of a plug-in that does just what you want:
http://plugins.jquery.com/project/getAttributes
A more modern (2020+) approach is:
function changeTag (element, tag) {
// prepare the elements
const newElem = document.createElement(tag)
const clone = element.cloneNode(true)
// move the children from the clone to the new element
while (clone.firstChild) {
newElem.appendChild(clone.firstChild)
}
// copy the attributes
for (const attr of clone.attributes) {
newElem.setAttribute(attr.name, attr.value)
}
return newElem
}
Compared to #James answer, you don't need to copy the cssText because it's already taken care by the browser. You also don't need the string/number dom properties since those are also properly migrated. It's also best to work on the cloned node only, not both of them (clone and elem)
The following replaceTag(element, tagName) function replaces the provided element with a new element of type tagName, e.g. replaceTag(someElement, 'p').
// BEGIN actual function
function replaceTag(element, tagName) {
const newElement = document.createElement(tagName);
newElement.append(...element.childNodes);
for (const attribute of element.attributes) {
newElement.setAttribute(attribute.name, attribute.value);
}
element.replaceWith(newElement);
return newElement;
}
// END actual function
// BEGIN from here onwards, example usage code
function divSpan2PEm() {
const divElements = document.querySelectorAll('div');
for (divElement of divElements) {
const containedSpanElements = divElement.querySelectorAll(':scope > span');
for (containedSpanElement of containedSpanElements) {
replaceTag(containedSpanElement, 'em');
}
replaceTag(divElement, 'p');
}
}
div {
background-color: red;
}
[role="complementary"] {
border: 2px solid blue;
}
span {
background-color: orange;
}
[data-bing] {
color: brown;
}
<div role="complementary">
<span>foo <strong>so strong</strong></span>
<span data-bing="true">bar</span>
</div>
<button type="button" onclick="divSpan2PEm()">Convert div-span to p-em</button>
replaceTag moves any existing children to the new element, as well as recreates any attributes. It does not and cannot replace any references to the original element, e.g. attached event handlers. Also, in general the interim and resulting HTML / updated DOM may not be valid.

Categories

Resources