Force IntersectionObserver update - javascript

Is there any way to force an update/run of an IntersectionObserver instance? The callback will be executed by default, when the viewport has changed. But I'm looking for a way to to execute it when other events happen, like a change of elements.
An Example:
On initialization everything works as expected. But when you change the position of the #red element, nothing happens.
// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');
// observer callback
let callback = entries => {
entries.forEach(entry => {
let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
console.log("#" + entry.target.id + " is " + isInside + " inside #container");
});
};
// start observer
let options = {root: document.querySelector('#container')};
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);
// button action
document.querySelector('button').addEventListener('click', () => {
red.style.right = red.style.right == "" ? "0px" : "";
});
#container {
width: 100px;
height: 100px;
background: blue;
position: relative;
}
#green, #red {
width: 50px;
height: 50px;
background: green;
position: absolute;
}
#red {
background: red;
right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
<div id="green"></div>
<div id="red"></div>
</div>
Is there any way to make this working? Only thing that would work is to unobserve the element and start observing it again. This may be work for an single element, but not if the Observer has hundreds of elements to watch.
// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');
// observer callback
let callback = entries => {
entries.forEach(entry => {
let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
console.log("#" + entry.target.id + " is " + isInside + " inside #container");
});
};
// start observer
let options = {root: document.querySelector('#container')};
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);
// button action
document.querySelector('button').addEventListener('click', () => {
red.style.right = red.style.right == "" ? "0px" : "";
observer.unobserve(red);
observer.observe(red);
});
#container {
width: 100px;
height: 100px;
background: blue;
position: relative;
}
#green, #red {
width: 50px;
height: 50px;
background: green;
position: absolute;
}
#red {
background: red;
right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
<div id="green"></div>
<div id="red"></div>
</div>

I don't think it is possible to force the intersection observer to update without calling unobserve/observe on the node, but you can do this for all observed nodes by saving them in a set:
class IntersectionObserverManager {
constructor(observer) {
this._observer = observer;
this._observedNodes = new Set();
}
observe(node) {
this._observedNodes.add(node);
this._observer.observe(node);
}
unobserve(node) {
this._observedNodes.remove(node);
this._observer.unobserve(node);
}
disconnect() {
this._observedNodes.clear();
this._observer.disconnect();
}
refresh() {
for (let node of this._observedNodes) {
this._observer.unobserve(node);
this._observer.observe(node);
}
}
}
Edit: use a Set instead of a WeakSet since they are iterable so there is no need to check if the element is being observed for each element in the body. Be carefull to call unobseve in order to avoid memory problems.

You just need to set threshold: 1.0 for your Intersection observer. This is a tricky parameter to comprehend. Threshold defines the percentage of the intersection at which the Observer should trigger the callback.
The default value is 0 which means callback will be triggered either when the very first or very last pixel of an element intersects a border of the capturing frame. Your element never completely leaves the capturing frame. This is why callback is never called.
If we set the threshold to 1 we tell the observer to trigger our callback when the element is 100% within the frame. It means the callback will be triggered on change in this state of 100% inclusiveness. I hope that sounds understandable :)
// elements
let green = document.querySelector('#green');
let red = document.querySelector('#red');
// observer callback
let callback = entries => {
entries.forEach(entry => {
let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
console.log("#" + entry.target.id + " is " + isInside + " inside #container");
});
};
// start observer
let options = {root: document.querySelector('#container'), threshold: 1.0 };
let observer = new IntersectionObserver(callback, options);
observer.observe(green);
observer.observe(red);
// button action
document.querySelector('button').addEventListener('click', () => {
red.style.right = red.style.right == "" ? "0px" : "";
});
#container {
width: 100px;
height: 100px;
background: blue;
position: relative;
}
#green, #red {
width: 50px;
height: 50px;
background: green;
position: absolute;
}
#red {
background: red;
right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
<div id="green"></div>
<div id="red"></div>
</div>

I may not get the question right, what I understand is you want to trigger the IntersectionObserver, so your callback get called. Why don't you call it directly?
document.querySelector('button').addEventListener('click', () => {
red.style.right = red.style.right == "" ? "0px" : "";
callback(red);
});

