Event listener firing numerous time - javascript

I have the following code which is firing Event a lot of times and I want it to fire one time each time the modal appears, the gist of the code is to wait for Modal to be a part of DOM after which I run the function hence setTimeout to override the Modal classes. After which I have Event which is firing numerous times.
If I use clearInterval, my CSS isn't applied
setInterval(() => {
if (document.querySelector('[data-test-id="modal"]')) {
poller(['[data-test-id="modal"]'], () => {
let msgNode = document.querySelectorAll('[class*="AvailabilityMessagesstyled__"]');
[...msgNode].forEach((delivery) => {
setTimeout(() => {
if (delivery.children[0].innerText.indexOf("Available") !== -1 ||
delivery.children[0].innerText.indexOf("available") !== -1) {
delivery.children[0].classList.add("TP")
}
if (delivery.children[0].innerText.indexOf("Unavailable") !== -1) {
delivery.children[0].classList.add("TP__red")
}
}, 200)
// qty
let qtyCounter = Array.from(document.querySelectorAll('[data-test-id="qty-selector"]'));
console.log(qtyCounter);
[...qtyCounter].forEach((counter) => {
counter.addEventListener("click", function() {
console.log('clicked')
})
})
});
})
}
}, 2000)
I am not sure what I am doing wrong?

Related

Mouse Leave is not triggered correctly

I have a button that expands on mousehover, and retracts on mouseleave, but in certain situations the button does not retract. I implemented a timer to avoid multiple triggers, but I was not successful, I tried to do it through css, but I had problems with synchronization between the animations.
It is possible to see this behavior through this fiddle
Is there any solution for this and why is this happening?
options.forEach(option => {
option.addEventListener('mouseenter', event => {
let btnExpandable = event.target.childNodes[1];
let optionsContainer = event.target.childNodes[3];
if(!animated) {
setTimeout(() => {
btnExpandable.classList.add('btn--expandable-expand');
optionsContainer.classList.add('options--expand');
animated = false;
},100);
}
animated = true;
});
option.addEventListener('mouseleave', event => {
let btnExpandable = event.target.childNodes[1];
let optionsContainer = event.target.childNodes[3];
if(!animated) {
setTimeout(() => {
btnExpandable.classList.remove('btn--expandable-expand');
optionsContainer.classList.remove('options--expand');
animated = false;
},100);
}
animated = true;
})
})

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()
}
})
}

Avoid numbers incrementing multiple times when calling a function multiple times

