Javascript window.print() issue on newly appended HTML - javascript

The problem is that the new content will not print (a few more elements).
When the user clicks my print link then I add more html to the document before window.print() is called.
I use ajax to fetch more chapters for a book before printing.
Code:
Print initialized:
var afterPrint = function () {
var timer = setInterval(function () {
afterContentPrint(); // Cleanup html (restore to initial state)
clearInterval(timer);
}, 900);
}
//window.onbeforeprint = beforePrint;
window.onafterprint = afterPrint;
Event print click:
$("#print-test").click(function (e) {
e.preventDefault();
beforeContentPrint(); // ajax call for additional content, finishing by calling window.print()
});
In function beforeContentPrint():
var jqxhr = $.ajax({
type: "GET",
url: bookURL,
success: function(data) {
.....
.....
$(article).each(function () {
parent.append(article);
});
},
complete: function() {
var timer = setInterval(function () {
    window.print();
}, 900);
}
}
The new content is visibly added to the HTML document, so it should work. But only the initial content (before ajax call) is picked up for print.
This solution is for IE and Firefox (onbeforeprint and onafterprint).
Using window.matchMedia('print') seems to work fine in Chrome with this logic.

I don't know why this is happening but there is a working around in mozilla docs; printing a hidden iframe, then you open more chapters of the book with no need to shows up.
Here the link https://developer.mozilla.org/en-US/docs/Printing
Here the code:
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>MDN Example</title>
<script type="text/javascript">
function closePrint () {
document.body.removeChild(this.__container__);
}
function setPrint () {
this.contentWindow.__container__ = this;
this.contentWindow.onbeforeunload = closePrint;
this.contentWindow.onafterprint = closePrint;
this.contentWindow.print();
}
function printPage (sURL) {
var oHiddFrame = document.createElement("iframe");
oHiddFrame.onload = setPrint;
oHiddFrame.style.visibility = "hidden";
oHiddFrame.style.position = "fixed";
oHiddFrame.style.right = "0";
oHiddFrame.style.bottom = "0";
oHiddFrame.src = sURL;
document.body.appendChild(oHiddFrame);
}
</script>
</head>
<body>
<p><span onclick="printPage('externalPage.html');" style="cursor:pointer;text-decoration:underline;color:#0000ff;">Print external page!</span></p>
</body>
</html>

1st Attempt : try putting asyn:false in the ajax request of beforeContentPrint, so that the elements will be added first then the print will be called.
var jqxhr = $.ajax({
type: "GET",
url: bookURL,
async:false,
success: function(data) {
.....
.....
$(article).each(function () {
parent.append(article);
});
},
complete: function() {
var timer = setInterval(function () {
window.print();
}, 900);
}
}
2ndAttempt: Take a look Here how to force execution of one function after another.
Hope this helps.

As the elements added aren't shown it can be a little hard to pinpoint the exact problem, but adding elements via appending/inserting into document doesn't always work that well for all browsers and in worst case you will need to add those elements manually by code (document.createElement(), appendChild() etc.).
In an attempt to create a work-around you can try to use MutationObservers to track changes for your article element which can hopefully help you trigger print when DOM is updated properly. The support is fairly good in new browsers (you may have to use prefix in some, f.ex. WebKitMutationObserver) and for older browsers you can provide a fallback - which of course then only get you so far.
This will monitor a target element for changes and fire a callback.
Generic example based on this article:
var article = document.querySelector('#articleID'),
doPrint = false, // must be available in global/parent scope
o,
useFallback = (typeof MutationObserver !== 'undefined');
if (!useFallback) {
o = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// you can do additional filtering here
// using the mutation object (.name, .type, ...)
if (doPrint) {
doPrint = false;
window.print();
}
});
});
var cfg = { attributes: true, childList: true, characterData: true };
o.observe(article, cfg);
}
Now that the observer is running you can do this modification in your success callback:
var timer; // keep track of setTimeout so we can cancel it
success: function(data) {
...
...
$(article).each(function () {
parent.append(article);
});
// after last element is added, add a "dummy" element
doPrint = true;
if (useFallback) {
// fallback to setTimeout or other solution
}
else {
parent.append('<br />');
}
}
I made an online demo here which sets up the observer and adds some elements. Open console to see actions. The demo is probably too limited data-wise to simulate your situation but can be useful to see the process.
The mechanism used here for triggering print dialog itself can be discussed if is the best - I just provide one for sake of example.
But you can see the observer is triggered when something is added to article. This way you know the DOM has been updated and it should be available for printing. Only the last element added need to trigger the print, hence the doPrint flag.
If you still have no success you will need to consider adding the elements manually the code way or perhaps predefine some elements that you inject when needed (as said, without knowing the full scenario here it has to be with the guess).