One way to do it is to use MutationObserver. If a mutation happens (in this case style change) call observe/unobserve on the element that has been changed. This way you don't have to do it for all elements.
Here is an example:
// observer callback
let callback = entries => {
entries.forEach(entry => {
let isInside = entry.intersectionRatio >= 1 ? "fully" : "NOT";
console.log("#" + entry.target.id + " is " + isInside + " inside #container");
});
};
// start observer
let options = {
root: document.querySelector('#container')
};
let observer = new IntersectionObserver(callback, options);
const boxes = document.querySelectorAll('#container > div');
boxes.forEach(box => {
observer.observe(box);
});
// button action
document.querySelector('button').addEventListener('click', () => {
red.style.right = red.style.right == "" ? "0px" : "";
});
// Mutation observer
const targetNode = document.querySelector('#container');
// Options for the observer (which mutations to observe). We only need style changes so we set attributes to true. Also, we are observing the children of container so subtree is true
const config = {
attributes: true,
childList: false,
subtree: true,
attributeFilter: ["style"]
};
// Callback function to execute when mutations are observed
const mutationCallback = (mutationsList, mutationObserver) => {
for (let mutation of mutationsList) {
if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
observer.unobserve(mutation.target);
observer.observe(mutation.target);
}
}
};
// Create an observer instance linked to the callback function
const mutationObserver = new MutationObserver(mutationCallback);
// Start observing the target node for configured mutations
mutationObserver.observe(targetNode, config);
#container {
width: 300px;
height: 300px;
background: lightblue;
position: relative;
}
#green,
#red {
width: 100px;
height: 100px;
background: green;
position: absolute;
}
#red {
background: purple;
right: -10px;
}
<button>move #red</button>
<br /><br />
<div id="container">
<div id="green"></div>
<div id="red"></div>
</div>

Related

Javascript while with time delay

