How to determine when a browser has finished painting an inserted element? - javascript

I have a web page that is injecting HTML delivered via AJAX. I don't know, a priori, the size of the content, but I need to position the content on the page based on its size. Here's a simple approach that doesn't work:
Set parent container's opacity to 0.
Insert the content into the DOM.
Measure the sizes, e.g. $el.outerWidth(true).
Position the content based on those sizes.
Set the parent container's opacity (back) to 1.0.
The problem with this approach is that the content is quite complex and includes images. When outerWidth() is called, the browser hasn't yet finished repainting the screen so that the sizes returned aren't accurate. (Interestingly, in my case the initial dimensions are always significantly larger than the ultimate values, not 0 as I might expect.)
I can reduce the problem by setting a timer between steps 2 and 3, but no matter how long I make the timer, there will surely be some browser on some system that will exceed it. And if I make the timer long enough to cover most cases, that will penalize users with faster systems by introducing unnecessary delays. Although a few of the answers referenced below suggest a setTimeout(fn, 0) is sufficient, I've found that not to be be the case. I've measured paint times as high as 60 ms on a 2012 MacBook Air, and one answer below suggests 500 ms.
It looks like Mozilla at one time had an onPaint event that might have solved my problem … if it still existed and if other browsers had adopted it. (Neither of which is the case.) And Mutation Observers don't seem to account for paint time when reporting changes in elements.
Solutions or comments very much appreciated. As noted below, this has been asked before but most of the questions are at least a year old and I'm desperate enough to try again.
Related questions that don't, unfortunately, offer a workable answer
How to detect when an image has finished rendering in the browser (i.e. painted)?
jQuery - How to execute script as soon as we know true height (with images) of ajax-loaded element?
DIV width immediately after append is calculating wrong?
jQuery returns height of 0 for div element immediately after appending element
Javascript: unable to get height of newly created element
jQuery: Why does .width() sometimes return 0 after inserting elements with .html()?
Update: Well, it appears there simply isn't a general solution for this problem. In my specific case, I found a property that would reliably change once the content was loaded and layout was complete. The JavaScript polls for a change in this property. Not at all elegant, but it was the best option I could find.

I've started looking at the current situation of this and my solution would be this:
doSomeDomManipulation();
requestAnimationFrame(() => {
setTimeout(() => {
alert("finished painting");
}, 0);
});
Source for this approach:
This used to be inside a note on the old MDN page on the topic of MozAfterPaint:
"Web pages that want to take an action after a repaint of the page can use requestAnimationFrame with a callback that sets a timeout of zero to then call the code that takes the desired post-repaint action."

You could see if window load will do the trick. Im not sure if it fires more than once, when new content is loaded , or not.
but for images, you could use something like this.
//Call a function after matching images have finished loading
function imagesLoadedEvent(selector, callback) {
var This = this;
this.images = $(selector);
this.nrImagesToLoad = this.images.length;
this.nrImagesLoaded = 0;
//check if images have already been cached and loaded
$.each(this.images, function (key, img) {
if (img.complete) {
This.nrImagesLoaded++;
}
if (This.nrImagesToLoad == This.nrImagesLoaded) {
callback(This.images);
}
});
this.images.load(function (evt) {
This.nrImagesLoaded++;
if (This.nrImagesToLoad == This.nrImagesLoaded) {
callback(This.images);
}
});
}
$("#btn").click(function () {
var c = $("#container"), evt;
c.empty();
c.append("<img src='" + $("#img1").val() + "' width=94>");
c.append("<img src='" + $("#img2").val() + "' width=94>");
c.append("<img src='" + $("#img3").val() + "' width=94>");
evt = new imagesLoadedEvent("img", allImagesLoaded);
});
function allImagesLoaded(imgs) {
//this is called when all images are loaded
//console.log("all " + imgs.length + " images loaded");
}
js fiddle for images loaded

Related

How to properly scroll IFrame to bottom in javascript

