Last active
May 13, 2019 20:01
-
-
Save henryjameslau/4d857052a44f64c83bf7a3e2c6c04af9 to your computer and use it in GitHub Desktop.
Odd's ratio
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"essential" : { | |
"graphic_data_url": "data.csv", | |
"colour_palette": [ "#274796"], | |
"negative_colour":["#D32F2F"], | |
"sourceText":["Labour Market Statistics, February 2015, Table A02"], | |
"sourceURL":["http://www.ons.gov.uk/ons/publications/re-reference-tables.html?edition=tcm%3A77-350752"], | |
"annotationChart": [ | |
], | |
"annotationBullet": [ | |
], | |
"annotationXY":[ | |
], | |
"annotationAlign":[ | |
], | |
"barHeight_sm_md_lg" : [40,40,40], | |
"xAxisLabel":"Odds ratio", | |
"xAxisScale":[0.125,0.25,0.5,1,2,4] | |
}, | |
"optional" : { | |
"margin_sm": [5, 20, 5, 135], | |
"margin_md": [5, 20, 5, 135], | |
"margin_lg": [5, 20, 5, 170], | |
"aspectRatio_sm" : [4,7], | |
"aspectRatio_md" : [1,1], | |
"aspectRatio_lg" : [16,12], | |
"mobileBreakpoint" : 510, | |
"x_num_ticks_sm_md_lg" : [6,8,10], | |
"vertical_line" : false, | |
"annotateLineX1_Y1_X2_Y2":[ [[25,"South East"],[25,"East"]]], | |
"annotateRect" : false, | |
"annotateRectX_Y" : [ [[40, "South West"],[75, "West Midlands"]]], | |
"lineColor_opcty" : [["#888", 0.2]], | |
"centre_line" : false, | |
"centre_line_value" : 10 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name | value | |
---|---|---|
North West | 1 | |
North East | 2 | |
Wales | 3 | |
South East | 0.125 | |
London | 0.25 | |
South West | 0.5 | |
East Midlands | 1 | |
Yorkshire and The Humber | 2 | |
Scotland | 4 | |
East | 0.125 | |
West Midlands | 0.25 | |
Northern Ireland | 0.5 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'> | |
<title></title> | |
<meta name="description" content=""> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" /> | |
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | |
<link rel="stylesheet" href="../lib/globalStyle.css" /> | |
<style type="text/css"> | |
</style> | |
</head> | |
<body> | |
<div id="graphic"> | |
<img src="fallback.png" alt="[Chart]" /> | |
</div> | |
<div id="keypoints"> | |
<p></p> | |
</div> | |
<div class="footer"> | |
</div> | |
<div id="footer"></div> | |
<script src="https://cdn.ons.gov.uk/vendor/d3/4.2.7/d3.min.js" type="text/javascript"></script> | |
<script src="modernizr.svg.min.js" type="text/javascript"></script> | |
<script src="pym.js" type="text/javascript"></script> | |
<script> | |
var graphic = d3.select('#graphic'); | |
var keypoints = d3.select('#keypoints'); | |
var footer = d3.select(".footer"); | |
var pymChild = null; | |
function drawGraphic(width) { | |
var threshold_md = 788; | |
var threshold_sm = dvc.optional.mobileBreakpoint; | |
//set variables for chart dimensions dependent on width of #graphic | |
if (parseInt(graphic.style("width")) < threshold_sm) { | |
var margin = { | |
top: dvc.optional.margin_sm[0], | |
right: dvc.optional.margin_sm[1], | |
bottom: dvc.optional.margin_sm[2], | |
left: dvc.optional.margin_sm[3] | |
}; | |
var chart_width = parseInt(graphic.style("width")) - margin.left - margin.right; | |
var height = dvc.essential.barHeight_sm_md_lg[0] * graphic_data.length - margin.top - margin.bottom; | |
} else if (parseInt(graphic.style("width")) < threshold_md) { | |
var margin = { | |
top: dvc.optional.margin_md[0], | |
right: dvc.optional.margin_md[1], | |
bottom: dvc.optional.margin_md[2], | |
left: dvc.optional.margin_md[3] | |
}; | |
var chart_width = parseInt(graphic.style("width")) - margin.left - margin.right; | |
var height = dvc.essential.barHeight_sm_md_lg[1] * graphic_data.length - margin.top - margin.bottom; | |
} else { | |
var margin = { | |
top: dvc.optional.margin_lg[0], | |
right: dvc.optional.margin_lg[1], | |
bottom: dvc.optional.margin_lg[2], | |
left: dvc.optional.margin_lg[3] | |
} | |
var chart_width = parseInt(graphic.style("width")) - margin.left - margin.right; | |
var height = dvc.essential.barHeight_sm_md_lg[2] * graphic_data.length - margin.top - margin.bottom; | |
} | |
// clear out existing graphics | |
graphic.selectAll("*").remove(); | |
keypoints.selectAll("*").remove(); | |
footer.selectAll("*").remove(); | |
var x = d3.scaleOrdinal() | |
.range([0, 1*chart_width/5, 2*chart_width/5,3*chart_width/5,4*chart_width/5, chart_width]) | |
var x2 = d3.scaleLog() | |
.range([0,chart_width]) | |
.domain([0.125,4]) | |
var y = d3.scaleBand() | |
.rangeRound([0, height]) | |
.paddingInner(0.1); | |
y.domain(graphic_data.map(function(d) { | |
return d.name; | |
})); | |
var yAxis = d3.axisLeft(y) | |
var xAxis = d3.axisBottom(x) | |
.tickSize(-height, 0, 0); | |
//specify number or ticks on x axis | |
if (parseInt(graphic.style("width")) <= threshold_sm) { | |
xAxis.ticks(dvc.optional.x_num_ticks_sm_md_lg[0]) | |
} else if (parseInt(graphic.style("width")) <= threshold_md) { | |
xAxis.ticks(dvc.optional.x_num_ticks_sm_md_lg[1]) | |
} else { | |
xAxis.ticks(dvc.optional.x_num_ticks_sm_md_lg[2]) | |
} | |
// parse data into columns | |
var bars = {}; | |
for (var column in graphic_data[0]) { | |
if (column == 'name') continue; | |
bars[column] = graphic_data.map(function(d) { | |
return { | |
'name': d.name, | |
'amt': d[column] | |
}; | |
}); | |
} | |
//y domain calculations : zero to intelligent max choice, or intelligent min and max choice, or interval chosen manually | |
if (dvc.essential.xAxisScale == "auto_zero_max") { | |
var xDomain = [ | |
0, | |
d3.max(d3.entries(bars), function(c) { | |
return d3.max(c.value, function(v) { | |
var n = v.amt; | |
return Math.ceil(n); | |
}); | |
}) | |
]; | |
} else if (dvc.essential.xAxisScale == "auto_min_max") { | |
var xDomain = [ | |
d3.min(d3.entries(bars), function(c) { | |
return d3.min(c.value, function(v) { | |
var n = v.amt; | |
return Math.floor(n); | |
}); | |
}), | |
d3.max(d3.entries(bars), function(c) { | |
return d3.max(c.value, function(v) { | |
var n = v.amt; | |
return Math.ceil(n); | |
}); | |
}) | |
]; | |
} else { | |
var xDomain = dvc.essential.xAxisScale; | |
} | |
x.domain(xDomain); | |
d3.select("#buttonid").on("click", function() { | |
saveSvgAsPng(document.getElementById("chart"), "diagram.png") | |
}); | |
d3.select('#graphic').select('svg') | |
.append("g") | |
.attr("id", "source") | |
.append("text") | |
.attr("text-anchor", "start") | |
.style("font-size", "12px") | |
.style("fill", "#666") | |
.attr('y', height + margin.top + margin.bottom - 10) | |
.attr('x', 0) | |
.text("Source: ") | |
.append("a") | |
.style("fill", "#4774CC") | |
.attr("href", dvc.essential.sourceURL) | |
.attr("target", "_blank") | |
.text(dvc.essential.sourceText); | |
//create svg for chart | |
var svg = d3.select('#graphic').append('svg') | |
.attr("id", "chart") | |
.style("background-color", "#fff") | |
.attr("width", chart_width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom + 30) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
svg.append("rect") | |
.attr("class", "svgRect") | |
.attr("width", chart_width) | |
.attr("height", height) | |
.attr("fill", "transparent") | |
svg.append('g') | |
.attr('class', 'x axis') | |
.attr("transform", "translate(0, " + height + ")") | |
.call(xAxis).append("text") | |
.attr("y", 25) | |
.attr("x", chart_width) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.attr("font-size", "12px") | |
.attr("fill", "#666") | |
.text(dvc.essential.xAxisLabel); | |
//create y axis, if x axis doesn't start at 0 drop x axis accordingly | |
svg.append('g') | |
.attr('class', 'y axis') | |
.attr('transform', function(d) { | |
if (xDomain[0] != 0) { | |
return 'translate(' + (0) + ',0)' | |
} else { | |
return 'translate(' + 0 + ', 0)' | |
} | |
}) | |
.call(yAxis); | |
if (parseInt(graphic.style("width")) < threshold_sm) { | |
d3.selectAll(".y .tick text") | |
.call(wrap, dvc.optional.margin_sm[3]-10); | |
} else if (parseInt(graphic.style("width")) < threshold_md) { | |
d3.selectAll(".y .tick text") | |
.call(wrap, dvc.optional.margin_md[3]-10); | |
} else { | |
d3.selectAll(".y .tick text") | |
.call(wrap, dvc.optional.margin_lg[3]-10); | |
}; | |
svg.append('g').attr("class", "bars").selectAll('rect') | |
.data(bars["value"]) | |
.enter() | |
.append('rect') | |
.attr("fill", function(d) { | |
if (d.amt > 0) { | |
return dvc.essential.colour_palette[0] | |
} else { | |
return dvc.essential.negative_colour | |
} | |
}) | |
.attr("width", function(d) { | |
return 0 + Math.abs(x2(d.amt) - x2(1)); | |
}) | |
.attr("x", function(d) { | |
return x2(Math.min(1, d.amt)); | |
}) | |
.attr("y", function(d) { | |
return y(d.name); | |
}) | |
.attr("height", y.bandwidth()) | |
//create centre line if required | |
if (dvc.optional.centre_line == true) { | |
svg.append("line") | |
.attr("id", "centreline") | |
.attr('y1', 0) | |
.attr('y2', height) | |
.attr('x1', x(dvc.optional.centre_line_value)) | |
.attr('x2', x(dvc.optional.centre_line_value)) | |
} | |
function wrap(text, width) { | |
text.each(function() { | |
var text = d3.select(this), | |
words = text.text().split(/\s+/).reverse(), | |
word, | |
line = [], | |
lineNumber = 0, | |
lineHeight = 1.1, // ems | |
y = text.attr("y"), | |
x = text.attr("x"), | |
dy = parseFloat(text.attr("dy")), | |
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"); | |
if (words.length > 2) { | |
while (word = words.pop()) { | |
line.push(word); | |
tspan.text(line.join(" ")); | |
if (tspan.node().getComputedTextLength() > width) { | |
if (lineNumber == 0) { | |
tspan.attr("dy", dy - 1.1 + "em") | |
} else { | |
tspan.attr("dy", -dy + 0.55 + "em") | |
} | |
line.pop(); | |
tspan.text(line.join(" ")); | |
line = [word]; | |
++lineNumber; | |
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", (0.55 * lineNumber - dy + 0.55) + "em").text(word); | |
} | |
} | |
} else { | |
while (word = words.pop()) { | |
line.push(word); | |
tspan.text(line.join(" ")); | |
if (tspan.node().getComputedTextLength() > width) { | |
if (lineNumber == 0) { | |
tspan.attr("dy", dy - 0.55 + "em") | |
} else { | |
tspan.attr("dy", -dy + 0.55 + "em") | |
} | |
line.pop(); | |
tspan.text(line.join(" ")); | |
line = [word]; | |
++lineNumber; | |
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", (1.1 * lineNumber - dy + 0.55) + "em").text(word); | |
} | |
} | |
} | |
}); | |
} | |
writeAnnotation(); | |
function writeAnnotation() { | |
if (parseInt(graphic.style("width")) < threshold_sm) { | |
dvc.essential.annotationBullet.forEach(function(d, i) { | |
d3.select("#keypoints").append("svg") | |
.attr("width", "20px") | |
.attr("height", "20px") | |
.attr("class", "circles") | |
.append("circle") | |
.attr("class", "annocirc" + (i)) | |
.attr("r", "2") | |
.attr('cy', "12px") | |
.attr("cx", "10px"); | |
d3.select("#keypoints").append("p").text(dvc.essential.annotationBullet[i]) | |
.attr("font-family", "'Open Sans', sans-serif") | |
.attr("font-size", "13px") | |
.attr("color", "#666") | |
.attr("font-weight", "500"); | |
}) // end foreach | |
} else { | |
dvc.essential.annotationChart.forEach(function(d, i) { | |
// draw annotation text based on content of var annotationArray ... | |
svg.append("text") | |
.text(dvc.essential.annotationChart[i]) | |
.attr("class", "annotext" + i) | |
.attr("text-anchor", dvc.essential.annotationAlign[i]) | |
.attr('y', y(dvc.essential.annotationXY[i][1])) | |
.attr('x', x(dvc.essential.annotationXY[i][0])) | |
.attr("font-family", "'Open Sans', sans-serif") | |
.attr("font-size", "13px") | |
.attr("fill", "#666") | |
.attr("font-weight", "500"); | |
d3.selectAll(".annotext" + (i)) | |
.each(insertLinebreaks) | |
.each(createBackRect); | |
function insertLinebreaks() { | |
var str = this; | |
var el1 = dvc.essential.annotationChart[i]; | |
var el = el1.data; | |
var words = el1.split(' '); | |
d3.select(this /*str*/ ).text(''); | |
for (var j = 0; j < words.length; j++) { | |
var tspan = d3.select(this).append('tspan').text(words[j]); | |
if (j > 0) | |
tspan.attr('x', x(dvc.essential.annotationXY[i][0])).attr('dy', '22'); | |
} | |
}; | |
function createBackRect() { | |
var BBox = this.getBBox() | |
svg.insert("rect", ".annotext" + (i)) | |
.attr("width", BBox.width) | |
.attr("height", BBox.height) | |
.attr("x", BBox.x) | |
.attr("y", BBox.y) | |
.attr("fill", "white") | |
.attr("opacity", 0.4); | |
}; // end function createBackRect() | |
}); // end foreach | |
} // end else ... | |
// If you have labels rather than years then you can split the lines using a double space (in the CSV file). | |
if (dvc.optional.vertical_line == true) { | |
dvc.optional.annotateLineX1_Y1_X2_Y2.forEach(function(d, i) { | |
svg.append("line").attr('x1', x(dvc.optional.annotateLineX1_Y1_X2_Y2[i][0][0])) | |
.attr('x2', x(dvc.optional.annotateLineX1_Y1_X2_Y2[i][1][0])) | |
.style('stroke', '#888') | |
.style('stroke-width', 2) | |
.attr('font-size', '13px') | |
//.style('stroke-dasharray', '5 5') .attr('y1',y(dvc.optional.annotateLineX1_Y1_X2_Y2[i][0][1])) | |
.attr('y2', y(dvc.optional.annotateLineX1_Y1_X2_Y2[i][1][1])); | |
}) | |
} | |
d3.selectAll("path").attr("fill", "none"); | |
d3.selectAll(".x line") | |
.attr("stroke", "#CCC") | |
.attr("stroke-width", "1px") | |
.attr("shape-rendering", "crispEdges"); | |
d3.selectAll("text").attr("font-family", "'Open Sans', sans-serif"); | |
d3.selectAll(".y text").attr("font-size", "12px").attr("fill", "#666"); | |
d3.selectAll(".x text").attr("font-size", "12px").attr("fill", "#666"); // dates - timelines | |
d3.selectAll(".y line") | |
.attr("stroke", "#CCC") | |
.attr("stroke-width", "1px") | |
.style("shape-rendering", "crispEdges"); | |
if (dvc.optional.annotateRect == true) { | |
dvc.optional.annotateRectX_Y.forEach(function(d, i) { | |
svg.append("rect") | |
.attr('x', x(dvc.optional.annotateRectX_Y[i][0][0])) | |
.attr('y', y(dvc.optional.annotateRectX_Y[i][0][1])) | |
.attr('height', y(dvc.optional.annotateRectX_Y[i][1][1]) - y(dvc.optional.annotateRectX_Y[i][0][1])) | |
.attr('width', x(dvc.optional.annotateRectX_Y[i][1][0]) - x(dvc.optional.annotateRectX_Y[i][0][0])) | |
.style('fill', dvc.optional.lineColor_opcty[i][0]) | |
.style('stroke-width', 2) | |
.style('opacity', dvc.optional.lineColor_opcty[i][1]); | |
}) | |
} | |
d3.select(".y").select("path").style("stroke", "#666") | |
// function insertLinebreaksLabels() { | |
// var str = this.textContent; | |
// | |
// var words = str.split(' '); | |
// | |
// d3.select(this/*str*/).text(''); | |
// | |
// for (var j = 0; j < words.length; j++) { | |
// var tspan = d3.select(this).append('tspan').text(words[j]); | |
// if (j > 0) | |
// tspan | |
// .attr('x', -10) | |
// .attr('dy', '1em'); | |
// } | |
// }; | |
// d3.selectAll(".y text").each(insertLinebreaksLabels) | |
} // end function writeAnnotation() | |
d3.select('#graphic').select('svg') | |
.append("g") | |
.attr("id", "source") | |
.append("text") | |
.attr("text-anchor", "start") | |
.style("font-size", "12px") | |
.style("fill", "#666") | |
.attr('y', height + margin.top + margin.bottom + 28) | |
.attr('x', 0) | |
.text("Source: ") | |
.append("a") | |
.style("fill", "#4774CC") | |
.attr("xlink:href", dvc.essential.sourceURL) | |
.attr("target", "_blank") | |
.text(dvc.essential.sourceText); | |
d3.selectAll("text").attr("font-family", "'Open Sans', sans-serif"); | |
//use pym to calculate chart dimensions | |
if (pymChild) { | |
pymChild.sendHeight(); | |
} | |
} | |
//check whether browser can cope with svg | |
if (Modernizr.svg) { | |
//load config | |
d3.json("config.json", function(error, config) { | |
dvc = config | |
//load chart data | |
d3.csv(dvc.essential.graphic_data_url, function(error, data) { | |
graphic_data = data; | |
// graphic_data.forEach(function(d) { | |
// d.name = d3.time.format(dvc.essential.dateFormat).parse(d.name); | |
// }); | |
names = d3.keys( /*data*/ graphic_data[0]).filter(function(key) { | |
return key !== "state"; | |
}); | |
/*data*/ | |
graphic_data.forEach(function(d) { | |
//d.ages = names.map(function(name) { return {name: name, value: +d[name]}; }); | |
}); | |
//use pym to create iframed chart dependent on specified variables | |
pymChild = new pym.Child({ | |
renderCallback: drawGraphic | |
}); | |
}); | |
}) | |
} else { | |
//use pym to create iframe containing fallback image (which is set as default) | |
pymChild = new pym.Child(); | |
if (pymChild) { | |
pymChild.sendHeight(); | |
} | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Modernizr 2.7.1 (Custom Build) | MIT & BSD | |
* Build: http://modernizr.com/download/#-svg | |
*/ | |
;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.7.1",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)t(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},u(""),h=j=null,e._version=d,e}(this,this.document); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! pym.js - v1.3.2 - 2018-02-13 */ | |
/* | |
* Pym.js is library that resizes an iframe based on the width of the parent and the resulting height of the child. | |
* Check out the docs at http://blog.apps.npr.org/pym.js/ or the readme at README.md for usage. | |
*/ | |
/** @module pym */ | |
(function(factory) { | |
if (typeof define === 'function' && define.amd) { | |
define(factory); | |
} | |
else if (typeof module !== 'undefined' && module.exports) { | |
module.exports = factory(); | |
} else { | |
window.pym = factory.call(this); | |
} | |
})(function() { | |
var MESSAGE_DELIMITER = 'xPYMx'; | |
var lib = {}; | |
/** | |
* Create and dispatch a custom pym event | |
* | |
* @method _raiseCustomEvent | |
* @inner | |
* | |
* @param {String} eventName | |
*/ | |
var _raiseCustomEvent = function(eventName) { | |
var event = document.createEvent('Event'); | |
event.initEvent('pym:' + eventName, true, true); | |
document.dispatchEvent(event); | |
}; | |
/** | |
* Generic function for parsing URL params. | |
* Via http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | |
* | |
* @method _getParameterByName | |
* @inner | |
* | |
* @param {String} name The name of the paramter to get from the URL. | |
*/ | |
var _getParameterByName = function(name) { | |
var regex = new RegExp("[\\?&]" + name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]') + '=([^&#]*)'); | |
var results = regex.exec(location.search); | |
if (results === null) { | |
return ''; | |
} | |
return decodeURIComponent(results[1].replace(/\+/g, " ")); | |
}; | |
/** | |
* Check the message to make sure it comes from an acceptable xdomain. | |
* Defaults to '*' but can be overriden in config. | |
* | |
* @method _isSafeMessage | |
* @inner | |
* | |
* @param {Event} e The message event. | |
* @param {Object} settings Configuration. | |
*/ | |
var _isSafeMessage = function(e, settings) { | |
if (settings.xdomain !== '*') { | |
// If origin doesn't match our xdomain, return. | |
if (!e.origin.match(new RegExp(settings.xdomain + '$'))) { return; } | |
} | |
// Ignore events that do not carry string data #151 | |
if (typeof e.data !== 'string') { return; } | |
return true; | |
}; | |
var _isSafeUrl = function(url) { | |
// Adapted from angular 2 url sanitizer | |
var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp):|[^&:/?#]*(?:[/?#]|$))/gi; | |
if (!url.match(SAFE_URL_PATTERN)) { return; } | |
return true; | |
}; | |
/** | |
* Construct a message to send between frames. | |
* | |
* NB: We use string-building here because JSON message passing is | |
* not supported in all browsers. | |
* | |
* @method _makeMessage | |
* @inner | |
* | |
* @param {String} id The unique id of the message recipient. | |
* @param {String} messageType The type of message to send. | |
* @param {String} message The message to send. | |
*/ | |
var _makeMessage = function(id, messageType, message) { | |
var bits = ['pym', id, messageType, message]; | |
return bits.join(MESSAGE_DELIMITER); | |
}; | |
/** | |
* Construct a regex to validate and parse messages. | |
* | |
* @method _makeMessageRegex | |
* @inner | |
* | |
* @param {String} id The unique id of the message recipient. | |
*/ | |
var _makeMessageRegex = function(id) { | |
var bits = ['pym', id, '(\\S+)', '(.*)']; | |
return new RegExp('^' + bits.join(MESSAGE_DELIMITER) + '$'); | |
}; | |
/** | |
* Underscore implementation of getNow | |
* | |
* @method _getNow | |
* @inner | |
* | |
*/ | |
var _getNow = Date.now || function() { | |
return new Date().getTime(); | |
}; | |
/** | |
* Underscore implementation of throttle | |
* | |
* @method _throttle | |
* @inner | |
* | |
* @param {function} func Throttled function | |
* @param {number} wait Throttle wait time | |
* @param {object} options Throttle settings | |
*/ | |
var _throttle = function(func, wait, options) { | |
var context, args, result; | |
var timeout = null; | |
var previous = 0; | |
if (!options) {options = {};} | |
var later = function() { | |
previous = options.leading === false ? 0 : _getNow(); | |
timeout = null; | |
result = func.apply(context, args); | |
if (!timeout) {context = args = null;} | |
}; | |
return function() { | |
var now = _getNow(); | |
if (!previous && options.leading === false) {previous = now;} | |
var remaining = wait - (now - previous); | |
context = this; | |
args = arguments; | |
if (remaining <= 0 || remaining > wait) { | |
if (timeout) { | |
clearTimeout(timeout); | |
timeout = null; | |
} | |
previous = now; | |
result = func.apply(context, args); | |
if (!timeout) {context = args = null;} | |
} else if (!timeout && options.trailing !== false) { | |
timeout = setTimeout(later, remaining); | |
} | |
return result; | |
}; | |
}; | |
/** | |
* Clean autoInit Instances: those that point to contentless iframes | |
* @method _cleanAutoInitInstances | |
* @inner | |
*/ | |
var _cleanAutoInitInstances = function() { | |
var length = lib.autoInitInstances.length; | |
// Loop backwards to avoid index issues | |
for (var idx = length - 1; idx >= 0; idx--) { | |
var instance = lib.autoInitInstances[idx]; | |
// If instance has been removed or is contentless then remove it | |
if (instance.el.getElementsByTagName('iframe').length && | |
instance.el.getElementsByTagName('iframe')[0].contentWindow) { | |
continue; | |
} | |
else { | |
// Remove the reference to the removed or orphan instance | |
lib.autoInitInstances.splice(idx,1); | |
} | |
} | |
}; | |
/** | |
* Store auto initialized Pym instances for further reference | |
* @name module:pym#autoInitInstances | |
* @type Array | |
* @default [] | |
*/ | |
lib.autoInitInstances = []; | |
/** | |
* Initialize Pym for elements on page that have data-pym attributes. | |
* Expose autoinit in case we need to call it from the outside | |
* @instance | |
* @method autoInit | |
* @param {Boolean} doNotRaiseEvents flag to avoid sending custom events | |
*/ | |
lib.autoInit = function(doNotRaiseEvents) { | |
var elements = document.querySelectorAll('[data-pym-src]:not([data-pym-auto-initialized])'); | |
var length = elements.length; | |
// Clean stored instances in case needed | |
_cleanAutoInitInstances(); | |
for (var idx = 0; idx < length; ++idx) { | |
var element = elements[idx]; | |
/* | |
* Mark automatically-initialized elements so they are not | |
* re-initialized if the user includes pym.js more than once in the | |
* same document. | |
*/ | |
element.setAttribute('data-pym-auto-initialized', ''); | |
// Ensure elements have an id | |
if (element.id === '') { | |
element.id = 'pym-' + idx + "-" + Math.random().toString(36).substr(2,5); | |
} | |
var src = element.getAttribute('data-pym-src'); | |
// List of data attributes to configure the component | |
// structure: {'attribute name': 'type'} | |
var settings = {'xdomain': 'string', 'title': 'string', 'name': 'string', 'id': 'string', | |
'sandbox': 'string', 'allowfullscreen': 'boolean', | |
'parenturlparam': 'string', 'parenturlvalue': 'string', | |
'optionalparams': 'boolean', 'trackscroll': 'boolean', | |
'scrollwait': 'number'}; | |
var config = {}; | |
for (var attribute in settings) { | |
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#Notes | |
if (element.getAttribute('data-pym-'+attribute) !== null) { | |
switch (settings[attribute]) { | |
case 'boolean': | |
config[attribute] = !(element.getAttribute('data-pym-'+attribute) === 'false'); // jshint ignore:line | |
break; | |
case 'string': | |
config[attribute] = element.getAttribute('data-pym-'+attribute); | |
break; | |
case 'number': | |
var n = Number(element.getAttribute('data-pym-'+attribute)); | |
if (!isNaN(n)) { | |
config[attribute] = n; | |
} | |
break; | |
default: | |
console.err('unrecognized attribute type'); | |
} | |
} | |
} | |
// Store references to autoinitialized pym instances | |
var parent = new lib.Parent(element.id, src, config); | |
lib.autoInitInstances.push(parent); | |
} | |
// Fire customEvent | |
if (!doNotRaiseEvents) { | |
_raiseCustomEvent("pym-initialized"); | |
} | |
// Return stored autoinitalized pym instances | |
return lib.autoInitInstances; | |
}; | |
/** | |
* The Parent half of a response iframe. | |
* | |
* @memberof module:pym | |
* @class Parent | |
* @param {String} id The id of the div into which the iframe will be rendered. sets {@link module:pym.Parent~id} | |
* @param {String} url The url of the iframe source. sets {@link module:pym.Parent~url} | |
* @param {Object} [config] Configuration for the parent instance. sets {@link module:pym.Parent~settings} | |
* @param {string} [config.xdomain='*'] - xdomain to validate messages received | |
* @param {string} [config.title] - if passed it will be assigned to the iframe title attribute | |
* @param {string} [config.name] - if passed it will be assigned to the iframe name attribute | |
* @param {string} [config.id] - if passed it will be assigned to the iframe id attribute | |
* @param {boolean} [config.allowfullscreen] - if passed and different than false it will be assigned to the iframe allowfullscreen attribute | |
* @param {string} [config.sandbox] - if passed it will be assigned to the iframe sandbox attribute (we do not validate the syntax so be careful!!) | |
* @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name passed to the iframe src | |
* @param {string} [config.parenturlvalue] - if passed it will be override the default parentUrl query string parameter value passed to the iframe src | |
* @param {string} [config.optionalparams] - if passed and different than false it will strip the querystring params parentUrl and parentTitle passed to the iframe src | |
* @param {boolean} [config.trackscroll] - if passed it will activate scroll tracking on the parent | |
* @param {number} [config.scrollwait] - if passed it will set the throttle wait in order to fire scroll messaging. Defaults to 100 ms. | |
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe iFrame} | |
*/ | |
lib.Parent = function(id, url, config) { | |
/** | |
* The id of the container element | |
* | |
* @memberof module:pym.Parent | |
* @member {string} id | |
* @inner | |
*/ | |
this.id = id; | |
/** | |
* The url that will be set as the iframe's src | |
* | |
* @memberof module:pym.Parent | |
* @member {String} url | |
* @inner | |
*/ | |
this.url = url; | |
/** | |
* The container DOM object | |
* | |
* @memberof module:pym.Parent | |
* @member {HTMLElement} el | |
* @inner | |
*/ | |
this.el = document.getElementById(id); | |
/** | |
* The contained child iframe | |
* | |
* @memberof module:pym.Parent | |
* @member {HTMLElement} iframe | |
* @inner | |
* @default null | |
*/ | |
this.iframe = null; | |
/** | |
* The parent instance settings, updated by the values passed in the config object | |
* | |
* @memberof module:pym.Parent | |
* @member {Object} settings | |
* @inner | |
*/ | |
this.settings = { | |
xdomain: '*', | |
optionalparams: true, | |
parenturlparam: 'parentUrl', | |
parenturlvalue: window.location.href, | |
trackscroll: false, | |
scrollwait: 100, | |
}; | |
/** | |
* RegularExpression to validate the received messages | |
* | |
* @memberof module:pym.Parent | |
* @member {String} messageRegex | |
* @inner | |
*/ | |
this.messageRegex = _makeMessageRegex(this.id); | |
/** | |
* Stores the registered messageHandlers for each messageType | |
* | |
* @memberof module:pym.Parent | |
* @member {Object} messageHandlers | |
* @inner | |
*/ | |
this.messageHandlers = {}; | |
// ensure a config object | |
config = (config || {}); | |
/** | |
* Construct the iframe. | |
* | |
* @memberof module:pym.Parent | |
* @method _constructIframe | |
* @inner | |
*/ | |
this._constructIframe = function() { | |
// Calculate the width of this element. | |
var width = this.el.offsetWidth.toString(); | |
// Create an iframe element attached to the document. | |
this.iframe = document.createElement('iframe'); | |
// Save fragment id | |
var hash = ''; | |
var hashIndex = this.url.indexOf('#'); | |
if (hashIndex > -1) { | |
hash = this.url.substring(hashIndex, this.url.length); | |
this.url = this.url.substring(0, hashIndex); | |
} | |
// If the URL contains querystring bits, use them. | |
// Otherwise, just create a set of valid params. | |
if (this.url.indexOf('?') < 0) { | |
this.url += '?'; | |
} else { | |
this.url += '&'; | |
} | |
// Append the initial width as a querystring parameter | |
// and optional params if configured to do so | |
this.iframe.src = this.url + 'initialWidth=' + width + | |
'&childId=' + this.id; | |
if (this.settings.optionalparams) { | |
this.iframe.src += '&parentTitle=' + encodeURIComponent(document.title); | |
this.iframe.src += '&'+ this.settings.parenturlparam + '=' + encodeURIComponent(this.settings.parenturlvalue); | |
} | |
this.iframe.src +=hash; | |
// Set some attributes to this proto-iframe. | |
this.iframe.setAttribute('width', '100%'); | |
this.iframe.setAttribute('scrolling', 'no'); | |
this.iframe.setAttribute('marginheight', '0'); | |
this.iframe.setAttribute('frameborder', '0'); | |
if (this.settings.title) { | |
this.iframe.setAttribute('title', this.settings.title); | |
} | |
if (this.settings.allowfullscreen !== undefined && this.settings.allowfullscreen !== false) { | |
this.iframe.setAttribute('allowfullscreen',''); | |
} | |
if (this.settings.sandbox !== undefined && typeof this.settings.sandbox === 'string') { | |
this.iframe.setAttribute('sandbox', this.settings.sandbox); | |
} | |
if (this.settings.id) { | |
if (!document.getElementById(this.settings.id)) { | |
this.iframe.setAttribute('id', this.settings.id); | |
} | |
} | |
if (this.settings.name) { | |
this.iframe.setAttribute('name', this.settings.name); | |
} | |
// Replace the child content if needed | |
// (some CMSs might strip out empty elements) | |
while(this.el.firstChild) { this.el.removeChild(this.el.firstChild); } | |
// Append the iframe to our element. | |
this.el.appendChild(this.iframe); | |
// Add an event listener that will handle redrawing the child on resize. | |
window.addEventListener('resize', this._onResize); | |
// Add an event listener that will send the child the viewport. | |
if (this.settings.trackscroll) { | |
window.addEventListener('scroll', this._throttleOnScroll); | |
} | |
}; | |
/** | |
* Send width on resize. | |
* | |
* @memberof module:pym.Parent | |
* @method _onResize | |
* @inner | |
*/ | |
this._onResize = function() { | |
this.sendWidth(); | |
if (this.settings.trackscroll) { | |
this.sendViewportAndIFramePosition(); | |
} | |
}.bind(this); | |
/** | |
* Send viewport and iframe info on scroll. | |
* | |
* @memberof module:pym.Parent | |
* @method _onScroll | |
* @inner | |
*/ | |
this._onScroll = function() { | |
this.sendViewportAndIFramePosition(); | |
}.bind(this); | |
/** | |
* Fire all event handlers for a given message type. | |
* | |
* @memberof module:pym.Parent | |
* @method _fire | |
* @inner | |
* | |
* @param {String} messageType The type of message. | |
* @param {String} message The message data. | |
*/ | |
this._fire = function(messageType, message) { | |
if (messageType in this.messageHandlers) { | |
for (var i = 0; i < this.messageHandlers[messageType].length; i++) { | |
this.messageHandlers[messageType][i].call(this, message); | |
} | |
} | |
}; | |
/** | |
* Remove this parent from the page and unbind it's event handlers. | |
* | |
* @memberof module:pym.Parent | |
* @method remove | |
* @instance | |
*/ | |
this.remove = function() { | |
window.removeEventListener('message', this._processMessage); | |
window.removeEventListener('resize', this._onResize); | |
this.el.removeChild(this.iframe); | |
// _cleanAutoInitInstances in case this parent was autoInitialized | |
_cleanAutoInitInstances(); | |
}; | |
/** | |
* Process a new message from the child. | |
* | |
* @memberof module:pym.Parent | |
* @method _processMessage | |
* @inner | |
* | |
* @param {Event} e A message event. | |
*/ | |
this._processMessage = function(e) { | |
// First, punt if this isn't from an acceptable xdomain. | |
if (!_isSafeMessage(e, this.settings)) { | |
return; | |
} | |
// Discard object messages, we only care about strings | |
if (typeof e.data !== 'string') { | |
return; | |
} | |
// Grab the message from the child and parse it. | |
var match = e.data.match(this.messageRegex); | |
// If there's no match or too many matches in the message, punt. | |
if (!match || match.length !== 3) { | |
return false; | |
} | |
var messageType = match[1]; | |
var message = match[2]; | |
this._fire(messageType, message); | |
}.bind(this); | |
/** | |
* Resize iframe in response to new height message from child. | |
* | |
* @memberof module:pym.Parent | |
* @method _onHeightMessage | |
* @inner | |
* | |
* @param {String} message The new height. | |
*/ | |
this._onHeightMessage = function(message) { | |
/* | |
* Handle parent height message from child. | |
*/ | |
var height = parseInt(message); | |
this.iframe.setAttribute('height', height + 'px'); | |
}; | |
/** | |
* Navigate parent to a new url. | |
* | |
* @memberof module:pym.Parent | |
* @method _onNavigateToMessage | |
* @inner | |
* | |
* @param {String} message The url to navigate to. | |
*/ | |
this._onNavigateToMessage = function(message) { | |
/* | |
* Handle parent scroll message from child. | |
*/ | |
if (!_isSafeUrl(message)) {return;} | |
document.location.href = message; | |
}; | |
/** | |
* Scroll parent to a given child position. | |
* | |
* @memberof module:pym.Parent | |
* @method _onScrollToChildPosMessage | |
* @inner | |
* | |
* @param {String} message The offset inside the child page. | |
*/ | |
this._onScrollToChildPosMessage = function(message) { | |
// Get the child container position using getBoundingClientRect + pageYOffset | |
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | |
var iframePos = document.getElementById(this.id).getBoundingClientRect().top + window.pageYOffset; | |
var totalOffset = iframePos + parseInt(message); | |
window.scrollTo(0, totalOffset); | |
}; | |
/** | |
* Bind a callback to a given messageType from the child. | |
* | |
* Reserved message names are: "height", "scrollTo" and "navigateTo". | |
* | |
* @memberof module:pym.Parent | |
* @method onMessage | |
* @instance | |
* | |
* @param {String} messageType The type of message being listened for. | |
* @param {module:pym.Parent~onMessageCallback} callback The callback to invoke when a message of the given type is received. | |
*/ | |
this.onMessage = function(messageType, callback) { | |
if (!(messageType in this.messageHandlers)) { | |
this.messageHandlers[messageType] = []; | |
} | |
this.messageHandlers[messageType].push(callback); | |
}; | |
/** | |
* @callback module:pym.Parent~onMessageCallback | |
* @param {String} message The message data. | |
*/ | |
/** | |
* Send a message to the the child. | |
* | |
* @memberof module:pym.Parent | |
* @method sendMessage | |
* @instance | |
* | |
* @param {String} messageType The type of message to send. | |
* @param {String} message The message data to send. | |
*/ | |
this.sendMessage = function(messageType, message) { | |
// When used alongside with pjax some references are lost | |
if (this.el.getElementsByTagName('iframe').length) { | |
if (this.el.getElementsByTagName('iframe')[0].contentWindow) { | |
this.el.getElementsByTagName('iframe')[0].contentWindow | |
.postMessage(_makeMessage(this.id, messageType, message), '*'); | |
} | |
else { | |
// Contentless child detected remove listeners and iframe | |
this.remove(); | |
} | |
} | |
}; | |
/** | |
* Transmit the current iframe width to the child. | |
* | |
* You shouldn't need to call this directly. | |
* | |
* @memberof module:pym.Parent | |
* @method sendWidth | |
* @instance | |
*/ | |
this.sendWidth = function() { | |
var width = this.el.offsetWidth.toString(); | |
this.sendMessage('width', width); | |
}; | |
/** | |
* Transmit the current viewport and iframe position to the child. | |
* Sends viewport width, viewport height | |
* and iframe bounding rect top-left-bottom-right | |
* all separated by spaces | |
* | |
* You shouldn't need to call this directly. | |
* | |
* @memberof module:pym.Parent | |
* @method sendViewportAndIFramePosition | |
* @instance | |
*/ | |
this.sendViewportAndIFramePosition = function() { | |
var iframeRect = this.iframe.getBoundingClientRect(); | |
var vWidth = window.innerWidth || document.documentElement.clientWidth; | |
var vHeight = window.innerHeight || document.documentElement.clientHeight; | |
var payload = vWidth + ' ' + vHeight; | |
payload += ' ' + iframeRect.top + ' ' + iframeRect.left; | |
payload += ' ' + iframeRect.bottom + ' ' + iframeRect.right; | |
this.sendMessage('viewport-iframe-position', payload); | |
}; | |
// Add any overrides to settings coming from config. | |
for (var key in config) { | |
this.settings[key] = config[key]; | |
} | |
/** | |
* Throttled scroll function. | |
* | |
* @memberof module:pym.Parent | |
* @method _throttleOnScroll | |
* @inner | |
*/ | |
this._throttleOnScroll = _throttle(this._onScroll.bind(this), this.settings.scrollwait); | |
// Bind required message handlers | |
this.onMessage('height', this._onHeightMessage); | |
this.onMessage('navigateTo', this._onNavigateToMessage); | |
this.onMessage('scrollToChildPos', this._onScrollToChildPosMessage); | |
this.onMessage('parentPositionInfo', this.sendViewportAndIFramePosition); | |
// Add a listener for processing messages from the child. | |
window.addEventListener('message', this._processMessage, false); | |
// Construct the iframe in the container element. | |
this._constructIframe(); | |
return this; | |
}; | |
/** | |
* The Child half of a responsive iframe. | |
* | |
* @memberof module:pym | |
* @class Child | |
* @param {Object} [config] Configuration for the child instance. sets {@link module:pym.Child~settings} | |
* @param {function} [config.renderCallback=null] Callback invoked after receiving a resize event from the parent, sets {@link module:pym.Child#settings.renderCallback} | |
* @param {string} [config.xdomain='*'] - xdomain to validate messages received | |
* @param {number} [config.polling=0] - polling frequency in milliseconds to send height to parent | |
* @param {number} [config.id] - parent container id used when navigating the child iframe to a new page but we want to keep it responsive. | |
* @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name expected on the iframe src | |
*/ | |
lib.Child = function(config) { | |
/** | |
* The initial width of the parent page | |
* | |
* @memberof module:pym.Child | |
* @member {string} parentWidth | |
* @inner | |
*/ | |
this.parentWidth = null; | |
/** | |
* The id of the parent container | |
* | |
* @memberof module:pym.Child | |
* @member {String} id | |
* @inner | |
*/ | |
this.id = null; | |
/** | |
* The title of the parent page from document.title. | |
* | |
* @memberof module:pym.Child | |
* @member {String} parentTitle | |
* @inner | |
*/ | |
this.parentTitle = null; | |
/** | |
* The URL of the parent page from window.location.href. | |
* | |
* @memberof module:pym.Child | |
* @member {String} parentUrl | |
* @inner | |
*/ | |
this.parentUrl = null; | |
/** | |
* The settings for the child instance. Can be overriden by passing a config object to the child constructor | |
* i.e.: var pymChild = new pym.Child({renderCallback: render, xdomain: "\\*\.npr\.org"}) | |
* | |
* @memberof module:pym.Child.settings | |
* @member {Object} settings - default settings for the child instance | |
* @inner | |
*/ | |
this.settings = { | |
renderCallback: null, | |
xdomain: '*', | |
polling: 0, | |
parenturlparam: 'parentUrl' | |
}; | |
/** | |
* The timerId in order to be able to stop when polling is enabled | |
* | |
* @memberof module:pym.Child | |
* @member {String} timerId | |
* @inner | |
*/ | |
this.timerId = null; | |
/** | |
* RegularExpression to validate the received messages | |
* | |
* @memberof module:pym.Child | |
* @member {String} messageRegex | |
* @inner | |
*/ | |
this.messageRegex = null; | |
/** | |
* Stores the registered messageHandlers for each messageType | |
* | |
* @memberof module:pym.Child | |
* @member {Object} messageHandlers | |
* @inner | |
*/ | |
this.messageHandlers = {}; | |
// Ensure a config object | |
config = (config || {}); | |
/** | |
* Bind a callback to a given messageType from the child. | |
* | |
* Reserved message names are: "width". | |
* | |
* @memberof module:pym.Child | |
* @method onMessage | |
* @instance | |
* | |
* @param {String} messageType The type of message being listened for. | |
* @param {module:pym.Child~onMessageCallback} callback The callback to invoke when a message of the given type is received. | |
*/ | |
this.onMessage = function(messageType, callback) { | |
if (!(messageType in this.messageHandlers)) { | |
this.messageHandlers[messageType] = []; | |
} | |
this.messageHandlers[messageType].push(callback); | |
}; | |
/** | |
* @callback module:pym.Child~onMessageCallback | |
* @param {String} message The message data. | |
*/ | |
/** | |
* Fire all event handlers for a given message type. | |
* | |
* @memberof module:pym.Child | |
* @method _fire | |
* @inner | |
* | |
* @param {String} messageType The type of message. | |
* @param {String} message The message data. | |
*/ | |
this._fire = function(messageType, message) { | |
/* | |
* Fire all event handlers for a given message type. | |
*/ | |
if (messageType in this.messageHandlers) { | |
for (var i = 0; i < this.messageHandlers[messageType].length; i++) { | |
this.messageHandlers[messageType][i].call(this, message); | |
} | |
} | |
}; | |
/** | |
* Process a new message from the parent. | |
* | |
* @memberof module:pym.Child | |
* @method _processMessage | |
* @inner | |
* | |
* @param {Event} e A message event. | |
*/ | |
this._processMessage = function(e) { | |
/* | |
* Process a new message from parent frame. | |
*/ | |
// First, punt if this isn't from an acceptable xdomain. | |
if (!_isSafeMessage(e, this.settings)) { | |
return; | |
} | |
// Discard object messages, we only care about strings | |
if (typeof e.data !== 'string') { | |
return; | |
} | |
// Get the message from the parent. | |
var match = e.data.match(this.messageRegex); | |
// If there's no match or it's a bad format, punt. | |
if (!match || match.length !== 3) { return; } | |
var messageType = match[1]; | |
var message = match[2]; | |
this._fire(messageType, message); | |
}.bind(this); | |
/** | |
* Resize iframe in response to new width message from parent. | |
* | |
* @memberof module:pym.Child | |
* @method _onWidthMessage | |
* @inner | |
* | |
* @param {String} message The new width. | |
*/ | |
this._onWidthMessage = function(message) { | |
/* | |
* Handle width message from the child. | |
*/ | |
var width = parseInt(message); | |
// Change the width if it's different. | |
if (width !== this.parentWidth) { | |
this.parentWidth = width; | |
// Call the callback function if it exists. | |
if (this.settings.renderCallback) { | |
this.settings.renderCallback(width); | |
} | |
// Send the height back to the parent. | |
this.sendHeight(); | |
} | |
}; | |
/** | |
* Send a message to the the Parent. | |
* | |
* @memberof module:pym.Child | |
* @method sendMessage | |
* @instance | |
* | |
* @param {String} messageType The type of message to send. | |
* @param {String} message The message data to send. | |
*/ | |
this.sendMessage = function(messageType, message) { | |
/* | |
* Send a message to the parent. | |
*/ | |
window.parent.postMessage(_makeMessage(this.id, messageType, message), '*'); | |
}; | |
/** | |
* Transmit the current iframe height to the parent. | |
* | |
* Call this directly in cases where you manually alter the height of the iframe contents. | |
* | |
* @memberof module:pym.Child | |
* @method sendHeight | |
* @instance | |
*/ | |
this.sendHeight = function() { | |
// Get the child's height. | |
var height = document.getElementsByTagName('body')[0].offsetHeight.toString(); | |
// Send the height to the parent. | |
this.sendMessage('height', height); | |
return height; | |
}.bind(this); | |
/** | |
* Ask parent to send the current viewport and iframe position information | |
* | |
* @memberof module:pym.Child | |
* @method sendHeight | |
* @instance | |
*/ | |
this.getParentPositionInfo = function() { | |
// Send the height to the parent. | |
this.sendMessage('parentPositionInfo'); | |
}; | |
/** | |
* Scroll parent to a given element id. | |
* | |
* @memberof module:pym.Child | |
* @method scrollParentTo | |
* @instance | |
* | |
* @param {String} hash The id of the element to scroll to. | |
*/ | |
this.scrollParentTo = function(hash) { | |
this.sendMessage('navigateTo', '#' + hash); | |
}; | |
/** | |
* Navigate parent to a given url. | |
* | |
* @memberof module:pym.Child | |
* @method navigateParentTo | |
* @instance | |
* | |
* @param {String} url The url to navigate to. | |
*/ | |
this.navigateParentTo = function(url) { | |
this.sendMessage('navigateTo', url); | |
}; | |
/** | |
* Scroll parent to a given child element id. | |
* | |
* @memberof module:pym.Child | |
* @method scrollParentToChildEl | |
* @instance | |
* | |
* @param {String} id The id of the child element to scroll to. | |
*/ | |
this.scrollParentToChildEl = function(id) { | |
// Get the child element position using getBoundingClientRect + pageYOffset | |
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | |
var topPos = document.getElementById(id).getBoundingClientRect().top + window.pageYOffset; | |
this.scrollParentToChildPos(topPos); | |
}; | |
/** | |
* Scroll parent to a particular child offset. | |
* | |
* @memberof module:pym.Child | |
* @method scrollParentToChildPos | |
* @instance | |
* | |
* @param {Number} pos The offset of the child element to scroll to. | |
*/ | |
this.scrollParentToChildPos = function(pos) { | |
this.sendMessage('scrollToChildPos', pos.toString()); | |
}; | |
/** | |
* Mark Whether the child is embedded or not | |
* executes a callback in case it was passed to the config | |
* | |
* @memberof module:pym.Child | |
* @method _markWhetherEmbedded | |
* @inner | |
* | |
* @param {module:pym.Child~onMarkedEmbeddedStatus} The callback to execute after determining whether embedded or not. | |
*/ | |
var _markWhetherEmbedded = function(onMarkedEmbeddedStatus) { | |
var htmlElement = document.getElementsByTagName('html')[0], | |
newClassForHtml, | |
originalHtmlClasses = htmlElement.className; | |
try { | |
if(window.self !== window.top) { | |
newClassForHtml = "embedded"; | |
}else{ | |
newClassForHtml = "not-embedded"; | |
} | |
}catch(e) { | |
newClassForHtml = "embedded"; | |
} | |
if(originalHtmlClasses.indexOf(newClassForHtml) < 0) { | |
htmlElement.className = originalHtmlClasses ? originalHtmlClasses + ' ' + newClassForHtml : newClassForHtml; | |
if(onMarkedEmbeddedStatus){ | |
onMarkedEmbeddedStatus(newClassForHtml); | |
} | |
_raiseCustomEvent("marked-embedded"); | |
} | |
}; | |
/** | |
* @callback module:pym.Child~onMarkedEmbeddedStatus | |
* @param {String} classname "embedded" or "not-embedded". | |
*/ | |
/** | |
* Unbind child event handlers and timers. | |
* | |
* @memberof module:pym.Child | |
* @method remove | |
* @instance | |
*/ | |
this.remove = function() { | |
window.removeEventListener('message', this._processMessage); | |
if (this.timerId) { | |
clearInterval(this.timerId); | |
} | |
}; | |
// Initialize settings with overrides. | |
for (var key in config) { | |
this.settings[key] = config[key]; | |
} | |
// Identify what ID the parent knows this child as. | |
this.id = _getParameterByName('childId') || config.id; | |
this.messageRegex = new RegExp('^pym' + MESSAGE_DELIMITER + this.id + MESSAGE_DELIMITER + '(\\S+)' + MESSAGE_DELIMITER + '(.*)$'); | |
// Get the initial width from a URL parameter. | |
var width = parseInt(_getParameterByName('initialWidth')); | |
// Get the url of the parent frame | |
this.parentUrl = _getParameterByName(this.settings.parenturlparam); | |
// Get the title of the parent frame | |
this.parentTitle = _getParameterByName('parentTitle'); | |
// Bind the required message handlers | |
this.onMessage('width', this._onWidthMessage); | |
// Set up a listener to handle any incoming messages. | |
window.addEventListener('message', this._processMessage, false); | |
// If there's a callback function, call it. | |
if (this.settings.renderCallback) { | |
this.settings.renderCallback(width); | |
} | |
// Send the initial height to the parent. | |
this.sendHeight(); | |
// If we're configured to poll, create a setInterval to handle that. | |
if (this.settings.polling) { | |
this.timerId = window.setInterval(this.sendHeight, this.settings.polling); | |
} | |
_markWhetherEmbedded(config.onMarkedEmbeddedStatus); | |
return this; | |
}; | |
// Initialize elements with pym data attributes | |
// if we are not in server configuration | |
if(typeof document !== "undefined") { | |
lib.autoInit(true); | |
} | |
return lib; | |
}); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment