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

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!

Related

When I use createElement and set it's text, I can't see it on `View Source`

private addStructuredData(el: ElementRef, schema: JsonLdSchemas) {
const script = this.renderer.createElement('script');
this.renderer.setAttribute(script, 'type', 'application/ld+json');
script.text = JSON.stringify(schema);
this.renderer.appendChild(el.nativeElement, script);
}
I have the above code as an Angular function. The idea is to create a script element then append it as a child to whichever element is passed in the function.
This function works well if you only want to see the result through Inspect Element. The schema appears perfectly there.
Though when I try to view it through View Source, the schema doesn't appear. Any Ideas? Thanks
That's expected. When you view the source of a page, you will be viewing the raw uncompressed response that the server sent you. It will not contain any modifications that may have been made by scripts to the DOM.
The changes will be viewable by inspecting the element in developer tools, or by examining the innerHTML / outerHTML of the element or one of its ancestors.
In your case, either the source or one of the scripts it requires will have the script text that appends the new script, in Javascript, eg, something like
addStructuredData(el, schema) {
const script = this.renderer.createElement('script');
this.renderer.setAttribute(script, 'type', 'application/ld+json');
script.text = JSON.stringify(schema);
this.renderer.appendChild(el.nativeElement, script);
}
(or perhaps converted into ES5 rather than ES6)
But the script that the above appends will not be visible by looking at the view-source of your page.

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

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;
}
...

jQuery on external text requesting images

I'm working on a chrome extension that uses jquery to parse the source of a page for specific things. In example I'm looking through Wikipedia to get the categories.
I get the source of the page via
chrome.tabs.executeScript(tabId, {
code: "chrome.extension.sendMessage({action: 'getContentText', source: document.body.innerHTML, location: window.location});"
}, function() {
if (chrome.extension.lastError)
console.log(chrome.extension.lastError.message);
});
I am then listening for this message (successfully) and then use jquery to parse the source key of the object, like so
if (request.action == "getContentText")
{
//console.log(request.source);
$('#mw-normal-catlinks > ul > li > a', request.source).each(function()
{
console.log("category", $(this).html());
});
}
This works as expected and logs a list of all the category links innerHTML. However the issue happens from that jQuery selector that it tries to load the images that are contained in request.source. This results in errors such as
GET chrome-extension://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Padlock-silver.svg/20px-Padlock-silver.svg.png net::ERR_FAILED
These are valid links, however they are being called (unneeded) from my extension with the chrome-extension:// prefix (which is invalid). I'm not sure why jquery would try to evaluate/request images from within source using a selector
I guess this is happening because Wikipedia uses relative paths on their images (instead of https:// or http://, simply // - so the content loaded is relative to the server). The requests are being made by jQuery and you can see here how to fix this issue (in future, please make sure to search SO more thoroughly).
A huge thank you to #timonwimmer for helping me in the chat. We both happened to find different solutions at the same time.
My solution was to use a regex to remove any occurances of the images. Via
var source = request.source.replace(/.*?\.wikimedia\.org\/.*?/g, "");
His was an answer on stack overflow already, that was derived from another answer. If you are interested this answer works perfectly
If you give jQuery a string with a complete element declaration it actually generates a new DOM element, similar to calling document.createElement(tagName) and setting all of the attributes.
For instance: var $newEl = $("<p>test</p>") or in your case img tag elements with $("<img/>"). That would get parsed and created as a new DOM HTML element and wrapped by jQuery so you can query it.
Since you are passing a complete and valid HTML string, it is parsing it into an actual DOM first. This is because jQuery uses the built in underlying document.querySelector methods and they act on the DOM not on strings -- think of the DOM as a database with indexes for id and class and attributes for querying. For instance, MongoDB cannot perform queries on a raw JSON string, it needs to first process the JSON into BSON and index it all and the queries are performed on that.
Your problem is less with jQuery and more so with how elements are created and what happens when attributes change for those elements. For instance, when the img elements are created with document.createElement('img') and then the src attribute is set with imgElement.src = "link to image" this automatically triggers the load for the image at location src.
You can test this out for yourself by running this in your JavaScript Developer Console:
var img = document.createElement('img');
img.src = "broken-link";
Notice that this will likely show and errors in your console after running stating that the image cannot be found.
So what you want, to ensure so it does not resolve the image's src, is to either
1) apply jQuery on an existing DOM (document.body, etc), or
2) let it parse and evaluate the string into a DOM and clean the string before hand (remove the img tags using Regex or something). Take a look at https://stackoverflow.com/a/11230103/2578205 for removing HTML tags from string.
Hope it works out!

Is there a way to better way to get outerHtml?

