HTML object is blocking eventlistener - javascript

I have imported a svg as an object in HTML:
<object data="mySVG.svg" type="image/svg+xml" id="circle">
<img src="mySVG.svg" />
</object>
and I am trying to set an eventlistener on the whole page:
window.addEventListener('click', function(){
alert('Hello')
})
The problem is that the object blocks the eventlistener and when the user clicks on the image the alert is not fired. But when the user clicks anywhere else or over other elements, the alert is fired. How can I make it so the object is acting as the other elements and doesn't block the eventlistener?
I tried wait after the object is beaing loaded and then set the eventlistener but it didn't work.
If I import the SVG directly into HTML with svg tag it works, but the svg is quit big and it makes the HTML code really messy. I can't use the img tag either becuase I am also interacting with parts of the SVG with JS later.
As it can be seen in this codepen I've made: https://codepen.io/Dimertuper/pen/rNJoLrK (When you click outside the image it triggers, inside the image it doesn't)

Your <object> acts like an <iframe>, just like we wouldn't want any website to be able to embed our bank website in an iframe and see where we clicked, the <object> has the same "protection".
Even if the page are same-origin and can talk to each other, by default they won't receive any events from the other one.
But anyway what you probably want is to make the SVG document react to these events. For this, add the event listeners on that document directly.
// Wait for the <object> to be loaded
window.addEventListener("load", (evt) => {
const objEl = document.querySelector("object");
const svgDoc = objEl.getSVGDocument();
// Now you have access to the SVG document
// you can add event listeners to it as you wish
svgDoc.addEventListener("click", (evt) => {
console.log("clicked on", evt.target.outerHTML);
});
});
Unfortunately StackSnippets's null-origined iframes won't allow us to make live demos, so here is one on JSFiddle.
But beware the <object> element isn't gathering much love from implementers and spec authors these days and it may get removed from the standards at some point in the future.
So instead, you may prefer to actually use an <iframe> directly. Moreover since here we would access the loaded document, we can do the one thing that <object> can do and <iframe> can't: auto-resizing to the image content.
For this, when we get our SVG document, we grab its documentElement's BBox and set our <iframe>'s width and height attributes to the BBox's ones.
// Wait for the <iframe> to be loaded
window.addEventListener("load", (evt) => {
const frameEl = document.querySelector("iframe");
const svgDoc = frameEl.getSVGDocument();
// Resize the iframe to its content's size
const bbox = svgDoc.documentElement.getBBox();
frameEl.width = bbox.width;
frameEl.height = bbox.height;
svgDoc.addEventListener("click", (evt) => {
console.log("clicked on", evt.target.outerHTML);
});
});
Once again as a JSFiddle.

Per OP's requirements -
Needs to be able to click on window/document and receive the alert message even when clicking on the HTML object tag.
We can do this by removing the object tag as a clickable element with CSS pointer-events: none;.
object {
pointer-events: none;
}
https://codepen.io/LTFoReal/pen/NWyerZg?editors=1111

This link has work around. Using a transparent div to cover object image, or directly use svg image instead.
I checked the specification of object element. It's for embeded external content usage. So it has ability to load a full document, your case is load as image. The available property to do event binding for this element is contentDocument or getSvgDocument(). Both are null under your case, as it's loaded as svg image.
document.getElementsByTagName("object")[0].contentDocument
Check this link for detail. Hope this helps you.

Related

How to Add Event Listeners to SVG Elements Loaded From File Dynamically?

