Tabs how can I add next and prev buttons? - javascript

Tabs how can I add next and prev buttons?
I'm have a tab component that needs the next and prev button as requirements.
I tried to build the component and now the extra bit is needed but I'm a bit stuck.
I'm not sure how to call the function and make it work within the existing one.
How do I add an extra click function for the buttons next and prev?
would be really useful to have it working. If anyone is able to put me on the right path, perhaps using a click even with some console.log in the right place?
class Tabs {
constructor() {
this.tabsBlocks = document.querySelectorAll(".tabs-block");
}
init() {
if (this.tabsBlocks.length > 0) {
Array.prototype.forEach.call(this.tabsBlocks, (tabBlock, index) => {
const tabContainer = tabBlock.querySelector(".tab-wrapper");
const tabs = tabBlock.querySelectorAll("button");
const panels = tabBlock.querySelectorAll(".panel");
const buttonNext = tabBlock.querySelector(".buttonNext");
const buttonPrev = tabBlock.querySelector(".buttonPrev");
tabContainer.setAttribute("role", "tablist");
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.open === "true") this.toggleTabs(tab, panels);
tab.setAttribute("role", "tab");
tab.setAttribute(
"aria-controls",
`panel-${tab.dataset.target}-block-${index + 1}`
);
const associatedPanel = tabBlock.querySelector(
`[data-panel="${tab.dataset.target}"]`
);
if (associatedPanel !== null) {
associatedPanel.id = `panel-${tab.dataset.target}-block-${
index + 1
}`;
tab.id = `tab-${tab.dataset.target}-block-${index + 1}`;
}
tab.addEventListener("click", () => {
this.toggleTabs(tab, panels);
});
});
Array.prototype.forEach.call(panels, (panel) => {
const associatedTab = tabBlock.querySelector(
`[data-target="${panel.dataset.panel}"]`
);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", `${associatedTab.id}`);
});
});
}
}
toggleTabs = (currentTab, panels, buttonNext, buttonPrev) => {
const tabs = currentTab.closest(".tabs-block").querySelectorAll("button");
const target = currentTab.dataset.target;
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.target !== target) {
tab.classList.remove("is-active");
tab.setAttribute("aria-selected", "false");
}
});
Array.prototype.forEach.call(panels, (panel) => {
if (panel.dataset.panel !== target) {
panel.classList.remove("is-active");
} else {
panel.classList.add("is-active");
currentTab.classList.add("is-active");
currentTab.setAttribute("aria-selected", "true");
}
});
};
}
const components = {
Tabs: new Tabs()
};
components.Tabs.init();
.tabs-block .tab-wrapper li {
flex: 1 1 0%;
text-align: center;
}
.tabs-block .tab-wrapper li button {
font-weight: lighter;
font-size: 20px;
}
.tabs-block .tab-wrapper li button.is-active {
font-weight: normal;
}
.tabs-block .panel {
display: none;
}
.tabs-block .panel.is-active {
display: block;
}
<section class="tabs-block">
<ul class="tab-wrapper">
<li><button data-target="1" data-open="true">Tab title 1</button></li>
<li><button data-target="2">Tab title 2</button></li>
</ul>
<div class="panel-wrapper">
<div data-panel="1" class="panel">
<p>Panel 1 content</p>
</div>
<div data-panel="2" class="panel">
<p>Panel 2 content</p>
</div>
</div>
<button class="buttonNext"><< Prev</button>
<button class="buttonPrev">Next >></button>
</section>

