Files
aladin-lite/website/doc/tutorials/interactive-finding-chart/js/pym.js
tboch b038f85455
2016-05-03 13:43:15 +00:00

586 lines
18 KiB
JavaScript

/*
* 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.
*/
/* global module */
(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 = {};
/**
* Generic function for parsing URL params.
* Via http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
*
* @method _getParameterByName
* @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
* @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; }
}
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
* @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
* @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) + '$');
};
/**
* Initialize Pym for elements on page that have data-pym attributes.
*
* @method _autoInit
*/
var _autoInit = function() {
var elements = document.querySelectorAll(
'[data-pym-src]:not([data-pym-auto-initialized])'
);
var length = elements.length;
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;
}
var src = element.getAttribute('data-pym-src');
var xdomain = element.getAttribute('data-pym-xdomain');
var config = {};
if (xdomain) {
config.xdomain = xdomain;
}
new lib.Parent(element.id, src, config);
}
};
/**
* The Parent half of a response iframe.
*
* @class Parent
* @param {String} id The id of the div into which the iframe will be rendered.
* @param {String} url The url of the iframe source.
* @param {Object} config Configuration to override the default settings.
*/
lib.Parent = function(id, url, config) {
this.id = id;
this.url = url;
this.el = document.getElementById(id);
this.iframe = null;
this.settings = {
xdomain: '*'
};
this.messageRegex = _makeMessageRegex(this.id);
this.messageHandlers = {};
// ensure a config object
config = (config || {});
/**
* Construct the iframe.
*
* @memberof Parent.prototype
* @method _constructIframe
*/
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 the fragment id
this.iframe.src = this.url +
'initialWidth=' + width +
'&childId=' + this.id +
'&parentUrl=' + encodeURIComponent(window.location.href) +
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');
// 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);
};
/**
* Send width on resize.
*
* @memberof Parent.prototype
* @method _onResize
*/
this._onResize = function() {
this.sendWidth();
}.bind(this);
/**
* Fire all event handlers for a given message type.
*
* @memberof Parent.prototype
* @method _fire
* @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 Parent.prototype
* @method remove
*/
this.remove = function() {
window.removeEventListener('message', this._processMessage);
window.removeEventListener('resize', this._onResize);
this.el.removeChild(this.iframe);
};
/**
* @callback Parent~onMessageCallback
* @param {String} message The message data.
*/
/**
* Process a new message from the child.
*
* @memberof Parent.prototype
* @method _processMessage
* @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 Parent.prototype
* @method _onHeightMessage
* @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 Parent.prototype
* @method _onNavigateToMessage
* @param {String} message The url to navigate to.
*/
this._onNavigateToMessage = function(message) {
/*
* Handle parent scroll message from child.
*/
document.location.href = message;
};
/**
* Bind a callback to a given messageType from the child.
*
* Reserved message names are: "height", "scrollTo" and "navigateTo".
*
* @memberof Parent.prototype
* @method onMessage
* @param {String} messageType The type of message being listened for.
* @param {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);
};
/**
* Send a message to the the child.
*
* @memberof Parent.prototype
* @method sendMessage
* @param {String} messageType The type of message to send.
* @param {String} message The message data to send.
*/
this.sendMessage = function(messageType, message) {
this.el.getElementsByTagName('iframe')[0].contentWindow.postMessage(_makeMessage(this.id, messageType, message), '*');
};
/**
* Transmit the current iframe width to the child.
*
* You shouldn't need to call this directly.
*
* @memberof Parent.prototype
* @method sendWidth
*/
this.sendWidth = function() {
var width = this.el.offsetWidth.toString();
this.sendMessage('width', width);
};
// Add any overrides to settings coming from config.
for (var key in config) {
this.settings[key] = config[key];
}
// Bind required message handlers
this.onMessage('height', this._onHeightMessage);
this.onMessage('navigateTo', this._onNavigateToMessage);
// 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.
*
* @class Child
* @param {Object} config Configuration to override the default settings.
*/
lib.Child = function(config) {
this.parentWidth = null;
this.id = null;
this.parentUrl = null;
this.settings = {
renderCallback: null,
xdomain: '*',
polling: 0
};
this.messageRegex = null;
this.messageHandlers = {};
// Ensure a config object
config = (config || {});
/**
* Bind a callback to a given messageType from the child.
*
* Reserved message names are: "width".
*
* @memberof Child.prototype
* @method onMessage
* @param {String} messageType The type of message being listened for.
* @param {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 Child~onMessageCallback
* @param {String} message The message data.
*/
/**
* Fire all event handlers for a given message type.
*
* @memberof Parent.prototype
* @method _fire
* @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 Child.prototype
* @method _processMessage
* @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 Child.prototype
* @method _onWidthMessage
* @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 Child.prototype
* @method sendMessage
* @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 Child.prototype
* @method sendHeight
*/
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);
}.bind(this);
/**
* Scroll parent to a given element id.
*
* @memberof Child.prototype
* @method scrollParentTo
* @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 Parent.prototype
* @method navigateParentTo
* @param {String} url The url to navigate to.
*/
this.navigateParentTo = function(url) {
this.sendMessage('navigateTo', url);
};
// 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('parentUrl');
// Bind the required message handlers
this.onMessage('width', this._onWidthMessage);
// Initialize settings with overrides.
for (var key in config) {
this.settings[key] = config[key];
}
// 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) {
window.setInterval(this.sendHeight, this.settings.polling);
}
return this;
};
// Initialize elements with pym data attributes
_autoInit();
return lib;
});