ScrollIntoView() doesn't work - vanilla JS, why? - javascript

My idea is to have a simple search function, where people can type text in a search box and see if there is anything matched with the text in the html. If it does, the matched part(let's say it might be in later half of the whole HTML page that's not seenable on your current screen/viewport) will jump/scroll into view. If it doesn't, you get "no match found" message in the #feedback tag.
HTML looks like this:
<input id="search-text">
<p id="feedback"></p>
<p>title1</p>
<p>title2</p>
<p>title3</p>
JS is like this:
let searchText = ''
const titlesNodes = document.querySelectorAll('p')
const titles = Array.from(titlesNodes)
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
searchFunction()
})
const searchFunction = () => {
const filteredTitles = titles.filter((title) => title.innerText.toLowerCase().includes(searchText.toLowerCase()) )
const msg = document.querySelector('#feedback')
msg.innerHTML = ''
if (filteredTitles.length > 0){
filteredTitles[0].scrollIntoView()
} else {
msg.textContent = 'No match found'
}
}
So far I'm able to get the "no matched found" but when I type something that's only in the later half of the page, it doesn't jump to the view. I tested on both Chromium and Firefox and I use Linux.
What is wrong with my code? Thanks!

This is because by default, browser will set your <input> element into screen when you type in. So your own call is overridden by browser's default one.
Not sure what's the best way to avoid that though...
You may try to call scrollIntoView after a small timeout, so it occurs after the one of the input. If you use requestAnimationFrame timing function, it should occur just before the next paint, so you shouldn't notice the one of the input occurred.
let searchText = '';
const titlesNodes = document.querySelectorAll('p')
const titles = Array.from(titlesNodes)
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
searchFunction()
})
const searchFunction = () => {
const filteredTitles = titles.filter((title) => title.innerText.toLowerCase().includes(searchText.toLowerCase()))
const msg = document.querySelector('#feedback')
msg.innerHTML = ''
if (filteredTitles.length > 0) {
requestAnimationFrame(() => { // wait just a bit
filteredTitles[0].scrollIntoView()
});
} else {
msg.textContent = 'No match found'
}
}
#feedback { margin-bottom: 125vh }
p { margin-bottom: 50vh }
<input id="search-text">
<p id="feedback"></p>
<p>title1</p>
<p>title2</p>
<p>title3</p>

Try changing:
- filteredTitles[0].scrollIntoView()
+ filteredTitles[0].scrollIntoView({alignToTop: true})
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
- searchFunction()
+ setTimeout(searchFunction, 500)})
})

Related

Close button popup doesn't work (JAVASCRIPT)

i'm trying to create a custom pupop in javascript, this is my first time with this.
I have a problem with the close button, the "x" target correctly the div to close, but doesn't remove the "active" class at click.
https://demomadeingenesi.it/demo-cedolino/
HTML CODE
<div class="spot spot-2">
<div class="pin"></div>
<div class="contenuto-spot flex flex-col gap-3">
<img class="chiudi-popup" src="img/chiudi.svg" />
[---CONTENT---]
</div>
</div>
</div>
JAVASCRIPT CODE
const tooltips = function () {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
const chiudiPopup = document.querySelector(".chiudi-popup");
spot.classList.add("active");
contenuto.classList.add("show");
if (curActive && curActive !== spot) {
curActive.classList.toggle("active");
contActive.classList.toggle("show");
}
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
});
});
const chiudiPopup = document.querySelector(".chiudi-popup");
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
What the code above does is adding an click listener, but it's inside another click listener, so all it's doing is adding an click listener on the first .chiudi-popup that removes .active and .show from the last spot element.
It's hard to see if this is correct, because you haven't given us enough to reproduce the problem, but I moved the code above outside the spot.addEventListener("click", () => { and instead of searching the whole document with const chiudiPopup = document.querySelector(".chiudi-popup"); the code nows only targets the .chuidi-popup element within the spot: const chiudiPopup = spot.querySelector(".chiudi-popup");
const tooltips = function() {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
spot.classList.add("active");
contenuto.classList.add("show");
if (curActive && curActive !== spot) {
curActive.classList.toggle("active");
contActive.classList.toggle("show");
}
});
// MOVED FROM THE CLICK LISTENER
const chiudiPopup = spot.querySelector(".chiudi-popup");
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
});
EDIT: I missed that you have the img.chiudi-popup inside your container, which will trigger both event listeners. I would honestly just simplify the code and always hide the container when clicking on it again. You can still have the img.chiudi-popup (close image) to make it easier for the users to understand that they can click on it.
const tooltips = function() {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
if (curActive !== spot) {
spot.classList.add("active");
contenuto.classList.add("show");
}
if (curActive) {
curActive.classList.remove("active");
contActive.classList.remove("show");
}
});

