How to prevent inconsistent 'load' event firing from <object> (SVG) [duplicate] - javascript

I have a simple SVG loaded inside a object tag like the code below. On Safari, the load event is fired just once, when I load the first time the page after opening the browser. All the other times it doesn't. I'm using the load event to initialize some animations with GSAP, so I need to know when the SVG is fully loaded before being able to select the DOM nodes. A quick workaround that seems to work is by using setTimeout instead of attaching to the event, but it seems a bit akward as slower networks could not have load the object in the specified amount of time. I know this event is not really standardized, but I don't think I'm the first person that faced this problem. How would you solve it?
var myElement = document.getElementById('my-element').getElementsByTagName('object')[0];
myElement.addEventListener('load', function () {
var svgDocument = this.contentDocument;
var myNode = svgDocument.getElementById('my-node');
...
}

It sounds more like the problem is that, when the data is cached, the load event fires before you attached the handler.
What you can try is to reset the data attribute once you attached the event :
object.addEventListener('load', onload_handler);
// reset the data attribte so the load event fires again if it was cached
object.data = object.data;

I also ran into this problem while developing an Electron application. In my workflow I edit index.html and renderer.js in VSCode, and hit <Ctrl>+R to see the changes. I only restart the debugger to capture changes made to the main.js file.
I want to load an SVG that I can then manipulate from my application. Because the SVG is large I prefer to keep it in an external file that gets loaded from disk. To accomplish this, the HTML file index.html contains this declaration:
<object id="svgObj" type="image/svg+xml" data="images/file.svg"></object>
The application logic in renderer.js contains:
let svgDOM // global to access SVG DOM from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
svgDOM = this.contentDocument
mySvgReady(this)
}
The problem is non-obvious because it appears intermittent: When the debugger/application first starts this works fine. But when reloading the application via <Ctrl>+R, the .contentDocument property is null.
After much investigation and hair-pulling, a few long-form notes about this include:
Using svgObj.addEventListener ('load', function() {...}) instead of
svgObj.onload makes no difference. Using addEventListener
is better because attempting to set another handler via 'onload'
will replace the current handler. Contrary to other Node.js
applications, you do not need to removeEventListener when the element
is removed from the DOM. Old versions of IE (pre-11) had problems but
this should now be considered safe (and doesn't apply to Electron anyway).
Usage of this.contentDocument is preferred. There is a nicer-looking
getSVGDocument() method that works, but this appears to be for backwards
compatibility with old Adobe tools, perhaps Flash. The DOM returned is the same.
The SVG DOM appears to be permanently cached once loaded as described by #Kaiido, except that I believe the event never fires. What's more, in Node.js, the SVG DOM remains cached in the same svgDOM variable it was loaded into. I don't understand this at all. My intuition suggests that the require('renderer.js') code in index.html has cached this in the module system somewhere, but changes to renderer.js do take effect so this can't be the whole answer.
Regardless, here is an alternate approach to capturing the SVG DOM in Electron's render process that is working for me:
let svgDOM // global to access from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
if (svgDOM) return mySvgReady(this) // Done: it already loaded, somehow
if (!this.contentDocument) { // Event fired before DOM loaded
const oldDataUri = svgObj.data // Save the original "data" attribute
svgObj.data = '' // Force it to a different "data" value
// setImmediate() is too quick and this handler can get called many
// times as the data value bounces between '' and the actual SVG data.
// 50ms was chosen and seemed to work, and no other values were tested.
setTimeout (x => svgObj.data = oldDataUri, 50)
return;
}
svgDOM = this.contentDocument
mySvgReady(this)
}
Next, I was very disappointed to learn that the CSS rules loaded by index.html can't access the elements within the SVG DOM. There are a number of ways to inject the stylesheet into the SVG DOM programmatically, but I ended up changing my index.html to this format:
<svg id="svgObj" class="svgLoader" src="images/file.svg"></svg>
I then added this code to my DOM setup code in renderer.js to load the SVG directly into the document. If you are using a compressed SVG format I expect you will need to do the decompression yourself.
const fs = require ('fs') // This is Electron/Node. Browsers need XHR, etc.
document.addEventListener('DOMContentLoaded', function() {
...
document.querySelectorAll ('svg.svgLoader').forEach (el => {
const src = el.getAttribute ('src')
if (!src) throw "SVGLoader Element missing src"
const svgSrc = fs.readFileSync (src)
el.innerHTML = svgSrc
})
...
})
I don't necessarily love it, but this is the solution I'm going with because I can now change classes on the SVG object and my CSS rules apply to the elements within the SVG. For example, these rules from index.css can now be used to declaritively alter which parts of the SVG are displayed:
...
#svgObj.cssClassBad #groupBad,
#svgObj.cssClassGood #groupGood {
visibility: visible;
}
...

