Javascript 'click' onto wrong element when dynamically generating elements - javascript

I am adding some elements dynamically to a page when making this chrome extension. It is a list added like this
var result = document.createElement("UL");
var checkoutArea = document.getElementById("shipping");
result.setAttribute("id", "resList");
if (document.getElementById("resList") === null) {
checkoutArea.appendChild(result);
}
I am also adding <'li'> child to it later. But when I am trying to add onclick function to each item, it does not work. I checked when I click on the item dynamically generated, and it seems that those items are not clicked, as this gives wrong results:
document.addEventListener('click', function(e) {
e = e || window.event;
var target = e.target || e.srcElement,
text = target.textContent || text.innerText;
console.log(text);
}, false);
a screenshot of the DOM of dynamically generated list
Also when I 'inspect' on that 'home' item, chrome is giving me that highlighted element, which is the 'save & continue' button besides. I am thinking there's an overlap, so how should add any 'onclick' function to those items?? Great thanks in advance.

You probably need to try the Event delegation pattern. The idea is that if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them – we put a single handler on their common ancestor.
Read about it on this link: http://javascript.info/event-delegation
I managed to understand Event delegation thanks to video 15 that has the name LocalStorage and Event Delegation in the course javascript30 of Wes Bos. The course is free and worthwhile. I hope this information serves you

Related

How can I overwrite a portion of an element's text without affecting the rest of the element and any event listeners

