Add/Remove classes to body on scroll - javascript

I have a function that adds a class current to a menu item when its corresponding section comes into view. How would I go about also adding/removing different classes to body as well, based on currently visible section?
Edit: Per suggestion got it working finally with Intersection Observer but still trying to figure out how to add and swap classes to body:
function setColorScheme() {
const nav = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.55) {
document.querySelector('li.current').classList.remove('current');
var id = entry.target.getAttribute('id');
var newLink = document.querySelector(`[href$='#${id}']`).parentElement.classList.add('current');
//returning error
var newClass = $('body.home').classList.add('.' + id);
}
});
}
const options = {
threshold: 0.55,
rootMargin: '150px 0px'
};
const observer = new IntersectionObserver(nav,options);
const sections = document.querySelectorAll('section.op-section');
sections.forEach((section) => {observer.observe(section);});
}

Related

Why callback not detecting latest value of class instance variable

I have a web component which is basically a class:
class NavList extends HTMLElement {
_wrapper;
_observer;
_observerActive = true;
get observerState() {
return this._observerActive;
}
render() {
this._wrapper.innerHTML = "";
const activeList = window.location.hash.slice(1);
const container = htmlToElement(`<nav class="navlist"></nav>`);
for (let list in veritabani) {
container.appendChild(
htmlToElement(
`<a href=#${list} class="nav-entry ${
activeList === list ? "active" : ""
}">${list}</div>`
)
);
}
// prevent observer from changing hash during smooth scrolling
container.addEventListener("click", this.disableObserver);
this._wrapper.appendChild(container);
}
observe() {
let options = {
root: document.querySelector(".check-list"),
rootMargin: "0px",
threshold: 0.4,
};
let observer = new IntersectionObserver(
this.observerCallback.bind(this),
options
);
this._observer = observer;
const targets = document.querySelectorAll("check-list");
console.log("observer target:", targets);
for (let target of targets) {
observer.observe(target);
}
}
observerCallback(entries, observer) {
console.log("observer active?", this.observerState);
entries.forEach((entry) => {
if (entry.isIntersecting && this.observerState) {
const targetListName = entry.target.getAttribute("list");
console.log(entry, targetListName);
window.location.hash = targetListName;
this.render();
}
});
}
disableObserver() {
this._observerActive = false;
console.log("observer disabled", this._observerActive);
function enableObserver() {
this._observerActive = true;
console.log("observer enabled", this._observerActive);
}
const timer = setTimeout(enableObserver, 2000);
}
connectedCallback() {
console.log("hash", window.location.hash);
// wrapper for entire checklist element
const wrapper = this.appendChild(
htmlToElement(
`<span class="navlist-wrapper ${window.location.hash}"></span>`
)
);
this._wrapper = wrapper;
this.render();
setTimeout(() => {
this.observe();
}, 1);
}
// more code below
As you can see, I have an intersection observer and I am trying to disable its callback when an anchor is clicked.
The observer detects elements on the page and changes the URL hash so that the visible element name is highlighted on the navlist, this works fine but interferes with the function of the navlist since clicking on navlist entry should also scroll the page to that element!
My solution is to disable the intersection observer's callback after a navlist entry is clicked using setTimout:
disableObserver() {
this._observerActive = false;
console.log("observer disabled", this._observerActive);
function enableObserver() {
this._observerActive = true;
console.log("observer enabled", this._observerActive);
}
const timer = setTimeout(enableObserver, 2000);
}
The above code sets an instance variable to false after a click on navlist, the variable changes state to false but the observer's callback does not see the change and uses the old state which is true by default.
My Question: Why is this happening? and how can I fix it?
I tried delaying the callback function thinking that it is being activated before the state change, but it did not work.
UPDATE: Here is a link to a live demo of what I am doing
I found a solution though I still do not quite understand whats happening.
The solution is to move the flag _observerActive outside of the class Navlist:
let OBSERVER_STATE = true;
class NavList extends HTMLElement {
_wrapper;
_observer;
render() {
this._wrapper.innerHTML = "";
const activeList = window.location.hash.slice(1);
const container = htmlToElement(`<nav class="navlist"></nav>`);
for (let list in veritabani) {
console.log(`active? ${list}===${activeList}`);
container.appendChild(
htmlToElement(
`<a href=#${list} class="nav-entry ${
activeList === list ? "active" : ""
}">${list}</div>`
)
);
}
// prevent observer from changing hash during smooth scrolling
container.addEventListener("click", this.disableObserver);
const addButton = htmlToElement(
`<button class="nav-add">
<span class="nav-add-content">
<span class="material-symbols-outlined">add_circle</span>
<p>Yeni list</p>
</span>
</button>`
);
addButton.addEventListener("click", this.addList.bind(this));
this._wrapper.appendChild(container);
this._wrapper.appendChild(addButton);
}
disableObserver() {
OBSERVER_STATE = false;
console.log("observer disabled", this.OBSERVER_STATE);
function enableObserver() {
OBSERVER_STATE = true;
console.log("observer enabled", OBSERVER_STATE);
}
const timer = setTimeout(enableObserver, 2000);
}
addList() {
const inputGroup = htmlToElement(`
<div class="input-group">
</div>`);
const input = inputGroup.appendChild(
htmlToElement(`
<input placeholder="Liste Adi Giriniz"></input>`)
);
const button = inputGroup.appendChild(
htmlToElement(`
<button>✔</button>`)
);
button.addEventListener("click", () =>
this.addNewCheckList(input.value)
);
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
console.log(input.value);
this.addNewCheckList(input.value);
}
});
const addButton = document.querySelector(".nav-add");
console.log(this._wrapper);
this._wrapper.replaceChild(inputGroup, addButton);
}
addNewCheckList(baslik) {
veritabani[baslik] = {};
const checkListContainer = document.querySelector(".check-list");
const newCheckList = htmlToElement(`
<check-list
baslik="${baslik} Listem"
list="${baslik}"
placeholder="�� A��klamas�..."
></check-list>`);
checkListContainer.appendChild(newCheckList);
this._observer.observe(newCheckList);
this.render();
newCheckList.scrollIntoView();
}
observe() {
let options = {
root: document.querySelector(".check-list"),
rootMargin: "0px",
threshold: 0.4,
};
let observer = new IntersectionObserver(
this.observerCallback.bind(this),
options
);
this._observer = observer;
const targets = document.querySelectorAll("check-list");
console.log("observer target:", targets);
for (let target of targets) {
observer.observe(target);
}
}
observerCallback(entries, observer) {
console.log("observer active?", OBSERVER_STATE);
entries.forEach((entry) => {
if (entry.isIntersecting && OBSERVER_STATE) {
const targetListName = entry.target.getAttribute("list");
window.location.hash = targetListName;
this.render();
}
});
}
connectedCallback() {
console.log("hash", window.location.hash);
// wrapper for entire checklist element
const wrapper = this.appendChild(
htmlToElement(
`<span class="navlist-wrapper ${window.location.hash}"></span>`
)
);
this._wrapper = wrapper;
this.render();
setTimeout(() => {
this.observe();
}, 1);
}
}
If I understand correctly, you want to
Create a nav list that renders a link for each anchor (id) on a page.
When a target scrolls into view, highlight the associated link and update the location hash
When clicking on a link in the Navbar, scroll to the target and update the location hash
You don't have to keep track of the IntersectObserver state and you don't have to disable it. Just use pushState() instead of location.hash to update the hash. https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
index.html
<head>
<style>
/* Makes sure that the first section scrolls up enough to trigger the effect */
section { scroll-margin: 20px }
</style>
</head>
<body>
<div class="sidebar">
<nav-bar></nav-bar>
</div>
<div class="content">
<section id="One">
<header><h1>One</h1></header>
<p> A bunch of text</p>
</section>
<!-- A Bunch of Sections -->
</div>
</body>
component.js
export class NavList extends HTMLElement {
#template = `
<style>
.visible {
background-color: orange;
}
</style>
<menu></menu>
`;
constructor() {
super();
this.shadow = this.attachShadow({mode: "open"});
}
connectedCallback() {
const li = document.createElement('li');
const a = document.createElement('a');
this.tmpl = document.createRange().createContextualFragment(this.#template);
this.menu = this.tmpl.querySelector('menu');
this.anchors = document.querySelectorAll('[id]');
this.observer = new IntersectionObserver( entries => {
const entry = entries.shift();
const id = entry.target.getAttribute('id');
const link = this.menu.querySelector(`a[href="#${id}"]`);
Array.from(this.menu.querySelectorAll('a')).map(a => a.classList.remove('visible'));
link.classList.add('visible');
history.pushState({}, '', `#${id}`);
}, {threshold: 1});
for (let anchor of this.anchors) {
const item = li.cloneNode();
const link = a.cloneNode();
const id = anchor.getAttribute('id');
link.setAttribute('href', `#${id}`);
link.innerText = id;
link.addEventListener('click', evt => this.clicked(evt));
item.append(link);
this.menu.append(item);
this.observer.observe(anchor);
}
this.render();
}
disconnectedCallback() {
this.observer.disconnect();
}
clicked(evt) {
evt.preventDefault();
const target = evt.target.getAttribute('href');
const elem = document.querySelector(target);
elem.scrollIntoView({behavior: "smooth"});
history.pushState({}, '', `#${target}`);
}
render() {
this.shadow.append(this.tmpl);
}
}
customElements.define('nav-list', NavList);

