We are getting data from the backend and need to write it to an iframe. We have to set the iframe height to the height of the content. We can't get the correct height until the content is actually IN the iframe, which is not instantaneous for a large amount of content. Is there a callback to know when the write is done?
Right now, we have a timer, but it is brittle code:
//data received from backend
//write data to iframe
//$iframe is a jQuery DOM element
$iframe[0].contentDocument.open();
$iframe[0].contentDocument.write(data);
$iframe[0].contentDocument.close();
setTimeout(function (){
var scrollHeight = $iframe.contents().find('body')[0].scrollHeight
$iframe.css("height", scrollHeight );
}, 1000);
Have you tried listening for the load event on the iframe?
var doc = $iframe[0].contentDocument
doc.open()
doc.write(data)
doc.close()
$iframe.on('load', function () {
this.style.height = doc.body.scrollHeight + 'px'
})
iframes do have an onload event:
$iframe[0].contentDocument.open();
$iframe[0].contentDocument.write(data);
$iframe[0].contentDocument.close();
$iframe.onload = function() {
var scrollHeight = $iframe.contents().find('body')[0].scrollHeight
$iframe.css("height", scrollHeight);
};
Your best option is to use mutationObserver in the iFrame. This is not quite as simple as a callback I'm afraid.
Here's is the code the iFrame-resizer library uses to work this out. You would need to change the line that calls sendSize To call your callback method.
function setupBodyMutationObserver(){
function addImageLoadListners(mutation) {
function addImageLoadListener(element){
if (false === element.complete) {
console.log('Attach listeners to ' + element.src);
element.addEventListener('load', imageLoaded, false);
element.addEventListener('error', imageError, false);
elements.push(element);
}
}
if (mutation.type === 'attributes' && mutation.attributeName === 'src'){
addImageLoadListener(mutation.target);
} else if (mutation.type === 'childList'){
Array.prototype.forEach.call(
mutation.target.querySelectorAll('img'),
addImageLoadListener
);
}
}
function removeFromArray(element){
elements.splice(elements.indexOf(element),1);
}
function removeImageLoadListener(element){
console.log('Remove listeners from ' + element.src);
element.removeEventListener('load', imageLoaded, false);
element.removeEventListener('error', imageError, false);
removeFromArray(element);
}
function imageEventTriggered(event,type,typeDesc){
removeImageLoadListener(event.target);
sendSize(type, typeDesc + ': ' + event.target.src, undefined, undefined);
}
function imageLoaded(event) {
imageEventTriggered(event,'imageLoad','Image loaded');
}
function imageError(event) {
imageEventTriggered(event,'imageLoadFailed','Image load failed');
}
function mutationObserved(mutations) {
sendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);
//Deal with WebKit asyncing image loading when tags are injected into the page
mutations.forEach(addImageLoadListners);
}
function createMutationObserver(){
var
target = document.querySelector('body'),
config = {
attributes : true,
attributeOldValue : false,
characterData : true,
characterDataOldValue : false,
childList : true,
subtree : true
};
observer = new MutationObserver(mutationObserved);
console.log('Create body MutationObserver');
observer.observe(target, config);
return observer;
}
var
elements = [],
MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
observer = createMutationObserver();
return {
disconnect: function (){
if ('disconnect' in observer){
console.log('Disconnect body MutationObserver');
observer.disconnect();
elements.forEach(removeImageLoadListener);
}
}
};
}
maybe postMessage will help you.
var testData = 'test\ntest\ntest\ntest\ntest\ntest\ntest\ntest\ntest\ntest\ntest\ntest\n';
var $iframe = document.querySelector('#test-iframe');
var $button = document.querySelector('button');
// listen the callback message from iframe
window.self.addEventListener('message', function(ev) {
if (ev.data && ev.data.status === 'ready') {
$iframe.style.height = ev.data.height + 'px';
}
});
$button.addEventListener('click', function() {
// render content
$iframe.contentDocument.body.innerText = testData;
// send message to the top window
window.self.postMessage({
status: 'ready',
height: $iframe.contentDocument.body.scrollHeight
}, '*');
});
`
https://jsfiddle.net/caoyy/Lb5k13bw/2/
Related
When I run the code below with a too high TRANSITION_DURATION, the function never gets executed.
_el.height(_el[0].scrollHeight)
.one(Util.TRANSITION_END, complete)
.emulateTransitionEnd(TRANSITION_DURATION);
For example, when TRANSITION_DURATION is set to 350, the event never gets called. However when TRANSITION_DURATION is set to 1 the event occurs.
Struggling to understand the reasoning? Anyone understand this better?
For context of the code see below, the specific question regards the line right at the end of the _show function:
MetisMenu.prototype._show = function _show(element) {
if (this._transitioning || $(element).hasClass(this._config.collapsingClass)) {
return;
}
var _this = this;
var _el = $(element);
var startEvent = $.Event(Event.SHOW);
_el.trigger(startEvent);
if (startEvent.isDefaultPrevented()) {
return;
}
_el.parent(this._config.parentTrigger).addClass(this._config.activeClass);
if (this._config.toggle) {
this._hide(_el.parent(this._config.parentTrigger).siblings().children(this._config.subMenu + '.' + this._config.collapseInClass).attr('aria-expanded', false));
}
_el.removeClass(this._config.collapseClass).addClass(this._config.collapsingClass).height(0);
this.setTransitioning(true);
var complete = function complete() {
console.log("running complete")
_el.removeClass(_this._config.collapsingClass).addClass(_this._config.collapseClass + ' ' + _this._config.collapseInClass).height('').attr('aria-expanded', true);
_this.setTransitioning(false);
_el.trigger(Event.SHOWN);
};
if (!Util.supportsTransitionEnd()) {
complete();
return;
}
_el.height(_el[0].scrollHeight).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION);
};
I have 2 function that I am trying to run, one after another. For some reason they both run at the same time, but the second one does not load properly. Is there a way to run the first function wait then run the second function?:
//run this first
$('#abc').click(function() {
$('.test1').show();
return false;
});
//run this second
(function ($) {
"use strict";
// A nice closure for our definitions
function getjQueryObject(string) {
// Make string a vaild jQuery thing
var jqObj = $("");
try {
jqObj = $(string)
.clone();
} catch (e) {
jqObj = $("<span />")
.html(string);
}
return jqObj;
}
function printFrame(frameWindow, content, options) {
// Print the selected window/iframe
var def = $.Deferred();
try {
frameWindow = frameWindow.contentWindow || frameWindow.contentDocument || frameWindow;
var wdoc = frameWindow.document || frameWindow.contentDocument || frameWindow;
if(options.doctype) {
wdoc.write(options.doctype);
}
wdoc.write(content);
wdoc.close();
var printed = false;
var callPrint = function () {
if(printed) {
return;
}
// Fix for IE : Allow it to render the iframe
frameWindow.focus();
try {
// Fix for IE11 - printng the whole page instead of the iframe content
if (!frameWindow.document.execCommand('print', false, null)) {
// document.execCommand returns false if it failed -http://stackoverflow.com/a/21336448/937891
frameWindow.print();
}
// focus body as it is losing focus in iPad and content not getting printed
$('body').focus();
} catch (e) {
frameWindow.print();
}
frameWindow.close();
printed = true;
def.resolve();
}
// Print once the frame window loads - seems to work for the new-window option but unreliable for the iframe
$(frameWindow).on("load", callPrint);
// Fallback to printing directly if the frame doesn't fire the load event for whatever reason
setTimeout(callPrint, options.timeout);
} catch (err) {
def.reject(err);
}
return def;
}
function printContentInIFrame(content, options) {
var $iframe = $(options.iframe + "");
var iframeCount = $iframe.length;
if (iframeCount === 0) {
// Create a new iFrame if none is given
$iframe = $('<iframe height="0" width="0" border="0" wmode="Opaque"/>')
.prependTo('body')
.css({
"position": "absolute",
"top": -999,
"left": -999
});
}
var frameWindow = $iframe.get(0);
return printFrame(frameWindow, content, options)
.done(function () {
// Success
setTimeout(function () {
// Wait for IE
if (iframeCount === 0) {
// Destroy the iframe if created here
$iframe.remove();
}
}, 1000);
})
.fail(function (err) {
// Use the pop-up method if iframe fails for some reason
console.error("Failed to print from iframe", err);
printContentInNewWindow(content, options);
})
.always(function () {
try {
options.deferred.resolve();
} catch (err) {
console.warn('Error notifying deferred', err);
}
});
}
function printContentInNewWindow(content, options) {
// Open a new window and print selected content
var frameWindow = window.open();
return printFrame(frameWindow, content, options)
.always(function () {
try {
options.deferred.resolve();
} catch (err) {
console.warn('Error notifying deferred', err);
}
});
}
function isNode(o) {
/* http://stackoverflow.com/a/384380/937891 */
return !!(typeof Node === "object" ? o instanceof Node : o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string");
}
$.print = $.fn.print = function () {
// Print a given set of elements
var options, $this, self = this;
// console.log("Printing", this, arguments);
if (self instanceof $) {
// Get the node if it is a jQuery object
self = self.get(0);
}
if (isNode(self)) {
// If `this` is a HTML element, i.e. for
// $(selector).print()
$this = $(self);
if (arguments.length > 0) {
options = arguments[0];
}
} else {
if (arguments.length > 0) {
// $.print(selector,options)
$this = $(arguments[0]);
if (isNode($this[0])) {
if (arguments.length > 1) {
options = arguments[1];
}
} else {
// $.print(options)
options = arguments[0];
$this = $("html");
}
} else {
// $.print()
$this = $("html");
}
}
// Default options
var defaults = {
globalStyles: true,
mediaPrint: false,
stylesheet: null,
noPrintSelector: ".no-print",
iframe: true,
append: null,
prepend: null,
manuallyCopyFormValues: true,
deferred: $.Deferred(),
timeout: 750,
title: null,
doctype: '<!doctype html>'
};
// Merge with user-options
options = $.extend({}, defaults, (options || {}));
var $styles = $("");
if (options.globalStyles) {
// Apply the stlyes from the current sheet to the printed page
$styles = $("style, link, meta, base, title");
} else if (options.mediaPrint) {
// Apply the media-print stylesheet
$styles = $("link[media=print]");
}
if (options.stylesheet) {
// Add a custom stylesheet if given
$styles = $.merge($styles, $('<link rel="stylesheet" href="' + options.stylesheet + '">'));
}
// Create a copy of the element to print
var copy = $this.clone();
// Wrap it in a span to get the HTML markup string
copy = $("<span/>")
.append(copy);
// Remove unwanted elements
copy.find(options.noPrintSelector)
.remove();
// Add in the styles
copy.append($styles.clone());
// Update title
if (options.title) {
var title = $("title", copy);
if (title.length === 0) {
title = $("<title />");
copy.append(title);
}
title.text(options.title);
}
// Appedned content
copy.append(getjQueryObject(options.append));
// Prepended content
copy.prepend(getjQueryObject(options.prepend));
if (options.manuallyCopyFormValues) {
// Manually copy form values into the HTML for printing user-modified input fields
// http://stackoverflow.com/a/26707753
copy.find("input")
.each(function () {
var $field = $(this);
if ($field.is("[type='radio']") || $field.is("[type='checkbox']")) {
if ($field.prop("checked")) {
$field.attr("checked", "checked");
}
} else {
$field.attr("value", $field.val());
}
});
copy.find("select").each(function () {
var $field = $(this);
$field.find(":selected").attr("selected", "selected");
});
copy.find("textarea").each(function () {
// Fix for https://github.com/DoersGuild/jQuery.print/issues/18#issuecomment-96451589
var $field = $(this);
$field.text($field.val());
});
}
// Get the HTML markup string
var content = copy.html();
// Notify with generated markup & cloned elements - useful for logging, etc
try {
options.deferred.notify('generated_markup', content, copy);
} catch (err) {
console.warn('Error notifying deferred', err);
}
// Destroy the copy
copy.remove();
if (options.iframe) {
// Use an iframe for printing
try {
printContentInIFrame(content, options);
} catch (e) {
// Use the pop-up method if iframe fails for some reason
console.error("Failed to print from iframe", e.stack, e.message);
printContentInNewWindow(content, options);
}
} else {
// Use a new window for printing
printContentInNewWindow(content, options);
}
return this;
};
})(jQuery);
How would I run the first one wait 5 or so seconds and then run the jquery print? I'm having a hard time with this. So the id would run first and then the print would run adter the id="abc" Here is an example of the code in use:
<div id="test">
<button id="abc" class="btn" onclick="jQuery.print(#test1)"></button>
</div>
If I understand your problem correctly, you want the jQuery click function to be run first, making a div with id="test1" visible and then, once it's visible, you want to run the onclick code which calls jQuery.print.
The very first thing I will suggest is that you don't have two different places where you are handling the click implementation, that can make your code hard to follow.
I would replace your $('#abc').click with the following:
function printDiv(selector) {
$(selector).show();
window.setTimeout(function () {
jQuery.print(selector);
}, 1);
}
This function, when called, will call jQuery.show on the passed selector, wait 1ms and then call jQuery.print. If you need the timeout to be longer, just change the 1 to whatever you need. To use the function, update your example html to the following:
<div id="test">
<button id="abc" class="btn" onclick="printDiv('#test1')"</button>
</div>
When the button is clicked, it will now call the previously mentioned function and pass it the ID of the object that you want to print.
As far as your second function goes, where you have the comment **//run this second**, you should leave that alone. All it does is extend you jQuery object with the print functionality. You need it to run straight away and it currently does.
if (!slideOutObserver && doc.querySelector('.slide-out-cms')) {
mutObv.observe(doc.querySelector('.slide-out-cms'), { attributeFilter: ['class'] });
slideOutObserver = true;
}
The console error is as follows: script5022: SyntxError
Which I click on and it takes me to the above code
Any ideas or workarounds?
Entire script if needed, in this script we are showing the user a cookie consent message, which overlaps some fixed elements on the page. We are doing some other things to reposition and slide things up and down based on what elements are visible:
// On document ready
$(function() {
var cookieAckId = 'cookie-acknowledgment',
pureCloudId = 'chatTrigger',
slideOutClass = 'slide-out-cms.show',
mobileContinue = 'fixed-button-container',
slideOutObserver = false,
consentText = 'This site uses essential cookies to function correctly. For more detailed information, please see our privacy policy. By continuing to use this website you consent to our use of cookies.',
mutObv;
//If the user closes/accepts the acknowledgment
function closeAck() {
// Remove the Acknowledgment from the page and shift floaters back
$('#' + cookieAckId).slideUp().queue(function() {
$(this).remove();
repositionFloaters();
});
//Kill Mutation Observer
mutObv.disconnect();
//Kill the resize listener
$(root).off('resize', repositionFloaters);
//set cookie
var d = new Date;
d.setFullYear(d.getFullYear() + 2);
$.cookie('acceptedGdprCookies', 'true', {
path: '/',
domain: '.' + browser.getDomain(),
expires: d,
});
}
// floaters are there, let's alter their position!
function repositionFloaters() {
var $cookieAck = $('#' + cookieAckId),
cookieAckHeight = 0,
$pureCloud = $('#' + pureCloudId),
$slideOut = $('.' + slideOutClass),
$mobileContinue = $('.' + mobileContinue);
if (!slideOutObserver && doc.querySelector('.slide-out-cms')) {
mutObv.observe(doc.querySelector('.slide-out-cms'), { attributeFilter: ['class'] });
slideOutObserver = true;
}
// If cookie Acknowledgment exists, grab it's height
if ($cookieAck.length) {
cookieAckHeight = $cookieAck.outerHeight();
}
// Shift up Purecloud and Slide Out if they exist
[$pureCloud, $slideOut, $mobileContinue].forEach(function($item) {
if ($item.length) {
$item.css('transform', 'translateY(-' + cookieAckHeight + 'px)');
}
});
// If both Slideout and purecloud exist, stop trying to observe for their addition
if ($pureCloud.length && $slideOut.length && cookieAckHeight > 0) {
mutObv.disconnect();
}
}
// Create the Cookie Acknowledgment
var $cookieAck = $('<div id="' + cookieAckId + '"><div class="relative"><div class="ca-text col-xs-12"><span>' + consentText + '</span><div class="ca-accept"></div></div></div>'),
$acceptBtn = $('<button class="ca-accept-btn b">ACCEPT</button>').on('click', closeAck),
$closeBtn = $('<span class="ca-close-btn icon-closepositive"></span>').on('click', closeAck);
$cookieAck.append($closeBtn);
$cookieAck.find('.ca-accept').append($acceptBtn);
$('body').append($cookieAck);
// Create Mutation Observer to watch for Purecloud and Slideout being added to the page
mutObv = new MutationObserver(repositionFloaters);
mutObv.observe(doc.body, { childList: true });
// Add Listener if the page is resized
$(root).on('resize', repositionFloaters);
// Upon creating of Cookie Acknowledgment element, try to shift floaters
repositionFloaters();
});
If anyone is happens to need the answer for this, I finally solved it!
MS Edge does not like the mutation observer without the argument attributes: true.
So the correct code would look like this:
if (!slideOutObserver && doc.querySelector('.slide-out-cms')) {
mutObv.observe(doc.querySelector('.slide-out-cms'), {
attributes: true,
attributeFilter: ['class']
});
}
I have a published captivate html file that is loaded into an iframe of another html. I cannot communicate between the two, not even with localStorage. Can anyone tell me what I'm missing?
Parent html
var everythingLoaded = setInterval(function () {
if (/loaded|complete/.test(document.readyState)) {
clearInterval(everythingLoaded);
init();
}
}, 10);
function init() {
ScormProcessInitialize();
var studentID = ScormProcessGetValue("cmi.core.student_id");
var student_name = ScormProcessGetValue ("cmi.core.student_name");
var nameArraya = student_name.split(" ");
var nameArrayb = nameArraya[1].split(",");
var studentNumber = nameArrayb[0];
ScormProcessSetValue("cmi.core.lesson_status", "incomplete");
localStorage.setItem("_studentNumber", studentNumber);
alert("Student Number: " + studentNumber + " Student Mame: " + student_name);
setTimeout(function () {
document.getElementById("iFrame_a").innerHTML = "<iframe name='iframe_1' id='frame_1' src='//somepath.com/sandbox/somecourse/index.html' frameborder='0' width='1000px' height='605px'></iframe>";
}, 250);
}
function sendComplete() {
alert("Send from index start!");
ScormProcessSetValue("cmi.core.lesson_status", "completed");
alert("send status: Completed");
}
window.onbeforeunload = function (){
cpInfoCurrentSlide = localStorage.getItem("_cpInfoCurrentSlide")
alert(cpInfoCurrentSlide);
if(cpInfoCurrentSlide >= 40)
{
alert("onbeforeunload called: " + cpInfoCurrentSlide )
ScormProcessSetValue("cmi.core.lesson_status", "completed");
}
}
iframe code snippet
localStorage.setItem("_cpInfoCurrentSlide", cpInfoCurrentSlide);
I believe your problem is with onbeforeunload. As I remember captivate packages clobber any functions associated with onbeforeunload in the parent frame when they load.
Try this instead, override your SCORM api setvalue method:
var oldLMSSetValue = window.API.LMSSetValue;
window.API.LMSSetValue = function(key, value){
if(key === 'cmi.core.lesson_status' && value === 'completed'){
//do your stuff here
cpInfoCurrentSlide = localStorage.getItem("_cpInfoCurrentSlide")
alert(cpInfoCurrentSlide);
}
//call the original scorm api function so that it runs as expected.
oldLMSSetValue(key,value);
};
edit: this code would go in the parent window, not the iframe.
I'm using the iframe YouTube API and I want to track events, for example, sending data to google analytics, when user start and stop video.
<iframe src="https://www.youtube.com/embed/DjB1OvEYMhY"></iframe>
I looked https://developers.google.com/youtube/iframe_api_reference?csw=1 and did not find an example how to do that. The example creates iframe and defines onReady and onStateChange as well. How would I do same when I've only iframe on page?
This example listens to every play/pause action the user makes, using onPlayerStateChange with its different states, and prints (records) them.
However, you need to create your own record function to do whatever you want with this data.
You also need an ID on your iframe (#player in this case) and to add ?enablejsapi=1 at the end of its URL. And of course, make sure to include the Youtube iframe API.
Note
It's important to declare the API after your code, because it calls onYouTubeIframeAPIReady when it's ready.
<!DOCTYPE html>
<html>
<body>
<iframe id="player" src="https://www.youtube.com/embed/DjB1OvEYMhY?enablejsapi=1"></iframe>
<h5>Record of user actions:</h5>
<script>
var player;
function onYouTubeIframeAPIReady() {
player = new YT.Player( 'player', {
events: { 'onStateChange': onPlayerStateChange }
});
}
function onPlayerStateChange(event) {
switch(event.data) {
case 0:
record('video ended');
break;
case 1:
record('video playing from '+player.getCurrentTime());
break;
case 2:
record('video paused at '+player.getCurrentTime());
}
}
function record(str){
var p = document.createElement("p");
p.appendChild(document.createTextNode(str));
document.body.appendChild(p);
}
</script>
<script src="https://www.youtube.com/iframe_api"></script>
</body>
</html>
JS Fiddle Demo
Here is a version that doesn't use Youtubes iframe API script. The only drawback is that the iframe API might change.
<iframe id="player" src="https://www.youtube.com/embed/dQw4w9WgXcQ?enablejsapi=1"></iframe>
var addYoutubeEventListener = (function() {
var callbacks = [];
var iframeId = 0;
return function (iframe, callback) {
// init message listener that will receive messages from youtube iframes
if(iframeId === 0) {
window.addEventListener("message", function (e) {
if(e.origin !== "https://www.youtube.com" || e.data === undefined) return;
try {
var data = JSON.parse(e.data);
if(data.event !== 'onStateChange') return;
var callback = callbacks[data.id];
callback(data);
}
catch(e) {}
});
}
// store callback
iframeId++;
callbacks[iframeId] = callback;
var currentFrameId = iframeId;
// sendMessage to frame to start receiving messages
iframe.addEventListener("load", function () {
var message = JSON.stringify({
event: 'listening',
id: currentFrameId,
channel: 'widget'
});
iframe.contentWindow.postMessage(message, 'https://www.youtube.com');
message = JSON.stringify({
event: "command",
func: "addEventListener",
args: ["onStateChange"],
id: currentFrameId,
channel: "widget"
});
iframe.contentWindow.postMessage(message, 'https://www.youtube.com');
});
}
})();
addYoutubeEventListener(document.getElementById("player"), function(e) {
switch(e.info) {
case 1:
// playing
break;
case 0:
// ended
break;
}
});
Sometimes the event load is not enough to ensure that the document inside the iframe is ready. If the iframe is in a different domain it is not possible to subscribe to see when it is ready.
A possible workaround is to record when an event is received from the iframe, if after subscribing no event was received try again:
var addYoutubeEventListener = (function() {
var callbacks = [];
var iframeId = 0;
var subscribed = [];
return function (iframe, callback) {
// init message listener that will receive messages from youtube iframes
if(iframeId === 0) {
window.addEventListener("message", function (e) {
if(e.origin !== "https://www.youtube.com" || e.data === undefined) return;
try {
var data = JSON.parse(e.data);
subscribed[data.id] = true;
if(data.event !== 'onStateChange') return;
var callback = callbacks[data.id];
callback(data);
}
catch(e) {}
}, true);
}
// store callback
iframeId++;
callbacks[iframeId] = callback;
subscribed[iframeId] = false;
var currentFrameId = iframeId;
//console.log("adding event listener to iframe id " + iframeId);
// sendMessage to frame to start receiving messages
iframe.addEventListener("load", function () {
var tries = 0;
var checkSubscribed = function()
{
if (subscribed[currentFrameId]) {
//console.log("subscribed succesfully " + currentFrameId)
}
else
{
tries++;
//console.log("Try again " + currentFrameId + " (" + tries + ")");
if (tries < 100) {
doSubscribe();
}
else
{
console.log("Unable to subscribe" + currentFrameId );
}
}
}
var doSubscribe = function()
{
var message = JSON.stringify({
event: 'listening',
id: currentFrameId,
channel: 'widget'
});
iframe.contentWindow.postMessage(message, 'https://www.youtube.com');
message = JSON.stringify({
event: "command",
func: "addEventListener",
args: ["onStateChange"],
id: currentFrameId,
channel: "widget"
});
iframe.contentWindow.postMessage(message, 'https://www.youtube.com');
setTimeout(checkSubscribed, 100);
};
doSubscribe();
}, true);
}
})();