This example works on codepen but does not in here.. Maybe babel preprozessor does something which i do not know...
Copy this to codepen and you will be happy.
class Pagination {
constructor(tabComponent, prevBtnId, nextBtnId) {
this.arrayUtils = new ArrayUtils()
this.tabComponent = tabComponent
this.prevBtn = document.getElementById(prevBtnId)
this.nextBtn = document.getElementById(nextBtnId)
// listen to tabComponents newly created Toggle event
// in which we wanna make sure to disable Btns or something..
this.tabComponent.onTabsToggle((tabs, tabIndex) => {
this.applyPaginationRules(tabs, tabIndex)
})
}
setDisabled(btn, value) {
if(value) {
btn.setAttribute('disabled', 'true')
} else {
btn.removeAttribute('disabled')
}
}
applyPaginationRules(tabs, newActiveIndex) {
const nextBtnDisabled = newActiveIndex === (tabs.length -1)
const prevBtnDisabled = newActiveIndex === 0
this.setDisabled(this.nextBtn, nextBtnDisabled)
this.setDisabled(this.prevBtn, prevBtnDisabled)
}
paginate(btn, action) {
const block = btn.closest('.tabs-block')
const panels = block.querySelectorAll('.panel')
const tabs = block.querySelectorAll('.tab-wrapper > li > button')
const activeIndex = Array.from(tabs)
.findIndex(t => t.getAttribute('data-open') === 'true')
if (tabs.length < 2) {
console.log('0 OR 1 tabs to toggle so no action.')
return
}
var newActiveIndex
if(action === 'next') {
newActiveIndex = this.arrayUtils.nextIndex(activeIndex, tabs)
}
else if(action === 'prev') {
newActiveIndex = this.arrayUtils.previousIndex(activeIndex, tabs)
}
else {
throw 'Invalid toggle action ' + action
}
// enable / disable next and previous btns..
this.applyPaginationRules(tabs, newActiveIndex)
this.tabComponent.toggleTabs(tabs[newActiveIndex], panels)
}
addPaginationListener(btn, action) {
btn.addEventListener('click', e => {
this.paginate(btn, action)
})
}
init() {
this.addPaginationListener(this.prevBtn, 'prev')
this.addPaginationListener(this.nextBtn, 'next')
// disable prev button on beggining since we start at 0..
this.setDisabled(this.prevBtn, true)
}
}
class ArrayUtils {
// getting next index in array
nextIndex(currentIndex, array) {
// if 0 OR 1 elements, index stays the same..
if(array.length < 2) return currentIndex
// if possible increment.
if(currentIndex < array.length -1) {
return currentIndex + 1
}
// if index would exceed array size go to start.
return 0
}
// getting previous INdex in array:
previousIndex(currentIndex, array) {
// if 0 OR 1 elements, index stays the same..
if(array.length < 2) return currentIndex
// if possible decrement!
if(currentIndex > 0) {
return currentIndex - 1
}
// start at the end of array when end is reached ofc.
return array.length -1
}
}
class Tabs {
constructor() {
this.tabsBlocks = document.querySelectorAll(".tabs-block");
this.onToggleHandlers = []
}
onTabsToggle(fn) {
this.onToggleHandlers.push(fn)
}
emitTabsToggle(tabs, tabIndex) {
this.onToggleHandlers.forEach(fn => fn(tabs, tabIndex))
}
init() {
if (this.tabsBlocks.length > 0) {
Array.prototype.forEach.call(this.tabsBlocks, (tabBlock, index) => {
const tabContainer = tabBlock.querySelector(".tab-wrapper");
const tabs = tabBlock.querySelectorAll(".tab-wrapper li button");
const panels = tabBlock.querySelectorAll(".panel");
tabContainer.setAttribute("role", "tablist");
Array.prototype.forEach.call(tabs, (tab, tabIndex) => {
if (tab.dataset.open === "true") this.toggleTabs(tab, panels);
tab.setAttribute("role", "tab");
tab.setAttribute(
"aria-controls",
`panel-${tab.dataset.target}-block-${index + 1}`
);
const associatedPanel = tabBlock.querySelector(
`[data-panel="${tab.dataset.target}"]`
);
if (associatedPanel !== null) {
associatedPanel.id = `panel-${tab.dataset.target}-block-${
index + 1
}`;
tab.id = `tab-${tab.dataset.target}-block-${index + 1}`;
}
tab.addEventListener("click", () => {
this.toggleTabs(tab, panels);
this.emitTabsToggle(tabs, tabIndex)
});
});
Array.prototype.forEach.call(panels, (panel) => {
const associatedTab = tabBlock.querySelector(
`[data-target="${panel.dataset.panel}"]`
);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", `${associatedTab.id}`);
});
});
}
}
toggleTabs = (currentTab, panels) => {
const tabs = currentTab.closest(".tabs-block").querySelectorAll("button");
const target = currentTab.dataset.target;
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.target !== target) {
tab.classList.remove("is-active");
tab.setAttribute('data-open', 'false')
tab.setAttribute("aria-selected", "false");
}
});
Array.prototype.forEach.call(panels, (panel) => {
if (panel.dataset.panel !== target) {
panel.classList.remove("is-active");
} else {
panel.classList.add("is-active");
}
});
/// activate tab..
currentTab.classList.add("is-active");
currentTab.setAttribute("data-open", 'true');
currentTab.setAttribute("aria-selected", "true");
};
}
const components = {
Tabs: new Tabs()
};
components.Tabs.init();
// have the pagination more decoupled from tabs!
// it uses tabs component but you can remove it OR apply it to other
// classes like so more easily..
const prevBtnId = 'pagination-prev'
const nextBtnId = 'pagination-next'
const pagination = new Pagination(components.Tabs, prevBtnId, nextBtnId)
pagination.init()
.tabs-block .tab-wrapper li {
flex: 1 1 0%;
text-align: center;
}
.tabs-block .tab-wrapper li button {
font-weight: lighter;
font-size: rem(20px);
}
.tabs-block .tab-wrapper li button.is-active {
font-weight: normal;
}
.tabs-block .panel {
display: none;
}
.tabs-block .panel.is-active {
display: block;
}
<section class="tabs-block">
<ul class="tab-wrapper">
<li><button data-target="1" data-open="true">Tab title 1</button></li>
<li><button data-target="2">Tab title 2</button></li>
<li><button data-target="3">Tab title 3</button></li>
<li><button data-target="4">Tab title 4</button></li>
</ul>
<div class="panel-wrapper">
<div data-panel="1" class="panel">
<p>Panel 1 content</p>
</div>
<div data-panel="2" class="panel">
<p>Panel 2 content</p>
</div>
<div data-panel="3" class="panel">
<p>Panel 3 content</p>
</div>
<div data-panel="4" class="panel">
<p>Panel 4 content</p>
</div>
</div>
<button class="buttonPrev" id="pagination-prev"><< Prev</button>
<button class="buttonNext" id="pagination-next">Next >></button>
</section>

You have to add eventListener to your button elements
buttonNext.addEventListener("click", myFunction);
buttonPrev.addEventListener("click", myFunction);
function myFunction() {
console.log("next")
}

