app/assets/javascripts/shutterbug.js
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("jquery"));
else if(typeof define === 'function' && define.amd)
define(["jquery"], factory);
else if(typeof exports === 'object')
exports["shutterbug"] = factory(require("jquery"));
else
root["Shutterbug"] = factory(root["jQuery"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE_0__) {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_0__;
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _jquery = __webpack_require__(0);
var _jquery2 = _interopRequireDefault(_jquery);
var _shutterbugWorker = __webpack_require__(2);
var _shutterbugWorker2 = _interopRequireDefault(_shutterbugWorker);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Used by enable and disable functions.
var iframeWorker = null;
function parseSnapshotArguments(args) {
// Remember that selector is anything accepted by jQuery, it can be DOM element too.
var selector = void 0;
var doneCallback = void 0;
var dstSelector = void 0;
var options = {};
function assignSecondArgument(arg) {
if (typeof arg === 'string') {
dstSelector = arg;
} else if (typeof arg === 'function') {
doneCallback = arg;
} else if ((typeof arg === 'undefined' ? 'undefined' : _typeof(arg)) === 'object') {
options = arg;
}
}
if (args.length === 3) {
options = args[2];
assignSecondArgument(args[1]);
selector = args[0];
} else if (args.length === 2) {
assignSecondArgument(args[1]);
selector = args[0];
} else if (args.length === 1) {
options = args[0];
}
if (selector) {
options.selector = selector;
}
if (doneCallback) {
options.done = doneCallback;
}
if (dstSelector) {
options.dstSelector = dstSelector;
}
return options;
}
// Public API:
exports.default = {
snapshot: function snapshot() {
var options = parseSnapshotArguments(arguments);
var shutterbugWorker = new _shutterbugWorker2.default(options);
shutterbugWorker.getDomSnapshot();
},
enable: function enable(selector) {
this.disable();
selector = selector || 'body';
iframeWorker = new _shutterbugWorker2.default({ selector: selector });
iframeWorker.enableIframeCommunication();
},
disable: function disable() {
if (iframeWorker) {
iframeWorker.disableIframeCommunication();
iframeWorker = null;
}
},
// Supported events:
// 'saycheese' - triggered before snapshot is taken
// 'asyouwere' - triggered after snapshot is taken
on: function on(event, handler) {
(0, _jquery2.default)(window).on('shutterbug-' + event, handler);
},
off: function off(event, handler) {
(0, _jquery2.default)(window).off('shutterbug-' + event, handler);
}
};
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _jquery = __webpack_require__(0);
var _jquery2 = _interopRequireDefault(_jquery);
var _htmlTools = __webpack_require__(3);
var _replaceBlobsWithDataUrls = __webpack_require__(4);
var _replaceBlobsWithDataUrls2 = _interopRequireDefault(_replaceBlobsWithDataUrls);
var _defaultServer = __webpack_require__(5);
var _defaultServer2 = _interopRequireDefault(_defaultServer);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var MAX_TIMEOUT = 1500;
// Each shutterbug instance on a single page requires unique ID (iframe-iframe communication).
var _id = 0;
function getID() {
return _id++;
}
var ShutterbugWorker = function () {
function ShutterbugWorker(options) {
_classCallCheck(this, ShutterbugWorker);
var opt = options || {};
if (!opt.selector) {
throw new Error('missing required option: selector');
}
// Remember that selector is anything accepted by jQuery, it can be DOM element too.
this.element = opt.selector;
this.callback = opt.done;
this.failCallback = opt.fail;
this.alwaysCallback = opt.always;
this.imgDst = opt.dstSelector;
this.server = opt.server || _defaultServer2.default;
this.id = getID();
this.iframeReqTimeout = MAX_TIMEOUT;
// Bind and save a new function, so it works well with .add/removeEventListener().
this._postMessageHandler = this._postMessageHandler.bind(this);
}
_createClass(ShutterbugWorker, [{
key: 'enableIframeCommunication',
value: function enableIframeCommunication() {
var _this = this;
(0, _jquery2.default)(document).ready(function () {
window.addEventListener('message', _this._postMessageHandler, false);
});
}
}, {
key: 'disableIframeCommunication',
value: function disableIframeCommunication() {
window.removeEventListener('message', this._postMessageHandler, false);
}
}, {
key: 'getDomSnapshot',
value: function getDomSnapshot() {
var _this2 = this;
this.enableIframeCommunication(); // !!!
var timerID = null;
if (this.imgDst) {
// Start timer and update destination element.
var time = 0;
var counter = (0, _jquery2.default)('<span>');
counter.html(time);
(0, _jquery2.default)(this.imgDst).html('Creating snapshot: ').append(counter);
timerID = setInterval(function () {
time = time + 1;
counter.html(time);
}, 1000);
}
// Ask for HTML fragment and render it on server.
this.getHtmlFragment(function (htmlData) {
_jquery2.default.ajax({
url: _this2.server + '/make-snapshot',
type: 'POST',
data: JSON.stringify(htmlData)
}).done(function (msg) {
if (_this2.callback) {
_this2.callback(msg.url);
}
if (_this2.imgDst) {
(0, _jquery2.default)(_this2.imgDst).html('<img src=' + msg.url + '>');
}
}).fail(function (jqXHR, textStatus, errorThrown) {
if (_this2.failCallback) {
_this2.failCallback(jqXHR, textStatus, errorThrown);
}
if (_this2.imgDst) {
(0, _jquery2.default)(_this2.imgDst).html('Snapshot failed');
}
console.error(textStatus, errorThrown);
}).always(function () {
clearInterval(timerID);
_this2.disableIframeCommunication(); // !!!
if (_this2.alwaysCallback) {
_this2.alwaysCallback();
}
});
});
}
// Most important method. Returns HTML, CSS and dimensions of the snapshot.
}, {
key: 'getHtmlFragment',
value: function getHtmlFragment(callback) {
var _this3 = this;
var $element = (0, _jquery2.default)(this.element);
// .find('iframe').addBack("iframe") handles two cases:
// - element itself is an iframe - .addBack('iframe')
// - element descendants are iframes - .find('iframe')
var $iframes = $element.find('iframe').addBack('iframe');
this._iframeContentRequests = [];
$iframes.each(function (i, iframeElem) {
// Note that position of the iframe is used as its ID.
_this3._postHtmlFragRequestToIframe(iframeElem, i);
});
// Continue when we receive responses from all the nested iframes.
// Nested iframes descriptions will be provided as arguments.
_jquery2.default.when.apply(_jquery2.default, this._iframeContentRequests).done(function () {
$element.trigger('shutterbug-saycheese');
var clonedElement = $element.clone();
// remove all script elements from the clone we don't want the html fragment
// changing itself
clonedElement.find('script').remove();
// Nested iframes.
if (arguments.length > 0) {
var nestedIFrames = arguments;
// This supports two cases:
// - clonedElement itself is an iframe - .addBack('iframe')
// - clonedElement descendants are iframes - .find('iframe')
clonedElement.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;
(0, _jquery2.default)(iframeElem).attr('srcdoc', (0, _htmlTools.generateFullHtmlFromFragment)(nestedIFrames[i]));
});
}
// Canvases.
// .addBack('canvas') handles case when the clonedElement itself is a canvas.
var replacementCanvasImgs = $element.find('canvas').addBack('canvas').map(function (i, elem) {
var dataUrl = (0, _htmlTools.getDataURL)(elem);
var img = (0, _htmlTools.cloneDomItem)((0, _jquery2.default)(elem), '<img>');
img.attr('src', dataUrl);
return img;
});
if (clonedElement.is('canvas')) {
clonedElement = replacementCanvasImgs[0];
} else {
clonedElement.find('canvas').each(function (i, elem) {
(0, _jquery2.default)(elem).replaceWith(replacementCanvasImgs[i]);
});
}
// Video elements.
// .addBack('video') handles case when the clonedElement itself is a video.
var replacementVideoImgs = [];
$element.find('video').addBack('video').map(function (i, elem) {
var $elem = (0, _jquery2.default)(elem);
var canvas = (0, _htmlTools.cloneDomItem)($elem, '<canvas>');
canvas[0].getContext('2d').drawImage(elem, 0, 0, $elem.width(), $elem.height());
try {
var dataUrl = (0, _htmlTools.getDataURL)(canvas[0]);
var img = (0, _htmlTools.cloneDomItem)($elem, '<img>');
img.attr('src', dataUrl);
replacementVideoImgs.push(img);
} catch (e) {
// If the video isn't hosted on the same site this will catch the security error
// and push null to signal it doesn't need replacing. We don't use the return
// value of map() as returning null confuses jQuery.
replacementVideoImgs.push(null);
}
});
if (clonedElement.is('video')) {
if (replacementVideoImgs[0]) {
clonedElement = replacementVideoImgs[0];
}
} else {
clonedElement.find('video').each(function (i, elem) {
if (replacementVideoImgs[i]) {
(0, _jquery2.default)(elem).replaceWith(replacementVideoImgs[i]);
}
});
}
clonedElement.css({
// Make sure that clonedElement will be positioned in the top-left corner of the viewport.
'top': 0,
'left': 0,
'transform': 'translate3d(0, 0, 0)',
'margin': 0,
// Dimensions need to be set explicitly (e.g. otherwise 50% width wouldn't work as expected).
'width': $element.width(),
'height': $element.height()
});
var htmlString = (0, _jquery2.default)('<div>').append(clonedElement).html();
var cssString = (0, _jquery2.default)('<div>').append((0, _jquery2.default)('link[rel="stylesheet"]').clone()).append((0, _jquery2.default)('style').clone()).html();
// Process HTML and CSS content when it's a string. Some operations are easier when we can use regular expressions
// instead of traversing the DOM using jQuery.
var htmlDeferred = (0, _replaceBlobsWithDataUrls2.default)(htmlString);
var cssDeferred = (0, _replaceBlobsWithDataUrls2.default)(cssString);
_jquery2.default.when(htmlDeferred, cssDeferred).done(function (processedHTMLString, processedCssString) {
var htmlData = {
content: processedHTMLString,
css: processedCssString,
width: $element.outerWidth(),
height: $element.outerHeight(),
base_url: window.location.href
};
$element.trigger('shutterbug-asyouwere');
callback(htmlData);
});
});
}
// frame-iframe communication related methods:
// Basic post message handler.
}, {
key: '_postMessageHandler',
value: function _postMessageHandler(message) {
function handleMessage(message, type, handler) {
var data = message.data;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
if (data.type === type) {
handler(data, message.source);
}
} catch (e) {
// Not a json message. Ignore it. We only speak json.
}
}
}
handleMessage(message, 'htmlFragRequest', this._htmlFragRequestHandler.bind(this));
handleMessage(message, 'htmlFragResponse', this._htmlFragResponseHandler.bind(this));
}
// Iframe receives question about its content.
}, {
key: '_htmlFragRequestHandler',
value: function _htmlFragRequestHandler(data, source) {
// 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.
this.iframeReqTimeout = data.iframeReqTimeout != null ? data.iframeReqTimeout : MAX_TIMEOUT;
this.getHtmlFragment(function (html) {
var response = {
type: 'htmlFragResponse',
value: html,
iframeReqId: data.iframeReqId,
id: data.id // return to sender only
};
source.postMessage(JSON.stringify(response), '*');
});
}
// Parent receives content from iframes.
}, {
key: '_htmlFragResponseHandler',
value: function _htmlFragResponseHandler(data) {
if (data.id === this.id) {
// Backward compatibility: Shutterbug v0.1.x don't send iframeReqId.
var iframeReqId = data.iframeReqId != null ? data.iframeReqId : 0;
this._iframeContentRequests[iframeReqId].resolve(data.value);
}
}
// Parent asks iframes about their content.
}, {
key: '_postHtmlFragRequestToIframe',
value: function _postHtmlFragRequestToIframe(iframeElem, iframeId) {
var message = {
type: 'htmlFragRequest',
id: this.id,
iframeReqId: iframeId,
// 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: this.iframeReqTimeout * 0.6
};
iframeElem.contentWindow.postMessage(JSON.stringify(message), '*');
var requestDeferred = new _jquery2.default.Deferred();
this._iframeContentRequests[iframeId] = requestDeferred;
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 (requestDeferred.state() !== 'resolved') {
requestDeferred.resolve(null);
}
}, this.iframeReqTimeout);
}
}]);
return ShutterbugWorker;
}();
exports.default = ShutterbugWorker;
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.cloneDomItem = cloneDomItem;
exports.getDataURL = getDataURL;
exports.generateFullHtmlFromFragment = generateFullHtmlFromFragment;
var _jquery = __webpack_require__(0);
var _jquery2 = _interopRequireDefault(_jquery);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function cloneDomItem($elem, elemTag) {
var $returnElm = (0, _jquery2.default)(elemTag);
$returnElm.addClass($elem.attr('class'));
$returnElm.attr('id', $elem.attr('id'));
$returnElm.attr('style', $elem.attr('style'));
$returnElm.css('background', $elem.css('background'));
$returnElm.attr('width', $elem.width());
$returnElm.attr('height', $elem.height());
return $returnElm;
}
// element should be an instance of Canvas or Video element (element supported as an input to Canvas.drawImage method).
// In some cases dataURL should be rescaled down to real size of the element (high DPI displays).
// It doesn't make sense to send original data, as it might be really large and cause issues while rendering page on
// AWS Lambda.
function getDataURL(element) {
// Always use png to support transparent background.
var format = 'image/png';
var realWidth = (0, _jquery2.default)(element).width();
var realHeight = (0, _jquery2.default)(element).height();
// When element hasn't been added to DOM, realWidth and realHeight will be equal to 0.
var realDimensionsAvailable = realWidth > 0 && realHeight > 0;
var widthAttr = Number((0, _jquery2.default)(element).attr('width')) || realWidth;
var heightAttr = Number((0, _jquery2.default)(element).attr('height')) || realHeight;
if (!realDimensionsAvailable || realWidth === widthAttr && realHeight === heightAttr) {
return element.toDataURL(format);
}
// Scale down image to its real size.
var canvas = document.createElement('canvas');
canvas.width = realWidth;
canvas.height = realHeight;
var ctx = canvas.getContext('2d');
// Other canvas or video element can be used as a source in .drawImage.
ctx.drawImage(element, 0, 0, realWidth, realHeight);
return canvas.toDataURL(format);
}
function generateFullHtmlFromFragment(fragment) {
return '\n <!DOCTYPE html> \n <html> \n <head> \n <base href="' + fragment.base_url + '"> \n <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> \n <title>content from ' + fragment.base_url + '</title> \n ' + fragment.css + ' \n </head> \n <body> \n ' + fragment.content + ' \n </body> \n </html>\n ';
}
/***/ }),
/* 4 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = replaceBlobsWithDataURLs;
var _jquery = __webpack_require__(0);
var _jquery2 = _interopRequireDefault(_jquery);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Downloads `blobURL` and provides object with mapping to dataURL format.
// Async function, returns $.Deferred instance that will be resolved with the mapping.
function convertBlobToDataURL(blobURL) {
var requestDeferred = new _jquery2.default.Deferred();
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
var reader = new FileReader();
reader.addEventListener('loadend', function () {
requestDeferred.resolve({ blobURL: blobURL, dataURL: reader.result });
});
reader.readAsDataURL(this.response);
}
};
xhr.open('GET', blobURL);
xhr.responseType = 'blob';
xhr.send();
return requestDeferred;
}
// Converts all the blob URLs (e.g. "blob:http://examples.com/abc-def-ghi") in `htmlString` to data URLs.
// Async function, returns $.Deferred instance that will be resolved with the final HTML.
/* eslint-env browser */
function replaceBlobsWithDataURLs(htmlString) {
var deferred = new _jquery2.default.Deferred();
var blobURLs = htmlString.match(/["']blob:.*?["']/gi);
if (blobURLs === null) {
// Nothing to do.
deferred.resolve(htmlString);
return deferred;
}
var blobRequests = blobURLs
// .slice(1, -1) removes " or ' from the URI.
.map(function (blobURLWithQuotes) {
return blobURLWithQuotes.slice(1, -1);
}).map(function (blobURL) {
return convertBlobToDataURL(blobURL);
});
_jquery2.default.when.apply(_jquery2.default, blobRequests).done(function () {
// Convert arguments to real Array instance.
var mappings = Array.prototype.slice.call(arguments);
var newHtmlString = htmlString;
mappings.forEach(function (mapping) {
newHtmlString = newHtmlString.replace(mapping.blobURL, mapping.dataURL);
});
deferred.resolve(newHtmlString);
});
return deferred;
}
/***/ }),
/* 5 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
// Production:
var DEFAULT_SERVER = 'https://fh1fzvhx93.execute-api.us-east-1.amazonaws.com/production';
// Staging:
// const DEFAULT_SERVER = 'https://dgjr6g3z30.execute-api.us-east-1.amazonaws.com/staging'
// Local:
// const DEFAULT_SERVER = 'http://localhost:4000'
exports.default = DEFAULT_SERVER;
/***/ })
/******/ ])["default"];
});