I have this code that gets created automatically by Adoboe's RoboHelp 2017 into my HTML5 generated help file:
<object id="MapCtrl" classid="CLSID:A8ECFBF5-08CC-4065-BD74-0FAD1EA61317" data-rhtags="1" class="" width="1" height="1">
</object>
<p style="font-family:Arial;font-size:12pt;font-weight: normal;font-style: normal;text-decoration: none;" data-rhtags="1" class="" align="left">In this Topic <a class="dropspot" href="javascript:TextPopup(this)" id="MTHotSpot46267"><span class="MTText" style="display: none;">Show</span><span class="MTText">Hide</span></a></p>
I'm trying to use javascript to dynamically modify instances of the In this Topic text with a localized string without affecting the Show / Hide links in the anchor tags.
Here's my current attempt:
function localizeMiniTOC(){
const minitoc = document.querySelector('#MapCtrl + p');
const text = minitoc.textContent;
console.log (typeof text);
console.log(text.includes('In\xa0this\xa0Topic'));
if (text.includes('In\xa0this\xa0Topic') === true){
let html = minitoc.innerHTML;
linkStart = html.indexOf('<a');
remaining = html.substring(linkStart,html.length);
minitoc.innerHTML = 'En Este Tema ' + remaining;
}
}
This does change the text, but the problem I'm having, is that this also destroys the event listener that RoboHelp creates. For example, here's what the original Firefox Inspector looks like before my above code.
Notice there's this event listener:
Here's what it looks like after my above code above.
There's now no event listener:
I did some research today, and I understand from this link that using .innerHTML "removes internal data, like event listeners":
Element innerHTML getting rid of event listeners
But I can't seem to figure out a different way to overwrite the text before the anchor tag, yet leave the Show / Hide links and event listener unscathed.
Other things I've tried:
Based on this page (Is it possible to append to innerHTML without destroying descendants' event listeners?
), I've tried to use .insertAdjacentHTML(), but it didn't work either:
minitoc.insertAdjacentHTML('beforebegin','<p>En\xa0Este\xa0Tema\xa0' + remaining + '</p>');
(The example in the linked page shows how to do this by appending to an existing element, but I need to overwrite the text leading up to the anchor tag, not append to the element.)
I've tried to use .setAttribute to recreate the onclick event:
minitoc.setAttribute('onClick()',TextPopup('MTHotSpot46267'));
I've tried to recreate the event listener but that didn't work either:
minitoc.addEventListener('click', TextPopup('MTHotSpot46267'));
All the examples online seem to deal with appending or prepending, not overwriting text.
How can I dynamically overwrite the In this Topic text with a localized string without affecting the Show / Hide links and the event listener on those links?
Perhaps there's not a way to overwrite the text without nuking the event handler, but I did manage to fix my code so that it now at least adds the event handler back on after the minitoc.innerHTML = 'En Este Tema ' + remaining; line rewrites my paragraph.
Here's my updated function:
function localizeMiniTOC(){
const minitoc = document.querySelector('#MapCtrl + p');
const anchor = document.querySelector('#MapCtrl + p > a');
const text = minitoc.textContent;
if (text.includes('In\xa0this\xa0Topic') === true){
let html = minitoc.innerHTML;
linkStart = html.indexOf('<a');
remaining = html.substring(linkStart,html.length);
if (anchor.id.includes('MTHotSpot') === true){
minitoc.innerHTML = 'En Este Tema ' + remaining;
minitoc.addEventListener('click', function(e){
if (e.target.tagName === 'SPAN' && e.target.className==="MTText"){
TextPopup(anchor.id);
}
});
}
}
}
Notice that I had an error in my original attempted workaround to add the event listener back in. In this incorrect code, I had neglected to call the function properly in the second parameter:
minitoc.addEventListener('click', TextPopup('MTHotSpot46267'));
Here's the fixed code for that (taken from my updated function):
minitoc.addEventListener('click', function(e){
if (e.target.tagName === 'SPAN' && e.target.className==="MTText"){
TextPopup(anchor.id);
}
This results in the event listener getting added to the paragraph and then it was just a matter of making sure I was clicking on the span tag before running the TextPopup function.
In this pic, you can see the added event:

Handling event listeners on multiple pages

I've written a javascript file which has some event listeners in for things like tabs, accordions and so on. However these aren't present in every single page so it's looking for elements that don't exist and throws the entire js out of wack.
I know I could get around it by using multiple if statements, but it doesn't sound like it would be correct.
// Accordion
const accordions = document.querySelectorAll('.accordion li');
accordions.forEach(accordion =>{
accordion.addEventListener('click', (e) => {
e.preventDefault();
accordion.classList.toggle('open');
})
});
// Inline toggle
const inlineToggle = document.getElementById('inline-toggle');
inlineToggle.addEventListener('click', () => {
inlineToggle.nextElementSibling.classList.toggle('active');
});
const inlineToggleOptions = document.querySelectorAll('.inline-toggle-options button');
inlineToggleOptions.forEach(option => {
option.addEventListener('click', (e) => {
// Prevent default
e.preventDefault();
// Update sentence text
inlineToggle.innerHTML = option.dataset.payType;
// Remove selected class from options
inlineToggleOptions.forEach(option => {
option.classList.remove('selected');
});
// Add selected class to chosen option
option.classList.add('selected');
// Close dialog
inlineToggle.nextElementSibling.classList.remove('active');
})
});
// Cover bubbles
// Create the slidepanel
const placeholder = document.getElementById('slidepanel');
// Find all buttons
const button = document.querySelectorAll('.trigger-aside');
button.forEach((button => {
// Listen for clicks on buttons
button.addEventListener('click',(e) => {
// Prevent default
e.preventDefault();
// Get the target
const target = button.dataset.target;
console.log(target);
// Call the API
fetch(`http://****.****.uk/****/****/****/${target}`)
.then((res) => res.json())
.then(function(res) {
// Load HTML into slider panel
placeholder.innerHTML = res.object.content;
// Stop body overflow
document.body.classList.add('overflow-hidden');
// Create overlay and append
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
document.body.appendChild(overlay);
// Show the panel
placeholder.classList.add('active');
document.body.appendChild(placeholder);
// Listen for close
overlay.addEventListener('click', (e) =>{
// Close requested
document.body.classList.remove('overflow-hidden');
placeholder.classList.remove('active');
overlay.classList.remove('active');
});
})
.catch(function(err) {
// Log error
console.log(err);
});
})
}));
How do other people generally get around this issue? Any guidance appreciated!!
Event Delegation Pattern
If you have these UI elements grouped under a parent element in common across all pages(like a div wrapper), you could try making use of the Event Delegation pattern. Essentially, you can assign a click event to that parent element and make use of a function to only take action if the desired element is returned - i.e. your buttons. It would go something like...
const parent = document.querySelector('div.wrapper'); //Change selector to suit a common parent
const buttons = [...document.querySelectorAll('.inline-toggle-options button')]; // convert to array to make it easier to work with
const elementsToChange = document.querySelectorAll('.elements .to .change');
parent.addEventListener('click', toggleOptions);
function getEventTarget(e) {
e = e || window.event;
return e.target || e.srcElement; // For IE compatibility
}
function toggleOptions {
let target = getEventTarget(e);
if(buttons.includes(target)) {
// Trigger options on UI elements if any of the buttons are among the clicked elements
// Target refers to the buttons in particular, not the UI elements you want to change
}
}
Whichever way you want to refactor the code to take action on specific elements is up to you. You can group buttons by specific functionality into distinct arrays. If you had 2 or 3 arrays, you'd only need to write 2 or 3 options from a conditional statement.
For this purpose, you'll also save memory with this pattern since you're only assigning one event handler and letting child events on child elements bubble up to be taken care of by that single handler. Also, you shouldn't run into errors since the initial event handler is assigned to a parent element that's common across all pages.
Some Caveats
From the guide linked above:
Not all events bubble. The blur, focus, load and unload events don’t
bubble like other events. The blur and focus events can actually be
accessed using the capturing phase (in browsers other than IE) instead
of the bubbling phase but that’s a story for another day.
You need caution when managing some mouse events. If your code is
handling the mousemove event you are in serious risk of creating a
performance bottleneck because the mousemove event is triggered so
often. The mouseout event has a quirky behaviour that is difficult to
manage with event delegation.
I'd generally advise to think first about code organisation and second about efficiency. I agree with StevenB.'s suggestion and Luke Tubby's answer, to make the best of your current situation with a minimum of effort.
For more elaborate solutions, I would suggest to familiarise with build / packaging tools (e.g. Webpack ), that offer you ways to structure your code (and assets) in files and directories of your choice, and create page specific minified packages.
Another (completely different and independent) approach to the problem (of improving efficiency and code organisation) would be to build a single page application...

Jquery on mousedown not working on dynamically generated elements

So i'm trying to create a js/css "wave game" like tower defense ones.
When all the pre-generated enemys from first wave are dead, it spawns the second wave and so on.
So far so good.
The problem is that i just can't attack mobs dynamically spawned within second wave.
I used to try .live() in similar cases, but its deprecated, so i'm trying .on(), as instructed
$('.enemy').on('mousedown' , function(event) {
//attack code
}
Its working fine for initial mobs (1st wave) but it still just not working on dynamic mobs (>= 2nd wave)
Help, guys, please?
You need to specify an element that is already there when the DOM is created. In the parameters, you specify the elements you want to add the mousedown method. By simply assigning $('.enemy'), it will attach the method to those that are already present in the DOM.
$('body').on('mousedown', '.enemy', function(event) {
//attack code
}
As Wex mentioned in the comments, instead of writting $('body') you should use the container's name (the container which wraps the .enemy elements. This way, when a .enemy element is added, the event doesn't need to bubble all the way up to the body tag.
The binding '.on()' works only with the content that created earlier then the script ran.
So one solution could be you bind the event to the parent element.
$('.PARENT_ELEMENT').on('mousedown', '.enemy', function(event){
// your code here
}
That should do it.
I made this google like drop down suggestions search box and I faced a problem similar to yours where there was suggestions disappearing before the re-direct happened. I overcame it by using and modifing vyx.ca answer:
var mousedownHappened = false;
var clicked_link;
$("#search-box").blur(function(e) {
if (mousedownHappened)// cancel the blur event
{
mousedownHappened = false;
window.location.href = clicked_link;
} else {
// no link was clicked just remove the suggestions box if exists
if ($('#search-btn').next().hasClass('suggestions')) {
$(".suggestions").remove();
}
}
});
//attaching the event to the document is better
$(document).on('mousedown', '.suggestions a', function() {
clicked_link= $(this).attr('href');
mousedownHappened = true;
});

ctrl + click in JavaScript

I have browsed the internet extensively on this issue, including stackoverflow.
My problem is that I have a set of 'li', and I want multiple 'li' added to an array when I use ctrl+click gesture. I keep on getting (e) is not defined. I have found this: Detect CTRL and SHIFT key without keydown event?
But the answer provided, which seems to have worked for many, doesn't for me. Whenever I use that, even as the sole item in my script, firebug doesn't respond in the console, but I get: " ReferenceError: e is not defined." I'm using Firefox.
My biggest problem is getting this to add this to a function, and the function, which fires as an event, can distinguish between the a ctrl+click and normal click.
Any expertise to help me out? Vanilla Javascript preferred.
The point of this exercise is to remove the LI when clicked, but I want to delete multiple at once if I hold down ctrl. Perhaps by storing them in an array.
EDIT: Some Code
<ul id = "ulItem">
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
</ul>
<script>
window.onload = function(){
var ulItem = document.getElementById("ulItem"); //gets UL with the "ulItem" ID.
var ulList = ulItem.getElementsByTagName("li"); //gets ulItem's "li" in an array.
///prepareLI function.///
var prepareLi = function(){
for(i = 0; i < ulList.length; i++){
ulList[i].addEventListener('click', elementClick);
}
}
//adds the same event listener to each of the "li" inside "UlList" array. Each activated by a click.
///elementClick function.///
var elementClick = function(){
ulItem.removeChild(this);
} //if this is a child of parent, UlList, remove it.
prepareLi();
}
The browser is correctly telling you that you never declared e, while the example in Detect CTRL and SHIFT key without keydown event? has defined e, by declaring it as a formal argument to the listener function.
var elementClick = function(e){
if(e.ctrlKey) {
ulItem.removeChild(this);
}
}
Note the use of function(e){ rather than function(){
I think I found an alternative way to do this.
If you want to emulate a Ctrl+click in order to select multiple objects, this worked for me as a workaround:
make two variables, loader and loaderArray:
var loader = 1;
var loaderArray = [];
set a document event:
document.addEventListener('mousedown', MultiSelect);
and a click event on the item:
document.addEventListener('click', singleSelect);
Make sure the window event is is a mousedown item, also, that Ctrl is defined as keyCode == 17. The number part is VERY important. Here is code I used:
function loadArray(e) {
if (e.keyCode === 17) {
//console.log("key down");
loader = 2;
};
}
Then I set a separate function on the singleSelect event stating that if loader ==2, use loaderArray.push(this) rather than the normal way of doing things when loader == 1.
The loader Array then collects the variables, and you can do whatever you want with the array using for() or some other loop.
Now, whenever I click the Ctrl key, the mousevent on the document turns the loader variable to 2, and it can distinguish between Ctrl+click and normal click this way.
Thanks for all those that helped, and I hope this will help others! :)

Retrieving previously focused element

I would like to find out, in Javascript, which previous element had focus as opposed to the current focus. I've been looking through the DOM and haven't found what I need, yet. Is there a way to do this any help would be much appreciated
Each time an element is focused, you'd have to store which one it was. Then when another element is focused, you could retrieve the variable for the previous focused element.
So basically, your single focus handler would do 2 things:
Check if previousFocus is defined. If it is, retrieve it.
Set previousFocus to the currently focused element.
Here is a quick demo with jQuery (you can use raw JS too... just fewer lines w jQuery, so it's easier to understand imo):
// create an anonymous function that we call immediately
// this will hold our previous focus variable, so we don't
// clutter the global scope
(function() {
// the variable to hold the previously focused element
var prevFocus;
// our single focus event handler
$("input").focus(function() {
// let's check if the previous focus has already been defined
if (typeof prevFocus !== "undefined") {
// we do something with the previously focused element
$("#prev").html(prevFocus.val());
}
// AFTER we check upon the previously focused element
// we (re)define the previously focused element
// for use in the next focus event
prevFocus = $(this);
});
})();
working jsFiddle
Just found this question while solving the exact same problem and realised it was so old the jQuery world has moved on a bit :)
This should provide a more effective version of Peter Ajtais code, as it will use only a single delegated event handler (not one per input element).
// prime with empty jQuery object
window.prevFocus = $();
// Catch any bubbling focusin events (focus does not bubble)
$(document).on('focusin', ':input', function () {
// Test: Show the previous value/text so we know it works!
$("#prev").html(prevFocus.val() || prevFocus.text());
// Save the previously clicked value for later
window.prevFocus = $(this);
});
JSFiddle: http://jsfiddle.net/TrueBlueAussie/EzPfK/80/
Notes:
Uses $() to create an empty jQuery object (allows it to be used immediately).
As this one uses the jQuery :input selector it works with select & button elements as well as inputs.
It does not need a DOM ready handler as document is always present.
As the previously focused control is required "elsehere" is is simply stored on window for global use, so it does not need an IIFE function wrapper.
Well depending on what else your page is doing, it could be tricky, but for starters you could have a "blur" event handler attached to the <body> element that just stashes the event target.
To me this seems a slight improvement on Gone Coding's answer:
window.currFocus = document;
// Catch focusin
$(window).on( 'focusin', function () {
window.prevFocus = window.currFocus;
console.log( '£ prevFocus set to:');
console.log( window.currFocus );
window.currFocus = document.activeElement;
});
... there's no stipulation in the question that we're talking exclusively about INPUTs here: it says "previous elements". The above code would also include recording focus of things like BUTTONs, or anything capable of getting focus.
document.getElementById('message-text-area').addEventListener('focus',
event => console.log('FOCUS!')
);
event.relatedTarget has all the data about the previously focused element.
See also https://developer.mozilla.org/en-US/docs/Web/API/Event/Comparison_of_Event_Targets
Here is a slightly different approach which watches both focusin and focusout, in this case to prevent focus to a class of inputs:
<input type="text" name="xyz" value="abc" readonly class="nofocus">
<script>
$(function() {
var leaving = $(document);
$(document).on('focusout', function(e) {
leaving = e.target;
});
$( '.nofocus' ).on('focusin', function(e) {
leaving.focus();
});
$( '.nofocus' ).attr('tabIndex', -1);
});
</script>
Setting tabIndex prevents keyboard users from "getting stuck".

Categories

Resources