Load event not fired on Safari when reloading page - 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

A-Frame scene initializes before assets ready when dynamically adding `a-asset-item` elements

I have published an A-Frame component street (source) that depends on many assets to construct a streetscape.
I have followed a working example from another A-Frame component app vartiste-toolkit to inject a-asset-item elements dynamically prior to scene initialization. The goal is to use the A-Frame asset loader (and display its built-in loading screen) while also allowing a simple syntax for other developers to include this component in their scene, such as:
<a-assets>
<a-asset streetmix-assets-url="https://kfarr.github.io/streetmix3d/"></a-asset>
</a-assets>
I have created a test project that loads the component and assets in that fashion:
https://street-component-test.glitch.me
The assets and scene load as expected on the first page load when when loading all files new (no locally cached files). It looks like the "desired state" screenshot below.
However, on the second or subsequent loads (which use cached files) I am having an issue where some entities do not load as expected. It looks like the "error state" screenshot below.
In the "error state" scene the console includes many of these errors:
THREE.WebGLRenderer: Texture marked for update but image is incomplete
My hypothesis is that when loading the scene with locally cached files, the A-Frame scene initializes prior to the street component assets being injected into the a-assets section. This results in entities being acted on prior to their underlying textures being ready for scene to start rendering, and thus the above THREE console error.
How can I ensure "desired state" outcome even when loading with cached files?
Is there a better way to allow users to load a set of assets required for a component?
Screenshots:
Desired state:
Error state:
I think the best approach here is to create a custom A-Node element. Let's call it streetmix-assets. As long as we attach the streetmix-assets to the a-assets element before it has finished loading, we can force a-assets to wait for all of the rest of the assets to load.
There's two tricky parts here:
Convince a-assets to wait for streetmix-assets to finish loading.
Prevent streetmix-assets from loading until all of the assets we inject have finished loading.
We handle 1 by masquerading streetmix-assets as an a-asset-item entity (e.g., set isAssetItem = true and add a src attribute). We can handle 2 by reusing the methods of a-assets itself. The code to register the streetmix-assets element is below:
// Avoid adding everything twice
var alreadyAttached = false;
// Needed to masquerade as an a-assets element
var fileLoader = new THREE.FileLoader();
window.AFRAME.registerElement('streetmix-assets', {
prototype: Object.create(window.AFRAME.ANode.prototype, {
createdCallback: {
value: function() {
// Masquerade as a an a-asset-item so that a-assets will wait for it to load
this.setAttribute('src', '')
this.isAssetItem = true;
// Properties needed for compatibility with a-assets prototype
this.isAssets = true;
this.fileLoader = fileLoader;
this.timeout = null;
}
},
attachedCallback: {
value: function () {
if (alreadyAttached) return;
if (this.parentNode && this.parentNode.hasLoaded) console.warn("Assets have already loaded. streetmix-assets may have problems")
alreadyAttached = true;
// Set the innerHTML to all of the actual assets to inject
this.innerHTML = buildAssetHTML(this.getAttribute("url"));
var parent = this.parentNode
// Copy the parent's timeout, so we don't give up too soon
this.setAttribute('timeout', parent.getAttribute('timeout'))
// Make the parent pretend to be a scene, since that's what a-assets expects
this.parentNode.isScene = true
// Since we expect the parent element to be a-assets, this will invoke the a-asset attachedCallback,
// which handles waiting for all of the children to load. Since we're calling it with `this`, it
// will wait for the streetmix-assets's children to load
Object.getPrototypeOf(parent).attachedCallback.call(this)
// No more pretending needed
this.parentNode.isScene = false
}
},
load: {
value: function() {
// Wait for children to load, just like a-assets
AFRAME.ANode.prototype.load.call(this, null, function waitOnFilter (el) {
return el.isAssetItem && el.hasAttribute('src');
});
}
}
})
})
Some quick notes about this first snippet:
buildAssetHTML(assetUrl) is a function that returns a string containing the HTML code for all of your assets. (E.g., something like <img src="myimage.jpg><a-asset src="my-asset.glb></a-asset> etc.)
this.getAttribute("url") allows the user to specify the url to load the assets from. So it can be used like: <streetmix-asset url="http://mycdn.example.com/streetmix"></streetmix-asset>
alreadyAttached protects against accidentally adding the streetmix assets multiple times.
This snippet should get you to the point where a user can include your library and then put:
<a-assets>
<streetmix-asset></streetmix-asset>
</a-assets>
in their own scene to properly load your assets. However, if the user does not include the streetmix-assets element, then your assets will not load.
In order to ensure that your assets load no matter what (i.e. the intention behind the assets in aframe-vartiste-toolkit) there are three cases to consider:
The user has included the a-assets element and the streetmix-assets element.
The user has included neither the a-assets element nor the streetmix-assets element.
The user has included the a-assets element but not the streetmix-assets element.
The first case is already taken care of by registering the element. The second case could be handled in a fairly straightforward way by listening for the DOMContentLoaded, e.g.:
window.addEventListener('DOMContentLoaded', (e) => {
if (alreadyAttached) return;
let assets = document.querySelector('a-assets')
if (!assets)
{
assets = document.createElement('a-assets')
document.querySelector('a-scene').append(assets)
}
if (assets.hasLoaded)
{
console.warn("Assets already loaded. May lead to bugs")
}
let streetMix = document.createElement('streetmix-assets')
assets.append(streetMix)
});
However, in case 3 this would run into the same problem you're experiencing now, since the a-assets may have already finished loading by the time DOMContentLoaded is emitted. So we'll forget about the DOMContentLoaded event.
Instead, we'll tackle case 2 and case 3 at the same time, using the DOMSubtreeModified event. We can use the DOMSubtreeModified to add the streetmix-assets element as soon as the a-scene is in place. The code for this looks something like this:
var domModifiedHandler = function(evt) {
// Only care about events affecting an a-scene
if (evt.target.nodeName !== 'A-SCENE') return;
// Try to find the a-assets element in the a-scene
let assets = evt.target.querySelector('a-assets');
if (!assets) {
// Create and add the assets if they don't already exist
assets = document.createElement('a-assets')
evt.target.append(assets)
}
// Already have the streetmix assets. No need to add them
if (assets.querySelector('streetmix-assets')) {
document.removeEventListener("DOMSubtreeModified", domModifiedHandler);
return
}
// Create and add the custom streetmix-assets element
let streetMix = document.createElement('streetmix-assets')
assets.append(streetMix)
// Clean up by removing the event listener
document.removeEventListener("DOMSubtreeModified", domModifiedHandler);
}
document.addEventListener("DOMSubtreeModified", domModifiedHandler, false);
All of this should allow you to inject your assets from an included library into a user's scene pretty reliably. Just take note that this solution leans pretty heavily on some A-Frame implementation details that could change in future versions.
Like You say, there is a race between the script which injects assets into the document and the component which uses them
One solution would be to keep the information whether the assets are injected and use it in the said component:
// - assetsloaded can be a global flag, assets dataset attribute
// a-frame component / system attribute.
// - streetmix-assets-load can be an event emitted on the <a-assets> node
if (assetsloaded)
parseResponse(response)
else
assets.addEventListener("streetmix-assets-loaded", e => parseResponse(response))
You can see a similar approach here in aframe-extras.
You can modify assets.js to keep the info whether streetmix-assets are ready, emit a streetmix-assets-loaded, and make sure that the street component does its job when the assets are ready.
Like in this glitch, which is a remix of your glitch, where I've applied said logic in assets.js and street.js.