For a mockup-webpage used for research on interaction on websites, I created a mockup message-stream using JavaScript. This message stream is loaded in an IFrame and should show images at pre-set intervals and scroll to the bottom of the page after placing a new image at the bottom of the page. Getting the images to appear is working quite well with the provided script. However, both Chrome and IE seem to have trouble scrolling the page to the bottom. I would like to scroll to the bottom of the page as soon as the image is attached, but have for now added a 5 ms delay because that seemed to work sometimes. My questions are:
Is it okay to use document.body.scrollHeight for this purpose?
Can I make the scroll occur directly, or do I need a small interval before scrolling?
How to make the code scroll to the bottom of the IFrame directly after adding an image?
The following functions are used and trypost() is started onLoad:
function scrollToBottom(){
window.scrollBy(0,document.body.scrollHeight);
}
function trypost(){
point = point + 1;
if(point < interval.length){
//create and append a new image
var newImg = document.createElement("IMG");
newImg.src = "images/"+images[point]+".png";
document.getElementById('holder').appendChild(newImg);
//create and append a return
var br = document.createElement("br");
document.getElementById('holder').appendChild(br);
//time scroll to bottom (after an arbitrary 5 seconds)
var stb = window.setTimeout(scrollToBottom, 5);
//time next post
var nextupdate = interval[point]*400;
var tp = window.setTimeout(trypost, nextupdate);
}
}
My script section contains at least the following variables:
var point = -1;
var interval = [10, 10, 15];
var images = ["r1", "a1", "r2"];
This questions is a continuation of the project described in How to proper use setTimeout with IE?
To answer one of your questions, document.body.scrollHeight is appropriate for this purpose, but not if you're actually calling for document. That'll give you the scroll height of the document the iFrame is in, not the iFrame's document. The iFrame's document can be called upon by [insert variable for iFrame here].contentDocument.
Here's how I did it (and by that, I mean I tested it out with my own stuff to make sure it worked):
let i = document.querySelector('iframe')
i.contentWindow.scrollTo(0, i.contentDocument.body.scrollHeight);
That being said, the other answer by Thomas Urban will also work most of the time. The difference is only if your page has a really long scroll height. Most pages won't be longer than 999999 (for all I know that's impossible and that's why they chose that number), but if you have a page longer than that, the method I showed here would scroll to the bottom and the 999999 would scroll to somewhere not yet at the bottom.
Also note, if you have more than one iFrame, you're gonna want to query it in a different way than I did, like by ID.
Scrolling to bottom is always like scrolling to some ridiculously large top offset, e.g. 999999.
iframe.contentWindow.scrollTo( 0, 999999 );
In addition see this post: Scrolling an iframe with javascript?
If scrolling occurs too early it's probably due to images not being loaded yet. Thus, you will have to scroll as soon as added image has been loaded rather than on having placed it. Add
newImg.onload = function() { triggerScrolling(); };
after creating newImg, but before assigning property src.
If several events are required to trigger scrolling you might need to use some "event collector".
function getEventCollector( start, trigger ) {
return function() {
if ( --start == 0 ) { trigger(); )
};
}
You can then use it like this:
var collector = getEventCollector( 2, function() { triggerScrolling(); } );
newImg.onload = collector;
window.setTimeout( collector, 100 );
This way triggerScrolling() is invoked after 100ms at least and after image has been loaded for collector has to be invoked twice for triggerScrolling() being invoked eventually.

iframe contents not appearing on screen even when checking for onload event javascript