I have written a quantity selector function to display on a page. The page can open some modals, which need to have another quantity selector within each.
I am calling the function within the main page, and also within the modal (to enable the functionality once the modal is displayed.)
When I adjust the quantity in the modal, close the modal, and adjust the quantity on the main page, the quantity increments/decrements double (or 3 times if I was to call the function 3 times.)
Is there a way to "reset" each of these event listeners/functions, to only adjust for their respective elements?
I've looked into "removeEventListener" but haven't had any joy in implementing this within my code.
Example of my work so far here (you can see what I mean if you click the buttons.)
https://codepen.io/777333/pen/zYoKYRN
const quantitySelector = () => {
const qtyGroups = document.querySelectorAll('.qty-group');
if(qtyGroups) {
qtyGroups.forEach((qtyGroup) => {
const qtyDecrease = qtyGroup.querySelector('[data-quantity-decrease]');
const qtyIncrease = qtyGroup.querySelector('[data-quantity-increase]');
const qtyInput = qtyGroup.querySelector('[data-quantity-input]');
const disableEnableDecrease = () => {
if(qtyInput.value == 1) {
qtyDecrease.disabled = true;
} else {
qtyDecrease.disabled = false;
}
};
qtyDecrease.addEventListener('click', (event) => {
event.preventDefault();
if(qtyInput.value > 1) {
qtyInput.value--;
}
disableEnableDecrease();
});
qtyIncrease.addEventListener('click', (event) => {
event.preventDefault();
qtyInput.value++;
disableEnableDecrease();
});
qtyInput.addEventListener('keyup', () => {
disableEnableDecrease();
});
});
}
};
quantitySelector(); // called within main page
quantitySelector(); // called within modal
The issue at hand is that each time you're calling the function, a new event handler is added on top of the previous ones. The best way to avoid this is through Event Delegation where you add a global event handler only once.
// A global event handler
document.addEventListener(
"click",
function (event) {
// Find the qty-group if clicked on it
const qtyGroup = event.target.closest(".qty-group");
// Stop if the click was elsewhere
if (qtyGroup) {
// Get your elements
const qtyDecrease = qtyGroup.querySelector("[data-quantity-decrease]");
const qtyIncrease = qtyGroup.querySelector("[data-quantity-increase]");
const qtyInput = qtyGroup.querySelector("[data-quantity-input]");
const disableEnableDecrease = () => {
if (qtyInput.value == 1) {
qtyDecrease.disabled = true;
} else {
qtyDecrease.disabled = false;
}
};
// Match your elements against what was clicked on.
if (event.target == qtyDecrease) {
event.preventDefault();
if (qtyInput.value > 1) {
qtyInput.value--;
}
disableEnableDecrease();
}
if (event.target == qtyIncrease) {
event.preventDefault();
qtyInput.value++;
disableEnableDecrease();
}
}
},
false
);
Instead of listening to individual elements, you can capture all the clicks on the document, and then finding those that click on elements of interest. You can make a second event handler for the keyup event.
You can save the value of qtyInput on mousedown event and then in the increment you add or subtract one from the saved value instead of the current value of the input.
const quantitySelector = () => {
const qtyGroups = document.querySelectorAll('.qty-group');
if(qtyGroups) {
qtyGroups.forEach((qtyGroup) => {
const qtyDecrease = qtyGroup.querySelector('[data-quantity-decrease]');
const qtyIncrease = qtyGroup.querySelector('[data-quantity-increase]');
const qtyInput = qtyGroup.querySelector('[data-quantity-input]');
const disableEnableDecrease = () => {
if(qtyInput.value == 1) {
qtyDecrease.disabled = true;
} else {
qtyDecrease.disabled = false;
}
};
let savedValue = null;
const saveState = (evebt) => savedValue = Number(qtyInput.value);
qtyDecrease.addEventListener('mousedown', saveState)
qtyIncrease.addEventListener('mousedown', saveState)
qtyDecrease.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if(qtyInput.value > 1) {
qtyInput.value = savedValue - 1;
}
disableEnableDecrease();
});
qtyIncrease.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
qtyInput.value = savedValue + 1;
disableEnableDecrease();
});
qtyInput.addEventListener('keyup', () => {
disableEnableDecrease();
event.stopPropagation();
});
});
}
};
quantitySelector();
quantitySelector();
There is a method called removeEventListener (MDN) but I suggest you to reshape your code such that you do not add event listener if they are already present.
Put all of your addEventListener just when you create your elements, or in a "document ready" callback if they are instantiated by HTML code. Then, when you open your modal, just update your values.
UPDATING YOUR CODE
// hide/show modal function
function toggleModal() {
let modal = document.getElementById('modal');
modal.style.display = modal.style.display == 'none' ? 'block' : 'none';
}
// your document ready function
function onReady() {
const qtyGroups = document.querySelectorAll('.qty-group');
if(qtyGroups) {
qtyGroups.forEach((qtyGroup) => {
const qtyDecrease = qtyGroup.querySelector('[data-quantity-decrease]');
const qtyIncrease = qtyGroup.querySelector('[data-quantity-increase]');
const qtyInput = qtyGroup.querySelector('[data-quantity-input]');
const disableEnableDecrease = () => {
if(qtyInput.value == 1) {
qtyDecrease.disabled = true;
} else {
qtyDecrease.disabled = false;
}
};
qtyDecrease.addEventListener('click', (event) => {
event.preventDefault();
if(qtyInput.value > 1) {
qtyInput.value--;
}
disableEnableDecrease();
});
qtyIncrease.addEventListener('click', (event) => {
event.preventDefault();
qtyInput.value++;
disableEnableDecrease();
});
qtyInput.addEventListener('keyup', () => {
disableEnableDecrease();
});
});
}
// attach hide/show modal handler
const toggle = document.getElementById('modal_toggle');
toggle.addEventListener('click', toggleModal);
}
onReady();
<div class="qty-group">
<button data-quantity-decrease disabled>-</button>
<input data-quantity-input value="1">
<button data-quantity-increase>+</button>
</div>
<div class="qty-group" id="modal" style="display: none;">
<button data-quantity-decrease disabled>-</button>
<input data-quantity-input value="1">
<button data-quantity-increase>+</button>
</div>
<button id="modal_toggle">Toggle Modal</button>
REFACTORING
It is better in such cases to reason as Components. Components ensure code encapsulation, maintainability, reusage, single responsability and many other usefull principles:
// hide/show modal function
function toggleModal() {
// get the modal
let modal = document.getElementById('modal');
// hide the modal
modal.style.display = modal.style.display == 'none' ? 'block' : 'none';
// reset the input of the modal
modalInputReference.reset();
}
function createQuantityInput(target, initialQuantity=1, min=1, max=10, step=1) {
let quantity = 0;
// assign and check if should be disable, also bind to input value
let assign = (q) => {
quantity = Math.max(Math.min(q, max), min);
decrease.disabled = quantity <= min;
increase.disabled = quantity >= max;
input.value = quantity;
};
// CREATION
// This part is not mandatory, you can also get the elements from
// the target (document.querySelector('button.decrease') or similar)
// and then attach the listener.
// Creation is better: ensure encapsulation and single responsability
// create decrease button
let decrease = document.createElement('button');
decrease.addEventListener('click', () => { assign(quantity - step); });
decrease.innerText = '-';
// create increase button
let increase = document.createElement('button');
increase.addEventListener('click', () => { assign(quantity + step); });
increase.innerText = '+'
// create input field
let input = document.createElement('input');
input.value = quantity
input.addEventListener('change', () => { assign(parseFloat(input.value)); });
// resetting the quantity
assign(initialQuantity);
// appending the new component to its parent
target.appendChild(decrease);
target.appendChild(input);
target.appendChild(increase);
// return a reference to manipulate this component
return {
get quantity() { return quantity; },
set quantity(q) { assign(q); },
assign,
reset: () => assign(initialQuantity)
};
}
// this will be your modal reference
let modalInputReference;
function onReady() {
// inject all qty-group with a "quantityInput" component
document.querySelectorAll('.qty-group').forEach(elem => {
let input = createQuantityInput(elem);
if (elem.id == 'modal') {
// if it is the modal I save it for later use
// this is just an hack for now,
// a full code should split this part into a "modal" component maybe
modalInputReference = input;
}
});
// emualte the modal
let toggle = document.getElementById('modal_toggle')
toggle.addEventListener('click', toggleModal)
}
// this function should be wrapped by a
// $(document).ready(onReady) or any other
// function that ensure that all the DOM is successfully loaded
// and the code is not executed before the browser has generated
// all the elements present in the HTML
onReady();
<div class="qty-group"></div>
<div class="qty-group" id="modal" style="display: none;"></div>
<button id="modal_toggle">Toggle Modal</button>
It is shorter (without comments) and also more maintenable. Don't trust who says it is overengineered, it is just kind of time to learn to reason this way, then is much easier and faster. It is just a time investment to waste less time in the future. Try figure out why React or Angular(JS) have climbed the charts of the best frameworks so fast.

