Apple iOS browsers randomly won't render HTML objects loaded dynamically - javascript

We have a problem that is only evident on iOS browsers (iOS 12.0) with our SPA application that uses HTML object tags to load widgets (HTML/CSS/JS files) through JavaScript onto the page.
The issue is an intermittent one when the page is loaded some of the widgets don't display/render on the screen, yet are loaded into the DOM and can be viewed/highlighted with full element properties in the Safari Web Inspector. but are “invisible” to their user. The problem will occur about 50% of the time if there are 4 widgets to load on a page, 2 typically won't display and it will be different widgets not displaying each time, with no detectable pattern.
The widget javascript load events run properly and there are no errors in the console. In the Safari Web Inspector, we can see some of the HTML elements from the non-rendering object are loaded at position 0,0 but their style is correct in the DOM (left and top set correctly, display: inline, etc.).
Here is the code that loads the widgets (the fragment is added to the DOM after all widgets are setup):
function loadWidget(myFrag, widgetName) {
var widgetObj = document.createElement("object");
widgetObj.data = "widgets/" + widgets[widgetName].type + ".html"; // location of widget
widgetObj.className = "widget unselectable";
widgetObj.id = widgetName;
widgetObj.name = widgetName;
myFrag.appendChild(widgetObj); // build widgets onto fragment
widgetObj.addEventListener("load", widgetLoaded, false); // Run rest of widget initialisation after widget is in DOM
widgetObj.addEventListener("error", badLoad, true);
}
Here is the code in the load event that configures the widget once loaded (we work around a Chrome bug that also affects Safari where the load event is fired twice for every object loaded):
function widgetLoaded(e) {
var loadObj = e.target;
if (loadObj === null) {
// CHROME BUG: Events fire multiple times and error out early if widget file is missing so not loaded (but this still fires), force timeout
return;
}
var widgetName = loadObj.id;
// CHROME BUG: Workaround here is to just set the style to absolute so that the event will fire a second time and exit, then second time around run the entire widgetLoaded
if ((parent.g.isChrome || parent.g.isSafari) && !widgets[widgetName].loaded) {
widgets[widgetName].loaded = true; // CHROME: WidgetLoaded will get run twice due to bug, exit early first time.
loadObj.setAttribute("style", "position:absolute"); // Force a fake style to get it to fire again (without loading all the other stuff) and run through second time around
return;
}
var defView = loadObj.contentDocument.defaultView; // Pointer to functions/objects inside widget DOM
loadObj.setAttribute("style", "position:absolute;overflow:scroll;left:" + myWidget.locX + "px;top:" + myWidget.locY + "px;z-index:" + zIndex);
loadObj.width = myWidget.scaleX * defView.options.settings.iniWidth; // Set the width and height of the widget <object> in dashboard DOM
loadObj.height = myWidget.scaleY * defView.options.settings.iniHeight;
}
The code performs correctly in Chrome (Mac/Windows), IE and Safari (Mac), however, presents the random invisible loading issue in iOS Safari and also in iOS Chrome.
Any ideas what causes this and what the workaround could be?

We couldn't find the exact source of this issue after a lot of investigation and are fairly sure this is a webkit bug. However there is an acceptable workaround, which is to replace the object tag with an iframe tag, and it looks to be working exactly the same way (replace .data with .src) with a bonus it doesn't exhibit the chrome bug where onload events are fired twice, so Chrome runs our app faster now.

Related

force delay before browser print

TLDR
I want to increase the time available to manipulate the DOM before the print version is generated.
Use case
In a website I work on, we have embedded charts from a 3rd party provider (Qlik).
We now want to be able to print the page with the charts using the browser's native print functionality.
The charts need to be redrawn in a different size before printing, otherwise they will look cut off.
The scripts from the 3rd party do subscribe to a resize event, which I can trigger using e.g. window.dispatchEvent(new Event('resize'));. Or alternatively I can directly call qlik.resize();.
Problem
On print, if I attempt to trigger a redraw on 'beforeprint', I see the redrawn charts only in the actual webpage, whereas the print preview still shows the old version of the charts before the redraw, or a broken half-redrawn version.
Observations
It seems that starting with the 'beforeprint' event, I still have ~210 milliseconds to alter the DOM, after that further changes have no effect on the printed output.
I get to the 210ms with the following experiment:
// (This sample uses jQuery, adjust as you see fit)
const $display = $('h2#chart-section');
window.addEventListener('beforeprint', function () {
console.log('beforeprint', window.matchMedia('print').matches);
$display.append(' beforeprint');
for (let dt = 0; dt < 1000; dt += 10) {
window.setTimeout(function () {
console.log('beforeprint t0', window.matchMedia('print').matches);
$display.append(' t' + dt);
}, dt);
}
});
The h2 in the printed version will be like '...t210', whereas in the screen version it goes up to '...t990', even while the print preview is open.
Also interestingly the window.matchMedia('print').matches always gives false.
Question
How can I force more time to pass before the print version is "frozen"?