Making a Chrome extension for a site that uses React. How to persist changes after re-rendering?

I want my extension to add elements to a page. But the site uses React which means every time there's a change the page is re-rendered but without the elements that I added.
I'm only passingly familiar with React from some tutorial apps I built a long time ago.
I implemented #wOxxOm's comment to use MutationObserver and it worked very well.
static placeAndObserveMutations (insertionBoxSelector) {
let placer = new Placement ();
placer.place(insertionBoxSelector);
placer.observeMutations(insertionBoxSelector);
}
place (insertionBoxSelector) {
let box = $(insertionBoxSelector)
this.insertionBox = box; //insertionBox is the element that the content
// will be appended to
this.addedBox = EnterBox.addInfo(box); //addedBox is the content
// Worth noting that at this point it's fairly empty. It'll get filled by
// async ajax calls while this is running. And all that will still be there
// when it's added back in the callback later.
}
observeMutations(insertionBoxSelector) {
let observer = new MutationObserver (this.replaceBox.bind(this));
// this.insertionBox is a jQuery object and I assume `observe` doesn't accept that
let insertionBox = document.querySelector(insertionBoxSelector);
observer.observe(title, {attributes: true});
}
replaceBox () {
this.insertionBox.append(this.addedBox);
_position (this.addedBox);
}
That being said someone suggested adding the content above the React node (i.e. the body) and just positioning the added content absolutely relative to the window. And as this will actually solve a separate problem I was having on some pages of a site I'll probably do that. Also, it's far simpler.
But I thought this was still an interesting solution to a rare problem so I wanted to post it as well.

SVG onload event listener doesn't seem to fire in Edge [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;
}
...

viewer.js / pdf.js: Memory usage increases every time a pdf is rendered