JavasScript Show first two images, and then 1 random images from an array using a class method

I have all of the components for this project with the exception of a class method to show the first two images in the array (the Pokeball, and the default eevee) then randomly choose from an array one more image(eevee evolutions) and then stop.
Please be patient I am very new to this.
class Pokemon {
constructor() {
this.listOfCharmander = [
"./images/pokeball.png",
"./images/charmander/charmander0.png",
"./images/charmander/charmander1.png",
"./images/charmander/charmander2.png",
];
this.index = 0;
this.pokemon = document.createElement("img");
}
handleClick = () => {
if (this.index < 3) {
++this.index;
this.pokemon.src = this.listOfCharmander[this.index];
}
};
buildPokemon = () => {
this.pokemon.src = this.listOfCharmander[0];
this.pokemon.classList.add("pokemon");
this.pokemon.addEventListener("click", this.handleClick);
main.appendChild(this.pokemon);
};
}
const pokemon = new Pokemon();
const pokemon1 = new Pokemon();
pokemon.buildPokemon();
pokemon1.buildPokemon();
class Eevee {
constructor() {
this.eeveeEvolutions = [
"/images/pokeball.png",
"images/eevee/eevee0.png",
"images/eevee/eevee1.png",
"images/eevee/eevee2.png",
"images/eevee/eevee3.png",
"images/eevee/eevee4.png",
"images/eevee/eevee5.png",
"images/eevee/eevee6.png",
"images/eevee/eevee7.png",
"images/eevee/eevee8.png",
];
this.index = 0;
this.eevee = document.createElement("img");
}
handleClick = () => {
if (this.index < 9) {
++this.index;
this.eevee.src = this.eeveeEvolutions[this.index];
}
};
buildEevee = () => {
this.eevee.src = this.eeveeEvolutions[0];
this.eevee.classList.add("eevee");
this.eevee.addEventListener("click", this.handleClick);
main.appendChild(this.eevee);
};
}
const eevee = new Eevee();
const eevee1 = new Eevee();
eevee.buildEevee();
eevee1.buildEevee();
...
I apologize if my question isnt exactly clear. I need to add to my handClick method. A way display the first image (the Pokeball) and then the 2nd image (default eevee) And then randomly choose (including having it stay the same there could be no evolution at all) one more evolution from the remaining array.
if you always need first two images in your array then you can add extra attribute to your Eevee class to store those images path, and for random image you can add this code to your handleClick()
handleClick = () => {
this.index=Math.floor(Math.random(2,9)*10)
this.eevee.src = this.eeveeEvolutions[this.index];
};