Problem Statement
I'm trying to load an svg file dynamically, and then listen to click events on the individual svg elements. The svg loads fine, but I'm having trouble detecting /when/ it has loaded so that I can add the listeners.
Context
The svg is a map that will pop up when the user clicks on a field. They should then be able to select a country from the map. The svg needs to be loaded dynamically because the field can be parameterized with different maps.
What I've tried
It seems like the common recommendation is to listen for a "load" event on the dynamically created element and then access the actual svg elements through the element.contentDocument property or the element.getSVGDocument() function.
var element = document.createElement('embed');
element.src = this.mapSrc_;
element.type = 'image/svg+xml';
document.body.appendChild(element );
element.addEventListener('load', function() {
console.log(element.firstChild, element.contentDocument, element.getSVGDocument());
});
or:
var element = document.createElement('object');
element.setAttribute('data', this.mapSrc_);
element.setAttribute('type', 'image/svg+xml';
document.body.appendChild(element);
element.addEventListener('load', function() {
console.log(element.firstChild, element.contentDocument, element.getSVGDocument());
});
But in both cases I get a console log of:
null undefined null
What is the proper way to load an svg from a file and then add event listeners to it?
Thank you for taking the time to read this! I really appreciate any help :D
[EDIT: If you like you can view my actual code here, but be warned it is pretty thoroughly tied to the Blockly framework.]
After some trial and error it seems like this is a problem with running the page from file, specifically on Chrome and Firefox (works on Edge and IE11).
You can test this by downloading this example page. If you run it from file the icons stay orange, but the online page works.
I will continue with this by testing on Edge instead of Chrome, but other people may have different solutions for this problem.

RemoveEventListener doesn't seem to work

I attached an event listener to my SVG image in order to perform some code after the image has been loaded. As it sometimes happens, a bug might occur in my SVG generating code and the SVG file won't load. In that case, when user clicks another button, I want it to remove the event listener, hide the unsuccessful result and let the user select another SVG image.
Here's what I have:
// In external .js file, loaded prior to the other code in <head>
function setResultViewBox() {
var objectEl = document.getElementById("resultImage");
var svgDoc = objectEl.contentDocument;
var svg = svgDoc.childNodes[0];
// If there's no SVG tag, remove the listener etc.
// Tested via alert() and console messages that this actually works.
if (svg.tagName !== "svg") {
editBrush();
return;
}
// some other code to set the viewBox
}
// Attached to the main file at the end of <body>
var resultImage = document.getElementById("resultImage");
function resultImageLoaded(event) {
resultImage.removeEventListener("load", resultImageLoaded, false);
setResultViewBox();
hideProgressBar();
}
submitChanges() {
// Compute URI here
resultImage.data = uri;
resultImage.addEventListener("load", resultImageLoaded, false);
hidePreview();
}
function editBrush() {
alert();
resultImage.removeEventListener("load", resultImageLoaded, false);
hideResult();
hideProgressBar();
}
<object id="resultImage" type="image/svg+xml" width="420" height="420" data=""></object>
Make outline
Edit brush
This came to me as a surprise: for once, in Internet Explorer 11, it does exactly what I want it to do; on the other hand, in Opera it doesn't work (the hell must have frozen I guess).
Testing it on an image case I know for sure it won't load, it attempts to set the viewBox of the result image, fails to find the <svg> tag, throws an alert message as it goes to editBrush(), removes the listener and allows me to select another SVG file, which it loads correctly then. That means it creates the listener once again, loads the correct URI, recognizes <svg> tag, sets viewBox, removes the listener in the resultImageLoaded(event) itself and all is good.
In Opera, it attempts to set the viewBox of the result image, fails to find the <svg> tag, throws an alert message as it goes to editBrush() and I suspect now it doesn't actually remove the listener. When I select another SVG image, which it should load correctly now, nothing happens (tried to add another alert to resultImageLoaded(event) and it wasn't triggered).
Things I gave special attention to:
resultImageLoaded(event) isn't an anonymous function and is located above the code that is using it
reference to the function itself in the add/remove listeners
resultImage is stored in one variable and both add/remove listeners are on this one object
I can't see what I'm doing wrong, any help would be most appreciated.
EDIT: In console in Opera, it shows Internal server error (500) on GET request when I try to load the image that should fail loading. IE shows no such thing. Not sure if this can be of any help.
EDIT 2: Alright, I just found out this has probably nothing to do with removeEventListener(). Even when I comment out all lines where I remove event listeners, the behaviour is exactly the same as described in both browsers. Could the problem be in Opera reporting an error and IE ignoring it?
And question for mods: when I find out the original question's topic is no more relevant, but the problem still persists and I'm not even sure what might be causing it, what do I do? Brutally edit the original question or make a new one and leave the original one unanswered?
Try adding event.stopPropagation(); at the end of each eventListener, this will prevent bubbling of the event to your ancestors and it'll stop at your event.target
If that doesn't help, try event.stopImmediatePropagation();
http://www.kirupa.com/html5/handling_events_for_many_elements.htm
// In external .js file, loaded prior to the other code in <head>
function setResultViewBox() {
var objectEl = document.getElementById("resultImage");
var svgDoc = objectEl.contentDocument;
var svg = svgDoc.childNodes[0];
// If there's no SVG tag, remove the listener etc.
// Tested via alert() and console messages that this actually works.
if (svg.tagName !== "svg") {
editBrush();
return;
}
// some other code to set the viewBox
}
// Attached to the main file at the end of <body>
var resultImage = document.getElementById("resultImage");
function resultImageLoaded(event) {
resultImage.removeEventListener("load", resultImageLoaded, false);
setResultViewBox();
hideProgressBar();
event.stopPropagation();
}
submitChanges() {
// Compute URI here
resultImage.data = uri;
resultImage.addEventListener("load", resultImageLoaded, false);
hidePreview();
event.stopPropagation();
}
function editBrush() {
alert();
resultImage.removeEventListener("load", resultImageLoaded, false);
hideResult();
hideProgressBar();
event.stopPropagation();
}
<object id="resultImage" type="image/svg+xml" width="420" height="420" data=""></object>
Make outline
Edit brush