Cypress with stripe: elements height not loaded

I'm using cypress since a week, and I succesfully did an integration with the stripe iframe: I've used this code:
in cypress/support/command.js
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow)
} else {
$iframe.on('load', () => {
resolve(contentWindow)
})
}
})
})
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
Cypress.$(selector, document),
)
In cypress/integration/staging-web/web.test.js
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="exp-date"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('1225')
}
})
cy.get('iframe')
.eq(2)
.iframeLoaded()
.its('document')
.getInDocument('[name="cvc"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('123')
}
})
cy.get('.mt1 > .relative > .input').type('utente')
My problem is that during the page loading, cypress does not wait until stripe fields are fully loaded, and I get an error because happens this (sorry for not-english language, but it's a screenshot):
Those lines are:
cardnumber
expiration date , pin number
4th line is card owner
I've tried with .should('be.visibile') but it does nothing; plus I've tried with
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
but no way, it always gives me an error; in this latter case, it doesn't even give an error, it just goes on without filling the fields and after this it stops because it cant go on in the test.
If I add cy.wait(800) before the code in web.test.js it works fine, but I don't want to use wait, because it's basically wrong (what happens if it loads after 5 seconds?).
is there a way to check that those elements must have an height?
Remember that they are in an iframe (sadly).
If I add cy.wait(800) ... it works fine.
This is because you are not using Cypress commands with auto-retry inside getInDocument().
Cypress.$(selector) is jQuery, it just attempts to grab the element, not retry for async loading, and no test failure when not found.
Should use a proper command with retry, like
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).find(selector)
)
or you might need to work from body
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).its('body')
.find(selector)
.should('be.visible')
)
Without a test system I'm not sure exactly which one is correct syntax, but you get the idea.
Also, too many custom commands. You always follow .iframeLoaded() with .its('document'), so just wrap it all up in iframeLoaded custom command.
In fact, resolve(contentWindow.document.body) because it's a better point to chain .find(selector).
This is my test against the Stripe demo page,
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow.document.body)
} else {
$iframe.on('load', () => {
resolve(contentWindow.document.body)
})
}
})
})
it('finds card number', () => {
cy.viewport(1000, 1000)
cy.visit('https://stripe-payments-demo.appspot.com/')
cy.get('iframe')
.should('have.length.gt', 1) // sometimes 2nd iframe is slow to load
.eq(1)
.iframeLoaded()
.find('input[name="cardnumber"]')
.should('be.visible')
})
#Nanker Phelge by the way I updated my function in this way, I posted it here because there was no space in comments:
UPDATE: I put waitForMillisecs inside the function -_- it HAVE TO stay outside! Correction made.
const ADD_MS = 200
const STACK_LIMIT = 15
let stackNumber = 0
let waitForMillisecs = 500
function checkAgainStripe() {
stackNumber++
if (stackNumber >= STACK_LIMIT) {
throw new Error('Error: too many tries on Stripe iframe. Limit reached is', STACK_LIMIT)
}
cy.get('.btn').scrollIntoView().should('be.visible').click()
cy.get('iframe')
.eq(1)
.then($elem => {
//console.log($elem[0].offsetHeight)
//console.log($elem[0])
cy.log('now wait for ' + waitForMillisecs + '; stack number is ' + stackNumber)
cy.wait(waitForMillisecs)
waitForMillisecs = waitForMillisecs + ADD_MS
if (!$elem[0] || !$elem[0].offsetHeight || $elem[0].offsetHeight < 19) {
console.log('entered in if')
cy.reload()
return checkAgainStripe()
}
})
}

push is not a function javascript

I am new in JavaScript. I wrote function to save data to local storage, but every time I try to save a data,it doesnt work and it always get this error: push is not a function.and this error just appears a second in console.log and dissappear by itself. apart from this my problem is it doesn't save the data to the local storage How can I fix this? (the function,not working is "addTodoToStorage"
const form =document.querySelector("#todo-form");
const todoInput = document.querySelector("#todo");
const todolist= document.querySelector(".list-group");
const firstCardBody= document.querySelectorAll(".card-body")[0];
const secondCardBody= document.querySelectorAll(".card-body")[1];
const filter = document.querySelector("#filter");
const clearButton = document.querySelector("clear-todos");
eventListeners();
function eventListeners() {
form.addEventListener("submit",addTodo);
}
function addTodo(e) {
const newTodo = todoInput.value.trim();
if (newTodo === ""){
showAlert("danger","please type something");
}
else{
addTodoToUI(newTodo);
addTodoToStorage(newTodo);
showAlert("success","successfully added");
}
e.preventDefault();
}
function getTodosFromStorage() {
let todos;
if(localStorage.getItem("todos") === null){
todos = [];
}
else{
todos= JSON.parse(localStorage.getItem("todos"));
}
return todos;
}
function addTodoToStorage(newTodo) {
let todos = getTodosFromStorage();
todos.push(newTodo);
localStorage.setItem("todos",JSON.stringify(todos));
}
function showAlert(type,message) {
const alert = document.createElement("div");
alert.className= `alert alert-${type}`;
alert.textContent= message;
firstCardBody.appendChild(alert);
//set time out
setTimeout(function () {
alert.remove();
},2000);
}
function addTodoToUI(newTodo) {
const listItem = document.createElement("li");
const link = document.createElement("a");
link.href="#";
link.className="delete-item";
link.innerHTML='<i class = "fa fa-remove"></i>';
listItem.className="list-group-item d-flex justify-content-between";
listItem.appendChild(document.createTextNode(newTodo));
listItem.appendChild(link);
todolist.appendChild(listItem);
todoInput.value = "";
}
As for the error msg disappearing after a while, it's because you haven't prevented the default behavior of the form. Or rather, you have prevented it at the bottom, which is fine if you want the form to prevent GET-ing(its default method) to the URL(in action attribute).
But if an error occurs inside the submit handler (in your case, it probably does), the JavaScript stops the execution, and the form continues its default behavior, because there's is nothing to tell it that it can't do that. It can, so it will. HTML forms be ratchet like that. Due to which the page reloads.
For the part why there's an error, I couldn't find any errors here. If this is the same code you're using, there shouldn't be a problem.
EDIT
I tried to reproduce your example as follows and it works. It adds items to my localStorage successfully.
<form id="todo-form">
<input id="todo" type="text" />
</form>
<script>
const form =document.querySelector("#todo-form");
const todoInput = document.querySelector("#todo");
eventListeners();
function eventListeners() {
form.addEventListener("submit",addTodo);
}
function addTodo(e) {
e.preventDefault();
const newTodo = todoInput.value.trim();
if (newTodo === ""){
showAlert("danger","please type something");
}
else{
addTodoToStorage(newTodo);
showAlert("success","successfully added");
}
}
function getTodosFromStorage() {
let todos;
if(localStorage.getItem("todos") === null){
todos = [];
}
else{
todos= JSON.parse(localStorage.getItem("todos"));
}
return todos;
}
function addTodoToStorage(newTodo) {
let todos = getTodosFromStorage();
todos.push(newTodo);
localStorage.setItem("todos",JSON.stringify(todos));
}
function showAlert(type,message) {
const alert = document.createElement("div");
alert.className = `alert alert-${type}`;
alert.textContent = message;
document.body.appendChild(alert);
//set time out
setTimeout(function () {
alert.remove();
}, 2000);
}
</script>

Is this the correct format to write this? It's not working

I'm getting back to learning JavaScript and wondering why this isn't working. No errors are showing in the console.
I made it work with dry coding it but now I'm trying to not use dry and it's not working.
let simonLi = document.querySelector('#simon');
let simonPic = document.querySelector('#simon-pic');
let bruceLi = document.querySelector('#bruce');
let brucePic = document.querySelector('#bruce-pic');
let benLi = document.querySelector('#ben');
let benPic = document.querySelector('#ben-pic');
let pictureChange = pic => {
if (pic.className === "hide") {
pic.classList.remove("hide");
} else {
pic.classList.add("hide");
}
};
simonLi.addEventListener('click', pictureChange(simonPic));
bruceLi.addEventListener('click', pictureChange(brucePic));
benLi.addEventListener('click', pictureChange(benPic));
No error messages and it's suppose to hide and show the image whenever the li is click.
You are attaching the result of the function pictureChange (which happens to be undefined)to the click event, instead of attaching a function.
You can try replacing
let pictureChange = pic => {
if (pic.classList.contains("hide")) {
pic.classList.remove("hide");
} else {
pic.classList.add("hide");
}
};
With
let pictureChange = pic => {
return function() {
if (pic.className === "hide") {
pic.classList.remove("hide");
} else {
pic.classList.add("hide");
}
}
};
While your at it, consider using pic.classList.toggle() instead.
This might work better:
let simonLi = document.querySelector('#simon');
let simonPic = document.querySelector('#simon-pic');
let bruceLi = document.querySelector('#bruce');
let brucePic = document.querySelector('#bruce-pic');
let benLi = document.querySelector('#ben');
let benPic = document.querySelector('#ben-pic');
function pictureChange(pic) {
pic.classList.toggle('hide')
}
simonLi.addEventListener('click', function(e) {
pictureChange(simonPic)
});
bruceLi.addEventListener('click', function(e) {
pictureChange(brucePic)
});
benLi.addEventListener('click', function(e) {
pictureChange(benPic)
});
.hide {
color: red;
}
<div id="simon"><span id="simon-pic">Simon</span></div>
<br />
<div id="bruce"><span id="bruce-pic">Bruce</span></div>
<br />
<div id="ben"><span id="ben-pic">Ben</span></div>
This shortens your code sigificantly:
for (const li of document.querySelectorAll('li')) {
li.addEventListener('click', (event) => {
if (event.target.matches('li[id]')) {
document.getElementById(`${event.target.id}-pic`).classList.toggle('hide');
}
})
}
This assumes all your li work the way you've shown. Otherwise, give all those li a common CSS class, e.g. class="li-with-pic" and adjust the querySelector accordingly:
for (const li of document.querySelectorAll('.li-with-pic')) {
li.addEventListener('click', (event) => {
if (event.target.matches('.li-with-pic')) {
document.getElementById(`${event.target.id}-pic`).classList.toggle('hide');
}
})
}

