1

I have the following code:

const printString = // a long string w/ several base64 encoded images;
const printContainer = document.createElement('div');
printContainer.innerHTML = printString;
document.body.appendChild(printContainer);
window.print();

printString is a long string with several largeish base64 encoded images included. The string gets set as the innerHTML of the printContainer and then the whole thing gets printed.

This works okay, but on the initial load of the page, it apparently takes the browser a moment to render the base64 encoded images and in that time, window.print() goes ahead and fires, before all the images have actually loaded into the DOM.

That is, window.print() can fire before .innerHTML has finished rendering the new element.

If I add a brief delay to the window.print(), then everything works fine. Like so:

const printString = // a long string w/ several base64 encoded images;
const printContainer = document.createElement('div');
printContainer.innerHTML = printString;
document.body.appendChild(printContainer);
setTimeout(() => {
    window.print();
}, 100);

This isn't a great solution, however, and I would really like to find a solution along the lines of "you just wait until .innerHTML() is actually finished, window.print();

All of this is tested in Chrome, so far.

Any ideas appreciated!

Edit: an answer

This is a modest reworking of @Keith's answer below.

const imgs = document.querySelectorAll('img.images-in-question');
function checkDone() {
  if (ready === imgs.length) {
    // do stuffs
  }
}
function incrementReady(){
  ready++;
  checkDone();
}
for (const img of imgs) {
  if (img.complete) ready++;
  else {
    img.addEventListener('load', incrementReady);
  }
}
checkDone();
crowhill
  • 2,398
  • 3
  • 26
  • 55
  • https://stackoverflow.com/questions/30070865/event-that-occurs-after-appendchild – Adil Shaikh Jun 04 '18 at 23:05
  • Your best option here might be to traverse the dom, find all image tags, and attach to the onload event.. and count.. – Keith Jun 04 '18 at 23:11
  • @Keith No need to traverse the DOM. Use event delegation instead. – Sebastian Simon Jun 04 '18 at 23:12
  • 1
    @Xufox Yes, you could use that to attach the event, but you still need to traverse the DOM, or how would you know how many images to wait for.. :) Ps. when I say traverse the DOM, I of course mean `document.querySelectorAll("img")` – Keith Jun 04 '18 at 23:17

2 Answers2

2

Below is a simple script to wait for all images to load.

It basically does a querySelectAll to get all the images, and then attaches the onload event, when the amount of images loaded is equal to the amount of images in the list, everything is then loaded.

This then will of course work with both external URL, and data uri's..

Update, noticed a slight issue with my original image load check, in Chrome sometimes the onload is not fired, I assume it's because if it can pull the resources from cache, it might get loaded before the onload event is even attached, as such I've added a check for the complete flag first. This seems to fix the issue..

const imgs = document.querySelectorAll("img");

let waiting = 0;
let count = 0;

function checkDone() {
  if (count === waiting) {
    console.log("all images loaded");
  }
}

for (const img of imgs) {
  if (!img.complete) waiting ++;
  else img.addEventListener('load', () => {
    count ++;
    checkDone();
  });
}
checkDone();
<img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" style="width:100px;height:100px">
<img src="https://via.placeholder.com/350x150.svg">
<img src="https://via.placeholder.com/300x100.svg">
<img src="https://via.placeholder.com/200x400.svg">
Keith
  • 22,005
  • 2
  • 27
  • 44
  • Instead of just not incrementing `waiting`, you could also not attach a `load` event to images with the `complete` tag, since they will never fire the event. – Useless Code Jun 05 '18 at 02:14
  • @UselessCode Spot on, I did mean to do an else too. But is was late when I replied, must have forgot.. – Keith Jun 05 '18 at 06:34
  • This is the correct answer. Thank you for taking the time. I made some small edits and added them to my question up above. Thoughts? – crowhill Jun 05 '18 at 17:28
1

This would be a good case to use requestAnimationFrame.

const printString = // a long string w/ several base64 encoded images;
const printContainer = document.createElement('div');
printContainer.innerHTML = printString;
document.body.appendChild(printContainer);
requestAnimationFrame(function () {
    window.print();
});

requestAnimationFrame waits for the next paint and then runs the function, thus you can be sure that window.print() won't run until just after the HTML has been rendered.

Useless Code
  • 12,123
  • 5
  • 35
  • 40
  • This appears to have fixed the problem! Elegant solution. Thank you. – crowhill Jun 04 '18 at 23:24
  • 1
    I'm not really sure how this is any different to `setTimeout(func, 1)`.. The images here are base64 encoded and embedded, so should evaluate instantly,. But in theory don't have too, if the browser vender async loaded the based64 data, (maybe inside a worker), in theory your back to were you was. The proper way is to wait for the `onload`, regardless if the images are `data uri's` or external images. – Keith Jun 04 '18 at 23:35
  • @Keith Mind writing up a more complete solution? I'm not having much luck with the `onload` on the images. Browser doesn't complain, but they never fire, either. – crowhill Jun 05 '18 at 00:00
  • @crowhill I'll knock up a quick snippet. – Keith Jun 05 '18 at 00:04
  • @keith Yeah, functionally `setTimeout` and `requestAnimationFrame` are about the same in this situation, but semantically they are different. rAF essentially says, once you are done laying things out and painting it, do this; `setTimeout` says sometime in the future do this. If you want it to happen as soon as things have painted and are ready, rAF will do that, `setTimeout` won't, it will wait [at least 4ms](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified) to do it. – Useless Code Jun 05 '18 at 02:09
  • You are right that if there were assets loading in asynchronously, this wouldn't work. In this case with `data:` uris, that shouldn't be a problem, the data is already there, you don't have to wait for it to download, the browser can use it to layout the page right away. I suppose the browser might process the image in another process, but it still has to wait for that to be done before it can layout the page, so that shouldn't cause any asynchronicity issues. – Useless Code Jun 05 '18 at 02:10
  • The problem is, were not waiting for the layout. As far as I know data uri's should be treated the same as regular urls. Rfc `data items as "immediate" data, as if it had been included externally` for me this implies we should still wait for onload. – Keith Jun 05 '18 at 07:11