Related

Are there any events faster than DOMContentLoaded that can be set as the argument of addEventListener? what is that?

Suppose that get a DOM as reference Node, then create and insert a DOM as new Node.
If I create and insert the new Node as soon as possible, is DOMContentLoaded the best event option in the above situation?
The reference Node is not image, it's HTMLnknowElement such as a HTMLDivElement, HTMLSpanElement.
Here is an example code flow.
window.addEventListener("DOMContentLoaded", (event) => {
// Below is an example code as pseudo code.
// Get a reference DOM.
const referenceEl = document.querySelector(".foo .reference-dom");
// Create a new DOM.
const newEl = document.createElement(htmlTag);
// Insert the new DOM before reference DOM.
referenceEl.parentElement.insertBefore(newEl, referenceEl.nextElementSibling)
})
If I execute the code as soon as possible, is DOMContentLoaded the best event option in the above situation?
If you want the code to run as soon as possible, given what elements it depends on already existing in the DOM, the easiest way to do it would be to put your <script> tag just below the final element required for the script to run. For example, for the .reference-dom, you might do
<div class="reference-dom">
...
</div>
<script>
const referenceEl = document.querySelector(".foo .reference-dom");
// or
const referenceEl = document.currentScript.previousElementSibling;
// ... etc
If that's not an option, another avenue available to you would be to add a subtree MutationObserver to the document ahead of time (such as at the beginning of the <body>, and to watch the addedNodes to see if the node you're looking for appears - but that's expensive, overkill, and much more difficult to manage.
For very large pages (example), waiting for DOMContentLoaded could indeed take too long, in which case what you're doing would be a good idea - to add essential functionality to the app before that event fires, to ensure a good user experience. (Though, if what you're doing is just adding another tag to the HTML, a better approach would be to use a template engine or something server-side so that it serves the full HTML itself, rather than having to alter it client-side through JavaScript later)

Dynamically load SVGs (or anything) as objects. Chrome security feature?