Here are your Button Elements
<button id="nextBtn" class="buttonNext"><< Prev</button>
<button id="prevBtn" class="buttonPrev">Next >></button>
You have to get elements directly from DOM. use a unique class name or id for this purpose
const buttonNext = document.querySelector("#nextBtn");
const buttonPrev = document.querySelector("#prevBtn");
Now if the buttonNext and buttonPrev variables have the elements you can add eventListener. The event listener is of type click so whenever user clicks on any button the respective function will be called
buttonNext && buttonNext.addEventListener("click", handleNext);
buttonPrev && buttonPrev.addEventListener("click", handlePrev);
const handleNext = () => {
console.log("next")
}
const handlePrev = () => {
console.log("next")
}
I hope this will work for you. You can add any logic on next and prev buttons in respective functions

Working codepen demo
class Tabs {
constructor() {
this.tabsBlocks = document.querySelectorAll(".tabs-block");
}
init() {
if (this.tabsBlocks.length > 0) {
Array.prototype.forEach.call(this.tabsBlocks, (tabBlock, index) => {
const tabContainer = tabBlock.querySelector(".tab-wrapper");
const tabs = tabBlock.querySelectorAll("button");
const panels = tabBlock.querySelectorAll(".panel");
const Navigate= () => {
const buttonNext = document.querySelector("#nextBtn");
const buttonPrev = document.querySelector("#prevBtn");
const handleNext = () => {
console.log("next");
};
const handlePrev = () => {
console.log("prev");
};
buttonNext && buttonNext.addEventListener("click", handleNext);
buttonPrev && buttonPrev.addEventListener("click", handlePrev);
}
Navigate()
tabContainer.setAttribute("role", "tablist");
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.open === "true") this.toggleTabs(tab, panels);
tab.setAttribute("role", "tab");
tab.setAttribute(
"aria-controls",
`panel-${tab.dataset.target}-block-${index + 1}`
);
const associatedPanel = tabBlock.querySelector(
`[data-panel="${tab.dataset.target}"]`
);
if (associatedPanel !== null) {
associatedPanel.id = `panel-${tab.dataset.target}-block-${
index + 1
}`;
tab.id = `tab-${tab.dataset.target}-block-${index + 1}`;
}
tab.addEventListener("click", () => {
this.toggleTabs(tab, panels);
});
});
Array.prototype.forEach.call(panels, (panel) => {
const associatedTab = tabBlock.querySelector(
`[data-target="${panel.dataset.panel}"]`
);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", `${associatedTab.id}`);
});
});
}
}
toggleTabs = (currentTab, panels, buttonNext, buttonPrev) => {
const tabs = currentTab.closest(".tabs-block").querySelectorAll("button");
const target = currentTab.dataset.target;
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.target !== target) {
tab.classList.remove("is-active");
tab.setAttribute("aria-selected", "false");
}
});
Array.prototype.forEach.call(panels, (panel) => {
if (panel.dataset.panel !== target) {
panel.classList.remove("is-active");
} else {
panel.classList.add("is-active");
currentTab.classList.add("is-active");
currentTab.setAttribute("aria-selected", "true");
}
});
};
}
const components = {
Tabs: new Tabs()
};
components.Tabs.init();

Related

React conversion of MDN's touch event / gesture logger

