Ia Mraid Bridge
Ia Mraid Bridge
(function() {
// Establish the root mraidbridge object.
var mraidbridge = window.mraidbridge = {};
// Listeners for bridge events.
var listeners = {};
// Queue to track pending calls to the native SDK.
var nativeCallQueue = [];
// Whether a native call is currently in progress.
var nativeCallInFlight = false;
/////////////////////////////////////////////////////////////////////////////////
/////////////////
mraidbridge.windowLoaded = false;
mraidbridge.callbackIframe = undefined;
mraidbridge.loggingEnabled = false;
mraidbridge.log = function(msg) {
mraidbridge.loggingEnabled && console.log(msg);
}
window.addEventListener('load', function(e) {
var ifr = document.createElement('iframe');
var container = document.body || document.documentElement;
container.appendChild(ifr);
ifr.setAttribute('sandbox', '');
ifr.setAttribute('style', 'position: fixed; bottom: -20px; border: none;
display: none; height: 20px; z-index: -99999');
mraidbridge.callbackIframe = ifr;
mraidbridge.windowLoaded = true;
if (nativeCallQueue.length === 0) {
return;
}
var nextCall = nativeCallQueue.pop();
nativeCallInFlight = true;
mraidbridge.callbackIframe && mraidbridge.callbackIframe.setAttribute('src',
nextCall);
});
mraidbridge.fireReadyEvent = function() {
mraidbridge.fireEvent('ready');
};
mraidbridge.fireChangeEvent = function(properties) {
mraidbridge.fireEvent('change', properties);
};
mraidbridge.fireErrorEvent = function(message, action) {
mraidbridge.fireEvent('error', message, action);
};
mraidbridge.fireEvent = function(type) {
mraidbridge.log("mraidBridge: firing event of type: " + type);
var ls = listeners[type];
if (ls) {
var args = Array.prototype.slice.call(arguments);
args.shift();
mraidbridge.log("mraidBridge: firing event of type: " + type + " with args
- " + JSON.stringify(args));
var l = ls.length;
for (var i = 0; i < l; i++) {
ls[i].apply(null, args);
}
}
};
mraidbridge.nativeCallComplete = function(command) {
mraidbridge.log("mraidbridge: nativeCallComplete: " + command);
mraidbridge.log("native Call complete: " + command);
if (nativeCallQueue.length === 0) {
nativeCallInFlight = false;
return;
}
if (!mraidbridge.callbackIframe || nativeCallInFlight) {
nativeCallQueue.push(call);
} else {
nativeCallInFlight = true;
mraidbridge.callbackIframe &&
mraidbridge.callbackIframe.setAttribute('src', call);
}
};
/////////////////////////////////////////////////////////////////////////////////
/////////////////
mraidbridge.addEventListener = function(event, listener) {
mraidbridge.log("mraidbridge: addEventListener: " + listener);
var eventListeners;
listeners[event] = listeners[event] || [];
eventListeners = listeners[event];
for (var l in eventListeners) {
// LoadListener already registered, so no need to add it.
if (listener === l) return;
}
eventListeners.push(listener);
};
mraidbridge.removeEventListener = function(event, listener) {
mraidbridge.log("mraidbridge: removeEventListener: " + listener);
if (listeners.hasOwnProperty(event)) {
var eventListeners = listeners[event];
if (eventListeners) {
var idx = eventListeners.indexOf(listener);
if (idx !== -1) {
eventListeners.splice(idx, 1);
}
}
}
};
}());
(function() {
mraidbridge.log("mraidbridge: initializing mraid");
var mraid = window.mraid = {};
var bridge = window.mraidbridge;
//
Constants. ////////////////////////////////////////////////////////////////////////
////////////
var VERSION = mraid.VERSION = '2.0';
var STATES = mraid.STATES = {
LOADING: 'loading', // Initial state.
DEFAULT: 'default',
EXPANDED: 'expanded',
HIDDEN: 'hidden',
RESIZED: 'resized'
};
var EVENTS = mraid.EVENTS = {
ERROR: 'error',
INFO: 'info',
READY: 'ready',
STATECHANGE: 'stateChange',
VIEWABLECHANGE: 'viewableChange',
SIZECHANGE: 'sizeChange'
};
var PLACEMENT_TYPES = mraid.PLACEMENT_TYPES = {
UNKNOWN: 'unknown',
INLINE: 'inline',
INTERSTITIAL: 'interstitial'
};
// External MRAID state: may be directly or indirectly modified by
the ad JS. ////////////////////
// Properties which define the behavior of an expandable ad.
var expandProperties = {
width: -1,
height: -1,
useCustomClose: false,
isModal: true,
lockOrientation: false
};
var hasSetCustomSize = false;
var hasSetCreativeSize = false;
var hasSetCreativeOffset = false;
var hasSetCustomClose = false;
var listeners = {};
// Internal MRAID state. Modified by the native
SDK. /////////////////////////////////////////////
var state = STATES.LOADING;
var isViewable = false;
var screenSize = { width: -1, height: -1 };
var maxSize = { width: -1, height: -1 };
var adSize = { width: -1, height: -1 };
var currentPosition = {
x:0,
y:0,
width:0,
height:0
};
var mraidDefaultPosition = {
x:0,
y:0,
width:0,
height:0
};
// Properties which define the behavior of an resized ad.
var resizeProperties = {
width:-1,
height:-1,
customClosePosition:'top-right',
offsetX:0,
offsetY:0,
allowOffscreen:true
};
var orientationProperties = {
allowOrientationChange:true,
forceOrientation:'none'
};
var placementType = PLACEMENT_TYPES.UNKNOWN;
var supports = {
sms: false,
tel: false,
calendar: false,
storePicture: false,
inlineVideo: false
};
/////////////////////////////////////////////////////////////////////
/////////////////////////////
var EventListeners = function(event) {
this.event = event;
this.count = 0;
var listeners = {};
this.add = function(func) {
var id = String(func);
if (!listeners[id]) {
listeners[id] = func;
this.count++;
}
};
this.remove = function(func) {
var id = String(func);
if (listeners[id]) {
listeners[id] = null;
delete listeners[id];
this.count--;
return true;
} else {
return false;
}
};
this.removeAll = function() {
for (var id in listeners) {
if (listeners.hasOwnProperty(id)) this.remove(listeners[id]);
}
};
this.broadcast = function(args) {
var localListners = {}
for (var id in listeners) localListners[id] = listeners[id]
for (var id in localListners) {
if (localListners.hasOwnProperty(id)) localListners[id].apply({}, args);
}
};
this.toString = function() {
var out = [event, ':'];
for (var id in listeners) {
if (listeners.hasOwnProperty(id)) out.push('|', id, '|');
}
return out.join('');
};
};
var broadcastEvent = function() {
var args = new Array(arguments.length);
var l = arguments.length;
for (var i = 0; i < l; i++) args[i] = arguments[i];
var event = args.shift();
mraidbridge.log("listeners:" + JSON.stringify(listeners) + "event:" +
JSON.stringify(event))
if (listeners[event]) {
mraidbridge.log("broadcastEvent" + event + " with args -" +
JSON.stringify(args))
listeners[event].broadcast(args);
} else {
mraidbridge.log("broadcastEvent not sent")
}
};
var contains = function(value, array) {
for (var i in array) {
if (array[i] === value) return true;
}
return false;
};
var clone = function(obj) {
if (obj === null) return null;
var f = function() {};
f.prototype = obj;
return new f();
};
var stringify = function(obj) {
if (typeof obj === 'object') {
var out = [];
if (obj.push) {
// Array.
for (var p in obj) out.push(obj[p]);
return '[' + out.join(',') + ']';
} else {
// Other object.
for (var p in obj) out.push("'" + p + "': " + obj[p]);
return '{' + out.join(',') + '}';
}
} else return String(obj);
};
// Functions that will be invoked by the native SDK whenever a 'change' event
occurs.
var changeHandlers = {
state: function(val) {
if (state === STATES.LOADING) {
broadcastEvent(EVENTS.INFO, 'Native SDK initialized.');
}
state = val;
broadcastEvent(EVENTS.INFO, 'Set state to ' + stringify(val));
broadcastEvent(EVENTS.STATECHANGE, state);
},
viewable: function(val) {
isViewable = val;
broadcastEvent(EVENTS.INFO, 'Set isViewable to ' + stringify(val));
broadcastEvent(EVENTS.VIEWABLECHANGE, isViewable);
},
placementType: function(val) {
broadcastEvent(EVENTS.INFO, 'Set placementType to ' + stringify(val));
placementType = val;
},
adSize: function(val) {
mraidbridge.log("adSize change to " + JSON.stringify(val));
for (var key in val) {
if (val.hasOwnProperty(key)) adSize[key] = val[key];
}
if (!hasSetCustomSize) {
mraidbridge.log("sizeChange broadcast with" + JSON.stringify(adSize));
broadcastEvent(EVENTS.SIZECHANGE, adSize['width'], adSize['height']);
}
},
screenSize: function(val) {
broadcastEvent(EVENTS.INFO, 'Set screenSize to ' + stringify(val));
for (var key in val) {
if (val.hasOwnProperty(key)) screenSize[key] = val[key];
}
if (!hasSetCustomSize) {
expandProperties['width'] = screenSize['width'];
expandProperties['height'] = screenSize['height'];
}
},
maxSize: function(val) {
broadcastEvent(EVENTS.INFO, 'Set maxSize to ' + stringify(val));
for (var key in val) {
if (val.hasOwnProperty(key)) maxSize[key] = val[key];
}
},
currentPosition: function(val) {
broadcastEvent(EVENTS.INFO, 'Set currentPosition to ' + stringify(val));
for (var key in val) {
if (val.hasOwnProperty(key)) currentPosition[key] = val[key];
}
mraid.createCalendarEvent = function(parameters) {
CalendarEventParser.initialize(parameters);
if (CalendarEventParser.parse()) {
bridge.executeNativeCall.apply(this, CalendarEventParser.arguments);
} else {
broadcastEvent(EVENTS.ERROR, CalendarEventParser.errors[0],
'createCalendarEvent');
}
};
mraid.supports = function(feature) {
return supports[feature];
};
mraid.playVideo = function(uri) {
if (!mraid.isViewable()) {
broadcastEvent(EVENTS.ERROR, 'playVideo cannot be called until the ad is
viewable', 'playVideo');
return;
}
if (!uri) {
broadcastEvent(EVENTS.ERROR, 'playVideo must be called with a valid URI',
'playVideo');
} else {
bridge.executeNativeCall.apply(this, ['playVideo', 'uri', uri]);
}
};
mraid.storePicture = function(uri) {
if (!mraid.isViewable()) {
broadcastEvent(EVENTS.ERROR, 'storePicture cannot be called until the ad is
viewable', 'storePicture');
return;
}
if (!uri) {
broadcastEvent(EVENTS.ERROR, 'storePicture must be called with a valid URI',
'storePicture');
} else {
bridge.executeNativeCall.apply(this, ['storePicture', 'uri', uri]);
}
};
mraid.resize = function() {
if (state !== STATES.DEFAULT) {
broadcastEvent(EVENTS.ERROR, 'Ad can only be expanded from the default
state.', 'resize');
} else {
var args = ['resize'];
if (mraid.getHasSetCreativeSize()) {
if (resizeProperties.width >= 0 && resizeProperties.height >= 0) {
args = args.concat(['w', resizeProperties.width, 'h',
resizeProperties.height]);
}
}
if (mraid.getHasSetCreativeOffset()) {
args = args.concat(['offsetX', resizeProperties.offsetX, 'offsetY',
resizeProperties.offsetY]);
}
if (typeof resizeProperties.allowOffscreen !== 'undefined') {
args = args.concat(['allowOffscreen', resizeProperties.allowOffscreen]);
}
if (typeof resizeProperties.customClosePosition !== 'undefined') {
args = args.concat(['customClosePosition',
resizeProperties.customClosePosition]);
}
bridge.executeNativeCall.apply(this, args);
}
};
mraid.getResizeProperties = function() {
var properties = {
width: resizeProperties.width,
height: resizeProperties.height,
customClosePosition: resizeProperties.customClosePosition,
offsetX: resizeProperties.offsetX,
offsetY: resizeProperties.offsetY,
allowOffscreen: resizeProperties.allowOffscreen
};
return properties;
};
mraid.setResizeProperties = function(properties) {
if (validate(properties, resizePropertyValidators, 'setResizeProperties',
true)) {
if (properties.hasOwnProperty('width') ||
properties.hasOwnProperty('height')) {
hasSetCreativeSize = true;
}
if (properties.hasOwnProperty('offsetX') ||
properties.hasOwnProperty('offsetY')) {
hasSetCreativeOffset = true;
}
if (properties.hasOwnProperty('useCustomClose')) hasSetCustomClose = true;
var desiredProperties = ['width', 'height', 'offsetX', 'offsetY',
'customClosePosition', 'allowOffscreen'];
var length = desiredProperties.length;
for (var i = 0; i < length; i++) {
var propname = desiredProperties[i];
if (properties.hasOwnProperty(propname)) resizeProperties[propname] =
properties[propname];
}
}
};
bridge.executeNativeCall.apply(this, args);
};
mraid.getHasSetCreativeSize = function() {
return hasSetCreativeSize;
};
mraid.getHasSetCreativeOffset = function() {
return hasSetCreativeOffset;
};
mraid.getOrientationProperties = function () {
return clone(orientationProperties);
};
mraid.getCurrentPosition = function () {
return clone(currentPosition);
};
mraid.getDefaultPosition = function() {
return clone(mraidDefaultPosition);
};
mraid.getMaxSize = function() {
mraidbridge.log("getMaxSize" + JSON.stringify(maxSize))
return clone(maxSize);
};
mraid.getScreenSize = function() {
return clone(screenSize);
};
var CalendarEventParser = {
initialize: function(parameters) {
this.parameters = parameters;
this.errors = [];
this.arguments = ['createCalendarEvent'];
},
parse: function() {
if (!this.parameters) {
this.errors.push('The object passed to createCalendarEvent cannot be
null.');
} else {
this.parseDescription();
this.parseLocation();
this.parseSummary();
this.parseStartAndEndDates();
this.parseReminder();
this.parseRecurrence();
this.parseTransparency();
}
var errorCount = this.errors.length;
if (errorCount) {
this.arguments.length = 0;
}
return (errorCount === 0);
},
parseDescription: function() {
this._processStringValue('description');
},
parseLocation: function() {
this._processStringValue('location');
},
parseSummary: function() {
this._processStringValue('summary');
},
parseStartAndEndDates: function() {
this._processDateValue('start');
this._processDateValue('end');
},
parseReminder: function() {
var reminder = this._getParameter('reminder');
if (!reminder) {
return;
}
if (reminder < 0) {
this.arguments.push('relativeReminder');
this.arguments.push(parseInt(reminder) / 1000);
} else {
this.arguments.push('absoluteReminder');
this.arguments.push(reminder);
}
},
parseRecurrence: function() {
var recurrenceDict = this._getParameter('recurrence');
if (!recurrenceDict) {
return;
}
this.parseRecurrenceInterval(recurrenceDict);
this.parseRecurrenceFrequency(recurrenceDict);
this.parseRecurrenceEndDate(recurrenceDict);
this.parseRecurrenceArrayValue(recurrenceDict, 'daysInWeek');
this.parseRecurrenceArrayValue(recurrenceDict, 'daysInMonth');
this.parseRecurrenceArrayValue(recurrenceDict, 'daysInYear');
this.parseRecurrenceArrayValue(recurrenceDict, 'monthsInYear');
},
parseTransparency: function() {
var validValues = ['opaque', 'transparent'];
if (this.parameters.hasOwnProperty('transparency')) {
var transparency = this.parameters['transparency'];
if (contains(transparency, validValues)) {
this.arguments.push('transparency');
this.arguments.push(transparency);
} else {
this.errors.push('transparency must be opaque or transparent');
}
}
},
parseRecurrenceArrayValue: function(recurrenceDict, kind) {
if (recurrenceDict.hasOwnProperty(kind)) {
var array = recurrenceDict[kind];
if (!array || !(array instanceof Array)) {
this.errors.push(kind + ' must be an array.');
} else {
var arrayStr = array.join(',');
this.arguments.push(kind);
this.arguments.push(arrayStr);
}
}
},
parseRecurrenceInterval: function(recurrenceDict) {
if (recurrenceDict.hasOwnProperty('interval')) {
var interval = recurrenceDict['interval'];
if (!interval) {
this.errors.push('Recurrence interval cannot be null.');
} else {
this.arguments.push('interval');
this.arguments.push(interval);
}
} else {
// If a recurrence rule was specified without an interval, use a default
value of 1.
this.arguments.push('interval');
this.arguments.push(1);
}
},
parseRecurrenceFrequency: function(recurrenceDict) {
if (recurrenceDict.hasOwnProperty('frequency')) {
var frequency = recurrenceDict['frequency'];
var validFrequencies = ['daily', 'weekly', 'monthly', 'yearly'];
if (contains(frequency, validFrequencies)) {
this.arguments.push('frequency');
this.arguments.push(frequency);
} else {
this.errors.push("Recurrence frequency must be one of: 'daily, 'weekly',
'monthly', 'yearly'.");
}
}
},
parseRecurrenceEndDate: function(recurrenceDict) {
var expires = recurrenceDict['expires'];
if (!expires) {
return;
}
this.arguments.push('expires');
this.arguments.push(expires);
},
_getParameter: function(key) {
if (this.parameters.hasOwnProperty(key)) {
return this.parameters[key];
}
return null;
},
_processStringValue: function(kind) {
if (this.parameters.hasOwnProperty(kind)) {
var value = this.parameters[kind];
this.arguments.push(kind);
this.arguments.push(value);
}
},
_processDateValue: function(kind) {
if (this.parameters.hasOwnProperty(kind)) {
var dateString = this._getParameter(kind);
this.arguments.push(kind);
this.arguments.push(dateString);
}
}
};
}());
</script>