Intersection observer function scope

I am having problems with intersection observer. I want to add the classList "percent-animate" to only the element with the dataset.id = "percentage-status". However, this classList is also being added to the elements underneath it. If i return out of the if block for the percentageBar.dataset.id === "percentage-status" if statement, then the if block underneath doesn't run. Any ideas how to solve this?
const inLeft = document.querySelector(".in-left");
const inRight = document.querySelector(".in-right");
const animate = document.querySelector(".percent-animate");
const percentageBar = document.querySelector(".container__percent-progress");
const skillsObserver = new IntersectionObserver(
function (entries, skillsObserver) {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (percentageBar.dataset.id === "percentage-status") {
entry.target.classList.add("percent-animate");
console.log("it worked");
}
if (entry.isIntersecting) {
entry.target.classList.add("mergeLeft");
entry.target.classList.add("mergeRight");
skillsObserver.unobserve(entry.target);
return;
}
});
},
{
threshold: 0.5,
}
);
skillsObserver.observe(percentageBar);
skillsObserver.observe(inLeft);
skillsObserver.observe(inRight);

How do I create the dots on my custom carousel?

I'm still beginner with CSS and Javascript. I tried to make a carousel using CSS and JavaScript.
I would like to know how do I create the logic for the dots on my custom carousel?
I created the buttons, and they are working to pass the slides. But can you tell me how do I create the dots?
This is my project into codesandbox
export function usePosition(ref) {
const [prevElement, setPrevElement] = React.useState(null);
const [nextElement, setNextElement] = React.useState(null);
React.useEffect(() => {
const element = ref.current;
const update = () => {
const rect = element.getBoundingClientRect();
const visibleElements = Array.from(element.children).filter((child) => {
const childRect = child.getBoundingClientRect();
return rect.left <= childRect.left && rect.right >= childRect.right;
});
if (visibleElements.length > 0) {
setPrevElement(getPrevElement(visibleElements));
setNextElement(getNextElement(visibleElements));
}
};
update();
element.addEventListener("scroll", update, { passive: true });
return () => {
element.removeEventListener("scroll", update, { passive: true });
};
}, [ref]);
const scrollToElement = React.useCallback(
(element) => {
const currentNode = ref.current;
if (!currentNode || !element) return;
let newScrollPosition;
newScrollPosition =
element.offsetLeft +
element.getBoundingClientRect().width / 2 -
currentNode.getBoundingClientRect().width / 2;
console.log("newScrollPosition: ", newScrollPosition);
currentNode.scroll({
left: newScrollPosition,
behavior: "smooth"
});
},
[ref]
);
const scrollRight = React.useCallback(() => scrollToElement(nextElement), [
scrollToElement,
nextElement
]);
return {
hasItemsOnLeft: prevElement !== null,
hasItemsOnRight: nextElement !== null,
scrollRight,
scrollLeft
};
}
Thank you in advance for any help!!
below you will find my solution, I took your code from the sandbox and worked with it. I didn't understand if you wanted to show the dots, to scroll and sync the dots and click on the dots to change the image, so I did all of them .
codesandbox

