20

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:

  1. Set parent container's opacity to 0.
  2. Insert the content into the DOM.
  3. Measure the sizes, e.g. $el.outerWidth(true).
  4. Position the content based on those sizes.
  5. 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


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.

Community
  • 1
  • 1
Stephen Thomas
  • 13,843
  • 2
  • 32
  • 53
  • What about adding it to the DOM with `display:none`, deferring execution (with a `setTimeout`) and then arranging things based on its height/width ? – Benjamin Gruenbaum Nov 02 '13 at 01:00
  • Isn't the `load` event supposed to fire up once an image has been downloaded? And aren't the dimensions of the image known once retrieved? Maybe render the image outside of the viewport before including it where you need it to make sure the browser actually calculates dimensions (also considering CSS rules). – Kiruse Nov 02 '13 at 01:07
  • Wait until gap between last ResizeObserver event? – Nikos Jul 27 '23 at 07:49

2 Answers2

0

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."

Edit: An even better version that uses queueMicrotask instead of setTimeout (setTimeout queues a macro task, which would run later):

doSomeDomManipulation();
requestAnimationFrame(() => {
  queueMicrotask(() => {
    alert("finished painting");
  });
});
Marko Knöbl
  • 447
  • 2
  • 9
  • does this work with `await 0;...` instead of `setTimeout(() => ...);` – Bobby Morelli Jun 17 '23 at 05:49
  • @BobbyMorelli good question. There's definitely some difference between _setTimeout_ and zero-duration promises (https://dmitripavlutin.com/javascript-promises-settimeout/) - but I guess in this scenario both would probably behave the same way. – Marko Knöbl Jul 11 '23 at 08:20
  • Oh, looking into this again it looks like the best option is actually `queueMicroTask` - I'll update the answer accordingly – Marko Knöbl Jul 12 '23 at 08:20
-1

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

Rainer Plumer
  • 3,693
  • 2
  • 24
  • 42
  • onload won't work since the onload event fires before repaint is complete. also, in my case the content is markup (that may or may not include images), not images. – Stephen Thomas Nov 02 '13 at 12:20
  • If it doesnt contain images, then you can add a transparent/hidden/1px * 1px image at the very end of each markup block, so that its the last thing that will be added, and the image.load event will fire when everyhing has finished loading. – Rainer Plumer Nov 02 '13 at 13:52
  • as noted above, unload won't work since the unload event fires before repaint is complete. (also, in this particular case i have no control over the content anyway) – Stephen Thomas Nov 02 '13 at 14:34
  • load event for images will trigger when image is loaded( unless its cached) And if you dont have control over the content that will be loaded, you can still alter it. After you have loaded the content using ajax, dont add it to the page , but append an extra hidden img tag at the end of it. Then you should be able to use the code above. e.g var data = $(yourloadedhtmldatastring).append(""); $("#yourContainer").append(data); – Rainer Plumer Nov 02 '13 at 16:36