How to move an iFrame in the DOM without losing its state?

Take a look at this simple HTML:
<div id="wrap1">
<iframe id="iframe1"></iframe>
</div>
<div id="warp2">
<iframe id="iframe2"></iframe>
</div>
Let's say I wanted to move the wraps so that the #wrap2 would be before the #wrap1. The iframes are polluted by JavaScript. I am aware of jQuery's .insertAfter() and .insertBefore(). However, when I use those, the iFrame loses all of its HTML, and JavaScript variables and events.
Lets say the following was the iFrame's HTML:
<html>
<head>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
// The variable below would change on click
// This represents changes on variables after the code is loaded
// These changes should remain after the iFrame is moved
variableThatChanges = false;
$(function(){
$("body").click(function(){
variableThatChanges = true;
});
});
</script>
</head>
<body>
<div id='anything'>Illustrative Example</div>
</body>
</html>
In the above code, the variable variableThatChanges would...change if the user clicked on the body. This variable, and the click event, should remain after the iFrame is moved (along with any other variables/events that have been started)
My question is the following: with JavaScript (with or without jQuery), how can I move the wrap nodes in the DOM (and their iframe childs) so that the iFrame's window stays the same, and the iFrame's events/variables/etc stay the same?
It isn't possible to move an iframe from one place in the dom to another without it reloading.
Here is an example to show that even using native JavaScript the iFrames still reload:
http://jsfiddle.net/pZ23B/
var wrap1 = document.getElementById('wrap1');
var wrap2 = document.getElementById('wrap2');
setTimeout(function(){
document.getElementsByTagName('body')[0].appendChild(wrap1);
},10000);
This answer is related to the bounty by #djechlin
A lot of search on the w3/dom specs and didn't find anything final that specifically says that iframe should be reloaded while moving in the DOM tree, however I did find lots of references and comments in the webkit's trac/bugzilla/microsoft regarding different behavior changes over the years.
I hope someone will find anything specific regarding this issue, but for now here are my findings:
According to Ryosuke Niwa - "That's the expected behavior".
There was a "magic iframe" (webkit, 2010), but it was removed in 2012.
According to MS - "iframe resources are freed when removed from the DOM". When you appendChild(node) of existing node - that node is first removed from the dom.
Interesting thing here - IE<=8 didn't reload the iframe - this behavior is (somewhat) new (since IE>=9).
According to Hallvord R. M. Steen comment, this is a quote from the iframe specs
When an iframe element is inserted into a document that has a browsing context, the user agent must create a new browsing context, set the element's nested browsing context to the newly-created browsing context, and then process the iframe attributes for the "first time".
This is the most close thing I found in the specs, however it's still require some interpretation (since when we move the iframe element in the DOM we don't really do a full remove, even if the browsers uses the node.removeChild method).
Whenever an iframe is appended and has a src attribute applied it fires a load action similarly to when creating an Image tag via JS. So when you remove and then append them they are completely new entities and they refresh. Its kind of how window.location = window.location will reload a page.
The only way I know to reposition iframes is via CSS. Here is an example I put together showing one way to handle this with flex-box:
https://jsfiddle.net/3g73sz3k/15/
The basic idea is to create a flex-box wrapper and then define an specific order for the iframes using the order attribute on each iframe wrapper.
<style>
.container{
display: flex;
flex-direction: column;
}
</style>
<div class="container">
<div id="wrap1" style="order: 0" class="iframe-wrapper">
<iframe id="iframe1" src="https://google.com"></iframe>
</div>
<div id="warp2" style="order: 1" class="iframe-wrapper">
<iframe id="iframe2" src="https://bing.com"></iframe>
</div>
</div>
As you can see in the JS fiddle these order styles are inline to simplify the flip button so rotate the iframes.
I sourced the solution from this StackOverflow question: Swap DIV position with CSS only
Hope that helps.
If you have created the iFrame on the page and simply need to move it's position later try this approach:
Append the iFrame to the body and use a high z-index and top,left,width,height to put the iFrame where you want.
Even CSS zoom works on the body without reloading which is awesome!
I maintain two states for my "widget" and it is either injected in place in the DOM or to the body using this method.
This is useful when other content or libraries will squish or squash your iFrame.
BOOM!
Unfortunately, the parentNode property of an HTML DOM element is read-only. You can adjust the positions of the iframes, of course, but you can't change their location in the DOM and preserve their states.
See this jsfiddle I created that provides a good test bed. http://jsfiddle.net/RpHTj/1/
Click on the box to toggle the value. Click on the "move" to run the javascript.
This question is pretty old... but I did find a way to move an iframe without it reloading. CSS only. I have multiple iframes with camera streams, I dont like when they reload when i swap them. So i used a combination of float, position:absolute, and some dummy blocks to move them around without reloading them and having the desired layout on demand (resizing and all).
If you are using the iframe to access pages you control, you could create some javascript to allow your parent to communicate with the iframe via postMessage
From there, you could build login inside the iframe to record state changes, and before moving dom, request that as a json object.
Once moved, the iframe will reload, you can pass the state data into the iframe and the iframe listening can parse the data back into the previous state.
PaulSCoder has the right solution. Never manipulate the DOM for this purpose. The classic approach for this is to have a relative position and "flip" the positions in the click event. It's only not wise to put the click event on the body, because it bubbles from other elements too.
$("body").click(function () {
var frame1Height = $(frame1).outerHeight(true);
var frame2Height = $(frame2).outerHeight(true);
var pos = $(frame1).css("top");
if (pos === "0px") {
$(frame1).css("top", frame2Height);
$(frame2).css("top", -frame1Height);
} else {
$(frame1).css("top", 0);
$(frame2).css("top", 0);
}
});
If you only have content that is not cross-domain you could save and restore the HTML:
var htmlContent = $(frame).contents().find("html").children();
// do something
$(frame).contents().find("html").html(htmlContent);
The advantage of the first method is, that the frame keeps on doing what it was doing. With the second method, the frame gets reloaded and starts it's code again.
At least in some circumstances a shadow dom with slotting might be an option.
<template>
<style>div {outline:1px solid black; height:45px}</style>
<div><slot name="a" /></div>
<div><slot name="b" /></div>
</template>
<div id="shadowhost">
<iframe src="data:text/html,<button onclick='this.innerText+=`!`'>!</button>"
slot="a" height=40px ></iframe>
</div>
<button onclick="ifr.slot= (ifr.slot=='a') ? 'b' : 'a';">swap</button>
<script>
document.querySelector('#shadowhost').attachShadow({mode: 'open'}).appendChild(
document.querySelector('template').content
);
ifr=document.querySelector('iframe');
</script>
In response to the bounty #djechlin placed on this question, I have forked the jsfiddle posted by #matt-h and have come to the conclusion that this is still not possible.
http://jsfiddle.net/gr3wo9u6/
//this does not work, the frames reload when appended back to the DOM
function swapFrames() {
var w1 = document.getElementById('wrap1');
var w2 = document.getElementById('wrap2');
var f1 = w1.querySelector('iframe');
var f2 = w2.querySelector('iframe');
w1.removeChild(f1);
w2.removeChild(f2);
w1.appendChild(f2);
w2.appendChild(f1);
//f1.parentNode = w2;
//f2.parentNode = w1;
//alert(f1.parentNode.id);
}