The site itself - test.vancebeckett.com (it's an alpha, I know the text and some other things sucks now, it will be redone).
EDIT:
What's not working (example):
In JS file:
var p2ComicTileAttributes = ["class", "comicTiles p2Content", "id", "p2ComicTile", "type", "image/svg+xml", "data", "svg/p2Comic.svg", "onload", "p2Status++;p2Tiles.splice(0,1,this);"];
function loadPageSVGContent(attributesArray) {
var pageObj = document.createElement("object");
for (var i = 0; i < attributesArray.length; i += 2) {
pageObj.setAttribute(attributesArray[i], attributesArray[i + 1]);
}
document.getElementById("contentLayout").appendChild(pageObj)
};
loadPageSVGContent(p2ComicTileAttributes);
/*This creates an objects, appends them in DOM, sets thair attributes and even runs thair inline code (onload) in Edge and FF, but do neither in Chrome-based browsers. Is this another Chrome safety feature? Like it can't load SVG's (or any other xml) from another domain and have an access to its DOM, it also doesn't allow do the same from local folder, only from the server. "contentLayout" - div-wrapper in HTML document.
So, maybe there's a way to use ajax and somehow pass loaded document to "data" attribute?
About performance, if I will inject SVG's in the html - these SVG's are 100-200KB each, and I have many. I'm sure it will slow down any DOM manipulations. And also I want to find the best way anyway :).*/
What works:
You can check how it works in Edge and FF (maybe somewhere else too) by clicking "Some of my work" and "introduction" in the "Table of contents".
And js file itself for those who couldn't find it and gave a minus in rating: http://test.vancebeckett.com/main.js
(or in FF: Inspect Element - Debugger - main.js)
Ok, I have solved this by myself, finally!
Just use XMLHttpRequest to load .svg file, when it status shows, that it's loaded, create an "object" node with "data" attribute set to "path/to/your/image.svg" and append it to some wrapper in DOM - viola! You have an svg element which could be animated, and which isn't inline in your html!

<link> element's 'load' event not firing, and 'onload' non-operational too

The last thread on this was asked in 2010 and, as far as I can tell, there is still incomplete (or no) support for the 'load' event of a element. My code:
function loadedCb(){ console.log("Loaded!"); };
function errorCb(){ console.log("Error!"); };
var file = document.createElement("link");
file.type = "text/css";
file.rel = "stylesheet";
file.addEventListener('load', loadedCb); // or: file.onload = loadedCb;
file.addEventListener('error', errorCb); // or: file.onerror = errorCb;
file.src = "theDir/theFile.css"; // should immediately start to load.
document.getElementsByTagName('head')[0].appendChild(file);
When this code is run in Firefox 49.0.2, neither callback is ever invoked (and the CSS file also never loads in at any point, which perhaps suggests I've done something wrong). The corresponding code for loading in a .js file as a <script> element works perfectly, however.
Is there any elegant solution to running a callback upon loading in (or erroring upon) a CSS file as of 2016? Or am I doing something basic wrong? I am not using JQuery in my project, incidentally.
Found the main problem: was setting src instead of href. Will check after lunch to see whether the events are firing, but at a brief glance, it looks to be working so far.

How to re-evaluate a script that doesn't expose any global in a declarative-style component

I have been writing a reusable script, let's call it a plugin although it's not jQuery, that can be initialised in a declarative way from the HTML. I have extremely simplified it to explain my question so let's say that if a user inserts a tag like:
<span data-color="green"></span>
the script will fire because the attribute data-color is found, changing the color accordingly.
This approach proved very handy because it avoids anyone using the plugin having to initialise it imperatively in their own scripts with something like:
var elem = document.getElementsByTagName('span')[0];
myPlugin.init(elem);
Moreover by going the declarative way I could get away without defining any global (in this case myPlugin), which seemed to be a nice side effect.
I simplified this situation in an example fiddle here, and as you can see a user can avoid writing any js, leaving the configuration to the HTML.
Current situation
The plugin is wrapped in a closure like so:
;(function(){
var changeColor = {
init : function(elem){
var bg = elem.getAttribute('data-color');
elem.style.background = bg;
}
};
// the plugin itslef looks for appropriate HTML elements
var elem = document.querySelectorAll('[data-color]')[0];
// it inits itself as soon as it is evaluated at page load
changeColor.init(elem);
})();
The page loads and the span gets the correct colour, so everything is fine.
The problem
What has come up lately, though, is the need to let the user re-evaluate/re-init the plugin when he needs to.
Let's say that in the first example the HTML is changed dynamically after the page is loaded, becoming:
<span data-color="purple"></span>
With the first fiddle there's no way to re-init the plugin, so I am now testing some solutions.
Possible solutions
Exposing a global
The most obvious is exposing a global. If we go this route the fiddle becomes
http://jsfiddle.net/gleezer/089om9z5/4/
where the only real difference is removing the selection of the element, leaving it to the user:
// we remove this line
// var elem = document.querySelectorAll('[data-color]')[0];
and adding something like (again, i am simplifying for the sake of the question):
window.changeColor = changeColor;
to the above code in order to expose the init method to be called from anywhere.
Although this works I am not satisfied with it. I am really looking for an alternative solution, as I don't want to lose the ease of use of the original approach and I don't want to force anyone using the script adding a new global to their projects.
Events
One solution I have found is leveraging events. By putting something like this in the plugin body:
elem.addEventListener('init', function() {
changeColor.init(elem);
}, false);
anybody will be able to just create an event an fire it accordingly. An example in this case:
var event = new CustomEvent('init', {});
span.dispatchEvent(event);
This would re-init the plugin whenever needed. A working fiddle is to be found here:
http://jsfiddle.net/gleezer/tgztjdzL/1/
The question (finally)
My question is: is there a cleaner/better way of handling this?
How can i let people using this plugin without the need of a global or having to initialise the script themselves the first time? Is event the best way or am I missing some more obvious/better solutions?
You can override Element.setAttribute to trigger your plugin:
var oldSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function(name, value) {
oldSetAttribute.call(this, name, value);
if (name === 'data-color') {
changeColor.init(this);
}
}
Pros:
User does not have to explicitly re-initialize the plugin. It will happen automatically when required.
Cons:
This will, of course, only work if the user changes data-color attributes using setAttribute, and not if they create new DOM elements using innerHTML or via some other approach.
Modifying host object prototypes is considered bad practice by many, and for good reasons. Use at your own risk.

Run a piece of JavaScript as soon as a third-party script fails to load

I provide a JavaScript widget to several web sites, which they load asynchronously. My widget in turn needs to load a script provided by another party, outside my control.
There are several ways to check whether that script has successfully loaded. However, I also need to run different code if that script load has failed.
The obvious tools that don't work include:
I'm not willing to use JavaScript libraries, such as jQuery. I need a very small script to minimize my impact on the sites that use my widget.
I want to detect the failure as soon as possible, so using a timer to poll it is undesirable. I wouldn't mind using a timer as a last resort on old browsers, though.
I've found the <script> tag's onerror event to be unreliable in some major browsers. (It seemed to depend on which add-ons were installed.)
Anything involving document.write is right out. (Besides that method being intrinsically evil, my code is loaded asynchronously so document.write may do bad things to the page.)
I had a previous solution that involved loading the <script> in a new <iframe>. In that iframe, I set a <body onload=...> event handler that checked whether the <script onload=...> event had already fired. Because the <script> was part of the initial document, not injected asynchronously later, onload only fired after the network layer was done with the <script> tag.
However, now I need the script to load in the parent document; it can't be in an iframe any more. So I need a different way to trigger code as soon as the network layer has given up trying to fetch the script.
I read "Deep dive into the murky waters of script loading" in an attempt to work out what ordering guarantees I can count on across browsers.
If I understand the techniques documented there:
I need to place my failure-handling code in a separate .js file.
Then, on certain browsers I can ensure that my code runs only after the third-party script either has run or has failed. This requires browsers that support either:
Setting the <script async> attribute to false via the DOM,
or using <script onreadystatechange=...> on IE 6+.
Despite looking at the async support table, I can't tell whether I can rely on script ordering in enough browsers for this to be feasible.
So how can I reliably handle failure during loading of a script I don't control?
I believe I've solved the question I asked, though it turns out this doesn't solve the problem I actually had. Oh well. Here's my solution:
We want to run some code after the browser finishes attempting to load a third-party script, so we can check whether it loaded successfully. We accomplish that by constraining the load of a fallback script to happen only after the third-party script has either run or failed. The fallback script can then check whether the third-party script created the globals it was supposed to.
Cross-browser in-order script loading inspired by http://www.html5rocks.com/en/tutorials/speed/script-loading/.
var fallbackLoader = doc.createElement(script),
thirdPartyLoader = doc.createElement(script),
thirdPartySrc = '<URL to third party script>',
firstScript = doc.getElementsByTagName(script)[0];
// Doesn't matter when we fetch the fallback script, as long as
// it doesn't run early, so just set src once.
fallbackLoader.src = '<URL to fallback script>';
// IE starts fetching the fallback script here.
if('async' in firstScript) {
// Browser support for script.async:
// http://caniuse.com/#search=async
//
// By declaring both script tags non-async, we assert
// that they need to run in the order that they're added
// to the DOM.
fallbackLoader.async = thirdPartyLoader.async = false;
thirdPartyLoader.src = thirdPartySrc;
doc.head.appendChild(thirdPartyLoader);
doc.head.appendChild(fallbackLoader);
} else if(firstScript.readyState) {
// Use readyState for IE 6-9. (IE 10+ supports async.)
// This lets us fetch both scripts but refrain from
// running them until we know that the fetch attempt has
// finished for the first one.
thirdPartyLoader.onreadystatechange = function() {
if(thirdPartyLoader.readyState == 'loaded') {
thirdPartyLoader.onreadystatechange = null;
// The script-loading tutorial comments:
// "can't just appendChild, old IE bug
// if element isn't closed"
firstScript.parentNode.insertBefore(thirdPartyLoader, firstScript);
firstScript.parentNode.insertBefore(fallbackLoader, firstScript);
}
};
// Don't set src until we've attached the
// readystatechange handler, or we could miss the event.
thirdPartyLoader.src = thirdPartySrc;
} else {
// If the browser doesn't support async or readyState, we
// just won't worry about the case where script loading
// fails. This is <14% of browsers worldwide according to
// caniuse.com, and hopefully script loading will succeed
// often enough for them that this isn't a problem.
//
// If that isn't good enough, you might try setting an
// onerror listener in this case. That still may not work,
// but might get another small percentage of old browsers.
// See
// http://blog.lexspoon.org/2009/12/detecting-download-failures-with-script.html
thirdPartyLoader.src = thirdPartySrc;
firstScript.parentNode.insertBefore(thirdPartyLoader, firstScript);
}
Have you considered using the window's onerror handler? That will let you detect when most errors occur and you can take appropriate action then. As a fallback for any issues not caught this way you can also protect your own code with try/catch.
You should also check that the third-party script actually loaded:
<script type="text/javascript" onload="loaded=1" src="thirdparty.js"></script>
Then check if it loaded:
window.onload = function myLoadHandler() {
if (loaded == null) {
// The script doesn't exist or couldn't be loaded!
}
}
You can check which script caused the error using the url parameter.
window.onerror = function myErrorHandler(errorMsg, url, lineNumber) {
if (url == third_party_script_url) {
// Do alternate code
} else {
return false; // Do default error handling
}
}

Categories