We are trying to build an HTML game in which abobe edge is being used for animations and those animations are being inserted into iframes. We are trying to preload the iframes before removing the 'loading screen' so that the users won't see blank iframes initially.
Here is the code for loading the iframes.
We have a global variable var middleBotLoaded = false;
The following function tries to dynamically populate the iframe and once the iframe has loaded , we are assigning the variable to true
function _trackIFrameLoading()
{
if (document.getElementById("botzmiddleidlesequence_iframe").attachEvent)
{
document.getElementById("botzmiddleidlesequence_iframe").attachEvent("onload", function() { middleBotLoaded = true; });
}
else
{
document.getElementById("botzmiddleidlesequence_iframe").onload = function() { middleBotLoaded = true; };
}
document.getElementById("botzmiddleidlesequence_iframe").src = APP_BASE_URL + "blitzbotzidlesequence/blitzbotz/"+middleBotzId;
}
We have a method to check if the global variable has become true and if so , we are removing the loading screen.The method is being called in a interval of 500 milliseconds
setTimeout(_haveAllCharactersLoaded,500);
function _haveAllCharactersLoaded()
{
if(middleBotLoaded == true)
{
$(jOverlay).fadeOut(800, function() {
$(jOverlay).remove();
});
}
else
{
setTimeout(_haveAllCharactersLoaded,500);
}
}
The problem is that even after the loading screen disappears , the iframe contents take time to come up on the screen .
We have observed that the duration depends on the speed of the net connection , but then , isn't using onload the whole point of making sure that the contents have loaded.
Is there any other approach for dealing with this problem.
Thanks.
EDIT : I have to wait for two days before I can start a bounty but I am willing to award it to anyone who can provide a canonical answer to the question.
I have two answers here.
First, I think you should reconsider the way you're coding this game, unless it's a static, turn based game that only relies upon animations (think Pokemon.)
Second, I have a suggestion for you to try in fixing your code.
First Answer:
You asked if there is any other approach to dealing with this problem.
My first reaction to that, would be to skip using iFrames entirely. Adobe Edge may provide you with a good way to craft animations, but for use in a game engine you will only find yourself fighting against the design of how Adobe Edge handles it's animations.
Instead, I would recommend learning how to use HTML5's canvas element. Canvas is built to handle dynamically loaded content (such as your game engine will be generating.) I can only imagine the event of having particle effect animation overlayed onto a game character as he is hit by a weapon. With your current approach, would you place that in an iFrame? Then, how would you ensure that this particle effect is placed on the correct location on the object?
There are many resources out there to help you begin learning the code you need to make a true game engine in the browser. I would recommend beginning by learning how Canvas works. If you want to animate using the DOM, learn about requestAnimationFrame.
http://creativejs.com/
http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial
Second Answer:
I would recommend looking into the variable scope of your middleBotLoaded. This answer (Set variable in parent window from iframe) would be a good place to look.
Instead of using document.getElementById("botzmiddleidlesequence_iframe").attachEvent("onload", function() { middleBotLoaded = true; });
try using document.getElementById("botzmiddleidlesequence_iframe").attachEvent("onload", function() { parent.middleBotLoaded = true; });
Alternatively, try something along these lines:
onLoad event:
document.getElementById("botzmiddleidlesequence_iframe").attachEvent("onload", function() { parent.middleBotLoaded();});
Function to handle loading:
function middleBotLoaded()
{
$(jOverlay).fadeOut(800, function() {
$(jOverlay).remove();
});
}
It's a better practice to directly call an event, rather than polling for variable changes using setTimeout.

Placeholder while an image is loading with Ember.js [duplicate]