Why is document returning a different document?

I am making a web page reader that needs to inventory the text nodes in the document when it commences reading, because it reads each sentence down the page. So I'm "crawling" the text nodes you could say.
I have a procedure that uses document.createTreeWalker to take that inventory of text nodes.
I haven't figured out the pattern (I think there is one), but at one point when I use document.body, the document that gets pointed to is not the main page, but the document of an iframe. In my current debugging this happens to be a twitter widget, but I suppose it could be anything. This isn't a twitter question, but you can let that inform your answer if you happen to know twitter is doing something extra-ordinary to make document always go to it instead of the top document. In any case, regardless of the source, I need to get the right document.
What do I mean by the right document, you ask me? I'd say the document hosting the selected text, or if no text is selected then the top document.
But my real question is how did this happen, why is this happening? The last time I messed around with the dom was in 2009 when I wrote a web page reader in IE. Times have changed; I'm writing a Chrome extension and web pages seem 1000x more complex these days. Honestly, it's like a circus on the average web page, and most of it you don't see; it's buried beneath and lurking to trip up any robot like my reader.
I don't want to make a hard coded-rule for twitter, or any other widget. There must be a thousand such things that can end up adding / injecting themselves into the page. I literally can't get into the business of custom rules.
this.LoadAllTextNodes = function () {
this.AllTextNodes = textNodesUnder(document.body); // at some point, this document starts referring to something other than the top document. How did the definition of "document" change?
}
function textNodesUnder(root) {
var textNodes = [];
if (root.nodeType == 3)
textNodes.push(root);
else {
var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, getTextElements, false);
var node;
while (node = treeWalker.nextNode())
textNodes.push(node);
}
return textNodes; // Array
}
function getTextElements(node) {
if (['SCRIPT', 'STYLE'].indexOf(node.parentNode.tagName) !== 0 && node.nodeValue !== '') //filter out script elements and empty elements
return NodeFilter.FILTER_ACCEPT
else
return NodeFilter.FILTER_SKIP
}
The web page I'm testing on happens to be https://code.visualstudio.com/blogs/2016/02/23/introducing-chrome-debugger-for-vs-code. The fact that the topic matter of the page deals with Chrome debugging is just a coincidence. It has no relation to the question. I'm just adding it in the event you'd like to see the source of the page.
<iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" class="twitter-follow-button twitter-follow-button-rendered" title="Twitter Follow Button" src="https://platform.twitter.com/widgets/follow_button.d59f1863bc12f58215682d9908af95aa.en.html#dnt=false&id=twitter-widget-0&lang=en&screen_name=code&show_count=true&show_screen_name=true&size=m&time=1474137195557" style="position: static; visibility: visible; width: 191px; height: 20px;" data-screen-name="code"></iframe>
In a chrome extension, the content scripts get run for each and every window, including the top window and all iframes. In this way, Chrome extension access trumps cross-site restrictions which a script running in a script tag may have.
This was instantiating a context for each frame which pointed the duplications of my extension code running in that frame to their respective documents, not the top window's document.
It runs the code in parallel. In my case each frame was queuing up content to be read without me knowing it, for the singleton window.speechSynthesis to read.
The fix was simple; just don't run in non-top windows:
if (window != window.top) return; // don't run in frames

Javascript: Changing "document.body.onresize" does not take hold without "console.log"

