How to bind events on raw (imported) html via code? - javascript

I'm importing a html containing SVG code and render it via v-html directive, but I need to bind click events to the a-html tags from that file which aren't part of my template.
How do I get all the a-tag from that imported file?
And how do I bind events to these elements whitout using v-on directive?
The template
<v-flex mt-2 px-0 xs6
ref="map_svg"
v-html="mapHTML">
</v-flex>
Snippet of the imported file
<svg>
<g>
<g>
<a target="_blank" xlink:href="url to a subsite" >
<!-- content path which shows a circle -->
</a>
</g>
<g>
<a target="_blank" xlink:href="url to a differnt subsite" >
<!-- content path which shows a circle -->
</a>
</g>
<!-- many more elements like shown on the top -->
</g>
</svg>
I would need to bind an click event to these a-tags and remove or overwrite the xlink:href attribute so the click won't open a different tab in the browser.
Edit
I ended up doing it like this:
mounted() {
const that = this;
const links = this.$el.querySelectorAll('#map_svg svg > g > g > a');
links.forEach(el => {
const url = el.getAttribute('xlink:href');
if (url) {
// el.addEventListener('click', that.stationClick(url));
el.onclick = function (){
that.stationClick(url);
}
el.removeAttribute('xlink:href');
}
el.removeAttribute('target');
});
},

You can bind click events on those elements after you have loaded your svg:
export default {
name: "App",
data() {
return {
mapHTML: null
};
},
methods: {
async loadSvg() {
// Here, you load your svg
this.mapHTML = `
Google
`;
// You need to use $nextTick in order to wait for the DOM to be refreshed
this.$nextTick(() => {
this.bindClicks() // You bind click events here
})
},
bindClicks () {
// You search for all the a tags in your svg_container
this.$refs.map_svg.querySelectorAll('a').forEach(el => {
el.removeAttribute('_target') // You remove the _target attribute
el.addEventListener('click', this.handleClick) // You bind an event listener to the element
})
},
handleClick (e) {
// You handle the click like you would normally do
e.preventDefault()
}
}
};

You can use the ref you set and plain JS to bind to the tags.
this.$refs.map_svg.querySelectorAll('a').forEach(item => item.addEventListener('click', this.callback) )

Related

problems running the script in img

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.

How to run <script> when props of a component change?

Is it possible to execute a <Script/> every time the props of a react/nextjs component change?
I am converting markdown files to html using marked and, before rendering the html, I would like to have a [copy] button on each <pre> block (those are the code blocks). I have a <script/> that iterates through the <pre> blocks of the DOM document.querySelectorAll("pre") and injects the button needed. If the html changes though at a later stage, then I have found no way to re-run the script to add the copy buttons again.
I have the impression that this is not a very react/nextjs way of doing this, so any hints would be appreciated.
The Script to add the copy buttons. I have added this as the last tag of my <body>:
<Script id="copy-button">
{`
let blocks = document.querySelectorAll("pre");
blocks.forEach((block) => {
if (navigator.clipboard) {
let button = document.createElement("img");
button.src = "/images/ic_copy.svg"
button.title = "Copy"
button.id = "copy"
button.addEventListener("click", copyCode);
block.appendChild(button);
}
});
async function copyCode(event) {
const button = event.srcElement;
const pre = button.parentElement;
let code = pre.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
button.src = "/images/ic_done.svg"
setTimeout(()=> {
button.src = "/images/ic_copy.svg"
},1000)
}
`}
</Script>
the React component. Not much to say here. The content is coming from the backend. Not sure what would be the 'React' way to do this without the script.
export default function Contents({ content }) {
return (
<div className='pl-2 pr-2 m-auto w-full lg:w-2/3 mb-40 overflow-auto break-words'>
<div className="contents" dangerouslySetInnerHTML={{ __html: content }} />
</div>
)
}
You should absolutely not do this and instead incorporate this logic into your react app, but if you must you can leverage custom window events to make logic from your html script tags happen from react.
Here is an example script:
<script>
function addEvent() {
function runLogic() {
console.log("Stuff done from react");
}
window.addEventListener("runscript", runLogic);
}
addEvent();
</script>
And calling it form react like this:
export default function App() {
const handleClick = () => {
window.dispatchEvent(new Event("runscript"));
};
return (
<div className="App" onClick={handleClick}>
<h1>Hello</h1>
</div>
);
}

React click event not work when child element is clicked

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');
}
};

How to make an svg interactive to gather comments/annotations on depicted elements

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>

How to add an event listener to dynamically generated content using Renderer2?

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?

Categories