/*global $ */ (function(){ var $ = window.$; var MAX_TIMEOUT = 1500; var getBaseUrl = function() { var base = window.location.href; return base; }; var _useIframeSizeHack = false; var useIframeSizeHack = function(b) { _useIframeSizeHack = b; }; var cloneDomItem = function(elem, elemTag) { var width = elem.width(); var height = elem.height(); var returnElm = $(elemTag); returnElm.addClass($(elem).attr("class")); returnElm.attr("style", $(elem).attr("style")); returnElm.css('background', $(elem).css("background")); returnElm.attr('width', width); returnElm.attr('height', height); return returnElm; }; var generateFullHtmlFromFragment = function (fragment) { return "" + "" + "" + "" + "" + "content from " + fragment.base_url + "" + fragment.css + "" + "" + fragment.content + "" + ""; }; var getHtmlFragment = function(callback) { var self = this; var $element = $(this.element); // .find('iframe').addBack("iframe") handles two cases: // - element itself is an iframe - .addBack('iframe') // - element descentands are iframes - .find('iframe') var $iframes = $element.find('iframe').addBack("iframe"); this._iframeContentRequests = []; $iframes.each(function(i, iframeElem) { var message = { type: 'htmlFragRequest', id: self.id, iframeReqId: i, // We have to provide smaller timeout while sending message to nested iframes. // Otherwise, when one of the nested iframes timeouts, then all will do the // same and we won't render anything - even iframes that support Shutterbug. iframeReqTimeout: self.iframeReqTimeout * 0.6 }; iframeElem.contentWindow.postMessage(JSON.stringify(message), "*"); var requestDeffered = new $.Deferred(); self._iframeContentRequests[i] = requestDeffered; setTimeout(function() { // It handles a situation in which iframe doesn't support Shutterbug. // When we doesn't receive answer for some time, assume that we can't // render this particular iframe (provide null as iframe description). if (requestDeffered.state() !== "resolved") { requestDeffered.resolve(null); } }, self.iframeReqTimeout); }); $.when.apply($, this._iframeContentRequests).done(function() { // This function is called when we receive responses from all nested iframes. // Nested iframes descriptions will be provided as arguments. $element.trigger('shutterbug-saycheese'); var css = $('
').append($('link[rel="stylesheet"]').clone()).append($('style').clone()).html(); var width = $element.width(); var height = $element.height(); var element = $element.clone(); // remove all script elements from the clone we don't want the html fragement // changing itself element.find("script").remove(); if (arguments.length > 0) { var nestedIFrames = arguments; // This supports two cases: // - element itself is an iframe - .addBack('iframe') // - element descentands are iframes - .find('iframe') element.find("iframe").addBack("iframe").each(function(i, iframeElem) { // When iframe doesn't support Shutterbug, request will timeout and null will be received. // In such case just ignore this iframe, we won't be able to render it. if (nestedIFrames[i] == null) return; $(iframeElem).attr("src", "data:text/html," + generateFullHtmlFromFragment(nestedIFrames[i])); }); } var replacementImgs = $element.find('canvas').map( function(count,elem) { var dataUrl = elem.toDataURL('image/png'); var img = cloneDomItem($(elem),""); img.attr('src', dataUrl); return img; }); element.find('canvas').each(function(i,elm) { var backgroundDiv = cloneDomItem($(elm),"
"); // Add a backing (background) dom element for BG canvas property $(elm).replaceWith(replacementImgs[i]); backgroundDiv.insertBefore($(elm)); }); element.css({ 'top':0, 'left':0, 'margin':0, 'width':width, 'height':height }); // Due to a weird layout bug in PhantomJS, inner iframes sometimes don't render // unless we set the width small. This doesn't affect the actual output at all. if (_useIframeSizeHack) { width = 10; } var html_content = { content: $('
').append(element).html(), css: css, width: width, height: height, base_url: getBaseUrl() }; $element.trigger('shutterbug-asyouwere'); callback(html_content); }); }; var getDomSnapshot = function() { // Start timer. var self = this; var time = 0; var counter = $(""); counter.html(time); $(self.imgDst).html("creating snapshot: ").append(counter); var timer = setInterval(function(t) { time = time + 1; counter.html(time); }, 1000); // Ask for HTML fragment and render it on server. this.getHtmlFragment(function(html) { $.ajax({ url: "CONVERT_PATH", type: "POST", data: html }).success(function(msg) { if(self.imgDst) { $(self.imgDst).html(msg); } if (self.callback) { self.callback(msg); } clearInterval(timer); }).fail(function(jqXHR, textStatus, errorThrown) { $(self.imgDst).html("snapshot failed"); if (self.failCallback) { self.failCallback(jqXHR, textStatus, errorThrown); } clearInterval(timer); }); }); }; var requestHtmlFrag = function() { var destination = $(this.element)[0].contentWindow; var message = { type: 'htmlFragRequest', id: this.id }; destination.postMessage(JSON.stringify(message), "*"); }; var htmlSnap = function() { this.getHtmlFragment(function callback(fragment){ // FIXME btoa is not intended to encode text it is for for 8bit per char strings // so if you send it a UTF8 string with a special char in it, it will fail // this SO has a note about handling this: // http://stackoverflow.com/questions/246801/how-can-you-encode-a-string-to-base64-in-javascript // also note that btoa is only available in IE10+ var encodedContent = btoa(generateFullHtmlFromFragment(fragment)); window.open("data:text/html;base64," + encodedContent); }); }; var imageSnap = function() { var oldImgDst = this.imgDst, oldCallback = this.callback, self = this; this.imgDst = null; this.callback = function (msg){ // extract the url out of the returned html fragment var imgUrl = msg.match(/src='([^']*)'/)[1] window.open(imgUrl); self.imgDst = oldImgDst; self.callback = oldCallback; } this.getDomSnapshot(); }; var setFailureCallback = function(failCallback) { this.failCallback = failCallback; }; // TODO: Construct using opts instead of positional arguments. window.Shutterbug = function(selector, imgDst, callback, id, jQuery) { if (typeof(jQuery) != "undefined" && jQuery != null) { $ = jQuery; } // If we still don't have a valid jQuery, try setting it from the global jQuery default. // This can happen if shutterbug.js is included before jquery.js if ((typeof($) == "undefined" || $ == null) && typeof(window.$) != "undefined" && window.$ != null) { $ = window.$; } var shutterbugInstance = { element: selector, imgDst: imgDst, callback: callback, id: id, getDomSnapshot: getDomSnapshot, getHtmlFragment: getHtmlFragment, requestHtmlFrag: requestHtmlFrag, htmlSnap: htmlSnap, imageSnap: imageSnap, useIframeSizeHack: useIframeSizeHack, iframeReqTimeout: MAX_TIMEOUT, setFailureCallback: setFailureCallback }; var handleMessage = function(message, signature, func) { var data = message.data; if (typeof data === 'string') { try { data = JSON.parse(data); if (data.type === signature) { func(data); } } catch(e) { // Not a json message. Ignore it. We only speak json. } } }; var htmlFragRequestListen = function(message) { var send_response = function(data) { // Update timeout. When we receive a request from parent, we have to finish nested iframes // rendering in that time. Otherwise parent rendering will timeout. // Backward compatibility: Shutterbug v0.1.x don't send iframeReqTimeout. shutterbugInstance.iframeReqTimeout = data.iframeReqTimeout != null ? data.iframeReqTimeout : MAX_TIMEOUT; shutterbugInstance.getHtmlFragment(function(html) { var response = { type: 'htmlFragResponse', value: html, iframeReqId: data.iframeReqId, id: data.id // return to sender only... }; message.source.postMessage(JSON.stringify(response), "*"); }); }; handleMessage(message, 'htmlFragRequest', send_response); }; var htmlFragResponseListen = function(message) { var send_response = function(data) { if (data.id === shutterbugInstance.id) { // Backward compatibility: Shutterbug v0.1.x don't send iframeReqId. var ifeameReqId = data.iframeReqId != null ? data.iframeReqId : 0; shutterbugInstance._iframeContentRequests[ifeameReqId].resolve(data.value); } }; handleMessage(message, 'htmlFragResponse', send_response); }; $(document).ready(function () { window.addEventListener('message', htmlFragRequestListen, false); window.addEventListener('message', htmlFragResponseListen, false); }); return shutterbugInstance; }; })();