I'm writing a website with a canvas in it. The website has a script that runs successfully on every refresh except for a line at the end. When the script ends with:
document.body.onresize = function() {viewport.resizeCanvas()}
"document.body.onresize" is unchanged. (I double-checked in Chrome's javascript console: Entering "document.body.onresize" returns "undefined".)
However, when the script ends with:
document.body.onresize = function() {viewport.resizeCanvas()}
console.log(document.body.onresize)
"document.body.onresize" does change. The function works exactly as it should.
I can't explain why these two functionally identical pieces of code have different results. Can anyone help?
Edit: As far as I can tell, "document.body" is referring to the correct "document.body". When I call console.log(document.body) just before I assign document.body.onresize, the correct HTML is printed.
Edit 2: A solution (sort of)
When I substituted "window" for "document" the viewport's "resizeCanvas" function was called without fail every time I resized the window.
Why does "window" work while "document" only works if you call "console.log" first? Not a clue.
Resize events: no go
Most browsers don't support resize events on anything other than the window object. According to this page, only Opera supported detecting resizing documents. You can use the test page to quickly test it in multiple browsers. Another source that mentions a resize event on the body element specifically also notes that it doesn't work. If we look at these bug reports for Internet Explorer, we find out that having a resize event fire on arbitrary elements was an Internet Explorer-only feature, since removed.
Object.observe: maybe in the future
A more general method of figuring out changes to properties has been proposed and will most likely be implemented cross-browser: Object.observe(). You can observe any property for changes and run a function when that happens. This way, you can observe the element and when any property changes, such as clientWidth or clientHeight, you will get notified. It currently works only in Chrome with the experimental Javascript flag turned on. Plus, it is buggy. I could only get Chrome to notify me about properties that were changed inside Javascript, not properties that were changed by the browser. Experimental stuff; may or may not work in the future.
Current solution
Currently, you will have to do dirty checking: assign the value of the property that you want to watch to a variable and then check whether it has changed every 100 ms. For example, if you have the following HTML on a page:
<span id="editableSpan" contentEditable>Change me!</span>
And this script:
window.onload = function() {
function watch(obj, prop, func) {
var oldVal = null;
setInterval(function() {
var newVal = obj[prop];
if(oldVal != newVal) {
var oldValArg = oldVal;
oldVal = newVal;
func(newVal, oldValArg);
}
}, 100);
}
var span = document.querySelector('#editableSpan');
// add a watch on the offsetWidth property of the span element
watch(span, "offsetWidth", function(newVal, oldVal) {
console.log("width changed", oldVal, newVal);
});
}
This works similarly to Object.observe and for example the watch function in the AngularJS framework. It's not perfect, because with many such checks you will have a lot of code running every 100 ms. Additionally any action will be delayed 100 ms. You could possibly improve on this by using requestAnimationFrame instead of setInterval. That way, an update will be noticed whenever the browser redraws your webpage.
What you can do is that if you know for certain what particular action triggers a resize on your element that doesn't resize the full window you can trigger a resize event so your browser recalculate all of the divs (if by the case the browser is not triggering the event correctly).
With Jquery:
$(window).trigger('resize');
In the other hand, if you have an action that resizes an element you can always hold from that action to handle other following logic.
<script>
function body_OnResize() {
alert('resize');
}
</script>
<body onresize="body_OnResize()"></body>

IE8 not releasing memory for image elements

I have a JavaScript widget that switches between image frames to create motion,
the widget fetches these frames when its render method is called, and discards reference to them when the dispose method is called.
The implementation works by using PreloadJS (http://www.createjs.com/#!/PreloadJS/documentation) to load a set of frames, it then renders the current frame to a <canvas>, or toggles display:none and display:block on the image elements on legacy browsers.
IE8 is causing issues because it doesn't want to free up the memory from these frames when the dispose method is called. (Note: This only happens if the images are in the DOM, otherwise everything is okay.)
The complete source can be found here: http://pastie.org/8061442
The images are added to the DOM on line 201:
var i = 0, img, node = self.$node[0];
for ( ; i < self.imageSet.length; i += 1 ) {
img = self.queue._loadedResults[self.imageSet[i]]
img.setAttribute("draggable", "false")
img.style.display = "none"
node.appendChild(img)
img = null
}
i = node = null
If this code is removed (and the accompanying disposal code here on :251-259) then the images are disposed correctly, it's only when the elements are added to the DOM that there is a problem.
If the render method is called after the dispose fails, the classic Stack verflow at line: 0 error gets thrown.
Any ideas / suggestions would be very helpful. Please look at the code before suggesting though, all references to the images has been nulled and the instance of the image loader has been disposed also, there are no event listeners on the image elements that would create a circular reference, etc. As far as I can see the images should be freed.

scroll adjustment not working correctly in non-IE browsers after loading an image (JavaScript/DOM); timing issue w/innerHTML

I am having some frustrating javascript timing issues.
FYI, the page is a jsp file and attached to said page is a separate js file and the jQuery CDN file. For troubleshooting purposes, I eliminated all unnecessary content and code and pasted what I needed into separate jsp and js files to troubleshoot this specific problem.
If I could display the html and js someplace, that would be great. But for now, I'll describe it. The page has two buttons, one to load an image and one to toggle a "zoom" feature (more on that later). The user clicks a button, which loads an image using the DOM, specifically innerHTML. This image is surrounded by horizontal and vertical scrollbars. When the user turns on the "zoom" feature, the image records the mouse-click position in an onclick event. So, with this on, the user clicks on the image and a bigger version of the same image is loaded, again, using the DOM and innerHTML. The very last step, the most important one, using the mouse position, the scrollbars will focus and center on the point clicked (using scrollLeft and scrollTop).
This all works flawlessly in IE. However, in non-IE browers (i.e. FireFox), it takes a couple of clicks for the scroll adjustment to catch up to the innerHTML. That is, when the user "zooms" for the first time, the image loads but the scrollbars don't adjust. It takes two more successive clicks for it to work the same as in IE. I was researching innerHTML and it is slower in FireFox than IE.
How can I fix this? Has anybody else tried to load an image in FireFox using JavaScript and immediately adjust the scroll positioning on the image? Again, it works the first and each time after that in IE. But non-IE browsers are having issues.
I've tried using innerHTML, replaceChild, appendChild, nothing I tried so far fixes it.
Thank you.
Update: I wanted to see if this issue is anything inside the scrollbars or just images; so, I replaced the image with < p > ... < p > and programmed it to scroll immediately after the **first* image is loaded, via a user-initiated onclick event. Interestingly, it worked. I then replaced the text with the image and it was broken again.
So, after an image is loaded using the DOM (i.e. innerHTML), any attempts to programmatically scroll in non-IE browsers will break. If you programmatically scroll once more, though, it will behave normally.
Update2: I tried employing methods to programmatically cancel the event at the end of the call and immediately call the function again, but that didn't fix the issue.
Then, I tried loading the image using jquery and that seemed to work. I adapted it from two other stackoverflow articles: Can I get the image and load via ajax into div and img onload doesn't work well in IE7 (to circumvent a caching issue).
Here is the code I used:
image = new Image();
image.src = "sample.gif?d=" + new Date(); // passing unique url to fix IE (see link above)
image.onload = function () {
$("#imgcontainer").empty().append(image);
// document.getElementById("imgcontainer").appendChild(image); // This worked, too
// $("#imgcontainer").html("<img src=\"sample.gif?d=" + new Date() + "\"></img>"); // Failed
// document.getElementById("imgcontainer").innerHTML = "<img src=\"sample.gif?d" + new Date() + "\"></img>"; // Failed
$("#imgcontainer").scrollTop(25);
};
image.onerror = function () {
$("#imgcontainer").empty().html("That image is not available.");
}
$('#imgcontainer').empty().html('Loading...');
The key, I believe, was using the onload method. I tried employing jQuery.html() inside the onload method and it didn't work. So, that confirms there was definitely a timing issue related to innerHTML and how and when the image is loaded into the DOM. And the onload method, in combination with either the DOM's native appendChild method or jQuery's equivalent appendChild implementation, fixed the problem.
Update3:
Regarding mrtsherman's suggestion below--
Here is my code:
var outerDIV, innerDIV, html;
outerDIV = document.createElement("div");
outerDIV.id = "content";
document.getElementById("body_id").appendChild(outerDIV); // attach div to body
innerDIV = document.createElement("div");
innerDIV = "ImageHolder";
image = new Image();
image.src = "sample.gif?d=" + new Date();
document.getElementById("content").appendChild(innerDIV);
document.getElementById("ImageHolder").style.height=image.height + "px";
document.getElementById("ImageHolder").style.width=image.width + "px";
html = "<img src=\"sample.gif\"></img>";
$("#content").scrollTop(100);
$("#ImageHolder").html(html);
I created an inner div to place the image. After creating said div, I adjusted it's size, based on the dimensions of the image. I adjusted the scrolling in js and then attached the image to the DOM, via innerHTML, and it did not scroll. I changed the width and height to some fixed size larger than the image and it scrolled. But that is not the desired affect, to make a container bigger than the image. Thank you for your help.
Update4:
What is the equivalent of the code I wrote in Update2 when using document.createElement("img"), instead of new Image()? The onload event is not having the same affect as in the Image object; the onload event is an important ingredient, here.
Thank you.
If you know the new images dimensions then I would send those first. Then you can resize a container for the image, adjust scrollbars and then get the image.
Please see the original question, Update2, for the solution I employed.

Categories

Resources