My goal I want to run loop that decrements a global variable stepwise in n ms (for Example: 200ms) time intervals.
Thanks in advance!
What i already tried
I tried to use ascy await. But in combination with css transition i run in an infinite loop (In codepen.io). But here in SO you will see that it starts not running smoothly if you keep pressing arrow up.
const procentage = document.querySelector(".procentage");
const green = engine.querySelector(".green");
let number = 0;
let decrementing = false;
window.addEventListener('keydown', (e) => {
e = e || window.event;
e.preventDefault();
if (e.keyCode == '38') {
console.log("accelerate");
actionHandler( number++ );
decrementing = false;
downLoop();
}
});
function actionHandler(num) {
procentage.innerHTML = num;
const str = num + "%"
green.style.width = str;
procentage.innerHTML = str;
}
window.addEventListener('keyup', (e) => {
e = e || window.event;
e.preventDefault();
if (e.keyCode == '38') {
console.log("decelerate");
decrementing = true;
downLoop();
}
});
async function downLoop() {
if (! decrementing) {
return false
};
const timer = ms => new Promise(res => setTimeout(res, ms));
while (number > 1) {
// how to decrement ever 200ms???
actionHandler( number-- );
await timer(200)
}
}
#engine {
background-color:black;
height: 50px;
position: relative;
}
p {
text-align: center;
}
.green {
background:green;
height: 50px;
width:0%;
transition: width 0.2s;
text-align:center;
}
.procentage {
position:absolute;
top: 50%;
left: 50%;
transform: translate(0%,-50%);
color: white;
fon-weight: bold;
font-size:28px;
}
<div id="engine">
<div><span class="procentage">0</span></div>
<div class="green"></div>
</div>
<p>press arrow Up</p>
Whenever you animate, you shouldn't rely on setInterval or setTimeout, because that means that you will update "somewhere after X milliseconds" which will often end up in the middle of a screen repaint, and will therefor cause janky animation.
Instead, you should use RequestAnimationFrame which does a calculation before every repaint. So if you got a monitor with a framerate of 60 Hz, that means that you will do 60 repaints every second. For each repaint, check if enough time have passed since the last update (shouldTriggerUpdate() below) and then check if you should update the number.
I also added the class KeyHandler to keep track of which keys that have been pressed.
I got sloppy at the end and just added a decrement as an "else" of the if statement. You will figure something out when you get there when you want to set up more keys to be pressed.
You shouldn't use KeyboardEvent.keyCode, but instead KeyboardEvent.code.
const procentage = document.querySelector(".procentage");
const green = engine.querySelector(".green");
let number = 0;
let speed = 200 // ms
let lastUpdated = 0; // ms
let animationId = 0; // use later on to pause the animation
class KeyHandler {
ArrowLeft = false
ArrowUp = false
ArrowRight = false
ArrowDown = false
#setKey(code, value) { // private method
if (typeof this[code] != undefined) {
this[code] = value;
}
}
set pressedKey(code) {
this.#setKey(code, true);
}
set releasedKey(code) {
this.#setKey(code, false);
}
}
let keyHandler = new KeyHandler();
window.addEventListener('keydown', (e) => {
e = e || window.event;
e.preventDefault();
keyHandler.pressedKey = e.code;
});
window.addEventListener('keyup', (e) => {
e.preventDefault();
keyHandler.releasedKey = e.code
});
function actionHandler(num) {
const str = num + "%"
green.style.width = str;
procentage.innerHTML = str;
}
function shouldTriggerUpdate(timeInMillis) {
let difference = timeInMillis - lastUpdated;
return difference >= speed;
}
function planeAnimation() {
let timeInMillis = new Date().getTime();
if (shouldTriggerUpdate(timeInMillis)) {
lastUpdated = timeInMillis;
if (keyHandler.ArrowUp) {
actionHandler(++number)
} else if (number > 0) {
actionHandler(--number)
}
}
animationId = requestAnimationFrame(planeAnimation)
}
animationId = requestAnimationFrame(planeAnimation);
#engine {
background-color: black;
height: 50px;
position: relative;
}
p {
text-align: center;
}
.green {
background: green;
height: 50px;
width: 0%;
transition: width 0.2s;
text-align: center;
}
.procentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(0%, -50%);
color: white;
fon-weight: bold;
font-size: 28px;
}
<div id="engine">
<div><span class="procentage">0</span></div>
<div class="green"></div>
</div>
<p>press arrow up</p>
From the above comments ...
"Instead of incrementing each time the number value push a new async timer function, set to 200 msec delay but not immediately triggered, into an array. Create an async generator from it and iterate over the latter via the for-await...of statement where one could decrement number again." – Peter Seliger
"#PeterSeliger Hi Peter! Thank you for your comment. Can you make a small example please?" – Maik Lowrey
And here the requested demonstration.
function createWait(delay) {
return async function wait () {
let settle;
const promise = new Promise((resolve) => { settle = resolve;});
setTimeout(settle, delay, { delay, state: 'ok' });
return promise;
};
}
async function* getWaitIterables(list) {
let wait;
while (wait = list.shift()) {
yield wait();
}
}
// demo for ...
// - creating an async `wait` function
// or a list of such kind.
// - creating an async generator from
// a list of async `wait` functions.
// - iterating an async generator of
// async `wait` functions.
const waitingList = [ // const waitingList = [];
2000, // waitingList.push(createWait(2000));
1000, // waitingList.push(createWait(1000));
3000, // waitingList.push(createWait(3000));
].map(createWait); // - The OP of cause needs to push into.
let number = 3; // - The incremented `number` value e.g. ... 3.
(async () => {
for await (const { delay, state } of getWaitIterables(waitingList)) {
--number;
console.log({ number, delay, state });
}
})();
console.log('... running ...', { number });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Function firing multiple times