Here's Mozilla's code for handling touch gestures, such as two finger pinch/zoom, etc.
https://github.com/mdn/dom-examples/blob/main/touchevents/Multi-touch_interaction.html
And here is the live demo:
https://mdn.github.io/dom-examples/touchevents/Multi-touch_interaction.html
(This will only work on mobile—or at least it doesn't work on my Macbook Chrome browser.
I'm trying to replicate the same output using React. Here's what I have:
import React, { useState, useEffect } from 'react';
const TouchEventsContainer = () => {
const [logEvents, setLogEvents] = useState(false);
const [tpCache, setTpCache] = useState([]);
const [logText, setLogText] = useState("");
const [testText, setTestText] = useState("Tap Swipe");
const enableLog = () => {
setLogEvents(!logEvents);
setTestText(logText);
};
const clearLog = () => setLogText('');
const log = (name, ev, printTargetIds) => {
let s = `${name}: touches = ${ev.touches.length} ; targetTouches = ${ev.targetTouches.length} ; changedTouches = ${ev.changedTouches.length}`;
if (printTargetIds) {
s += "";
for (var i = 0; i < ev.targetTouches.length; i++) {
s += `... id = ${ev.targetTouches[i].identifier} <br>`;
}
}
setLogText((prevLogText) => prevLogText + s + "<br>");
};
const updateBackground = (ev) => {
switch (ev.targetTouches.length) {
case 1:
ev.target.style.background = "yellow";
break;
case 2:
ev.target.style.background = "pink";
break;
default:
ev.target.style.background = "lightblue";
}
}
const handlePinchZoom = (ev) => {
if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
let point1 = -1, point2 = -1;
for (let i = 0; i < tpCache.length; i++) {
if (tpCache[i].identifier === ev.targetTouches[0].identifier) point1 = i;
if (tpCache[i].identifier === ev.targetTouches[1].identifier) point2 = i;
}
if (point1 >= 0 && point2 >= 0) {
let diff1 = Math.abs(tpCache[point1].clientX - ev.targetTouches[0].clientX);
let diff2 = Math.abs(tpCache[point2].clientX - ev.targetTouches[1].clientX);
const PINCH_THRESHHOLD = ev.target.clientWidth / 10;
if (diff1 >= PINCH_THRESHHOLD && diff2 >= PINCH_THRESHHOLD)
ev.target.style.background = "green";
} else {
setTpCache([]);
}
}
}
const startHandler = (ev) => {
ev.preventDefault();
if (ev.targetTouches.length === 2) {
const newTpCache = [...tpCache];
for (let i = 0; i < ev.targetTouches.length; i++) {
newTpCache.push(ev.targetTouches[i]);
}
setTpCache(newTpCache);
}
if (logEvents) {
log("touchStart", ev, true);
}
updateBackground(ev);
};
const moveHandler = (ev) => {
ev.preventDefault();
if (logEvents) {
log("touchMove", ev, false);
}
if (!(ev.touches.length === 2 && ev.targetTouches.length === 2)) {
updateBackground(ev);
}
ev.target.style.outline = "dashed";
handlePinchZoom(ev);
};
const endHandler = (ev) => {
ev.preventDefault();
if (logEvents) {
log(ev.type, ev, false);
}
if (ev.targetTouches.length === 0) {
ev.target.style.background = "white";
ev.target.style.outline = "1px solid black";
}
};
const setHandlers = (name) => {
const el = document.getElementById(name);
el.ontouchstart = startHandler;
el.ontouchmove = moveHandler;
el.ontouchcancel = endHandler;
el.ontouchend = endHandler;
};
useEffect(() => {
setHandlers("target1");
setHandlers("target2");
setHandlers("target3");
setHandlers("target4");
}, []);
return (
<>
<h1>Multi-touch interaction</h1>
<div id="target1">{testText}</div>
<div id="target2"> Tap, Hold or Swipe me 2</div>
<div id="target3"> Tap, Hold or Swipe me 3</div>
<div id="target4"> Tap, Hold or Swipe me 4</div>
<button id="log" onClick={() => enableLog()}>Start/Stop event logging</button>
<button id="clearlog" onClick={() => clearLog()}>Clear the log</button>
<p></p>
<div>{logText}</div>
</>
);
};
export default TouchEventsContainer;
I have 2 problems:
My log output (currently in a ) doesn't produce anything. I think it's because my string contains HTML. I tried using "dangerousInnerHTML" and some other things, but to no avail.
The two finger pinch-zoom (when background color turns green) doesn't seem to be detected.
What is wrong with my React conversion?
Also I'm currently rendering my TouchEventsContainer within the context of a different JS view (on my app) for testing, but I don't think that's the issue.

Counter with clearInterval, not restarting after button click