I'm currently working on a web application which has a page which displays a single chart (a .png image). On another part of this page there are a set of links which, when clicked, the entire page reloads and looks exactly the same as before except for the chart in the middle of the page.
What I want to do is when a link is clicked on a page just the chart on the page is changed. This will speed things up tremendously as the page is roughly 100kb large, and don't really want to reload the entire page just to display this.
I've been doing this via JavaScript, which works so far, using the following code
document.getElementById('chart').src = '/charts/10.png';
The problem is that when the user clicks on the link, it may take a couple of seconds before the chart changes. This makes the user think that their click hasn't done anything, or that the system is slow to respond.
What I want to happen is display a spinner / throbber / status indicator, in place of where the image is while it is loading, so when the user clicks the link they know at least the system has taken their input and is doing something about it.
I've tried a few suggestions, even using a psudo time out to show a spinner, and then flick back to the image.
A good suggestion I've had is to use the following
<img src="/charts/10.png" lowsrc="/spinner.gif"/>
Which would be ideal, except the spinner is significantly smaller than the chart which is being displayed.
Any other ideas?
I've used something like this to preload an image and then automatically call back to my javascript when the image is finished loading. You want to check complete before you setup the callback because the image may already be cached and it may not call your callback.
function PreloadImage(imgSrc, callback){
var objImagePreloader = new Image();
objImagePreloader.src = imgSrc;
if(objImagePreloader.complete){
callback();
objImagePreloader.onload=function(){};
}
else{
objImagePreloader.onload = function() {
callback();
// clear onLoad, IE behaves irratically with animated gifs otherwise
objImagePreloader.onload=function(){};
}
}
}
You could show a static image that gives the optical illusion of a spinny-wheel, like these.
Using the load() method of jQuery, it is easily possible to do something as soon as an image is loaded:
$('img.example').load(function() {
$('#spinner').fadeOut();
});
See: http://api.jquery.com/load-event/
Use the power of the setTimeout() function (More info) - this allows you set a timer to trigger a function call in the future, and calling it won't block execution of the current / other functions (async.).
Position a div containing the spinner above the chart image, with it's css display attribute set to none:
<div> <img src="spinner.gif" id="spinnerImg" style="display: none;" /></div>
The nbsp stop the div collapsing when the spinner is hidden. Without it, when you toggle display of the spinner, your layout will "twitch"
function chartOnClick() {
//How long to show the spinner for in ms (eg 3 seconds)
var spinnerShowTime = 3000
//Show the spinner
document.getElementById('spinnerImg').style.display = "";
//Change the chart src
document.getElementById('chart').src = '/charts/10.png';
//Set the timeout on the spinner
setTimeout("hideSpinner()", spinnerShowTime);
}
function hideSpinner() {
document.getElementById('spinnerImg').style.display = "none";
}
Use CSS to set the loading animation as a centered background-image for the image's container.
Then when loading the new large image, first set the src to a preloaded transparent 1 pixel gif.
e.g.
document.getElementById('mainimg').src = '/images/1pix.gif';
document.getElementById('mainimg').src = '/images/large_image.jpg';
While the large_image.jpg is loading, the background will show through the 1pix transparent gif.
Building on Ed's answer, I would prefer to see something like:
function PreLoadImage( srcURL, callback, errorCallback ) {
var thePic = new Image();
thePic.onload = function() {
callback();
thePic.onload = function(){};
}
thePic.onerror = function() {
errorCallback();
}
thePic.src = srcURL;
}
Your callback can display the image in its proper place and dispose/hide of a spinner, and the errorCallback prevents your page from "beachballing". All event driven, no timers or polling, plus you don't have to add the additional if statements to check if the image completed loading while you where setting up your events - since they're set up beforehand they'll trigger regardless of how quickly the images loads.
Some time ago I have written a jQuery plugin which handles displaying a spinner automatically http://denysonique.github.com/imgPreload/
Looking in to its source code should help you with detecting when to display the spinner and with displaying it in the centre of the loaded image.
I like #duddle's jquery method but find that load() isn't always called (such as when the image is retrieved from cache in IE). I use this version instead:
$('img.example').one('load', function() {
$('#spinner').remove();
}).each(function() {
if(this.complete) {
$(this).trigger('load');
}
});
This calls load at most one time and immediately if it's already completed loading.
put the spinner in a div the same size as the chart, you know the height and width so you can use relative positioning to center it correctly.
Aside from the lowsrc option, I've also used a background-image on the img's container.
Be aware that the callback function is also called if the image src doesn't exist (http 404 error). To avoid this you can check the width of the image, like:
if(this.width == 0) return false;
#iAn's solution looks good to me. The only thing I'd change is instead of using setTimeout, I'd try and hook into the images 'Load' event. This way, if the image takes longer than 3 seconds to download, you'll still get the spinner.
On the other hand, if it takes less time to download, you'll get the spinner for less than 3 seconds.
I would add some random digits to avoid the browser cache.

Jquery.height() returns different results using F5 or CTRL+F5

