|
/* IFRAME cross communication for DataLayer integration. */ |
|
|
|
|
|
(function(window, document, jQuery){ |
|
|
|
var dataLayerName = 'dataLayer'; |
|
var _slice = Array.prototype.slice; |
|
var _filter = Array.prototype.filter; |
|
var _hasProp = Object.prototype.hasOwnProperty; |
|
var _listeners = []; // functions receiving message events |
|
var DEBUG = 0; |
|
|
|
var policy = { |
|
// should events received on the datalayer of child frames be copied |
|
// up to the parent? |
|
events_dispatch_up: true, |
|
|
|
// should events received on the datalayer of the parent be copied down |
|
// to the child frames? |
|
events_dispatch_down: true |
|
|
|
// NOTE: if both the above flags are `true`, child and parent dataLayer |
|
// instances will be kept "in sync" (for all new events loaded after |
|
// the framework loads.) |
|
}; |
|
|
|
|
|
function assert(value, message) { |
|
if (!value) { |
|
throw new Error(message); |
|
} |
|
} |
|
|
|
assert(_slice && _filter && window.postMessage && window.JSON, |
|
"Cross frame communication requires a newer browser."); |
|
|
|
|
|
function _getDataLayer(name){ |
|
return (window[name] = window[name] || []); |
|
} |
|
|
|
function _insertDatLayerCallback(name, callback){ |
|
var dl = _getDataLayer(name); |
|
var _attached = (callback.attached = callback.attached || []); |
|
var _push = dl.push; |
|
if(_attached.indexOf(name) < 0){ |
|
dl.push = function(){ |
|
_slice.call(arguments, 0).map(callback); |
|
return _push.apply(dl, arguments); |
|
}; |
|
_attached.push(name); |
|
} |
|
} |
|
|
|
|
|
function _event(name, element, props) { |
|
var dataLayer = _getDataLayer(dataLayerName); |
|
var eventObject = { |
|
'event': name, |
|
'gtm.element': element |
|
}; |
|
var k = null; |
|
if (props) |
|
for (k in props) { |
|
if (_hasProp.call(props, k)) { |
|
eventObject[k] = props[k]; |
|
} |
|
} |
|
return dataLayer.push(eventObject); |
|
} |
|
|
|
// Simplified means of attaching event handlers... |
|
function attach(nodes, event, handler, capture) { |
|
var _nodes = (nodes && nodes.length) ? nodes : [nodes]; |
|
var _listen = (nodes[0].addEventListener || nodes[0].attachEvent); |
|
_nodes.map(function(n) { |
|
event.replace(/(on)?([a-z]+)/ig, function($0, _on, _event) { |
|
_listen.call(n, _event, handler, capture); |
|
}); |
|
}); |
|
} |
|
|
|
// Install single global listener for the message bus. |
|
attach([window], 'message', function(e) { |
|
_listeners.map(function(callback) { |
|
callback.call(window, e); |
|
}); |
|
}); |
|
|
|
function cloneEvent(e){ |
|
return e; // nevermind, JSON handling within the browser will assure its safe. |
|
} |
|
|
|
// Within the parent (window), handler for interaction with child IFRAMEs |
|
function attachChildClients(callback) { |
|
var frames = window.frames; |
|
var clients = []; |
|
_slice.call(frames, 0).map(function(item) { |
|
if ((item === window) || !(item.frameElement)) return false; |
|
|
|
var frm = item.frameElement; |
|
var targetWindow = frm.contentWindow; |
|
var client = { state: { active: false } }; |
|
var name = (frm.getAttribute('name') || frm.getAttribute('id')); |
|
|
|
|
|
function send(message, mark) { |
|
if(typeof message != 'string') |
|
message = JSON.stringify(cloneEvent(message)); |
|
return targetWindow.postMessage(message, targetWindow.location.href); |
|
} |
|
|
|
client.frame = frm; |
|
client.send = send; |
|
client.url = function(){ return targetWindow.location.href; }; |
|
client.name = name; |
|
|
|
_listeners.push(function(e) { |
|
if (e.source && (e.source === targetWindow)) { |
|
if(DEBUG > 2) console.debug('parent received', e); |
|
switch (e.data) { |
|
case 'syn': |
|
return send('synack'); |
|
case 'ack': |
|
client.state.active = true; |
|
return callback.call(client, 'init', client.state.active); |
|
|
|
default: |
|
return callback.call(client, e.data, client.state.active); |
|
} |
|
} |
|
}); |
|
|
|
return clients.push(client); |
|
}); |
|
return clients; |
|
} |
|
|
|
|
|
// Within a child (iframe), handler for interaction with parent. |
|
function attachParentClient(callback) { |
|
var parent = window.parent; |
|
var parent_url = document.referrer; |
|
|
|
if (parent === window) return null; // this is the root. |
|
|
|
function send(message) { |
|
if(typeof message != 'string') |
|
message = JSON.stringify(cloneEvent(message)); |
|
return parent.postMessage(message, parent_url); |
|
} |
|
|
|
var client = { |
|
state: { acknowledged: 0 }, |
|
frame: parent, |
|
send: send, |
|
url: parent_url, |
|
name: null |
|
}; |
|
|
|
_listeners.push(function(e) { |
|
if(DEBUG > 2) console.debug('child received', e); |
|
if (e.source && (e.source === parent)) { |
|
switch (e.data) { |
|
case 'synack': |
|
client.state.acknowledged++; |
|
send('ack') |
|
return callback.call(client, 'init', client.state.acknowledged); |
|
default: |
|
return callback.call(client, e.data, client.state.acknowledged); |
|
} |
|
} |
|
}); |
|
|
|
// notify parent of my existence... |
|
client.interval = setInterval(function() { |
|
if (!(client.state.acknowledged)) { |
|
send('syn'); |
|
clearInterval(client.interval); |
|
} |
|
}, 500); |
|
|
|
return client; |
|
} |
|
|
|
function isJSON(text){ |
|
// VERY loose qualifier of JSON object strings. |
|
return (text.charAt(0) == '{') && (text.charAt(text.length - 1) == '}'); |
|
} |
|
|
|
function pushJSONEvent(text, mark){ |
|
if(isJSON(text)){ |
|
var data = JSON.parse(text); |
|
// Prevent echoing events we've already seen. |
|
if(mark && data[mark]) return false; |
|
return _getDataLayer(dataLayerName).push(data); |
|
} |
|
} |
|
|
|
function getDataContext(elem){ |
|
var nodes = jQuery && jQuery(elem); |
|
return nodes && nodes.dataContext && nodes.dataContext(); |
|
} |
|
|
|
function onready(callback){ |
|
if(onready.ready) callback.call(document); |
|
else (onready.queue = onready.queue || []).push(callback); |
|
} |
|
|
|
(function(m){ |
|
|
|
function _trigger(){ |
|
m.ready = true; |
|
m.queue.map(m); |
|
m.queue.length = 0; |
|
} |
|
|
|
if(document.readyState != 'loading'){ |
|
_trigger(); |
|
} else { |
|
attach([window], 'load', _trigger); |
|
// attach([document], 'DOMContentLoaded', _trigger); |
|
} |
|
|
|
}(onready)); |
|
|
|
|
|
var _export = (window.iframeRelay = window.iframeRelay || {}); |
|
_export.listeners = _listeners; |
|
|
|
onready(function(){ |
|
var now = (new Date()).getTime(); |
|
var mark = ('$cfr_' + Math.floor(Math.random() * 1E10) + now); |
|
|
|
// Parent should... |
|
var children = attachChildClients(function(message) { |
|
var element = this.frame; |
|
if(message.indexOf(mark) > 0) return false; |
|
switch (message) { |
|
case 'init': |
|
var passback = { |
|
'event': 'iframeContextData', |
|
'dataContext': getDataContext(element) |
|
}; |
|
passback[mark] = true; |
|
this.send(passback); |
|
return _event('dom.iframeLoaded', element, { |
|
'iframeName': this.name, |
|
'iframeURL': this.url() |
|
}); |
|
default: |
|
// Assume that unhandled messages should reach the dataLayer only. |
|
if(policy.events_dispatch_up) pushJSONEvent(message, mark); |
|
} |
|
|
|
}); |
|
|
|
// Child should... |
|
var parent = attachParentClient(function(message) { |
|
if(message.indexOf(mark) > 0) return false; |
|
switch(message){ |
|
case 'init': |
|
break; |
|
default: |
|
// Assume that unhandled messages should reach the dataLayer only. |
|
if(policy.events_dispatch_down) pushJSONEvent(message, mark); |
|
} |
|
}); |
|
|
|
|
|
if(policy.events_dispatch_down && children.length){ |
|
_insertDatLayerCallback(dataLayerName, function(e){ |
|
// Mark the event as "Seen", so we can ignore it if it rebounds. |
|
e[mark] = true; |
|
children.map(function(c){ |
|
c.send(e); |
|
}); |
|
}); |
|
} |
|
|
|
if(policy.events_dispatch_up && parent){ |
|
_insertDatLayerCallback(dataLayerName, function(e){ |
|
e[mark] = true; |
|
parent.send(e); |
|
}); |
|
} |
|
|
|
_export.parent = parent; |
|
_export.children = children; |
|
_export.mark = mark; |
|
|
|
}); |
|
|
|
|
|
}(window, document, window.jQuery)); |