mirror of
https://github.com/cds-astro/aladin-lite.git
synced 2026-01-12 13:15:22 -08:00
586 lines
18 KiB
JavaScript
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;
|
|
});
|