It looks like the setInterval, clearInterval is what is messing you up. Change to a setTimeout and get rid of the clearInterval in your afterprint function. I made a fiddle that works in FF and IE9. Fiddle
complete: function() {
var timer = setTimeout(function () {
window.print();
}, 900);
}

Related

Make Javascript wait for an HTML element to exist [duplicate]

This question already has answers here:
How can I be notified when an element is added to the page?
(8 answers)
Closed 6 years ago.
I am trying to make a bot that sends virtual currency over to another user. I have the bot search through a database for users. Before searching, the inner html of a division has no elements at all. After searching, it is then filled with several user links.
Because it takes a short while for results to appear, I need Javascript to wait for at least one anchor tag to exist. How can I do this?
There are many, many better ways to do this, all of which stem from actually checking when the AJAX data populates the element itself, but the following will work:
var t = setInterval(function () {
if ($("element").children().length > 0) {
clearInterval(t);
// do stuff
}
}, 50);
Using setTimeout() to delay the code a few seconds is risky, since on older browser/machines it may take longer than expected.
Use promise() instead, You can find documentation https://api.jquery.com/promise/ .
Using onload event, You can use onload with tag a.
EX: http://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_img_onload
I'm guessing this is an AJAX call.
You could use AJAX callback to check if you got any results from the server.
Something like this:
var tags_available = false;
$.ajax({
... the ajax stuff;
}).done(function(data){ // The callback
if(data || $('#tags_element').lenght != 0){
tags_available = true;
}else{
tags_available = false;
}
})
Then:
if(tags_available){
console.log("Tags available")
}
If I've understood you correctly you need to check if dom element have been updated/populated with new elements. There are a few ways you can achieve that:
1.) Using window.setTimeout function
function checkForChanges() {
var observeThis = document.getElementById('observethis');
if (observeThis.hasChildNodes()) {
alert('yes');
return;
/*this is gonna execute only once */
}
window.setTimeout(function() {
checkForChanges();
}, 25);
}
checkForChanges();
/* this part is only relevant for demonstration.
It shows what happens when dom element gets new child */
(function() {
var observeThis = document.getElementById('observethis');
var button = document.getElementById('button-append');
button.addEventListener('click', function() {
var anchorElement = document.createElement('a');
anchorElement.href = "http://example.com";
anchorElement.target = "_blank";
anchorElement.innerHTML = "Link";
observeThis.appendChild(anchorElement);
}, false);
})();
<div id="observethis"></div>
<button id="button-append">append anchor</button>
2.) MutationObserver class
this is modern approach (I would also say recommended one).
function checkForChanges() {
var observeThis = document.getElementById('observethis');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
alert("insert your own code");
}
});
});
var config = {
attributes: true,
childList: true,
characterData: true
};
observer.observe(observeThis, config);
//observer.disconnect();
//use observer.disconnect to end observations
}
checkForChanges();
/* this part is only relevant for demonstration.
It shows what happens when dom element gets new child */
(function() {
var observeThis = document.getElementById('observethis');
var button = document.getElementById('button-append');
button.addEventListener('click', function() {
var anchorElement = document.createElement('a');
anchorElement.href = "http://example.com";
anchorElement.target = "_blank";
anchorElement.innerHTML = "Link";
observeThis.appendChild(anchorElement);
}, false);
})();
<div id="observethis"></div>
<button id="button-append">Append Child</button>
Read more about MutationObserver here
3.) If you are just waiting to get a response from ajax callback and don't actually need to observe changes in dom then just use XMLHttpRequest. Or even better. Use new javascript fetch API (you are gonna need polyfill to ensure it works in most browsers)