I'm trying to get a consistent, cross browser outerHtml for a jQuery element, particularly $("html").outerHtml(). For example if the page source is this:
<html><script src="blah.js"></script><div class="foo" id='bar'></p></div></html>
I want to be able to use $("html").outerHtml(); to get the HTML string including the element itself, which would be:
<html><script src="blah.js"></script><div class="foo" id="bar"><p></p></div></html>
I've been using Brandon Aaron's outerHtml plugin which looks something like this:
return $('<div>').append($("html").first().clone()).html();
However this seems to actually try to reload any externally referenced files in the document (scripts, stylesheets), which seems pretty excessive just to get the HTML source of a wrapper element. Can Javascript even put an HTML element inside a DIV element?
Locally, I get this error. Something to do with AJAX rules?
XMLHttpRequest cannot load file:///C:/demo.js?_=1311466511031. Origin null is not allowed by Access-Control-Allow-Origin.
Is there a better way to get outerHtml? I'd really like to avoid any network calls when doing this.
Wrote my own solution, which simply renders the wrapping element:
(function($){
$.fn.outerHtml = function() {
if (this.length == 0) return false;
var elem = this[0], name = elem.tagName.toLowerCase();
if (elem.outerHTML) return elem.outerHTML;
var attrs = $.map(elem.attributes, function(i) { return i.name+'="'+i.value+'"'; });
return "<"+name+(attrs.length > 0 ? " "+attrs.join(" ") : "")+">"+elem.innerHTML+"</"+name+">";
};
})(jQuery);
https://gist.github.com/1102076
jQuery outerHTML plugin from https://github.com/darlesson/jquery-outerhtml uses browser's native outerHTML when existent, as second option clone the nodes in a temporary document fragment and return its innerHTML or uses jQuery clone solution similar to Brandon Aaron's plugin.
This plugin might help you avoid the load of the reference files. In my tests in Chrome, Firefox and Internet Explorer the issue did not happen calling the code below in a page with external JavaScript files.
var outerHTML = $(document.documentElement).outerHTML();

Dynamically including javascript files only once

I have a javascript function I'm writing which is being used to include an external JS file, but only once. The reason I need such a function is because it is being called when some content is loaded via AJAX and I need to run page-specific code to that content (no, just using .live won't cover it).
Here's my attempt, shortened for brevity:
$.include_once = function(filename) {
if ($("script[src='" + filename + "']").length === 0) {
var $node = $("<script></script>")
.attr({
src : filename,
type : "text/javascript"
})
;
$(document.body).append($node);
}
};
This works fine: the function is called, it loads the external file, and that file is being run when loaded. Perfect.
The problem is that it will always re-load that external file: the query I'm using to check for the presence of the script always finds nothing!
When debugging this, I added some lines:
alert($("script").length); // alerts: 4
$(document.body).append($node);
alert($("script").length); // alerts: 4
Looking in the dynamic source (the HTML tab of Firebug), I can't find the script tag at all.
I know that I could maintain an array of files that I've previously included, but I was hoping to go with a method such as this, which (if it worked), seems a bit more robust, since not all the JS files are being included in this way.
Can anyone explain the behaviour seen in this second snippet?
jQuery is a bit of a dumb-dumb in this case; it doesn't do at all what you'd expect. When you append($node) jQuery does this:
jQuery.ajax({
url: $node.src,
async: false,
dataType: "script"
})
Woops! For local files (eg on the same domain) jQuery performs a standard XMLHttpRequest for the .js file body, and proceeds to "eval" it by a whole convoluted process of creating a <script> tag (again!) and settings it's contents to your .js file body. This is to simulate eval but in the global context.
For cross-domain files, since it cannot perform the standard XMLHttpRequest due to the same-domain policy, jQuery once again creates a <script> element and inserts it into <head>.
In both the local and cross-domain cases above jQuery finally gets around to doing this:
head.removeChild(script);
And booms your .length check! Bummer.
So on to your problem, don't bother jQuery with this. Just do
document.getElementsByTagName('head')[0]
.appendChild(
document.createElement('script')
)
.src = filename;
Which will do what you'd expect, particularly wrt querying for it later.
You're trying to solve a problem that has already been solved several times over. Try LazyLoad for example. There are also similar plugins for jQuery.
Instead of setting the source attribute of the script-tag, set the "text" attribute of the script tag. This works in all modern browsers (the application where I use that in practice does not support IE6, so I do not know about this creep...).
In practice it would look like this (you HAVE TO add code to omit double inclusion on yourself - e.g. a simple array of all alread loaded scripts, though thats very application specific, and why should you anyway load code twice? try to omit double-loading code...!):
var script_source_code_string = <some dynamically loaded script source code>;
var $n = $("<script></script>");
$n.get(0).text = script_source_code_string;
$(document.body).append($n);
Or even simpler (without jquery, my code at this stage does not know jquery, it may also be loaded dynamically):
var script_source_code_string = <some dynamically loaded script source code>;
var s = document.createElement('script');
document.getElementsByTagName('head')[0].appendChild(s);
s.text = script_source_code_string;

Categories