So I am trying to find the height of my images then add a top margin this enables me to impose a a vertical center.
I'm running this code, and on an F5 refresh I get correct height but on CTRL+F5 refresh it gives me a much smaller height. I kind of assume this is a loading/delay thing, but I am using document ready so not really sure whats going on. I tried using a php function but it slows the site down amazingly so have to stick with jquery.
you can see it working here. www.mzillustration.com
jQuery(document).ready(function() {
if (jQuery('.imagedisplay').length != 0) {
jQuery('.imagedisplay').each(function(){
var imgheight = jQuery(this).find('img').height();
var topmarg = ((240 - imgheight) / 2) ;
jQuery(this).find('img').css({'margin-top':topmarg+'px'});
});
});
any ideas/help/explanation much appreciated.
thanks
There is a difference between onload and onready.
ready will wait until the actual DOM-tree is done, while onload will wait until ALL of the content displayed on the page is finnished loading. So an explanation would be that when clearing the cache and refreshing, the dom tree finishes much faster than the images, hence giving the wrong heigh.
Try using the onload-event instead and see if you get a different result.
You need to insure the image has loaded before asking the browser for its height. If that image path is living in the html you will unfortunately need a jquery pluggin to handle this in a cross browser manner.
https://github.com/alexanderdickson/waitForImages
http://desandro.github.com/imagesloaded/
Or you will have to wait for the window.onload event which in jquery looks like this:
$(window).on('load', function(){....
However if you use the window load event, it will wait until ALL resources have loaded and depending on your site that can be a serious delay when compared to measuring just the image itself.
Or if you are comfortable with loading the image from javascript, simply ordering your code properly will handle this:
var loadTester = new Image(),
imgH;
$(loadTest).on('load',function(){
imgH = $('#image').attr('src',loadTester.src).height();
}
loadTester.src = "paht/to/image.jpg";
The reason you are seeing a difference in the manner you reload the page, is that a simple refresh does not clear the cache, so the image is already loaded. When you hit ctrl+f5 it clears the cache and so the image is not yet loaded when you ask the browser for the height.
For cache control durring development consider getting the firefox web-developer toolbar.
Try this approach:
jQuery(function() {
jQuery('.imagedisplay img').each(function() {
var $this = jQuery(this),
height = $this.height();
if (height) {
$this.css('margin-top', ((240 - height) / 2) + 'px');
} else {
$this.on('load', function() {
$this.css('margin-top', ((240 - $this.height()) / 2) + 'px');
});
}
});
});
images are/can be cached/loaded separately from the actual page content. the document being ready can (and in my experience usually) occurs before everything is loaded.
try adding an event listener to the actual element being loaded.
You need to make sure the image has loaded before extracting a height. You can easily check this using the complete property on the image. Try this:
var setH = function() {
$(this).css('margin-top', (240 - this.height) / 2);
}
$('.imagedisplay img').each(function() {
if( this.complete ) {
setH.call(this); // apply height straight away
return;
}
$(this).load(setH); // apply height when the image has loaded
});

Computing a cross-browser iframe height

One of the most difficult problems in my javascript experience has been the correct (that is "cross-browser") computing of a iframe height.
In my applications I have a lot of dynamically generated iframe and I want them all do a sort of autoresize at the end of the load event to adjust their height and width.
In the case of height computing my best solution is the following (with the help of jQuery):
function getDocumentHeight(doc) {
var mdoc = doc || document;
if (mdoc.compatMode=='CSS1Compat') {
return mdoc.body.offsetHeight;
}
else {
if ($.browser.msie)
return mdoc.body.scrollHeight;
else
return Math.max($(mdoc).height(), $(mdoc.body).height());
}
}
I searched the internet without success. I also tested Yahoo library that has some methods for document and viewport dimensions, but it's not satisfactory.
My solution works decently, but sometimes it calculates a taller height.
I've studied and tested tons of properties regarding document height in Firefox/IE/Safari: documentElement.clientHeight, documentElement.offsetHeight, documentElement.scrollHeight, body.offsetHeight, body.scrollHeight, ...
Also jQuery doesn't have a coherent behavior in various browser with the calls $(document.body).height(), $('html', doc).height(), $(window).height()
I call the above function not only at the end of load event, but also in the case of dynamically inserted DOM elements or elements hidden or shown. This is a case that sometimes breaks the code that works only in the load event.
Does someone have a real cross-browser (at least Firefox/IE/Safari) solution? Some tips or hints?
Although I like your solution, I've always found IFRAMEs to be more trouble than they're worth.
Why ? 1. The sizing issue. 2. the iframe has that src attribute to worry about. i.e. absolute path. 3. the extra complexity with the pages.
My solution - DIVs which are dynamically loaded through AJAX calls. DIVs will auto size. Although the AJAX code requires javascript work (which can be done through frameworks) they are relative to where the site is. 3 is a wash, you're trading complexity in pages up to javascript.
Instead of <IFRAME ...> use <DIV id="mystuff" />
Do the ajax call to fill the mystuff div with data and let the browser worry about it.
This has been without an accepted answer for awhile, so I wanted to contribute the solution I ended up going with after some research. This is cross-browser and cross-domain (e.g. when the iframe points to content from a different domain)
I ended up using html5's message passing mechanism wrapped in a jQuery pluging that makes it compatible with older browsers using various methods (some of them described in this thread).
The end solution is very simple.
On the host (parent) page:
// executes when a message is received from the iframe, to adjust
// the iframe's height
$.receiveMessage(
function( event ){
$( 'my_iframe' ).css({
height: event.data
});
});
// Please note this function could also verify event.origin and other security-related checks.
On the iframe page:
$(function(){
// Sends a message to the parent window to tell it the height of the
// iframe's body
var target = parent.postMessage ? parent : (parent.document.postMessage ? parent.document : undefined);
$.postMessage(
$('body').outerHeight( true ) + 'px',
'*',
target
);
});
I've tested this on Chrome 13+, Firefox 3.6+, IE7, 8 and 9 on XP and W7, safari on OSX and W7. ;)
Since in your example you're accessing the document inside the IFRAME I guess you're talking about knowing the height of the document and not of the frame itself. Also, that means that the content comes from your own website and you have control over it's contents.
So, why don't you simply place a DIV around your content and then use the clientHeight of that DIV?
Code you load in your IFRAME:
...
<body>
<div id="content">
...
</div>
</body>
The parent document:
function getDocumentHeight(mdoc) {
return mdoc.getElementById("content").clientHeight;
}
BTW, this part of your example function does not make sense as "document" does not refer to the IFRAME:
var mdoc = doc || document;
Here is a solution that seems to work. Basically, the scrollHeight is the correct value in most cases. However, in IE (specifically 6 and 7), if the content is simply contained in text nodes, the height is not calculated and just defaults to the height set in CSS or on the "height" attribute on the iframe. This was found through trial and error, so take it for what it is worth.
function getDocumentHeight(doc) {
var mdoc = doc || document;
var docHeight = mdoc.body.scrollHeight;
if ($.browser.msie) {
// IE 6/7 don't report body height correctly.
// Instead, insert a temporary div containing the contents.
var child = $("<div>" + mdoc.body.innerHTML + "</div>", mdoc);
$("body", mdoc).prepend(child);
docHeight = child.height();
child.remove();
}
return docHeight;
}
I have found this solution to work in ie 6+, ff 3.5+, safari 4+. I am creating and appending an iframe to the document. The following code is executed at the end of jQuery's load event after some other dom manipulation. The timeout is needed for me because of the additional dom manipulation taking place in the load event.
// sizing - slight delay for good scrollheight
setTimeout(function() {
var intContentHeight = objContentDoc.body.scrollHeight;
var $wrap = $("#divContentWrapper", objContentFrameDoc);
var intMaxHeight = getMaxLayeredContentHeight($wrap);
$this.height(intContentHeight > intMaxHeight ? intMaxHeight : intContentHeight);
// animate
fireLayeredContentAnimation($wrap);
}, 100);
I have some sizing constraints to consider, which is what the getMaxLayeredContentHeight call is checking for me. Hope that helps.
Here's a script that resizes the iFrame depending on the body inside it's height.

Categories

Resources