Event repeating multiple times during click

A small development problem, first, I declare a div to represent an icon to enter my inventory.
<<if setup.tagValue("map") !== 'MainMenu'>>
<div id="generalMenu">
<div id="Inventory"></div>
</div>
<</if>>
Now the real code, to activate this.
$(document).on(':passageend', () => {
window.inventoryIcons()
})
window.inventoryIcons = () => {
const _local = passage()
const _icon = $('#Inventory')
if (['BattleAPI'].includes(_local)) {
_icon
.removeClass()
.addClass("disabled")
}
else {
_icon.addClass("genMenuHover")
_icon.ariaClick(() => {
window.playerEquipIcon()
window.setInvIcons()
window.playerInventoryHover()
Engine.play('Inventory')
console.log(_icon)
})
}
}
This is having a mysterious effect, where each execution returns more and more events to the click.

event for file upload cancel in javascript

We can use file.onchange if we gonna set an event callback for file reading using javascript, but how to set event for when user cancel the upload (close the browse panel)?
There is no API for the file input modal. Besides, if the user closes the browser your code won't be running anymore, will it?
Of course there is the window.onunload method which allows you to detect the example you give.
Per the comments, the best thing I can come up with that would be helpful is that if nothing is selected, file.value.length will be 0.
That’s a great solution:
const createUpload = () => {
let lock = false
return new Promise((resolve, reject) => {
const el = document.createElement('input');
el.id = +new Date();
el.style.display = 'none';
el.setAttribute('type', 'file');
document.body.appendChild(el)
el.addEventListener('change', () => {
lock = true;
const file = el.files[0];
resolve(file)
document.body.removeChild(document.getElementById(el.id));
}, { once: true })
window.addEventListener('focus', () => { // file blur
setTimeout(() => {
if (!lock && document.getElementById(el.id)) {
reject(new Error('onblur'))
document.body.removeChild(document.getElementById(el.id))
}
}, 300)
}, { once: true })
el.click() // open file select box
})
}
Ussage:
// $label.classList.add('loading');
createUpload()
.then(function (file) {
// Sent file
// $label.classList.remove('loading');
})
.catch(function (err) {
// Your Cancel Event
// $label.classList.remove('loading');
});
It is very simple with jQuery :
$("#fileInputId").change(function () {
//implement your code here
});

Categories

Resources