Make iframe listen to same event as parent and synchronize using jquery

I have two iframes in an html page in the same domain
<body>
<table border="1" width="100%" height="100%">
<tr>
<th><iframe class="isiframe" width="100%" height="100%" src="/example"></iframe></th>
<th><iframe class="isiframe" width="100%" height="100%" src="/example"></iframe></th>
</tr>
</table>
</body>
I have given the click event for all the a tag in the webpage inside the iframe
$('a').bind('click', function(e){
var path = $(this).getPath();
var ahash={
'path':path
};
if (getFrameElement())
window.parent.document.Aaddevent(e, ahash);
});
The "path" in the click event gives the path of the clicked a tag (eg. html > body > div#bar > ul#abc.def.ghi > li#foo). The getFrameElement() returns the frame which is clicked. Now what i want to do is use this path and trigger the click event in other iframe
I have defined sendevent function from where the other iframe gets the event of the parent iframe and triggers the same event as parent and synchronize.
document.sendevent=function(e, ahash){
var iframes= parent.document.getElementsByTagName('iframe');
for (var i= iframes.length; i--;) {
var iframe= iframes[i];
if(iframe){
$(ahash.path).trigger('click');
}
}
};
This is how i want to do and make the iframe work, follow the path of the parent iframe clicked element and then trigger the click event to make other iframe synchronize with the parent iframe using the path
The click event is not getting triggered inside the sendevent function but i am getting the path of the parent iframe clicked element when i do console.log(ahash .path) inside the sendevent function. How can i make this method work or something similar like that. Please suggest me some solution how to do this.
You cannot follow links using $("a").trigger("click"). You can use location.href = $("a").attr("href"). If you are simply trying to navigate to the same url in both frames don't bother with .getPath(), just pass the href. In your parent page, use this to bind the links in first frame to also navigate the second frame:
$("iframe:first").contents().find("a[href]").click(function() {
$("iframe:last", top.document)[0].contentWindow.location.href = this.href;
});
Edit: In IE and newer versions of FireFox and Opera, you can use the native DOM method .click() to simulate an actual click on the link. Chrome doesn't support it yet, but probably will eventually. To use .getPath() in order to find and click the link, try this in the parent page:
$("iframe:first").load(function() {
$(this).contents().find("a[href]").click(function() {
$("iframe:last", top.document).contents().find($(this.getPath())[0].click();
});
});

Is there a faster way to add icons next to urls/links in a web page - alternative to using "onload" event?

I am writing a Firefox add-on that adds little icons next to all the urls in a web page.
The way I am doing it right now is :
window.addEventListener("load", AddIconsToURLs, false);
In AddIconsToURLs function:
Get all elements by tag name "a"
For each "a" element, append an "img" element.
However, for some web pages, the icons take a lot of time to appear since the "onload" event takes time.
Is there any faster way to make this happen ?
Rather than waiting for all the links to load and then adding icons next to each of them, is there any way to add an icon next to a link as soon as that particular link is loaded ?
Any help is appreciated. Thanks.
What do you want to do with the images? Anything special? If it is just for the look, much easier would be to load a CSS style sheet in your addon and use the :after pseudo element:
a:after {
content: url('chrome://youraddon/skin/imagefile.png');
}
No JavaScript processing involved :)
You can load a custom CSS file with:
var sss = Components.classes["#mozilla.org/content/style-sheet-service;1"]
.getService(Components.interfaces.nsIStyleSheetService);
var ios = Components.classes["#mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var uri = ios.newURI("chrome://youraddon/skin/cssfile.css", null, null);
if(!sss.sheetRegistered(uri, sss.AGENT_SHEET)) {
sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
}
See also Using the Stylesheet Service.
There is another event you can listen to:
document.addEventListener("DOMContentLoaded", AddIconsToURLs, false);

Categories