There is a word-translation card page which includes:
A 3-second countdown that runs immediately after the page loads and when you move to a new word after press the 'Correct' button.
Word and his translate which appears after 'Excellent' button pressed. This button stops and hide the counter.
Button 'Show answer' which appears if 'Excellent' didn't pressed in 3 sec.
Buttons 'Wrong' and 'Correct' which appears if 'Excellent' or 'Show answer' buttons are pressed.
The problem is how the countdown works (unstable). Sometimes it doesn't restart after clicking "Correct". I tried manage countdown in separate function, but this approach provides much more issues. So now there is a timer that is called globally and a timer that is called when you click on the "Correct" button.I think the problem is near timerId. I will be glad to any comments and ideas on the code.
<div class="deck-container">
<div class="words-container">
<h2 id="primary-word"></h2>
<h2 id="secondary-word"></h2>
</div>
<div class="btn-container">
<p id="timer-count">3</p>
<button id="btn-excellent" onClick="excellent()">Excellent!</button>
<button id="btn-show-answer" onClick="showAnswerF()">Show Answer</button>
<button id="btn-wrong">Wrong</button>
<button id="btn-correct" onClick="correctF()">Correct</button>
</div>
</div>
let currentWord = 0
let timerCount = 3
let fetched_data = {}
async function getDeck () {
let response = await fetch('/api/deck_words/2/', {
method: 'get',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
}
)
let data = await response.json()
let primaryElement = document.getElementById('primary-word')
primaryElement.innerText = await data[currentWord]['primary_word']
let secondaryElement = document.getElementById('secondary-word')
secondaryElement.innerText = await data[currentWord]['secondary_word']
return data
}
fetched_data = getDeck()
const getData = async () => {
return await getDeck()
}
data = getData();
function countDecrease() {
timerCount --
if (timerCount > 0) {
document.getElementById("timer-count").innerHTML = timerCount
} else {
hideExcellent()
}
}
function hideExcellent() {
showElement(true,'btn-excellent')
showElement(true,'timer-count')
showElement(false,'btn-show-answer')
}
let timerId = setInterval(() => countDecrease(), 1000)
setTimeout(() => {
clearInterval(timerId)
}, 3000)
function showElement(showProperty, elementClass) {
showProperty = !showProperty
let element = document.getElementById(elementClass)
element.style.display = (showProperty === true) ? "inline-block" : "none";
}
function showAnswerF() {
showElement(true,'btn-show-answer')
showElement(false,'secondary-word')
showElement(false,'btn-wrong')
showElement(false,'btn-correct')
}
function excellent() {
showElement(true,'timer-count')
showElement(true,'btn-excellent')
showElement(false,'btn-wrong')
showElement(false,'btn-correct')
showElement(false,'secondary-word')
clearInterval(timerId)
timerCount = 3
}
function correctF() {
currentWord++
const changeWords = () => {
fetched_data.then((data) => {
document.getElementById('primary-word').innerText = data[currentWord]['primary_word']
document.getElementById('secondary-word').innerText = data[currentWord]['secondary_word']
document.getElementById("timer-count").innerText = '3'
timerCount = 3
timerId = setInterval(() => countDecrease(), 1000)
setTimeout(() => {
clearInterval(timerId)
}, 3000)
})
}
changeWords()
let countElement = document.getElementById('timer-count')
countElement.style.display = "block"
showElement(false,'btn-excellent')
showElement(true,'btn-wrong')
showElement(true,'btn-correct')
showElement(true,'secondary-word')
}
I think this would be better handled with an async function, maybe like so.
const timerCount = document.querySelector("#timer-count")
const reset = document.querySelector("button")
function delay(ms) {
return new Promise(res => setTimeout(res, ms))
}
async function countDown(signal) {
const aborted = new Promise(resolve => signal.addEventListener("abort", resolve))
for (let i = 10; i >= 0 && signal?.aborted != true; --i) {
timerCount.textContent = i
await Promise.race([delay(1000), aborted])
}
timerCount.textContent = ""
}
async function startCountdown() {
const ac = new AbortController()
const abort = () => ac.abort()
reset.addEventListener("click", abort, { once: true })
reset.textContent = "Cancel"
await countDown(ac.signal)
reset.removeEventListener("click", abort)
reset.addEventListener("click", startCountdown, { once: true })
reset.textContent = "Start"
}
startCountdown()
<p id="timer-count"></p>
<button>Start</button>
Alternatively, you might want to model the countdown as an object that implements EventTarget.
const timerCount = document.querySelector("#timer-count")
const btn = document.querySelector("button")
class Timer extends EventTarget {
#value; #tick_rate; #enabled; #interval_handle
constructor(tick_rate = 1000) {
super()
this.#value = 0
this.#tick_rate = tick_rate
this.#enabled = false
this.#interval_handle = null
}
get value() {
return this.#value
}
set value(value) {
this.#value = value
this.dispatchEvent(new Event("update"))
}
get tick_rate() {
return this.#tick_rate
}
get enabled() {
return this.#enabled
}
start() {
if (this.#enabled) return
this.#enabled = true
this.#interval_handle = setInterval(Timer.#tick, this.#tick_rate, this)
this.dispatchEvent(new Event("start"))
}
stop() {
if (!this.#enabled) return
this.#enabled = false
clearInterval(this.#interval_handle)
this.dispatchEvent(new Event("stop"))
}
static #tick(timer) {
timer.value = Math.max(0, timer.value - 1)
if (timer.value == 0) timer.stop()
}
}
const timer = new Timer()
timer.addEventListener("start", function() {
btn.textContent = "Stop"
})
timer.addEventListener("stop", function() {
timerCount.textContent = ""
btn.textContent = "Start"
})
timer.addEventListener("update", function() {
timerCount.textContent = timer.value
})
btn.addEventListener("click", function() {
if (timer.enabled == false) {
timer.value = 5
timer.start()
} else {
timer.stop()
}
})
<p id="timer-count"></p>
<button>Start</button>
Here's a countdown. The first count is after 1 second.
let count = 3;
let timer = [];
const start = () => {
new Array(count).fill(true).forEach((_,i) => {
timer.push(setTimeout(() => console.log('count',count - i),(i+1) * 1000))
})
}
const stop = () => timer.forEach(clearTimeout);
<button onclick="stop()">STOP</button>
<button onclick="start()">START</button>

Error on Sortting Items is not a function

I have a problem with my code
So, there is picture with like, when i click on the heart that add a like
there is a container down ma page and then the likes are also displayed in it.
(picture to show you a context)
The problem is everything work fine, but when you use my function "filter"
the likes are working on the photos but not down the page and that return me an error i don't really understand from where that come.
So can you help me please ?
import showMethods from "../factories/showMethods.js";
import lightbox from "../factories/lightbox.js";
import formularSecur from "../factories/formularSecur.js";
export default class OnePhotographer {
constructor() {
this.photographer = null;
this.medias = [];
this.likes = 0;
this.lightbox = lightbox;
this.indexLightbox = 0;
this.getPhotographer();
this.eInit();
}
eInit() {
document.getElementById("filtre").addEventListener("change", (e) => {
this.filter(e.target.value);
});
document.getElementById("filtre").addEventListener("click", (e) => {
this.filter(e.target.value);
});
document.getElementById("contactForm").addEventListener("submit", (e) => {
e.preventDefault();
formularSecur.checkForm(this);
document.getElementById("contactForm").reset();
});
this.lightbox.init(this);
document.getElementById("closeLightbox").addEventListener("click", () => {
this.lightbox.closeLightbox();
});
document.getElementById("prevBox").addEventListener("click", () => {
this.lightbox.navLightbox("prev");
});
document.getElementById("nextBox").addEventListener("click", () => {
this.lightbox.navLightbox("next");
});
document.addEventListener("keydown", (event) => {
if (event.code == "ArrowLeft") {
this.lightbox.navLightbox("prev");
} else if (event.code == "ArrowRight") {
this.lightbox.navLightbox("next");
} else if (event.code == "Escape") {
this.lightbox.closeLightbox();
}
});
}
filter(param) {
if (param === "popular") {
this.medias = this.medias.sort(this.order(param));
} else {
this.medias = this.medias.sort(this.order(param)).reverse();
}
document.querySelector(".containerMedias").innerHTML = "";
this.medias.forEach((media, index) => {
showMethods.showMedia(media, index);
showMethods.showLikes(this, instance);
});
}
order(settings) {
return function (x, y) {
if (x[settings] < y[settings]) {
return -1;
}
if (x[settings] > y[settings]) {
return 1;
}
return 0;
};
}
setMedias(media) {
this.medias = media;
}
async getPhotographer() {
const response = await fetch("./data/photographers.json");
const res = await response.json();
const id = this.idUrlCatch();
res.photographers.forEach((photographer) => {
if (photographer.id == id) {
this.photographer = photographer;
res.media.forEach((media) => {
if (media.photographerId == this.photographer.id) {
this.medias.push(media);
this.likes += media.likes;
}
});
this.medias = this.medias.sort(this.order("popular"));
}
});
showMethods.showData(this);
showMethods.showLikes(this, instance);
}
idUrlCatch() {
const str = window.location.href;
const url = new URL(str);
if (url.searchParams.get("id")) {
const id = url.searchParams.get("id");
return id;
}
}
showData() {
showMethods.showData(this);
showMethods.showLikes(this, instance);
}
showHeader() {
showMethods.showHeader(this);
}
showMedia(media, index) {
showMethods.showMedia(media, index, this);
}
showLikes() {
showMethods.showLikes(this, instance);
}
setSrcLightbox() {
showMethods.setSrcLightbox(this);
}
closeLightbox() {
showMethods.closeLightbox(this);
}
navLightbox() {
}
}
this is the second part of my code
import lightbox from "../factories/lightbox.js";
export default {
showData: function (instance) {
this.showHeader(instance);
instance.medias.forEach((media, index) => {
this.showMedia(media, index, instance);
});
instance.showLikes();
},
showHeader: function (instance) {
const infoHeader = document.querySelector(".photograph-header .containerInfo");
const title = document.createElement("h1");
title.textContent = instance.photographer.name;
const loc = document.createElement("h3");
loc.textContent = `${instance.photographer.city}, ${instance.photographer.country}`;
const tagline = document.createElement("span");
tagline.textContent = instance.photographer.tagline;
infoHeader.appendChild(title);
infoHeader.appendChild(loc);
const headerImg = document.querySelector(
".photograph-header .containerImg"
);
const pic = `assets/photographers/${instance.photographer.portrait}`;
const img = document.createElement("img");
img.setAttribute("src", pic);
img.setAttribute("alt", instance.photographer.name);
headerImg.appendChild(img);
},
showMedia: function (media, index, instance) {
const mediaSection = document.querySelector(".containerMedias");
const card = document.createElement("div");
card.classList.add("cardMedia");
const containerImg = document.createElement("div");
containerImg.classList.add("containerImg");
if (media.image) {
const mediaUrl = `assets/medias/${media.image}`;
const mediaItem = document.createElement("img");
mediaItem.dataset.index = index;
mediaItem.setAttribute("tabindex", 0);
mediaItem.addEventListener("click", (e) => {
lightbox.showLightbox(e, instance);
});
mediaItem.addEventListener("focus", () => {
mediaItem.addEventListener("keydown", (e) => {
if (e.code == "Enter") {
lightbox.showLightbox(e, instance);
}
});
});
mediaItem.setAttribute("src", mediaUrl);
mediaItem.setAttribute("alt", media.title);
containerImg.appendChild(mediaItem);
} else {
const mediaUrl = `assets/medias/${media.video}`;
const mediaItem = document.createElement("video");
mediaItem.dataset.index = index;
mediaItem.controls = true;
mediaItem.classList.add("media");
mediaItem.addEventListener("click", (e) => {
lightbox.showLightbox(e, instance);
});
mediaItem.setAttribute("src", mediaUrl);
mediaItem.setAttribute("alt", media.title);
mediaItem.setAttribute("data-index", index);
containerImg.appendChild(mediaItem);
}
const subContain = document.createElement("div");
subContain.classList.add("subContain");
const picTitle = document.createElement("h3");
picTitle.textContent = media.title;
const like = document.createElement("span");
like.setAttribute("tabindex", 0);
like.classList.add("like");
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
like.addEventListener("click", () => {
like.classList.toggle("active");
if (like.classList.contains("active")) {
media.likes += 1;
instance.likes += 1;
like.innerHTML = `${media.likes} <i class="fa-solid fa-heart"></i>`;
} else {
media.likes -= 1;
instance.likes -= 1;
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
}
instance.showLikes();
});
like.addEventListener("focus", () => {
like.addEventListener("keydown", (event) => {
if (event.code == "Enter") {
like.classList.toggle("active");
if (like.classList.contains("active")) {
media.likes += 1;
instance.likes += 1;
like.innerHTML = `${media.likes} <i class="fa-solid fa-heart"></i>`;
} else {
media.likes -= 1;
instance.likes -= 1;
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
}
instance.showLikes();
}
});
});
subContain.appendChild(picTitle);
subContain.appendChild(like);
card.appendChild(containerImg);
card.appendChild(subContain);
mediaSection.appendChild(card);
},
showLikes: function (instance) {
const priceContainer = document.createElement("div");
priceContainer.classList.add("priceContainer");
const likes = document.createElement("span");
likes.classList.add("likes");
likes.innerHTML = `<i class="fa-solid fa-heart"></i> ${instance.likes}`;
const priceDay = document.createElement("span");
priceDay.classList.add("priceDay");
priceDay.innerHTML = `<i class="fa-solid fa-dollar-sign"></i> ${instance.photographer.price}`;
priceContainer.appendChild(likes);
priceContainer.appendChild(priceDay);
document.querySelector(".photograph-header").appendChild(priceContainer);
},
};
i don't really get why with my function showMedia everithing is fine, but with showLikes not
the code error
Uncaught TypeError: instance.showLikes is not a function
at HTMLSpanElement.<anonymous> (showMethods.js:101:16)
(anonyme) # showMethods.js:101
if you help me that will save my life
Thank you for your time <3
So i find why the bug is happening when i use my "filter" Function.
I lose Instance on "showLikes" Function, so i don't have accès to the current instance.
One of my solution make my code in the onePhotograph.js, so no Instance ==> No bug.
I know this is maybe not the only solution, and maybe not the best. But this is a great solution also !
Best Regards

Adding auto play to Javascript image slider

I have a slider and I want to add auto play every 3 seconds. I tried to use SetInterval but nothing happened.
Also I would like to remove the code for removing elements from the slider. I want to remove all the controls also and have only the slider changing image every 3 seconds.
this is the slider code
const galleryContainer = document.querySelector('.gallery-container');
const galleryControlsContainer = document.querySelector('.gallery-controls');
const galleryControls = ['Предидущий', '', 'Следующий'];
const galleryItems = document.querySelectorAll('.gallery-item');
class Carousel {
constructor(container, items, controls) {
this.carouselContainer = container;
this.carouselControls = controls;
this.carouselArray = [...items];
}
updateGallery() {
this.carouselArray.forEach(el => {
el.classList.remove('gallery-item-1');
el.classList.remove('gallery-item-2');
el.classList.remove('gallery-item-3');
el.classList.remove('gallery-item-4');
el.classList.remove('gallery-item-5');
});
this.carouselArray.slice(0, 5).forEach((el, i) => {
el.classList.add(`gallery-item-${i+1}`);
});
}
setCurrentState(direction) {
if (direction.className == 'gallery-controls-previous') {
this.carouselArray.unshift(this.carouselArray.pop());
} else {
this.carouselArray.push(this.carouselArray.shift());
}
this.updateGallery();
}
setControls() {
this.carouselControls.forEach(control => {
galleryControlsContainer.appendChild(document.createElement('button')).className = `gallery-controls-${control}`;
document.querySelector(`.gallery-controls-${control}`).innerText = control;
});
}
useControls() {
const triggers = [...galleryControlsContainer.childNodes];
triggers.forEach(control => {
control.addEventListener('click', e => {
e.preventDefault();
if (control.className == 'gallery-controls-add') {
const newItem = document.createElement('img');
const latestItem = this.carouselArray.length;
const latestIndex = this.carouselArray.findIndex(item => item.getAttribute('data-index') == this.carouselArray.length)+1;
Object.assign(newItem,{
className: 'gallery-item',
src: `http://fakeimg.pl/300/?text=${this.carouselArray.length+1}`
});
newItem.setAttribute('data-index', this.carouselArray.length+1);
this.carouselArray.splice(latestIndex, 0, newItem);
document.querySelector(`[data-index="${latestItem}"]`).after(newItem);
this.updateGallery();
} else {
this.setCurrentState(control);
}
});
});
}
}
const exampleCarousel = new Carousel(galleryContainer, galleryItems, galleryControls);
exampleCarousel.setControls();
exampleCarousel.useControls();
See the below code, I have added auto slide functionality with interval time 5 seconds.
If you want to change the interval, Please update the setInterval time in the "constructor" and "setCurrentState" functions.
I have removed the controls. To remove the controls, we need to comment the last 2 lines
//exampleCarousel.setControls();
//exampleCarousel.useControls();
const galleryContainer = document.querySelector('.gallery-container');
const galleryControlsContainer = document.querySelector('.gallery-controls');
const galleryControls = ['Предидущий', '', 'Следующий'];
const galleryItems = document.querySelectorAll('.gallery-item');
class Carousel {
constructor(container, items, controls) {
this.carouselContainer = container;
this.carouselControls = controls;
this.carouselArray = [...items];
this.mySlideInterval = null;
this.mySlideInterval = setInterval(
this.autoSlide.bind(this),
5000
);
}
autoSlide() {
this.carouselArray.push(this.carouselArray.shift());
this.updateGallery();
}
updateGallery() {
this.carouselArray.forEach(el => {
el.classList.remove('gallery-item-1');
el.classList.remove('gallery-item-2');
el.classList.remove('gallery-item-3');
el.classList.remove('gallery-item-4');
el.classList.remove('gallery-item-5');
});
this.carouselArray.slice(0, 5).forEach((el, i) => {
el.classList.add(`gallery-item-${i+1}`);
});
}
setCurrentState(direction) {
if (direction.className == 'gallery-controls-previous') {
this.carouselArray.unshift(this.carouselArray.pop());
} else {
this.carouselArray.push(this.carouselArray.shift());
}
clearInterval(this.mySlideInterval);
this.updateGallery();
this.mySlideInterval = setInterval(
this.autoSlide.bind(this),
5000
);
}
setControls() {
this.carouselControls.forEach(control => {
galleryControlsContainer.appendChild(document.createElement('button')).className = `gallery-controls-${control}`;
document.querySelector(`.gallery-controls-${control}`).innerText = control;
});
}
useControls() {
const triggers = [...galleryControlsContainer.childNodes];
triggers.forEach(control => {
control.addEventListener('click', e => {
e.preventDefault();
if (control.className == 'gallery-controls-add') {
const newItem = document.createElement('img');
const latestItem = this.carouselArray.length;
const latestIndex = this.carouselArray.findIndex(item => item.getAttribute('data-index') == this.carouselArray.length)+1;
Object.assign(newItem,{
className: 'gallery-item',
src: `http://fakeimg.pl/300/?text=${this.carouselArray.length+1}`
});
newItem.setAttribute('data-index', this.carouselArray.length+1);
this.carouselArray.splice(latestIndex, 0, newItem);
document.querySelector(`[data-index="${latestItem}"]`).after(newItem);
this.updateGallery();
} else {
this.setCurrentState(control);
}
});
});
}
}
const exampleCarousel = new Carousel(galleryContainer, galleryItems, galleryControls);
//exampleCarousel.setControls();
//exampleCarousel.useControls();

Issue with todo counter

Hello I've got a basic todo going but I am having an issue where if user clicks on a todo item as completed and then deletes it, the counter showing how many todos are left decreases twice. Is there a way to prevent this?
let todoCounter = 0;
counterDisplay = document.querySelector('#todo-counter');
counterDisplay.textContent = `${todoCounter} todos left`;
document.querySelector('#new-todo').addEventListener('keypress', e =>
{
text = e.target.value;
if (e.keyCode === 13 || e.which === 13)
{
addTodo(text);
todoCounter++;
counterDisplay.textContent = `${todoCounter} todos left`;
}
});
const addTodo = text =>
{
const todoEl = document.createElement('li');
const todoText = document.createTextNode(text);
const deleteButton = document.createElement('button');
todoEl.appendChild(todoText);
todoEl.appendChild(deleteButton);
const todoDom = document.querySelector('#todos');
todoDom.appendChild(todoEl);
};
document.querySelector('#todos').addEventListener('click', e =>
{
target = e.target;
if (!target.matches('button'))
{
if(target.matches('li'))
{
target.classList.toggle('completed');
if (target.classList.contains('completed'))
{
todoCounter--;
counterDisplay.textContent = `${todoCounter} todos left`;
}
else
{
todoCounter++;
counterDisplay.textContent = `${todoCounter} todos left`;
}
}
return;
}
target.parentNode.parentNode.removeChild(target.parentNode);
todoCounter--;
counterDisplay.textContent = `${todoCounter} todos left`;
})
https://jsfiddle.net/dk5690/aLn310y2/
The condition in your if statement when true triggers the .completed CSS Class and the todoCounter is either decremented or incremented (toggles as you click on the li tag), when it's decremented there's no (code) to check if the parent node is in the completed state, not to decrement the counter twice for the same todo when the delete button is clicked.
Here's the code fragment that you could add to ensure the completed state of the parent node (i.e the li tag) is checked. The fragment should be added immediately after your if statement closing tag, and the rest of the code after that deleted.
else if (target.matches('button') && target.parentNode.classList.contains('completed')) {
target.parentNode.parentNode.removeChild(target.parentNode);
}
else if (target.matches('button')) {
target.parentNode.parentNode.removeChild(target.parentNode);
todoCounter--;
counterDisplay.textContent = `${todoCounter} todos left`;
}
Below is the code refactored and running:
let todoCounter = 0;
counterDisplay = document.querySelector('#todo-counter');
counterDisplay.textContent = `${todoCounter} todos left`;
document.querySelector('#new-todo').addEventListener('keypress', e => {
text = e.target.value;
if (e.keyCode === 13 || e.which === 13) {
addTodo(text);
todoCounter++;
counterDisplay.textContent = `${todoCounter} todos left`;
document.getElementById('new-todo').value = '';
}
});
const addTodo = text => {
const todoEl = document.createElement('li');
const todoText = document.createTextNode(text);
let deleteButton = document.createElement('button');
// and give it some content
let deleteContent = document.createTextNode("Delete");
// add the text node to the newly created button
deleteButton.appendChild(deleteContent);
todoEl.appendChild(todoText);
todoEl.appendChild(deleteButton);
const todoDom = document.querySelector('#todos');
todoDom.appendChild(todoEl);
};
document.querySelector('#todos').addEventListener('click', e => {
target = e.target;
if (target.matches('li')) {
target.classList.toggle('completed');
if (target.classList.contains('completed')) {
todoCounter--;
counterDisplay.textContent = `${todoCounter} todos left`;
}
else {
todoCounter++;
counterDisplay.textContent = `${todoCounter} todos left`;
}
}
else if (target.matches('button') && target.parentNode.classList.contains('completed')) {
target.parentNode.parentNode.removeChild(target.parentNode);
}
else if (target.matches('button')) {
target.parentNode.parentNode.removeChild(target.parentNode);
todoCounter--;
counterDisplay.textContent = `${todoCounter} todos left`;
}
});
.completed {
text-decoration: line-through;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo</title>
</head>
<body>
<div id="todo-counter"></div>
<input type="text" id="new-todo"/>
<ul id="todos"></ul>
</body>
</html>

Categories

Resources