My problem is that the memory usage of my application increases every time I render a pdf file with viewer.js.
I render my pdf this way:
container = document.getElementById('viewerContainer');
viewer = document.getElementById('viewer');
pdfViewer = new PDFViewer({
container: container,
viewer: viewer
});
$scope.pdfFindController = new PDFFindController({
pdfViewer: pdfViewer
});
pdfViewer.setFindController($scope.pdfFindController);
container.addEventListener('pagesinit', function () {
pdfViewer.currentScaleValue = 'page-width';
});
PDFJS.getDocument($scope.getPageLink(pdf)).then(function (pdfDocument) {
documentPdf = pdfDocument;
pdfViewer.setDocument(pdfDocument);
});
I render the file in a separate view. When I go back to my previous view and open another file, the memory usage increases by ~20MB.
I tried this:
documentPdf.destroy();
Now, the memory usage decreases a bit, but not as much as it was allocated before.
Is there a solution for this?
UPDATE:
Pdf.js version: 1.6.210
pdf.js worker version: 1.6.210
You need to call the destroy method on the promise of the DocumentPageProxy.
The documentation describes it as followed:
Destroys current document instance and terminates worker.
Source: https://github.com/mozilla/pdf.js/blob/master/src/display/api.js (Line 621)
There are some tests in the current pdf.js library, which tests the behaviour of the destroy method. (https://github.com/mozilla/pdf.js/blob/master/test/unit/api_spec.js (Line 86)
In your case something like:
// a variable to store the callback function
var loadingTask = PDFJS.getDocument(basicApiUrl);
...
// when the document should get destroyed call
loadingTask.destroy();
I think that by calling documentPdf.destroy(); you don't free the memory taken by pdfViewer:
I didn't find any methods to destroy pdfViewer, but you can try to call
delete pdfViewer;
delete documentPdf;
and if you are not confident that deleting properties is enough, you can set both to null.
If you still experience memory leaks it can be that the HTML stored in the history cache is using up your memory, so try to replace the viewer or container HTML with an empty element (or remove it completely)
document.getElementById('viewerContainer').outerHTML = '';
or
container.parentNode.removeChild(container);

Extending <object> in Dart

The Dart <object> element does not support a getter to access <object>.contentDocument and thus I thought about extending the object to add the functionality.
I took a look at the implementation of the ObjectElement and I basically need to add these lines:
#DomName('HTMLObjectElement.contentDocument')
#DocsEditable()
Document get contentDocument => _blink.BlinkHTMLObjectElement.instance.contentDocument_Getter_(this);
However, I have no idea how to do this. The solution I am using at this time is with a proxy which redirects all calls to the underlying JsObject but to be honest, this is not just dirty, it impossible to maintain.
/* Updated to explain the root of all evil */
When starting the project I am working on, I wanted to display SVGs, which are uploaded by the user, on the website and let the user manipulate these SVGs by inserting additional SvgElements or removing others.
When downloading the SVGs as a String and displaying them by
container.append(new SvgElement(svgCode))
I got really strange display bugs such that embeded images in the SVGs are displaced or even removed and other bugs with masks.
The problem was solved by using an <object> tag and set setting its data attribute to the SVG's url. The SVGs are rendered correctly. That being said, another issue came up. I wasn't able to access and manipulate the SVGs DOM because it's inside an <object> tag and the tag's document cannot be accessed by using contentDocument.
When taking all this into account, there are pretty much only two options left:
I use the <object> tag with no display bugs but not being able to manipulate the SVGs or
I create new SvgElements fromt the SVG's source and append them to the DOM which let's me manipulate the SVGs but having display bugs.
Since having display bugs isn't really a solution I can only make use of the first option, using an <object> tag and working around with Javascript to access the object's contentDocument.
As you can see, accessing the contentDocument is not always a security issue and not allowing to make use of it, is just a quick and dirty solution of a problem.
When accessing the contentDocument by using a JsObject, I get a JsObject back and not an Element. Thus I do not only have to update my code pretty much everywhere, but it gets also pretty ugly since I have to use the JsObject with callMethod(blabla).
class MyObjectElement extends ObjectElement {
static bool _isRegistered = false;
static register() {
if (!_isRegistered) {
document.registerElement('my-object', MyObjectElement,
extendsTag: 'object');
_isRegistered = true;
}
}
factory MyObjectElement() {
var result = document.createElement('object', 'my-object');
return result;
}
MyObjectElement.created() : super.created();
js.JsObject get contentDocument {
// doesn't seem to work with a custom element.
return new js.JsObject.fromBrowserObject(this)['contentDocument'];
}
}
use it like
MyObjectElement.register();
var obj = new MyObjectElement()
..data =
"https://www.suntico.com/wp-content/uploads/DemoStampRotate01-e1400242575670.png";
document.body.append(obj);

Categories

Resources