Waiting for multiple iFrames to load before executing function

Forgive my naivety, this probably is quite obvious, I just can't see it now.
Please tell me what is wrong with the following code:
$('#iframe1').load(function(){
$('#iframe2').load(function(){
alert('loaded!');
});
});
The idea is to wait until both iframes have fully loaded, then alert "loaded" - of course this is a simplified example for the sake of stack.
The script sits in script tags at the end of the body of the html doc.
#Quertiy answer is perfectly fine, but not very jQuery-ish. It is hard-coded for 2 iframes only.
The beauty of jQuery is that you can make it work for the most number of people, with as little friction as possible.
I've advised a very simplistic plugin that does nearly what is present on that answer, but in a more open way. It not only works on iframes, but also on images, audio, video and whatever has a onload event!
Without further due, here's the code:
(function($){
$.fn.extend({allLoaded: function(fn){
if(!(fn instanceof Function))
{
throw new TypeError('fn must be a function');
}
var $elems = this;
var waiting = this.length;
var handler = function(){
--waiting;
if(!waiting)
{
setTimeout(fn.bind(window), 4);
}
};
return $elems.one('load.allLoaded', handler);
}});
})(window.jQuery);
It works by adding a load handler to every element in that selection. Since it is a plugin, you can use in whatever way you decide to use it.
Here's an example, that loads 30 random images:
//plugin code
(function($){
$.fn.extend({allLoaded: function(fn){
if(!(fn instanceof Function))
{
throw new TypeError('fn must be a function');
}
var $elems = this;
var waiting = this.length;
var handler = function(){
--waiting;
if(!waiting)
{
setTimeout(fn.bind(window), 4);
}
};
return $elems.one('load.allLoaded', handler);
}});
})(window.jQuery);
$(function(){
//generates the code for the 30 images
for(var i = 0, html = ''; i < 30; i++)
html += '<img data-src="http://lorempixel.com/g/400/200/?_=' + Math.random() + '">';
//stuffs the code into the body
$('#imgs').html(html);
//we select all images now
$('img')
.allLoaded(function(){
//runs when done
alert('loaded all')
})
.each(function(){
//the image URL is on a `data` attribute, to delay the loading
this.src = this.getAttribute('data-src')
})
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js"></script>
<div id="imgs"></div>
Your problem, as said before many times, is that you have a load event attached to your iframe. That event is fired everytime the content change.
After that, you set a new event on #iframe2. When it's content changes, it will fire events left and right, above and beyound what you wish!
The best aproach is to keep track of which ones you loaded or not. After all have been loaded, you simply run the function.
The problem is that you're waiting until #iframe1 loads before you attach a handler for #iframe2 loading. So if #iframe2 loads first, you'll never get your callback.
Instead, watch the load event on both of them and track which ones you've seen:
var seen1 = false,
seen2 = false;
$('#iframe1, #iframe2').load(function(){
if (this.id == "iframe1") {
seen1 = true;
} else {
seen2 = true;
}
if (seen1 && seen2) {
alert('loaded!');
}
});
Why do you expect 2nd iframe to load after the first one?
~function () {
var loaded = 0;
$('#iframe1, #iframe2').load(function (){
if (++loaded === 2) {
alert('loaded!');
}
});
}()

Alternatives to SetTimeout

Hi I have written some javascript code to generate an iFrame and insert content inside. But I don't want to use setTimeout and I want to refactor the code. I tried to use document.ready() and window.onLoad() and they don't work.
This is my code:
onShow: function() {
//TODO: why 200 ms wait? why not wait for specific event
setTimeout(this.injectHtml.bind(this), 200);
},
injectHtml: function() {
var iframe = this.el;
if (iframe.contentWindow === null) { //Check for contentWindow for null.
$(iframe).html(this.params.html);
} else { //If contentWindow is present then open the document for writing.
iframe = iframe.contentWindow;
iframe.document.open('text/html');
iframe.document.write(this.params.html);
iframe.document.close();
$(this.el).on('load', function() {
var iframe = $(this).contents();
iframe.find("#langSelect").off("change");
iframe.find("#langSelect").on("change", function() { //If another language is selected from Dropdown.
abc.languages.setSelected($(this).val());
abc.elements.setLanguage();
Vent.trigger(Vent.Event.LANGUAGE_SELECTED, $(this).val());
});
});
}
}
I'm really confused here. Can anyone shed light?
I have tried this as well:
Case#1:
var _this=this;
$(document).ready(function(){
_this.injectHtml.bind(_this); //Does not work. Everything loads, I get empty page
});
Case#2:
window.onLoad = function(){
this.injectHtml.bind(this);
};
can anyone help?

jQuery, update content when necessary

I'm trying to update contents of a chat located in div (div1) but only when the contents of div1 change (a message was submitted into db and picked up in div1).
I tried the solution from here but my get fails to compare the data.
This solution works perfectly but without content comparison:
window.onload = startInterval;
function startInterval()
{
setInterval("startTime();",2000);
}
function startTime()
{
jQuery('#div1').load('index.php #div1 > *');
}
This is the modification based on this, which fails:
window.onload = startInterval;
function startInterval()
{
setInterval("startTime();",2000);
}
function startTime()
{
var $main = $('#div1');
$.get('chat.php #div1', function (data)
{
if ($main.html() !== data) $main.html(data);
});
}
I tried various modifications of this code but to no avail...
I can't reload the entire page and I don't want to do this if not necessary since it makes the chat harder to read if you have to scroll trough the messages.
How can this be fixed?
UPDATE
Based on #T.J's suggestions I modified the code which now works perfectly:
window.onload = startInterval;
function startInterval()
{
setInterval(startTime,3000);
scrolDown();
}
function startTime()
{
var $main = $('#div1');
$.get('#div1', function (data)
{
elements = $(data);
thisHTML = elements.find("#div1").html();
if ($main.html() !== thisHTML) {
$main.html(thisHTML);
scrolDown();
}
});
}
The other problem was that get required library:
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.pack.js"></script>
which apparently was not required by the load which I used previously.
You want to use get, but you want the fragment feature of load, so you'll have to do that work yourself. Then remember what you got last time, and only update if it's not the same:
var lastHTML;
function startTime()
{
var $main = $('#div1');
$.get('chat.php', function (data) // <== Or index.php, the question has both
{
var elements, html;
// Turn the HTML into elements
elements = $(data);
// Get the HTML of *only* the contents of #div1
html = elements.find("#div1").html();
// If that has changed, use it
if (lastHTML !== thisHTML) {
lastHTML = thisHTML;
$main.html(thisHTML);
}
});
}
Note that that's a fairly basic implementation of the fragment feature (it doesn't, for instance, strip out scripts the way load does). You may want to look at how load does its fragment stuff and replicate that (the joy of open source).

jQuery .ready in a dynamically inserted iframe

We are using jQuery thickbox to dynamically display an iframe when someone clicks on a picture. In this iframe, we are using galleria a javascript library to display multiple pictures.
The problem seems to be that $(document).ready in the iframe seems to be fired too soon and the iframe content isn't even loaded yet, so galleria code is not applied properly on the DOM elements. $(document).ready seems to use the iframe parent ready state to decide if the iframe is ready.
If we extract the function called by document ready in a separate function and call it after a timeout of 100 ms. It works, but we can't take the chance in production with a slow computer.
$(document).ready(function() { setTimeout(ApplyGalleria, 100); });
My question: which jQuery event should we bind to to be able to execute our code when the dynamic iframe is ready and not just it's a parent?
I answered a similar question (see Javascript callback when IFRAME is finished loading?).
You can obtain control over the iframe load event with the following code:
function callIframe(url, callback) {
$(document.body).append('<IFRAME id="myId" ...>');
$('iframe#myId').attr('src', url);
$('iframe#myId').load(function() {
callback(this);
});
}
In dealing with iframes I found good enough to use load event instead of document ready event.
Using jQuery 1.3.2 the following worked for me:
$('iframe').ready(function() {
$('body', $('iframe').contents()).html('Hello World!');
});
REVISION:!
Actually the above code sometimes looks like it works in Firefox, never looks like it works in Opera.
Instead I implemented a polling solution for my purposes. Simplified down it looks like this:
$(function() {
function manipIframe() {
el = $('body', $('iframe').contents());
if (el.length != 1) {
setTimeout(manipIframe, 100);
return;
}
el.html('Hello World!');
}
manipIframe();
});
This doesn't require code in the called iframe pages. All code resides and executes from the parent frame/window.
In IFrames I usually solve this problem by putting a small script to the very end of the block:
<body>
The content of your IFrame
<script type="text/javascript">
//<![CDATA[
fireOnReadyEvent();
parent.IFrameLoaded();
//]]>
</script>
</body>
This work most of the time for me. Sometimes the simplest and most naive solution is the most appropriate.
Following DrJokepu's and David Murdoch idea I implemented a more complete version.
It requires jQuery on both the parent and iframe and the iframe to be in your control.
iframe code:
var iframe = window.frameElement;
if (iframe){
iframe.contentDocument = document;//normalization: some browsers don't set the contentDocument, only the contentWindow
var parent = window.parent;
$(parent.document).ready(function(){//wait for parent to make sure it has jQuery ready
var parent$ = parent.jQuery;
parent$(iframe).trigger("iframeloading");
$(function(){
parent$(iframe).trigger("iframeready");
});
$(window).load(function(){//kind of unnecessary, but here for completion
parent$(iframe).trigger("iframeloaded");
});
$(window).unload(function(e){//not possible to prevent default
parent$(iframe).trigger("iframeunloaded");
});
$(window).on("beforeunload",function(){
parent$(iframe).trigger("iframebeforeunload");
});
});
}
parent test code:
$(function(){
$("iframe").on("iframeloading iframeready iframeloaded iframebeforeunload iframeunloaded", function(e){
console.log(e.type);
});
});
Found the solution to the problem.
When you click on a thickbox link that open a iframe, it insert an iframe with an id of TB_iframeContent.
Instead of relying on the $(document).ready event in the iframe code, I just have to bind to the load event of the iframe in the parent document:
$('#TB_iframeContent', top.document).load(ApplyGalleria);
This code is in the iframe but binds to an event of a control in the parent document. It works in FireFox and IE.
This function from this answer is the best way to handle this as $.ready explicitly fails for iframes. Here's the decision not to support this.
The load event also doesn't fire if the iframe has already loaded. Very frustrating that this remains a problem in 2020!
function onIframeReady($i, successFn, errorFn) {
try {
const iCon = $i.first()[0].contentWindow,
bl = "about:blank",
compl = "complete";
const callCallback = () => {
try {
const $con = $i.contents();
if($con.length === 0) { // https://git.io/vV8yU
throw new Error("iframe inaccessible");
}
successFn($con);
} catch(e) { // accessing contents failed
errorFn();
}
};
const observeOnload = () => {
$i.on("load.jqueryMark", () => {
try {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href !== bl || src === bl || src === "") {
$i.off("load.jqueryMark");
callCallback();
}
} catch(e) {
errorFn();
}
});
};
if(iCon.document.readyState === compl) {
const src = $i.attr("src").trim(),
href = iCon.location.href;
if(href === bl && src !== bl && src !== "") {
observeOnload();
} else {
callCallback();
}
} else {
observeOnload();
}
} catch(e) {
errorFn();
}
}
Basically what others have already posted but IMHO a bit cleaner:
$('<iframe/>', {
src: 'https://example.com/',
load: function() {
alert("loaded")
}
}).appendTo('body');
Try this,
<iframe id="testframe" src="about:blank" onload="if (testframe.location.href != 'about:blank') testframe_loaded()"></iframe>
All you need to do then is create the JavaScript function testframe_loaded().
I'm loading the PDF with jQuery ajax into browser cache. Then I create embedded element with data already in browser cache. I guess it will work with iframe too.
var url = "http://example.com/my.pdf";
// show spinner
$.mobile.showPageLoadingMsg('b', note, false);
$.ajax({
url: url,
cache: true,
mimeType: 'application/pdf',
success: function () {
// display cached data
$(scroller).append('<embed type="application/pdf" src="' + url + '" />');
// hide spinner
$.mobile.hidePageLoadingMsg();
}
});
You have to set your http headers correctly as well.
HttpContext.Response.Expires = 1;
HttpContext.Response.Cache.SetNoServerCaching();
HttpContext.Response.Cache.SetAllowResponseInBrowserHistory(false);
HttpContext.Response.CacheControl = "Private";
This was the exact issue I ran into with our client. I created a little jquery plugin that seems to work for iframe readiness. It uses polling to check the iframe document readyState combined with the inner document url combined with the iframe source to make sure the iframe is in fact "ready".
The issue with "onload" is that you need access to the actual iframe being added to the DOM, if you don't then you need to try to catch the iframe loading which if it is cached then you may not. What I needed was a script that could be called anytime, and determine whether or not the iframe was "ready" or not.
Here's the question:
Holy grail for determining whether or not local iframe has loaded
and here's the jsfiddle I eventually came up with.
https://jsfiddle.net/q0smjkh5/10/
In the jsfiddle above, I am waiting for onload to append an iframe to the dom, then checking iframe's inner document's ready state - which should be cross domain because it's pointed to wikipedia - but Chrome seems to report "complete". The plug-in's iready method then gets called when the iframe is in fact ready. The callback tries to check the inner document's ready state again - this time reporting a cross domain request (which is correct) - anyway it seems to work for what I need and hope it helps others.
<script>
(function($, document, undefined) {
$.fn["iready"] = function(callback) {
var ifr = this.filter("iframe"),
arg = arguments,
src = this,
clc = null, // collection
lng = 50, // length of time to wait between intervals
ivl = -1, // interval id
chk = function(ifr) {
try {
var cnt = ifr.contents(),
doc = cnt[0],
src = ifr.attr("src"),
url = doc.URL;
switch (doc.readyState) {
case "complete":
if (!src || src === "about:blank") {
// we don't care about empty iframes
ifr.data("ready", "true");
} else if (!url || url === "about:blank") {
// empty document still needs loaded
ifr.data("ready", undefined);
} else {
// not an empty iframe and not an empty src
// should be loaded
ifr.data("ready", true);
}
break;
case "interactive":
ifr.data("ready", "true");
break;
case "loading":
default:
// still loading
break;
}
} catch (ignore) {
// as far as we're concerned the iframe is ready
// since we won't be able to access it cross domain
ifr.data("ready", "true");
}
return ifr.data("ready") === "true";
};
if (ifr.length) {
ifr.each(function() {
if (!$(this).data("ready")) {
// add to collection
clc = (clc) ? clc.add($(this)) : $(this);
}
});
if (clc) {
ivl = setInterval(function() {
var rd = true;
clc.each(function() {
if (!$(this).data("ready")) {
if (!chk($(this))) {
rd = false;
}
}
});
if (rd) {
clearInterval(ivl);
clc = null;
callback.apply(src, arg);
}
}, lng);
} else {
clc = null;
callback.apply(src, arg);
}
} else {
clc = null;
callback.apply(this, arguments);
}
return this;
};
}(jQuery, document));
</script>

Categories

Resources