I'm trying to make a SPA with html, css and vanilla JS (I have very little idea of JS). The problem I have is that the method I am using, works correctly in my header where I have only "a" with text inside. But when I want to use an img as "a", the script does not work inside the img, it only allows me to click around the img and not inside it.
I appreciate any help.
This is my script and my html in the part where I have the problem.
const route = (event) => {
event = event || window.event;
event.preventDefault();
window.history.pushState({}, "", event.target.href);
handleLocation();
};
const routes = {
404: "./pages/404.html",
"/": "./pages/index.html",
"/vehicles": "./pages/vehicles.html",
"/services": "./pages/services.html",
"/contact": "./pages/contact.html",
"/financing": "./pages/financing.html",
"/locations": "./pages/locations.html",
};
const handleLocation = async () => {
const path = window.location.pathname;
const route = routes[path] || routes[404];
const html = await fetch(route).then((data) => data.text());
document.getElementById("main-page").innerHTML = html;
};
window.onpopstate = handleLocation;
window.route = route;
handleLocation();
<a href="/financing" onclick="route()" class="mainServices-section">
<div class="main-title">
<h2>Financing</h2>
</div>
<img src="../image/financing-image-colored.svg" alt="">
</a>
The issue is that when you click the <img> the target isn't set to your <a> tag so event.target.href is undefined.
Instead of using onclick attributes, use a delegated event listener and check if it originates from an <a> tag or a descendant.
document.addEventListener("click", (e) => {
const anchor = e.target.closest("a[href]");
if (anchor) {
e.preventDefault();
window.history.pushState({}, "", anchor.href);
handleLocation();
}
});
Demo ~ https://jsfiddle.net/dgn1r5zh/
This behavior happens because event.target is a reference to the element that fired the event. In this case that is your image.
First of let me point out that inline event handlers should not be used. They can create unexpected behavior and make you write JS inside HTML. More about this topic. Learn to use addEventListener instead.
You can fix your issue by adding some CSS to your element that you want to click. With CSS the pointer-events you can determine if an element should be able to become a target.
Implement this by selecting all the elements within the element that you want to have as your target and set pointer-events: none; to disable any pointer events on those elements. This ensures that the <a> tag is the only element that can be considered a target. So event.target in your code will always point to the correct element.
.mainServices-section * {
pointer-events: none;
}
Alternatively, if you can't use pointer-events because, for example you need some other pointer event to fire on the children of .mainServices-section, then you should extend your code by explicitly checking if the current target is the <a> tag and otherwise select the <a> tag if possible.
You can search for the <a> from any child within the element with the closest that is on the clicked element. It walks up and looks for the element that you're searching for.
const route = (event) => {
const target = event.target.closest('.mainServices-section');
if (target === null) {
return;
}
event.preventDefault();
window.history.pushState({}, "", target.href);
handleLocation();
};
const mainServicesSections = document.querySelectorAll('.mainServices-section');
for (const mainServiceSection of mainServicesSections) {
mainServiceSection.addEventListener('click', route);
}
You need to look for the .closest("a") of the clicked element ev.target:
var hist="none";
const route = (event) => {
event = event || window.event;
event.preventDefault();
hist=event.target.closest("a").getAttribute("href");
handleLocation();
};
const routes = {
404: "./pages/404.html",
"/": "./pages/index.html",
"/vehicles": "./pages/vehicles.html",
"/services": "./pages/services.html",
"/contact": "./pages/contact.html",
"/financing": "./pages/financing.html",
"/locations": "./pages/locations.html",
};
const handleLocation = async () => {
const route = routes[hist] || routes[404];
console.log(route);
// fetch() etc. to be placed here ...
};
// window.onpopstate = handleLocation;
// window.route = route;
handleLocation();
<a href="/financing" onclick="route()" class="mainServices-section">
<div class="main-title">
<h2>Financing</h2>
</div>
<img src="https://picsum.photos/200" alt="">
</a>
Another thing I changed is the way to look for the href attribute of the <a> element: DOMelement.href will return an absolute path, starting with "https://" while DOMelement.getAttribute("href") will actually get you the string as defined in the element's attribute.
Related
I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)
This is my code,
export default myComponent = () => {
const handleClick = (e) => {
const parent = e.target.parentElement;
if (parent.classList.contains('selected_category')) {
parent.classList.remove('selected_category');
} else {
parent.classList.add('selected_category');
}
};
return (
<>
<ul>
<li className="">
<a onClick={handleClick}>
content<span class="text-gray-25 font-size-12 font-weight-normal">
121
</span>
</a>
</li>
<li className="">
<a onClick={handleClick}>
content<span class="text-gray-25 font-size-12 font-weight-normal">
121
</span>
</a>
</li>
</ul>
</>
);
}
I wrote this code for when I clicked tag element the parent element of it gets the class 'selected_category' if it doesn't already have. But the problem here is when I click children class then 'selected_category' class is added to parent tag. is there any solution to prevent it?
This is my code in sand box
To further elaborate on my comment: the issue comes from the use of e.target, which can refer to the element where the event listener is bound to OR its descendant(s). In this case, your <a> tag has <span> as a child. When a click event bubbles up to your <a> tag that originated from the inner <span>, then e.target will refer to the latter: which is not what you want.
To ensure that you get the reference to the actual element which the event listener is bound to is all times, you need to use e.currentTarget:
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.
Therefore your updated function should simply use currentTarget instead, i.e.:
const handleClick = (e) => {
// NOTE: Use e.currentTarget here instead of e.target
const parent = e.currentTarget.parentElement;
if (parent.classList.contains('selected_category')) {
parent.classList.remove('selected_category');
} else {
parent.classList.add('selected_category');
}
};
I was trying to test a few novice tricks from a project tutorial. Wanted to create a small scale task app and ran into a weird problem. The last document.addEventListener below should theoretically call the closest element with the class name of ".name" should be detected since its in the same parent div with the button. However it is returning NULL. Am I applying the .closest() method wrong?
The event listener detects the button after everytime a task is created. Not sure why it returns NULL when, after creating the task via addTaskButton, the task with the class name of ".name". I even tried to create a data attribute id based off of the taskName itself to see if it'll detect, but still NULL / erroring.
const list = []
const container = document.querySelector('.container');
const itemContainer = document.querySelector('.item');
const addTaskButton = document.querySelector('.add-task');
const taskInput = document.querySelector('#task-name');
function renderTasks(){
itemContainer.innerHTML = ''
list.forEach(task => {
const itemElement = document.createElement('div')
itemElement.innerHTML = `
<div class="name">
${task.taskName}
</div>
<button class="retrieval">Retrieve ID</button>
`
itemElement.dataset.itemName = task.taskName
itemContainer.appendChild(itemElement);
})
}
addTaskButton.addEventListener('click', (e)=>{
e.preventDefault();
list.push({ taskName: taskInput.value})
renderTasks()
})
document.addEventListener('click', (e)=>{
if(e.target.matches('.retrieval')){
const taskName = e.target.closest('.name');
console.log(taskName)
}
})
Ok, I double checked the mdn article it says:
closestElement is the Element which is the closest ancestor of the
selected element. It may be null.
That means it only looks for parents and parents of parents and so on, not 'siblings'.
I create directed graphs like the following from wikidata with the help of networkx and nxv. The result is an svg file which might be embedded in some html page.
Now I want that every node and every edge is "clickable", such that a user can add their comments to specific elements of the graph. I think this could be done with a modal dialog popping up. This dialog should know from which element it was triggered and it should send the content of the textarea to some url via a post request.
What would be the best way to achieve this?
Wrapped in a W3C standard Web Component (supported in all Modern Browsers) you can make it generic for any src="filename.svg"
Simple example: How to get SVG document data to be inserted into the DOM?
More complex example:
<graphviz-svg-annotator src="https://graphviz.org/Gallery/directed/fsm.svg">
</graphviz-svg-annotator>
The SVG is loaded with an async fetch
Nodes and Edges are clickable in this SO Snippet
add your own, better modal, window and saving to database
Try the SVGs from: https://graphviz.org/Gallery/directed/Genetic_Programming.html
<graphviz-svg-annotator src="fsm.svg"></graphviz-svg-annotator>
<graphviz-svg-annotator src="Linux_kernel_diagram.svg"></graphviz-svg-annotator>
<style>
svg .annotate { cursor:pointer }
</style>
<script>
customElements.define('graphviz-svg-annotator', class extends HTMLElement {
constructor() {
let loadSVG = async ( src , container = this.shadowRoot ) => {
container.innerHTML = `<style>:host { display:inline-block }
::slotted(svg) { width:100%;height:200px }
</style>
<slot name="svgonly">Loading ${src}</slot>`;
this.innerHTML = await(await fetch(src)).text(); // load full XML in lightDOM
let svg = this.querySelector("svg");
svg.slot = "svgonly"; // show only SVG part in shadowDOM slot
svg.querySelectorAll('g[id*="node"],g[id*="edge"]').forEach(g => {
let label = g.querySelector("text")?.innerHTML || "No label";
let shapes = g.querySelectorAll("*:not(title):not(text)");
let fill = (color = "none") => shapes.forEach(x => x.style.fill = color);
let prompt = "Please annotate: ID: " + g.id + " label: " + label;
g.classList.add("annotate");
g.onmouseenter = evt => fill("lightgreen");
g.onmouseleave = evt => fill();
g.onclick = evt => g.setAttribute("annotation", window.prompt(prompt));
})
}
super().attachShadow({ mode: 'open' });
loadSVG("//graphviz.org/Gallery/directed/"+this.getAttribute("src"));
}});
</script>
Detailed:
this.innerHTML = ... injects the full XML in the component ligthDOM
(because the element has shadowDOM, the lightDOM is not visible in the Browser)
But you only want the SVG part (graphviz XML has too much data)... and you don't want a screen flash; that is why I put the XML .. invisible.. in lightDOM
A shadowDOM <slot> is used to only reflect the <svg>
with this method the <svg> can still be styled from global CSS (see cursor:pointer)
With multiple SVGs on screen <g> ID values could conflict.
The complete SVG can be moved to shadowDOM with:
let svg = container.appendChild( this.querySelector("svg") );
But then you can't style the SVG with global CSS any more, because global CSS can't style shadowDOM
As far as I know, nxv generates a g element with class "node" for each node, all nested inside a graph g. So basically you could loop over all gs elements inside the main group and attach a click event listener on each one. (actually, depending of the desired behavior, you might want to attach the event listener to the shape inside the g, as done below. For the inside of the shape to be clickable, it has to be filled)
On click, it would update a form, to do several things: update its style to show it as a modal (when submitted, the form should go back to hiding), and update an hidden input with the text content of the clicked g.
Basically it would be something like that:
<svg>Your nxv output goes here</svg>
<form style="display: none;">
<input type="hidden" id="node_title">
<textarea></textarea>
<input type="submit" value="Send!">
</form>
<script>
const graph = document.querySelector("svg g");
const form = document.querySelector("form");
[...graph.querySelectorAll("g")].map(g => { //loop over each g element inside graph
if (g.getAttribute("class") == "node") { //filter for nodes
let target = "polygon";
if (g.querySelector("polygon") === null) {
target = "ellipse";
}
g.querySelector(target).addEventListener("click",() => {
const node_title = g.querySelector("text").innerHTML;
form.querySelector("#node_title").setAttribute("value", node_title);
form.setAttribute("style","display: block;");
});
}
});
const submitForm = async (e) => { //function for handling form submission
const endpoint = "path to your POST endpoint";
const body = {
source_node: form.querySelector("#node_title").value,
textarea: form.querySelector("textarea").value
}
e.preventDefault(); //prevent the default form submission behavior
let response = await fetch(endpoint, { method: "POST", body: JSON.stringify(body) });
// you might wanna do something with the server response
// if everything went ok, let's hide this form again & reset it
form.querySelector("#node_title").value = "";
form.querySelector("textarea").value = "";
form.setAttribute("style","display: none;");
}
form.addEventListener("submit",submitForm);
</script>
I am trying to add a click event listener to a div that is dynamically generated after page load but I can't seem to get the event to register. I am following the instructions found in this answer however, it is not working for me.
In my ngOnInit() I have a combineLatest():
combineLatest([this.params$, this.user$]).subscribe(([params, user]: [Params, User]) => {
this.artistId = parseInt(params['artist']);
this.user = user;
if (this.artistId) {
this.artistProfileGQL.watch({
id: this.artistId
}).valueChanges.subscribe((response: ApolloQueryResult<ArtistProfileQuery>) => {
this.artist = response.data.artist;
this.initElements(); // WHERE I CALL TO INITIALIZE DYNAMIC DOM ELEMENTS
});
})
In this block, I call initElements() which is where I create certain DOM elements. I've included most of them below. Essentially, I have a header element, and inside this header element, I create a followBtn, that looks like this (i removed the title, followers, elements etc from the code for brevity). I added comments in caps for the most relevant lines:
initElements() {
const parentElement = this.el.nativeElement;
this.header = parentElement.querySelector('ion-header');
// Create image overlay
this.imageOverlay = this.renderer.createElement('div');
this.renderer.addClass(this.imageOverlay, 'image-overlay');
this.colorOverlay = this.renderer.createElement('div');
this.renderer.addClass(this.colorOverlay, 'color-overlay');
this.colorOverlay.appendChild(this.imageOverlay);
this.header.appendChild(this.colorOverlay);
var artistHeader = this.renderer.createElement('div');
this.renderer.addClass(artistHeader, 'artist-header');
// HERES WHERE I CREATE MY BUTTON ELEMENT
this.followBtn = this.renderer.createElement('div');
this.renderer.addClass(this.followBtn, "follow-btn");
var followText = this.renderer.createText('FOLLOW');
this.renderer.appendChild(this.followBtn, followText);
this.renderer.appendChild(artistHeader, this.followBtn);
this.renderer.appendChild(this.imageOverlay, artistHeader);
// HERES WHERE I CREATE MY LISTENER
this.followButtonListener = this.renderer.listen(this.followBtn, 'click', (event) => {
console.log(event);
});
}
However, when I click on the element, I don't get anything printed to my console. If I change the target of the listener to a DOM element, the click listener works. What am I doing wrong?