Custom event on fragment change in Reveal.js

What would be the right way to programmatically add fragments to a slide in Reveal.js? I have a JavaScript widget on a slide that can go through 5 states, and I would like to go through them with fragment transitions.
I tried to achieve something similar with dummy fragments, like in the representative example below. This is intended to change the src of an image on fragment change. The example has an issue, though. When approaching a slide by pressing previous a number of times, the slide should start at its last fragment state. In the example, however, the image src starts in state 1, and doesn't know how to go further back on additional previous-steps.
Any pointers would be appreciated!
<img src="img1.png" id="my-image">
<span class="fragment update-img-src" data-target="my-image" data-src="img2.svg"></span>
<script>
Reveal.addEventListener('fragmentshown', function(event) {
if (event.fragment.classList.contains('update-img-src')) {
// Find the target image by ID
var target = document.getElementById(event.fragment.dataset.target);
// Keep a stack of previously shown images, so we can always revert back on 'fragmenthidden'
if (target.dataset.stack == null) {
target.dataset.stack = JSON.stringify([target.getAttribute('src')]);
}
target.dataset.stack = JSON.stringify([event.fragment.dataset.src, ...JSON.parse(target.dataset.stack)]);
// Update the image
target.setAttribute('src', event.fragment.dataset.src);
}
});
Reveal.addEventListener('fragmenthidden', function(event) {
if (event.fragment.classList.contains('update-img-src')) {
// Return to the previously shown image.
// Remove the top from the history stack
var target = document.getElementById(event.fragment.dataset.target);
if (target.dataset.stack == null) {
console.log('Trying to hide', event.fragment.dataset.src, 'but there is no stack.');
} else {
var [_, ...tail] = JSON.parse(target.dataset.stack);
target.dataset.stack = JSON.stringify(tail);
// Set the image source to the previous value
target.setAttribute('src', tail[0]);
}
}
});
</script>
Here's a hacky solution that I put together. It allows you to register any number of fragments on a slide with a callback function.
function registerFakeFragments(slide, fragmentIndices, stateChangeHandler) {
const identifier = `fake-${Math.round(1000000000*Math.random())}`;
let i = 1;
for (let fragmentIndex of fragmentIndices) {
const span = document.createElement('span');
span.dataset.target = identifier;
span.classList.add('fragment');
span.classList.add('fake-fragment');
span.setAttribute('data-fragment-index', JSON.stringify(fragmentIndex));
span.dataset.stateIndex = JSON.stringify(i);
slide.appendChild(span);
++i;
}
let currentState = null; // last reported state
const listener = () => {
const currentSlide = Reveal.getCurrentSlide();
if (currentSlide && currentSlide === slide) {
// Find the latest visible state
let state = 0;
currentSlide.querySelectorAll(`.fake-fragment.visible[data-target=${identifier}]`).forEach(f => {
const index = JSON.parse(f.dataset.stateIndex);
if (index > state) {
state = index;
}
});
// If the state changed, call the handler.
if (state != currentState) {
stateChangeHandler(state);
currentState = state;
}
}
};
Reveal.addEventListener('fragmentshown', listener);
Reveal.addEventListener('fragmenthidden', listener);
Reveal.addEventListener('slidechanged', listener);
}

Categories

Resources