Rendering array items after making changes to its properties

I'm having an issue re-rendering items in an array after changes are made to elements in the array. Whether I add by pushing or remove by splicing, when the array is rendered again on the page, it like more items are being added to the array. So if I push onto the array, the item is added, but the old items are then duplicated into the array. Something similar happens when I remove items. The item looks to be removed, but the elements that were in the array show on the page, they are then duplicated and the item that was spliced is gone.
I'm trying to avoid a location.reload('/edit.html') to refresh the page. Kind of cheating. It seems to work, but I'm trying to get the page to refresh with my renderIngredients function. The toggleIngredient function is also duplicating the list of items when I check an item.
import { initializeEditPage, generateLastEdited } from './views'
import { updateRecipe, removeRecipe, saveRecipes, getRecipes, createIngredient } from './recipes'
const titleElement = document.querySelector('#recipe-title')
const bodyElement = document.querySelector('#recipe-body')
const removeElement = document.querySelector('#remove-recipe')
const addElement = document.querySelector('#add-recipe')
const dateElement = document.querySelector('#last-updated')
const addIngredient = document.querySelector('#new-ingredient')
const recipeStatus = document.querySelector('#recipe-status')
const recipeId = location.hash.substring(1)
const recipeOnPage = getRecipes().find((item) => item.id === recipeId)
titleElement.addEventListener('input', (e) => {
const recipe = updateRecipe(recipeId, {
title: e.target.value
})
dateElement.textContent = generateLastEdited(recipe.updatedAt)
})
bodyElement.addEventListener('input', (e) => {
const recipe = updateRecipe(recipeId, {
body: e.target.value
})
dateElement.textContent = generateLastEdited(recipe.updatedAt)
})
addElement.addEventListener('click', () => {
saveRecipes()
location.assign('/index.html')
})
removeElement.addEventListener('click', () => {
removeRecipe(recipeId)
location.assign('/index.html')
})
addIngredient.addEventListener('submit', (e) => {
const text = e.target.elements.text.value.trim()
e.preventDefault()
if (text.length > 0) {
createIngredient(recipeId, text)
e.target.elements.text.value = ''
}
renderIngredients(recipeId)
saveRecipes()
//location.reload('/edit.html')
})
const removeIngredient = (text) => {
const ingredientIndex = recipeOnPage.ingredients.findIndex((ingredient)=> ingredient.text === text)
if (ingredientIndex > -1) {
recipeOnPage.ingredients.splice(ingredientIndex, 1)
}
saveRecipes()
renderIngredients(recipeId)
//location.reload('/edit.html')
}
const toggleIngredient = (text) => {
const ingredient = recipeOnPage.ingredients.find((ingredient) => ingredient.text === text)
if (ingredient.included) {
ingredient.included = false
} else {
ingredient.included = true
}
//location.reload('/edit.html')
}
const ingredientSummary = (recipe) => {
let message
const allUnchecked = recipeOnPage.ingredients.every((ingredient) => ingredient.included === false)
const allChecked = recipeOnPage.ingredients.every((ingredient) => ingredient.included === true)
if (allUnchecked) {
message = `none`
} else if (allChecked) {
message = `all`
} else {
message = `some`
}
return `You have ${message} ingredients for this recipe`
}
const generateIngredientDOM = (ingredient) => {
const ingredientEl = document.createElement('label')
const containerEl = document.createElement('div')
const checkbox = document.createElement('input')
const ingredientText = document.createElement('span')
const removeButton = document.createElement('button')
recipeStatus.textContent = ingredientSummary(recipeOnPage)
// Setup ingredient container
ingredientEl.classList.add('list-item')
containerEl.classList.add('list-item__container')
ingredientEl.appendChild(containerEl)
// Setup ingredient checkbox
checkbox.setAttribute('type', 'checkbox')
checkbox.checked = ingredient.included
containerEl.appendChild(checkbox)
// Create checkbox button in ingredient div
checkbox.addEventListener('click', () => {
toggleIngredient(ingredient.text)
saveRecipes()
renderIngredients(recipeId)
})
// Setup ingredient text
ingredientText.textContent = ingredient.text
containerEl.appendChild(ingredientText)
// Setup the remove button
removeButton.textContent = 'remove'
removeButton.classList.add('button', 'button--text')
ingredientEl.appendChild(removeButton)
// Create remove button in ingredient div
removeButton.addEventListener('click', () => {
removeIngredient(ingredient.text)
saveRecipes()
renderIngredients(recipeId)
})
return ingredientEl
}
const renderIngredients = (recipeId) => {
// Grab the ingredient display from the DOM
const ingredientList = document.querySelector('#ingredients-display')
const recipe = getRecipes().find((item) => {
return item.id === recipeId
})
// Iterate through the list of ingredients on the page and render all items from recipeDOM
recipe.ingredients.forEach((ingredient) => {
const recipeDOM = generateIngredientDOM(ingredient)
ingredientList.appendChild(recipeDOM)
})
}
renderIngredients(recipeId)
I believe the issue stems from my renderIngredients function but I can't figure out how to fix it. Again, when I refresh the page, the results I want display, but I want to avoid using location.reload. I'm expecting the removeIngredient function to remove the ingredient with a button click and the page refreshes with the renderIngredients function. Also expecting the toggleIngredient function to just display a checkbox next to the ingredient I checked off, but that's not what's happening. The Same thing is happening when I use the addIngredient function, the ingredient is being added, but the ingredient that was already on the page is being duplicated.
I guess you want to clear the list before adding the elements again:
ingredientList.innerHTML = "";

Categories