could someone help me out with this piece of Javascript?
I am trying to make some sort of "whack-a-mole" game, and this is what I came up with; I set up a way to keep track of the score by adding 1 (score++) every time the user clicks on the picture that pops up. My problem is that the code runs the function more times than needed—for example, if I click on the first image that pops up, the function to add +1 to the score fires once, if I click on the second, the function fires twice, threee times on the third, etc...
What am I doing wrong?
//gid
const grid = document.querySelector('.grid');
//score display value
const scoreValue = document.querySelector('#scoreValue');
//score
let score = 0;
const timer = setInterval(() => {
//output random number
let output = Math.floor(Math.random() * 16);
//select hole
let hole = document.getElementById(output);
hole.innerHTML = '<img src="img/kiseki.png" alt=""></img>';
setTimeout(() => {
hole.innerHTML = '';
}, 2000);
grid.addEventListener('click', e => {
if (e.target.tagName === "IMG") {
score++;
scoreValue.textContent = score;
console.log(score);
hole.innerHTML = '';
}
});
}, 4000);
Since you're ading a new eventListener every time the interval runs, so in order to solve your problem, just add it once, before starting the setInterval that pops your moles.
Example code:
const grid = document.querySelector('.grid');
const scoreValue = document.querySelector('#scoreValue');
const newMoleTimer = 4000;
const moleTimeout = 2000
let score = 0;
let hole;
grid.addEventListener('click', e => {
if (e.target.tagName === "IMG") {
score++;
scoreValue.textContent = score;
if(hole) hole.innerHTML = '';
}
});
const timer = setInterval(() => {
let output = Math.floor(Math.random() * 16);
hole = document.getElementById(output);
hole.innerHTML = '<img src="img/kiseki.png" alt=""></img>';
setTimeout(() => {
hole.innerHTML = '';
}, moleTimeout);
}, newMoleTimer);
*updated code according to #Meika commentary
You need to separate the eventlistener from the settimer function.
In this example I created div elements with a color. Only blue color score and can only score one point pr. timer.
//gid
const grid = document.querySelector('#grid');
//score display value
const scoreValue = document.querySelector('#scoreValue');
//score
let score = 0;
grid.addEventListener('click', e => {
if (e.target.score) {
score++;
scoreValue.textContent = score;
e.target.score = false;
}
});
const timer = setInterval(() => {
//output random number
let output = 1 + Math.floor(Math.random() * 3);
//select hole
let hole = document.querySelector(`div.box:nth-child(${output})`)
hole.classList.add('blue');
hole.score = true;
setTimeout(() => {
hole.classList.remove('blue');
hole.score = false;
}, 1000);
}, 2000);
div#grid {
display: flex;
}
div.box {
width: 100px;
height: 100px;
border: thin solid black;
background-color: red;
}
div.blue {
background-color: blue;
}
<div id="grid">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
<div id="scoreValue"></div>
Rewrite, a mole is a DOM element, attach the click event to it on load then, in the game timer you only need to pick a random mole and toggle a class, within the click event you can check for that class, if it is there then the mole must be showing, add a score.
For example:
const moles = document.querySelectorAll('.grid .mole')
const hitScore = document.querySelector('.score .hit')
const missScore = document.querySelector('.score .miss')
const gameOver = document.querySelector('.gameover')
let score = {
hit: 0,
miss: 0
}
// assign clicks to all moles
moles.forEach((elm) => {
elm.addEventListener('click', e => {
if (e.target.classList.contains('show')) {
hitScore.textContent = ++score.hit
e.target.classList.remove('show')
}
})
})
// game timer
const timer = setInterval(() => {
// get random mole element
const randMole = moles[Math.floor(Math.random() * moles.length)]
// check if has class, i.e miss
if (randMole.classList.contains('show')) {
missScore.textContent = ++score.miss
}
// toggle show
randMole.classList.toggle('show')
// 5 misses and game over
if (score.miss >= 5) {
clearInterval(timer)
gameOver.style.display = 'block'
}
}, 1000)
.grid {
width: 310px;
height: 310px;
background-image: url(https://i.imgur.com/s6lUgud.png);
position: relative
}
.mole {
position: absolute;
width: 100px;
height: 100px
}
.mole.show {
background-image: url(https://i.imgur.com/uScpWV4.png);
background-repeat: no-repeat;
background-size: 48px 51px;
background-position: center
}
.mole:nth-of-type(1) {
top: 0;
left: 0
}
.mole:nth-of-type(2) {
top: 0;
left: 108px
}
.mole:nth-of-type(3) {
top: 0;
left: 214px
}
.mole:nth-of-type(4) {
top: 100px;
left: 0
}
.mole:nth-of-type(5) {
top: 100px;
left: 108px
}
.mole:nth-of-type(6) {
top: 100px;
left: 214px
}
.mole:nth-of-type(7) {
top: 200px;
left: 0px
}
.mole:nth-of-type(8) {
top: 200px;
left: 107px
}
.mole:nth-of-type(9) {
top: 200px;
left: 214px
}
.gameover {
display: none;
color: red
}
<div class="score">
<strong>Score:</strong> Hit:
<span class="hit">0</span> Miss:
<span class="miss">0</span>
</div>
<div class="gameover">Game Over</div>
<div class="grid">
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
<div class="mole"></div>
</div>

How to auto-scroll a ReactJS chat when images are uploaded and only when scrolled all the way down?

I have figured out to auto-scroll a reactjs chat when text messages are entered and only when the scroll is all the way down or on the bottom. In other words, if a user has scrolled up, the auto-scroll will not work when new text messages arrive which is the desired outcome. However, I can not find a good solution to auto-scroll when images are uploaded into the chat. I have almost found a way to do so but it seems that it only works when an image is a certain height (148px including padding). The problem gets even more complex if multiple images are uploaded at the same time.
Here's the function that does the scrolling. I know it is way too complex so I hope to find a simpler solution:
scrollToBottom = () => {
// Get the chat's div with the "messages" id
const messages = document.getElementById("messages");
// Get the height of the new message
const newMessage = messages.lastChild.previousSibling
const newMessageStyles = getComputedStyle(newMessage)
const newMessageMargin = parseInt(newMessageStyles.marginBottom)
const newMessageMarginTop = parseInt(newMessageStyles.marginTop)
const newMessageHeight = newMessage.offsetHeight + newMessageMargin
const clientHeight = messages.clientHeight
// Do the first auto-scroll all the way down when the chat is first started
if (this.state.justStarted) {
//this.myRef is a div that I placed on the bottom of the chat to scroll down to
this.myRef.current.scrollIntoView({ behavior: "smooth" });
this.setState({justStarted: false})
// I check for the number of files before scrolling down because there is a different scroll-down approach when there are multiple files.
// You also notice that I have various conditions for the auto-scroll because as it turns out it matters what type of content is added.
// For example, if an image is added, I need to use one of the 2nd, 3rd or 4th conditions because images have different dimensions.
// Also, the sequence of what is added to the chat matters in my code. For example, if an image is added after text or vice versa, one of the conditions fires off.
// I know this is complex and I do not really like it. I hope to see a simpler solution.
} else if (this.state.chatFilesLength <= 1 || this.state.chatFilesLength === null) {
if (Math.trunc(this.myDiv.current.scrollTop) + newMessageHeight + clientHeight === this.myDiv.current.scrollHeight) {
this.myRef.current.scrollIntoView({ behavior: "smooth" });
} else if (Math.trunc(this.myDiv.current.scrollTop) + newMessageHeight + clientHeight + 16 === this.myDiv.current.scrollHeight) {
this.myRef.current.scrollIntoView({ behavior: "smooth" });
} else if (Math.trunc(this.myDiv.current.scrollTop) + newMessageHeight + clientHeight + 20 === this.myDiv.current.scrollHeight) {
this.myRef.current.scrollIntoView({ behavior: "smooth" });
} else if (Math.trunc(this.myDiv.current.scrollTop) + newMessageHeight + clientHeight > this.myDiv.current.scrollHeight) {
this.myRef.current.scrollIntoView({ behavior: "smooth" });
}
} else if (this.state.chatFilesLength > 1) {
const number = ((this.state.chatFilesLength -1) * newMessageHeight) + (20 * this.state.chatFilesLength);
if (Math.trunc(this.myDiv.current.scrollTop) + newMessageHeight + clientHeight + number === this.myDiv.current.scrollHeight) {
this.myRef.current.scrollIntoView({ behavior: "smooth" });
}
}
}
Chat's JSX:
<div className="chatArea" id='messages' ref={this.myDiv}>
{
this.state.messages.map((message, index) => {
return message.body.uid === this.state.uid && !message.body.imageUrl
?
<p className="message-sent" key={index}>{message.body.content}</p>
:
message.body.uid === this.state.uid && message.body.imageUrl
?
<img src={message.body.imageUrl} className="message-sent" key={index}></img>
:
message.body.uid === "admin" && !message.body.imageUrl
?
<p className="message-received" key={index}>{message.body.content}</p>
:
message.body.uid === "admin" && message.body.imageUrl
?
<img src={message.body.imageUrl} className="message-received" key={index}></img>
:
null
})
}
<div style={{ float:"left", clear: "both" }}
ref={this.myRef}>
</div>
</div>
And if it is of any use, here's how the scrollToBottom function is called:
startChat () {
this.setState({justStarted: true})
document.getElementById("myForm").style.display = "block";
const ref = firebase.firestore().collection('Chats').doc(this.state.uid).collection('Messages');
const query = ref.orderBy('timestamp', 'desc').limit(10)
this.unsubFromMessages = query.onSnapshot((snapshot) => {
if (snapshot.empty) {
console.log('No matching documents.');
firebase.firestore().collection('Chats').doc(this.state.uid).
set({
name: this.state.displayName,
uid: this.state.uid,
email: this.state.email
}).then(console.log("info saved"))
.catch((error) => {
console.log("Error saving info to document: ", error);
});
}
snapshot.docChanges().reverse().forEach((change) => {
if (change.type === 'removed') {
console.log(change.doc.data().content)
}
else if (change.type === 'added') {
this.setState(state => {
const messages = [...state.messages, {id: change.doc.id, body: change.doc.data()}]
return {
messages
}
}, )
setTimeout( this.scrollToBottom, 1500)
}
else if (change.type === 'modified') {
const filteredMessages = this.state.messages.filter(message => message.body.allowed === "yes")
this.setState(state => {
const messages = [...filteredMessages, {id: change.doc.id, body: change.doc.data()}]
return {
messages
}
})
setTimeout( this.scrollToBottom, 1500)
}
});
}, (error) => {console.log(error)});
}
In short, I need to find a way to auto-scroll the chat when new images are added. A good solution must consider that more than one image can be added. It must always consider whether or not a user has scrolled up in which case the auto-scroll must NOT be triggered.
Thank you.
Found a much simpler and easier solution! Here it is:
scrollToBottom = () => {
// Messages
const messages = document.getElementById("messages");
// Height of the new message
const newMessage = messages.lastChild.previousSibling
const newMessageStyles = getComputedStyle(newMessage)
const newMessageMarginBottom = parseInt(newMessageStyles.marginBottom)
const newMessageMarginTop = parseInt(newMessageStyles.marginTop)
const newMessageHeight = newMessage.offsetHeight + newMessageMarginBottom + newMessageMarginTop;
// Visible height
const visibleHeight = messages.offsetHeight
// or clientHeight
const clientHeight = messages.clientHeight
// Height of messages container
const scrollHeight = messages.scrollHeight
// How far have I scrolled?
const scrollOffset = messages.scrollTop + visibleHeight
// Inital scroll down when chat is first started
if (this.state.justStarted) {
this.myRef.current.scrollIntoView();
this.setState({justStarted: false})
} else if (scrollHeight - newMessageHeight <= scrollOffset) {
this.myRef.current.scrollIntoView();
// For multiple files
} else if (this.state.chatFilesLength > 1) {
if (scrollHeight - (newMessageHeight * this.state.chatFilesLength) <= scrollOffset) {
this.myRef.current.scrollIntoView();
}
}
}
CSS
.message-sent {
position: relative;
left: 100px;
background-color: white;
border-radius: 20px;
max-width: 128px;
margin-bottom: 20px;
margin-top: 16px;
padding: 10px;
color: #000;
}
.message-sent-image {
position: relative;
left: 100px;
background-color: white;
border-radius: 20px;
max-width: 128px;
max-height: 128px;
min-height: 128px;
margin-bottom: 20px;
margin-top: 16px;
padding: 10px;
color: #000;
}
.message-received {
position: relative;
right: 10px;
background-color: black;
border-radius: 20px;
color: white;
max-width: 128px;
height: 128px;
margin-bottom: 20px;
margin-top: 16px;
padding: 10px;
}
You might want to add CSS for message-received-image for images received in chat. You can follow the message-sent-image model with some configuration.
JSX
<div className="chatArea" id='messages' ref={this.myDiv}>
{
this.state.messages.map((message, index) => {
return message.body.uid === this.state.uid && !message.body.imageUrl
?
<p className="message-sent" key={index}>{message.body.content}</p>
:
message.body.uid === this.state.uid && message.body.imageUrl
?
<img src={message.body.imageUrl} className="message-sent-image" key={index}></img>
:
message.body.uid === "admin" && !message.body.imageUrl
?
<p className="message-received" key={index}>{message.body.content}</p>
:
message.body.uid === "admin" && message.body.imageUrl
?
<img src={message.body.imageUrl} className="message-received" key={index}></img>
:
null
})
}
<div style={{ float:"left", clear: "both" }}
ref={this.myRef}>
</div>
</div>

How to convert Jquery code to Javascript for GithubPages

I am new to javascript and still learning. I have an svg map of a country separated in regions, and I want to display some information when hovering over each region.
The code below is working fine (jquery) when running it locally but when uploading it to Github as a Github page it isn't working.
I would like some advice on how to transfom the below part of my code into javascript. (I have tried something with addEventListener and body.appendChild but with no success)
$('#regions > *').mouseover(function (e) {
var region_data = $(this).data('region');
// Info box informations
$('<div class="info_box">' + region_data.region_name + '<br>' + '</div>').appendTo('body');
});
// Show info box when mousemove over a region
$('#regions > *').mousemove(function(e) {
var mouseX = e.pageX,
mouseY = e.pageY;
// Position of information box
$('.info_box').css({
top: mouseY-50,
left: mouseX+10
});
}).mouseleave(function () {
$('.info_box').remove();
});
I have tried something like the following :
var mapRegion = document.querySelectorAll("#regions > *");
mapRegion.forEach(function(reg){
reg.addEventListener('mouseover', function(el){
var perif_data = this.data('region');
document.body.appendChild('<div class="info_box">' + region_data.region_name + '<br>' + '</div>');
});
reg.addEventListener('mousemove', function(e){
var mouseX = e.pageX;
var mouseY = e.pageY;
// Position of information box
document.querySelector('info_box').style.top = mouseY-50;
document.querySelector('info_box').style.css = mouseX+10;
});
reg.addEventListener('mouseleave', function(){
reg.classList.remove('.info_box');
});
});
But I'm getting on console :
this.data is not a function
document.querySelector(...) is null
Modern JavaScript makes this very easy. You just need to iterate over the results of the querySelectorAll call and add the listener to each child.
Also, it looks like your data is a JSON object, so you may need to parse it using JSON.parse.
I recommend not destroying and re-creating the infobox each time. Just update it with the latest info and hide/show it depending on whether or not your are currently mousing-over a region.
Array.from(document.querySelectorAll('#regions > *')).forEach(region => {
region.addEventListener('mouseover', e => {
const infobox = document.querySelector('.info_box')
const regionData = JSON.parse(e.target.dataset.region)
infobox.textContent = regionData.region_name
infobox.classList.toggle('hidden', false)
})
region.addEventListener('mousemove', e => {
const infobox = document.querySelector('.info_box')
if (!infobox.classList.contains('hidden')) {
Object.assign(infobox.style, {
top: (e.pageY - 50) + 'px',
left: (e.pageX + 10) + 'px'
})
}
})
region.addEventListener('mouseleave', e => {
const infobox = document.querySelector('.info_box')
infobox.classList.toggle('hidden', true)
})
})
.info_box {
position: absolute;
top: 0;
left: 0;
border: thin solid grey;
background: #FFF;
padding: 0.25em;
}
.info_box.hidden {
display: none;
}
.region {
display: inline-block;
width: 5em;
height: 5em;
line-height: 5em;
text-align: center;
margin: 0.5em;
border: thin solid grey;
}
<div id="regions">
<div class="region" data-region='{"region_name":"A"}'>Section A</div>
<div class="region" data-region='{"region_name":"B"}'>Section B</div>
<div class="region" data-region='{"region_name":"C"}'>Section C</div>
</div>
<div class="info_box hidden">
</div>
You can simply this by implementing an addListeners function that loops over all the elements and applies various event listeners.
const addListeners = (selector, eventName, listener) => {
Array.from(document.querySelectorAll(selector)).forEach(el => {
typeof eventName === 'string' && listener != null
? el.addEventListener(eventName, listener)
: Object.keys(eventName).forEach(name =>
el.addEventListener(name, eventName[name]))
})
}
addListeners('#regions > *', {
mouseover: e => {
const infobox = document.querySelector('.info_box')
const regionData = JSON.parse(e.target.dataset.region)
infobox.textContent = regionData.region_name
infobox.classList.toggle('hidden', false)
},
mousemove: e => {
const infobox = document.querySelector('.info_box')
if (!infobox.classList.contains('hidden')) {
Object.assign(infobox.style, {
top: (e.pageY - 50) + 'px',
left: (e.pageX + 10) + 'px'
})
}
},
mouseleave: e => {
const infobox = document.querySelector('.info_box')
infobox.classList.toggle('hidden', true)
}
})
.info_box {
position: absolute;
top: 0;
left: 0;
border: thin solid grey;
background: #FFF;
padding: 0.25em;
}
.info_box.hidden {
display: none;
}
.region {
display: inline-block;
width: 5em;
height: 5em;
line-height: 5em;
text-align: center;
margin: 0.5em;
border: thin solid grey;
}
<div id="regions">
<div class="region" data-region='{"region_name":"A"}'>Section A</div>
<div class="region" data-region='{"region_name":"B"}'>Section B</div>
<div class="region" data-region='{"region_name":"C"}'>Section C</div>
</div>
<div class="info_box hidden">
</div>
//Get the body for Adding and removing the info_box
const body = document.querySelector("body");
//Get All Descendants of #Regions
const elements = document.querySelectorAll("#regions > *");
//Create the info_box Element
const infoBoxElement = document.createElement("div");
//Set the class
infoBoxElement.className = "info_box";
//Iterate over each descendant of Regions
elements.forEach((element) => {
//Let's add MouseOver Event
element.addEventListener("mouseover", (e) => {
//get the "data-"" of the element and Parse it
const regionData = JSON.parse(element.dataset.region);
//Let's reuse the infoBoxElement and Assign the innerHTML
infoBoxElement.innerHTML = regionData.region_name + "<br>";
//Appending the infoBoxElement to the Body
body.append(infoBoxElement);
});
//Let's add MouseMove Event
element.addEventListener("mousemove", (e) => {
const mouseX = e.pageX,
mouseY = e.pageY;
//Get the Infobox HTML element
const infoBox = document.getElementsByClassName("info_box")[0];
//Lets add the css Style
infoBox.style.top = mouseX - 50;
infoBox.style.top = mouseY + 10;
});
//Let's add MouseLeave Event
element.addEventListener("mouseleave", (e) => {
//Get the Infobox HTML element
const infoBox = document.getElementsByClassName("info_box")[0];
//Lets get rid of it
infoBox.remove();
});
});

Fast scroll to change style

Is it possible to scroll to .project and make the background red without to scroll slow and near the class .project?
Basically I want the user to be able to scroll and get the red color displayed even if he or she scrolls quickly, but when the user is above or under projectPosition.top, the background should be the standard color (black).
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY == projectPosition.top) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
Thanks in advance.
Instead of listen for the scroll event you could use the Intersection Observer API which can monitor elements that come in and out of view. Every time an observed element either enters or leaves the view, a callback function is fired in which you can check if an element has entered or left the view, and handle accordingly.
It's also highly performant and saves you from some top and height calculations.
Check it out in the example below.
If you have any questions about it, please let me know.
Threshold
To trigger the callback whenever an element is fully into view, not partially, set the threshold option value to [1]. The default is [0], meaning that it is triggered whenever the element is in view by a minimum of 1px. [1] states that 100% of the element has to be in view to trigger. The value can range from 0 to 1 and can contain multiple trigger points. For example
const options = {
threshold: [0, 0.5, 1]
};
Which means, the start, halfway, and fully in to view.
const project = document.querySelector('.project');
const observerCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('red');
} else {
entry.target.classList.remove('red');
}
});
};
const options = {
threshold: [1]
}
const observer = new IntersectionObserver(observerCallback, options);
observer.observe(project);
.top,
.bottom{
height: 700px;
width: 100%;
}
.project {
background: black;
height: 200px;
width: 100%;
}
.project.red {
background: red;
}
<div class="top"></div>
<div class="project"></div>
<div class="bottom"></div>
To make it 'fast' you better will have to use the >= operator than ==:
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY >= projectPosition.top && scrollY <= projectPosition.top + projectPosition.height) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>

Categories

Resources