Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up/* eslint camelcase: "off" */ | |
import Config from './config'; | |
import { _, console, userAgent } from './utils'; | |
import { autotrack } from './autotrack'; | |
/* | |
* robotmia JS Library | |
* | |
* Copyright 2012, robotmia, Inc. All Rights Reserved | |
* http://robotmia.com/ | |
* | |
* Includes portions of Underscore.js | |
* http://documentcloud.github.com/underscore/ | |
* (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. | |
* Released under the MIT License. | |
*/ | |
// ==ClosureCompiler== | |
// @compilation_level ADVANCED_OPTIMIZATIONS | |
// @output_file_name robotmia-2.8.min.js | |
// ==/ClosureCompiler== | |
/* | |
SIMPLE STYLE GUIDE: | |
this.x === public function | |
this._x === internal - only use within this file | |
this.__x === private - only use within the class | |
Globals should be all caps | |
*/ | |
var init_type; // MODULE or SNIPPET loader | |
var robotmia_master; // main robotmia instance / object | |
var INIT_MODULE = 0; | |
var INIT_SNIPPET = 1; | |
/* | |
* Constants | |
*/ | |
/** @const */ var PRIMARY_INSTANCE_NAME = 'robotmia'; | |
/** @const */ var SET_QUEUE_KEY = '__mps'; | |
/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; | |
/** @const */ var ADD_QUEUE_KEY = '__mpa'; | |
/** @const */ var APPEND_QUEUE_KEY = '__mpap'; | |
/** @const */ var UNION_QUEUE_KEY = '__mpu'; | |
/** @const */ var SET_ACTION = '$set'; | |
/** @const */ var SET_ONCE_ACTION = '$set_once'; | |
/** @const */ var ADD_ACTION = '$add'; | |
/** @const */ var APPEND_ACTION = '$append'; | |
/** @const */ var UNION_ACTION = '$union'; | |
// This key is deprecated, but we want to check for it to see whether aliasing is allowed. | |
/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; | |
/** @const */ var ALIAS_ID_KEY = '__alias'; | |
/** @const */ var CAMPAIGN_IDS_KEY = '__cmpns'; | |
/** @const */ var EVENT_TIMERS_KEY = '__timers'; | |
/** @const */ var RESERVED_PROPERTIES = [ | |
SET_QUEUE_KEY, | |
SET_ONCE_QUEUE_KEY, | |
ADD_QUEUE_KEY, | |
APPEND_QUEUE_KEY, | |
UNION_QUEUE_KEY, | |
PEOPLE_DISTINCT_ID_KEY, | |
ALIAS_ID_KEY, | |
CAMPAIGN_IDS_KEY, | |
EVENT_TIMERS_KEY | |
]; | |
/* | |
* Dynamic... constants? Is that an oxymoron? | |
*/ | |
var HTTP_PROTOCOL = (('https:' === document.location.protocol) ? 'https://' : 'http://'); | |
// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ | |
// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials | |
var USE_XHR = (window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); | |
// IE<10 does not support cross-origin XHR's but script tags | |
// with defer won't block window.onload; ENQUEUE_REQUESTS | |
// should only be true for Opera<12 | |
var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); | |
/* | |
* Module-level globals | |
*/ | |
var DEFAULT_CONFIG = { | |
'api_host': HTTP_PROTOCOL + 'api.robotmia.com', | |
'app_host': HTTP_PROTOCOL + 'robotmia.com', | |
'autotrack': true, | |
'cdn': HTTP_PROTOCOL + 'cdn.mxpnl.com', | |
'cross_subdomain_cookie': true, | |
'persistence': 'cookie', | |
'persistence_name': '', | |
'cookie_name': '', | |
'loaded': function() {}, | |
'store_google': true, | |
'save_referrer': true, | |
'test': false, | |
'verbose': false, | |
'img': false, | |
'track_pageview': true, | |
'debug': false, | |
'track_links_timeout': 300, | |
'cookie_expiration': 365, | |
'upgrade': false, | |
'disable_persistence': false, | |
'disable_cookie': false, | |
'secure_cookie': false, | |
'ip': true, | |
'property_blacklist': [] | |
}; | |
var DOM_LOADED = false; | |
/** | |
* DomTracker Object | |
* @constructor | |
*/ | |
var DomTracker = function() {}; | |
// interface | |
DomTracker.prototype.create_properties = function() {}; | |
DomTracker.prototype.event_handler = function() {}; | |
DomTracker.prototype.after_track_handler = function() {}; | |
DomTracker.prototype.init = function(robotmia_instance) { | |
this.mp = robotmia_instance; | |
return this; | |
}; | |
/** | |
* @param {Object|string} query | |
* @param {string} event_name | |
* @param {Object=} properties | |
* @param {function(...[*])=} user_callback | |
*/ | |
DomTracker.prototype.track = function(query, event_name, properties, user_callback) { | |
var that = this; | |
var elements = _.dom_query(query); | |
if (elements.length === 0) { | |
console.error('The DOM query (' + query + ') returned 0 elements'); | |
return; | |
} | |
_.each(elements, function(element) { | |
_.register_event(element, this.override_event, function(e) { | |
var options = {}; | |
var props = that.create_properties(properties, this); | |
var timeout = that.mp.get_config('track_links_timeout'); | |
that.event_handler(e, this, options); | |
// in case the robotmia servers don't get back to us in time | |
window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); | |
// fire the tracking event | |
that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); | |
}); | |
}, this); | |
return true; | |
}; | |
/** | |
* @param {function(...[*])} user_callback | |
* @param {Object} props | |
* @param {boolean=} timeout_occured | |
*/ | |
DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { | |
timeout_occured = timeout_occured || false; | |
var that = this; | |
return function() { | |
// options is referenced from both callbacks, so we can have | |
// a 'lock' of sorts to ensure only one fires | |
if (options.callback_fired) { return; } | |
options.callback_fired = true; | |
if (user_callback && user_callback(timeout_occured, props) === false) { | |
// user can prevent the default functionality by | |
// returning false from their callback | |
return; | |
} | |
that.after_track_handler(props, options, timeout_occured); | |
}; | |
}; | |
DomTracker.prototype.create_properties = function(properties, element) { | |
var props; | |
if (typeof(properties) === 'function') { | |
props = properties(element); | |
} else { | |
props = _.extend({}, properties); | |
} | |
return props; | |
}; | |
/** | |
* LinkTracker Object | |
* @constructor | |
* @extends DomTracker | |
*/ | |
var LinkTracker = function() { | |
this.override_event = 'click'; | |
}; | |
_.inherit(LinkTracker, DomTracker); | |
LinkTracker.prototype.create_properties = function(properties, element) { | |
var props = LinkTracker.superclass.create_properties.apply(this, arguments); | |
if (element.href) { props['url'] = element.href; } | |
return props; | |
}; | |
LinkTracker.prototype.event_handler = function(evt, element, options) { | |
options.new_tab = ( | |
evt.which === 2 || | |
evt.metaKey || | |
evt.ctrlKey || | |
element.target === '_blank' | |
); | |
options.href = element.href; | |
if (!options.new_tab) { | |
evt.preventDefault(); | |
} | |
}; | |
LinkTracker.prototype.after_track_handler = function(props, options) { | |
if (options.new_tab) { return; } | |
setTimeout(function() { | |
window.location = options.href; | |
}, 0); | |
}; | |
/** | |
* FormTracker Object | |
* @constructor | |
* @extends DomTracker | |
*/ | |
var FormTracker = function() { | |
this.override_event = 'submit'; | |
}; | |
_.inherit(FormTracker, DomTracker); | |
FormTracker.prototype.event_handler = function(evt, element, options) { | |
options.element = element; | |
evt.preventDefault(); | |
}; | |
FormTracker.prototype.after_track_handler = function(props, options) { | |
setTimeout(function() { | |
options.element.submit(); | |
}, 0); | |
}; | |
/** | |
* robotmia Persistence Object | |
* @constructor | |
*/ | |
var robotmiaPersistence = function(config) { | |
this['props'] = {}; | |
this.campaign_params_saved = false; | |
if (config['persistence_name']) { | |
this.name = 'mp_' + config['persistence_name']; | |
} else { | |
this.name = 'mp_' + config['token'] + '_robotmia'; | |
} | |
var storage_type = config['persistence']; | |
if (storage_type !== 'cookie' && storage_type !== 'localStorage') { | |
console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); | |
storage_type = config['persistence'] = 'cookie'; | |
} | |
var localStorage_supported = function() { | |
var supported = true; | |
try { | |
var key = '__mplssupport__', | |
val = 'xyz'; | |
_.localStorage.set(key, val); | |
if (_.localStorage.get(key) !== val) { | |
supported = false; | |
} | |
_.localStorage.remove(key); | |
} catch (err) { | |
supported = false; | |
} | |
if (!supported) { | |
console.error('localStorage unsupported; falling back to cookie store'); | |
} | |
return supported; | |
}; | |
if (storage_type === 'localStorage' && localStorage_supported()) { | |
this.storage = _.localStorage; | |
} else { | |
this.storage = _.cookie; | |
} | |
this.load(); | |
this.update_config(config); | |
this.upgrade(config); | |
this.save(); | |
}; | |
robotmiaPersistence.prototype.properties = function() { | |
var p = {}; | |
// Filter out reserved properties | |
_.each(this['props'], function(v, k) { | |
if (!_.include(RESERVED_PROPERTIES, k)) { | |
p[k] = v; | |
} | |
}); | |
return p; | |
}; | |
robotmiaPersistence.prototype.load = function() { | |
if (this.disabled) { return; } | |
var entry = this.storage.parse(this.name); | |
if (entry) { | |
this['props'] = _.extend({}, entry); | |
} | |
}; | |
robotmiaPersistence.prototype.upgrade = function(config) { | |
var upgrade_from_old_lib = config['upgrade'], | |
old_cookie_name, | |
old_cookie; | |
if (upgrade_from_old_lib) { | |
old_cookie_name = 'mp_super_properties'; | |
// Case where they had a custom cookie name before. | |
if (typeof(upgrade_from_old_lib) === 'string') { | |
old_cookie_name = upgrade_from_old_lib; | |
} | |
old_cookie = this.storage.parse(old_cookie_name); | |
// remove the cookie | |
this.storage.remove(old_cookie_name); | |
this.storage.remove(old_cookie_name, true); | |
if (old_cookie) { | |
this['props'] = _.extend( | |
this['props'], | |
old_cookie['all'], | |
old_cookie['events'] | |
); | |
} | |
} | |
if (!config['cookie_name'] && config['name'] !== 'robotmia') { | |
// special case to handle people with cookies of the form | |
// mp_TOKEN_INSTANCENAME from the first release of this library | |
old_cookie_name = 'mp_' + config['token'] + '_' + config['name']; | |
old_cookie = this.storage.parse(old_cookie_name); | |
if (old_cookie) { | |
this.storage.remove(old_cookie_name); | |
this.storage.remove(old_cookie_name, true); | |
// Save the prop values that were in the cookie from before - | |
// this should only happen once as we delete the old one. | |
this.register_once(old_cookie); | |
} | |
} | |
if (this.storage === _.localStorage) { | |
old_cookie = _.cookie.parse(this.name); | |
_.cookie.remove(this.name); | |
_.cookie.remove(this.name, true); | |
if (old_cookie) { | |
this.register_once(old_cookie); | |
} | |
} | |
}; | |
robotmiaPersistence.prototype.save = function() { | |
if (this.disabled) { return; } | |
this._expire_notification_campaigns(); | |
this.storage.set( | |
this.name, | |
_.JSONEncode(this['props']), | |
this.expire_days, | |
this.cross_subdomain, | |
this.secure | |
); | |
}; | |
robotmiaPersistence.prototype.remove = function() { | |
// remove both domain and subdomain cookies | |
this.storage.remove(this.name, false); | |
this.storage.remove(this.name, true); | |
}; | |
// removes the storage entry and deletes all loaded data | |
// forced name for tests | |
robotmiaPersistence.prototype.clear = function() { | |
this.remove(); | |
this['props'] = {}; | |
}; | |
/** | |
* @param {Object} props | |
* @param {*=} default_value | |
* @param {number=} days | |
*/ | |
robotmiaPersistence.prototype.register_once = function(props, default_value, days) { | |
if (_.isObject(props)) { | |
if (typeof(default_value) === 'undefined') { default_value = 'None'; } | |
this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; | |
_.each(props, function(val, prop) { | |
if (!this['props'][prop] || this['props'][prop] === default_value) { | |
this['props'][prop] = val; | |
} | |
}, this); | |
this.save(); | |
return true; | |
} | |
return false; | |
}; | |
/** | |
* @param {Object} props | |
* @param {number=} days | |
*/ | |
robotmiaPersistence.prototype.register = function(props, days) { | |
if (_.isObject(props)) { | |
this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; | |
_.extend(this['props'], props); | |
this.save(); | |
return true; | |
} | |
return false; | |
}; | |
robotmiaPersistence.prototype.unregister = function(prop) { | |
if (prop in this['props']) { | |
delete this['props'][prop]; | |
this.save(); | |
} | |
}; | |
robotmiaPersistence.prototype._expire_notification_campaigns = _.safewrap(function() { | |
var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY], | |
EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN) | |
if (!campaigns_shown) { | |
return; | |
} | |
for (var campaign_id in campaigns_shown) { | |
if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) { | |
delete campaigns_shown[campaign_id]; | |
} | |
} | |
if (_.isEmptyObject(campaigns_shown)) { | |
delete this['props'][CAMPAIGN_IDS_KEY]; | |
} | |
}); | |
robotmiaPersistence.prototype.update_campaign_params = function() { | |
if (!this.campaign_params_saved) { | |
this.register_once(_.info.campaignParams()); | |
this.campaign_params_saved = true; | |
} | |
}; | |
robotmiaPersistence.prototype.update_search_keyword = function(referrer) { | |
this.register(_.info.searchInfo(referrer)); | |
}; | |
// EXPORTED METHOD, we test this directly. | |
robotmiaPersistence.prototype.update_referrer_info = function(referrer) { | |
// If referrer doesn't exist, we want to note the fact that it was type-in traffic. | |
this.register_once({ | |
'$initial_referrer': referrer || '$direct', | |
'$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' | |
}, ''); | |
}; | |
robotmiaPersistence.prototype.get_referrer_info = function() { | |
return _.strip_empty_properties({ | |
'$initial_referrer': this['props']['$initial_referrer'], | |
'$initial_referring_domain': this['props']['$initial_referring_domain'] | |
}); | |
}; | |
// safely fills the passed in object with stored properties, | |
// does not override any properties defined in both | |
// returns the passed in object | |
robotmiaPersistence.prototype.safe_merge = function(props) { | |
_.each(this['props'], function(val, prop) { | |
if (!(prop in props)) { | |
props[prop] = val; | |
} | |
}); | |
return props; | |
}; | |
robotmiaPersistence.prototype.update_config = function(config) { | |
this.default_expiry = this.expire_days = config['cookie_expiration']; | |
this.set_disabled(config['disable_persistence']); | |
this.set_cross_subdomain(config['cross_subdomain_cookie']); | |
this.set_secure(config['secure_cookie']); | |
}; | |
robotmiaPersistence.prototype.set_disabled = function(disabled) { | |
this.disabled = disabled; | |
if (this.disabled) { | |
this.remove(); | |
} | |
}; | |
robotmiaPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { | |
if (cross_subdomain !== this.cross_subdomain) { | |
this.cross_subdomain = cross_subdomain; | |
this.remove(); | |
this.save(); | |
} | |
}; | |
robotmiaPersistence.prototype.get_cross_subdomain = function() { | |
return this.cross_subdomain; | |
}; | |
robotmiaPersistence.prototype.set_secure = function(secure) { | |
if (secure !== this.secure) { | |
this.secure = secure ? true : false; | |
this.remove(); | |
this.save(); | |
} | |
}; | |
robotmiaPersistence.prototype._add_to_people_queue = function(queue, data) { | |
var q_key = this._get_queue_key(queue), | |
q_data = data[queue], | |
set_q = this._get_or_create_queue(SET_ACTION), | |
set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), | |
add_q = this._get_or_create_queue(ADD_ACTION), | |
union_q = this._get_or_create_queue(UNION_ACTION), | |
append_q = this._get_or_create_queue(APPEND_ACTION, []); | |
if (q_key === SET_QUEUE_KEY) { | |
// Update the set queue - we can override any existing values | |
_.extend(set_q, q_data); | |
// if there was a pending increment, override it | |
// with the set. | |
this._pop_from_people_queue(ADD_ACTION, q_data); | |
// if there was a pending union, override it | |
// with the set. | |
this._pop_from_people_queue(UNION_ACTION, q_data); | |
} else if (q_key === SET_ONCE_QUEUE_KEY) { | |
// only queue the data if there is not already a set_once call for it. | |
_.each(q_data, function(v, k) { | |
if (!(k in set_once_q)) { | |
set_once_q[k] = v; | |
} | |
}); | |
} else if (q_key === ADD_QUEUE_KEY) { | |
_.each(q_data, function(v, k) { | |
// If it exists in the set queue, increment | |
// the value | |
if (k in set_q) { | |
set_q[k] += v; | |
} else { | |
// If it doesn't exist, update the add | |
// queue | |
if (!(k in add_q)) { | |
add_q[k] = 0; | |
} | |
add_q[k] += v; | |
} | |
}, this); | |
} else if (q_key === UNION_QUEUE_KEY) { | |
_.each(q_data, function(v, k) { | |
if (_.isArray(v)) { | |
if (!(k in union_q)) { | |
union_q[k] = []; | |
} | |
// We may send duplicates, the server will dedup them. | |
union_q[k] = union_q[k].concat(v); | |
} | |
}); | |
} else if (q_key === APPEND_QUEUE_KEY) { | |
append_q.push(q_data); | |
} | |
console.log('robotmia PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); | |
console.log(data); | |
this.save(); | |
}; | |
robotmiaPersistence.prototype._pop_from_people_queue = function(queue, data) { | |
var q = this._get_queue(queue); | |
if (!_.isUndefined(q)) { | |
_.each(data, function(v, k) { | |
delete q[k]; | |
}, this); | |
this.save(); | |
} | |
}; | |
robotmiaPersistence.prototype._get_queue_key = function(queue) { | |
if (queue === SET_ACTION) { | |
return SET_QUEUE_KEY; | |
} else if (queue === SET_ONCE_ACTION) { | |
return SET_ONCE_QUEUE_KEY; | |
} else if (queue === ADD_ACTION) { | |
return ADD_QUEUE_KEY; | |
} else if (queue === APPEND_ACTION) { | |
return APPEND_QUEUE_KEY; | |
} else if (queue === UNION_ACTION) { | |
return UNION_QUEUE_KEY; | |
} else { | |
console.error('Invalid queue:', queue); | |
} | |
}; | |
robotmiaPersistence.prototype._get_queue = function(queue) { | |
return this['props'][this._get_queue_key(queue)]; | |
}; | |
robotmiaPersistence.prototype._get_or_create_queue = function(queue, default_val) { | |
var key = this._get_queue_key(queue); | |
default_val = _.isUndefined(default_val) ? {} : default_val; | |
return this['props'][key] || (this['props'][key] = default_val); | |
}; | |
robotmiaPersistence.prototype.set_event_timer = function(event_name, timestamp) { | |
var timers = this['props'][EVENT_TIMERS_KEY] || {}; | |
timers[event_name] = timestamp; | |
this['props'][EVENT_TIMERS_KEY] = timers; | |
this.save(); | |
}; | |
robotmiaPersistence.prototype.remove_event_timer = function(event_name) { | |
var timers = this['props'][EVENT_TIMERS_KEY] || {}; | |
var timestamp = timers[event_name]; | |
if (!_.isUndefined(timestamp)) { | |
delete this['props'][EVENT_TIMERS_KEY][event_name]; | |
this.save(); | |
} | |
return timestamp; | |
}; | |
/** | |
* robotmia Library Object | |
* @constructor | |
*/ | |
var robotmiaLib = function() {}; | |
/** | |
* robotmia People Object | |
* @constructor | |
*/ | |
var robotmiaPeople = function() {}; | |
var MPNotif; | |
/** | |
* create_mplib(token:string, config:object, name:string) | |
* | |
* This function is used by the init method of robotmiaLib objects | |
* as well as the main initializer at the end of the JSLib (that | |
* initializes document.robotmia as well as any additional instances | |
* declared before this file has loaded). | |
*/ | |
var create_mplib = function(token, config, name) { | |
var instance, | |
target = (name === PRIMARY_INSTANCE_NAME) ? robotmia_master : robotmia_master[name]; | |
if (target && init_type === INIT_MODULE) { | |
instance = target; | |
} else { | |
if (target && !_.isArray(target)) { | |
console.error('You have already initialized ' + name); | |
return; | |
} | |
instance = new robotmiaLib(); | |
} | |
instance._init(token, config, name); | |
instance['people'] = new robotmiaPeople(); | |
instance['people']._init(instance); | |
// if any instance on the page has debug = true, we set the | |
// global debug to be true | |
Config.DEBUG = Config.DEBUG || instance.get_config('debug'); | |
instance['__autotrack_enabled'] = instance.get_config('autotrack'); | |
if (instance.get_config('autotrack')) { | |
var num_buckets = 100; | |
var num_enabled_buckets = 100; | |
if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) { | |
instance['__autotrack_enabled'] = false; | |
console.log('Not in active bucket: disabling Automatic Event Collection.'); | |
} else if (!autotrack.isBrowserSupported()) { | |
instance['__autotrack_enabled'] = false; | |
console.log('Disabling Automatic Event Collection because this browser is not supported'); | |
} else { | |
autotrack.init(instance); | |
} | |
try { | |
add_dom_event_counting_handlers(instance); | |
} catch (e) { | |
console.error(e); | |
} | |
} | |
// if target is not defined, we called init after the lib already | |
// loaded, so there won't be an array of things to execute | |
if (!_.isUndefined(target) && _.isArray(target)) { | |
// Crunch through the people queue first - we queue this data up & | |
// flush on identify, so it's better to do all these operations first | |
instance._execute_array.call(instance['people'], target['people']); | |
instance._execute_array(target); | |
} | |
return instance; | |
}; | |
// Initialization methods | |
/** | |
* This function initializes a new instance of the robotmia tracking object. | |
* All new instances are added to the main robotmia object as sub properties (such as | |
* robotmia.library_name) and also returned by this function. To define a | |
* second instance on the page, you would call: | |
* | |
* robotmia.init('new token', { your: 'config' }, 'library_name'); | |
* | |
* and use it like so: | |
* | |
* robotmia.library_name.track(...); | |
* | |
* @param {String} token Your robotmia API token | |
* @param {Object} [config] A dictionary of config options to override | |
* @param {String} [name] The name for the new robotmia instance that you want created | |
*/ | |
robotmiaLib.prototype.init = function (token, config, name) { | |
if (_.isUndefined(name)) { | |
console.error('You must name your new library: init(token, config, name)'); | |
return; | |
} | |
if (name === PRIMARY_INSTANCE_NAME) { | |
console.error('You must initialize the main robotmia object right after you include the robotmia js snippet'); | |
return; | |
} | |
var instance = create_mplib(token, config, name); | |
robotmia_master[name] = instance; | |
instance._loaded(); | |
return instance; | |
}; | |
// robotmia._init(token:string, config:object, name:string) | |
// | |
// This function sets up the current instance of the robotmia | |
// library. The difference between this method and the init(...) | |
// method is this one initializes the actual instance, whereas the | |
// init(...) method sets up a new library and calls _init on it. | |
// | |
robotmiaLib.prototype._init = function(token, config, name) { | |
this['__loaded'] = true; | |
this['config'] = {}; | |
this.set_config(_.extend({}, DEFAULT_CONFIG, config, { | |
'name': name, | |
'token': token, | |
'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' | |
})); | |
this['_jsc'] = function() {}; | |
this.__dom_loaded_queue = []; | |
this.__request_queue = []; | |
this.__disabled_events = []; | |
this._flags = { | |
'disable_all_events': false, | |
'identify_called': false | |
}; | |
this['persistence'] = this['cookie'] = new robotmiaPersistence(this['config']); | |
this.register_once({'distinct_id': _.UUID()}, ''); | |
}; | |
// Private methods | |
robotmiaLib.prototype._loaded = function() { | |
this.get_config('loaded')(this); | |
// this happens after so a user can call identify/name_tag in | |
// the loaded callback | |
if (this.get_config('track_pageview')) { | |
this.track_pageview(); | |
} | |
}; | |
robotmiaLib.prototype._dom_loaded = function() { | |
_.each(this.__dom_loaded_queue, function(item) { | |
this._track_dom.apply(this, item); | |
}, this); | |
_.each(this.__request_queue, function(item) { | |
this._send_request.apply(this, item); | |
}, this); | |
delete this.__dom_loaded_queue; | |
delete this.__request_queue; | |
}; | |
robotmiaLib.prototype._track_dom = function(DomClass, args) { | |
if (this.get_config('img')) { | |
console.error('You can\'t use DOM tracking functions with img = true.'); | |
return false; | |
} | |
if (!DOM_LOADED) { | |
this.__dom_loaded_queue.push([DomClass, args]); | |
return false; | |
} | |
var dt = new DomClass().init(this); | |
return dt.track.apply(dt, args); | |
}; | |
/** | |
* _prepare_callback() should be called by callers of _send_request for use | |
* as the callback argument. | |
* | |
* If there is no callback, this returns null. | |
* If we are going to make XHR/XDR requests, this returns a function. | |
* If we are going to use script tags, this returns a string to use as the | |
* callback GET param. | |
*/ | |
robotmiaLib.prototype._prepare_callback = function(callback, data) { | |
if (_.isUndefined(callback)) { | |
return null; | |
} | |
if (USE_XHR) { | |
var callback_function = function(response) { | |
callback(response, data); | |
}; | |
return callback_function; | |
} else { | |
// if the user gives us a callback, we store as a random | |
// property on this instances jsc function and update our | |
// callback string to reflect that. | |
var jsc = this['_jsc']; | |
var randomized_cb = '' + Math.floor(Math.random() * 100000000); | |
var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; | |
jsc[randomized_cb] = function(response) { | |
delete jsc[randomized_cb]; | |
callback(response, data); | |
}; | |
return callback_string; | |
} | |
}; | |
robotmiaLib.prototype._send_request = function(url, data, callback) { | |
if (ENQUEUE_REQUESTS) { | |
this.__request_queue.push(arguments); | |
return; | |
} | |
// needed to correctly format responses | |
var verbose_mode = this.get_config('verbose'); | |
if (data['verbose']) { verbose_mode = true; } | |
if (this.get_config('test')) { data['test'] = 1; } | |
if (verbose_mode) { data['verbose'] = 1; } | |
if (this.get_config('img')) { data['img'] = 1; } | |
if (!USE_XHR) { | |
if (callback) { | |
data['callback'] = callback; | |
} else if (verbose_mode || this.get_config('test')) { | |
// Verbose output (from verbose mode, or an error in test mode) is a json blob, | |
// which by itself is not valid javascript. Without a callback, this verbose output will | |
// cause an error when returned via jsonp, so we force a no-op callback param. | |
// See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 | |
data['callback'] = '(function(){})'; | |
} | |
} | |
data['ip'] = this.get_config('ip')?1:0; | |
data['_'] = new Date().getTime().toString(); | |
url += '?' + _.HTTPBuildQuery(data); | |
if ('img' in data) { | |
var img = document.createElement('img'); | |
img.src = url; | |
document.body.appendChild(img); | |
} else if (USE_XHR) { | |
try { | |
var req = new XMLHttpRequest(); | |
req.open('GET', url, true); | |
// send the mp_optout cookie | |
// withCredentials cannot be modified until after calling .open on Android and Mobile Safari | |
req.withCredentials = true; | |
req.onreadystatechange = function () { | |
if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 | |
if (req.status === 200) { | |
if (callback) { | |
if (verbose_mode) { | |
callback(_.JSONDecode(req.responseText)); | |
} else { | |
callback(Number(req.responseText)); | |
} | |
} | |
} else { | |
var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; | |
console.error(error); | |
if (callback) { | |
if (verbose_mode) { | |
callback({status: 0, error: error}); | |
} else { | |
callback(0); | |
} | |
} | |
} | |
} | |
}; | |
req.send(null); | |
} catch (e) { | |
console.error(e); | |
} | |
} else { | |
var script = document.createElement('script'); | |
script.type = 'text/javascript'; | |
script.async = true; | |
script.defer = true; | |
script.src = url; | |
var s = document.getElementsByTagName('script')[0]; | |
s.parentNode.insertBefore(script, s); | |
} | |
}; | |
/** | |
* _execute_array() deals with processing any robotmia function | |
* calls that were called before the robotmia library were loaded | |
* (and are thus stored in an array so they can be called later) | |
* | |
* Note: we fire off all the robotmia function calls && user defined | |
* functions BEFORE we fire off robotmia tracking calls. This is so | |
* identify/register/set_config calls can properly modify early | |
* tracking calls. | |
* | |
* @param {Array} array | |
*/ | |
robotmiaLib.prototype._execute_array = function(array) { | |
var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; | |
_.each(array, function(item) { | |
if (item) { | |
fn_name = item[0]; | |
if (typeof(item) === 'function') { | |
item.call(this); | |
} else if (_.isArray(item) && fn_name === 'alias') { | |
alias_calls.push(item); | |
} else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { | |
tracking_calls.push(item); | |
} else { | |
other_calls.push(item); | |
} | |
} | |
}, this); | |
var execute = function(calls, context) { | |
_.each(calls, function(item) { | |
this[item[0]].apply(this, item.slice(1)); | |
}, context); | |
}; | |
execute(alias_calls, this); | |
execute(other_calls, this); | |
execute(tracking_calls, this); | |
}; | |
/** | |
* push() keeps the standard async-array-push | |
* behavior around after the lib is loaded. | |
* This is only useful for external integrations that | |
* do not wish to rely on our convenience methods | |
* (created in the snippet). | |
* | |
* ### Usage: | |
* robotmia.push(['register', { a: 'b' }]); | |
* | |
* @param {Array} item A [function_name, args...] array to be executed | |
*/ | |
robotmiaLib.prototype.push = function(item) { | |
this._execute_array([item]); | |
}; | |
/** | |
* Disable events on the robotmia object. If passed no arguments, | |
* this function disables tracking of any event. If passed an | |
* array of event names, those events will be disabled, but other | |
* events will continue to be tracked. | |
* | |
* Note: this function does not stop other robotmia functions from | |
* firing, such as register() or people.set(). | |
* | |
* @param {Array} [events] An array of event names to disable | |
*/ | |
robotmiaLib.prototype.disable = function(events) { | |
if (typeof(events) === 'undefined') { | |
this._flags.disable_all_events = true; | |
} else { | |
this.__disabled_events = this.__disabled_events.concat(events); | |
} | |
}; | |
/** | |
* Track an event. This is the most important and | |
* frequently used robotmia function. | |
* | |
* ### Usage: | |
* | |
* // track an event named 'Registered' | |
* robotmia.track('Registered', {'Gender': 'Male', 'Age': 21}); | |
* | |
* To track link clicks or form submissions, see track_links() or track_forms(). | |
* | |
* @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. | |
* @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. | |
* @param {Function} [callback] If provided, the callback function will be called after tracking the event. | |
*/ | |
robotmiaLib.prototype.track = function(event_name, properties, callback) { | |
if (typeof(callback) !== 'function') { | |
callback = function() {}; | |
} | |
if (_.isUndefined(event_name)) { | |
console.error('No event name provided to robotmia.track'); | |
return; | |
} | |
if (this._event_is_disabled(event_name)) { | |
callback(0); | |
return; | |
} | |
// set defaults | |
properties = properties || {}; | |
properties['token'] = this.get_config('token'); | |
// set $duration if time_event was previously called for this event | |
var start_timestamp = this['persistence'].remove_event_timer(event_name); | |
if (!_.isUndefined(start_timestamp)) { | |
var duration_in_ms = new Date().getTime() - start_timestamp; | |
properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); | |
} | |
// update persistence | |
this['persistence'].update_search_keyword(document.referrer); | |
if (this.get_config('store_google')) { this['persistence'].update_campaign_params(); } | |
if (this.get_config('save_referrer')) { this['persistence'].update_referrer_info(document.referrer); } | |
// note: extend writes to the first object, so lets make sure we | |
// don't write to the persistence properties object and info | |
// properties object by passing in a new object | |
// update properties with pageview info and super-properties | |
properties = _.extend( | |
{}, | |
_.info.properties(), | |
this['persistence'].properties(), | |
properties | |
); | |
try { | |
if (this.get_config('autotrack') && event_name !== 'mp_page_view' && event_name !== '$create_alias') { | |
// The point of $__c is to count how many clicks occur per tracked event. Since we're | |
// tracking an event in this function, we need to reset the $__c value. | |
properties = _.extend({}, properties, this.mp_counts); | |
this.mp_counts = {'$__c': 0}; | |
_.cookie.set('mp_' + this.get_config('name') + '__c', 0, 1, true); | |
} | |
} catch (e) { | |
console.error(e); | |
} | |
var property_blacklist = this.get_config('property_blacklist'); | |
if (_.isArray(property_blacklist)) { | |
_.each(property_blacklist, function(blacklisted_prop) { | |
delete properties[blacklisted_prop]; | |
}); | |
} else { | |
console.error('Invalid value for property_blacklist config: ' + property_blacklist); | |
} | |
var data = { | |
'event': event_name, | |
'properties': properties | |
}; | |
var truncated_data = _.truncate(data, 255); | |
var json_data = _.JSONEncode(truncated_data); | |
var encoded_data = _.base64Encode(json_data); | |
console.log('robotmia REQUEST:'); | |
console.log(truncated_data); | |
this._send_request( | |
this.get_config('api_host') + '/track/', | |
{ 'data': encoded_data }, | |
this._prepare_callback(callback, truncated_data) | |
); | |
return truncated_data; | |
}; | |
/** | |
* Track a page view event, which is currently ignored by the server. | |
* This function is called by default on page load unless the | |
* track_pageview configuration variable is false. | |
* | |
* @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. | |
* @api private | |
*/ | |
robotmiaLib.prototype.track_pageview = function(page) { | |
if (_.isUndefined(page)) { | |
page = document.location.href; | |
} | |
this.track('mp_page_view', _.info.pageviewInfo(page)); | |
}; | |
/** | |
* Track clicks on a set of document elements. Selector must be a | |
* valid query. Elements must exist on the page at the time track_links is called. | |
* | |
* ### Usage: | |
* | |
* // track click for link id #nav | |
* robotmia.track_links('#nav', 'Clicked Nav Link'); | |
* | |
* ### Notes: | |
* | |
* This function will wait up to 300 ms for the robotmia | |
* servers to respond. If they have not responded by that time | |
* it will head to the link without ensuring that your event | |
* has been tracked. To configure this timeout please see the | |
* set_config() documentation below. | |
* | |
* If you pass a function in as the properties argument, the | |
* function will receive the DOMElement that triggered the | |
* event as an argument. You are expected to return an object | |
* from the function; any properties defined on this object | |
* will be sent to robotmia as event properties. | |
* | |
* @type {Function} | |
* @param {Object|String} query A valid DOM query, element or jQuery-esque list | |
* @param {String} event_name The name of the event to track | |
* @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement | |
*/ | |
robotmiaLib.prototype.track_links = function() { | |
return this._track_dom.call(this, LinkTracker, arguments); | |
}; | |
/** | |
* Track form submissions. Selector must be a valid query. | |
* | |
* ### Usage: | |
* | |
* // track submission for form id 'register' | |
* robotmia.track_forms('#register', 'Created Account'); | |
* | |
* ### Notes: | |
* | |
* This function will wait up to 300 ms for the robotmia | |
* servers to respond, if they have not responded by that time | |
* it will head to the link without ensuring that your event | |
* has been tracked. To configure this timeout please see the | |
* set_config() documentation below. | |
* | |
* If you pass a function in as the properties argument, the | |
* function will receive the DOMElement that triggered the | |
* event as an argument. You are expected to return an object | |
* from the function; any properties defined on this object | |
* will be sent to robotmia as event properties. | |
* | |
* @type {Function} | |
* @param {Object|String} query A valid DOM query, element or jQuery-esque list | |
* @param {String} event_name The name of the event to track | |
* @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement | |
*/ | |
robotmiaLib.prototype.track_forms = function() { | |
return this._track_dom.call(this, FormTracker, arguments); | |
}; | |
/** | |
* Time an event by including the time between this call and a | |
* later 'track' call for the same event in the properties sent | |
* with the event. | |
* | |
* ### Usage: | |
* | |
* // time an event named 'Registered' | |
* robotmia.time_event('Registered'); | |
* robotmia.track('Registered', {'Gender': 'Male', 'Age': 21}); | |
* | |
* When called for a particular event name, the next track call for that event | |
* name will include the elapsed time between the 'time_event' and 'track' | |
* calls. This value is stored as seconds in the '$duration' property. | |
* | |
* @param {String} event_name The name of the event. | |
*/ | |
robotmiaLib.prototype.time_event = function(event_name) { | |
if (_.isUndefined(event_name)) { | |
console.error('No event name provided to robotmia.time_event'); | |
return; | |
} | |
if (this._event_is_disabled(event_name)) { | |
return; | |
} | |
this['persistence'].set_event_timer(event_name, new Date().getTime()); | |
}; | |
/** | |
* Register a set of super properties, which are included with all | |
* events. This will overwrite previous super property values. | |
* | |
* ### Usage: | |
* | |
* // register 'Gender' as a super property | |
* robotmia.register({'Gender': 'Female'}); | |
* | |
* // register several super properties when a user signs up | |
* robotmia.register({ | |
* 'Email': 'jdoe@example.com', | |
* 'Account Type': 'Free' | |
* }); | |
* | |
* @param {Object} properties An associative array of properties to store about the user | |
* @param {Number} [days] How many days since the user's last visit to store the super properties | |
*/ | |
robotmiaLib.prototype.register = function(props, days) { | |
this['persistence'].register(props, days); | |
}; | |
/** | |
* Register a set of super properties only once. This will not | |
* overwrite previous super property values, unlike register(). | |
* | |
* ### Usage: | |
* | |
* // register a super property for the first time only | |
* robotmia.register_once({ | |
* 'First Login Date': new Date().toISOString() | |
* }); | |
* | |
* ### Notes: | |
* | |
* If default_value is specified, current super properties | |
* with that value will be overwritten. | |
* | |
* @param {Object} properties An associative array of properties to store about the user | |
* @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' | |
* @param {Number} [days] How many days since the users last visit to store the super properties | |
*/ | |
robotmiaLib.prototype.register_once = function(props, default_value, days) { | |
this['persistence'].register_once(props, default_value, days); | |
}; | |
/** | |
* Delete a super property stored with the current user. | |
* | |
* @param {String} property The name of the super property to remove | |
*/ | |
robotmiaLib.prototype.unregister = function(property) { | |
this['persistence'].unregister(property); | |
}; | |
robotmiaLib.prototype._register_single = function(prop, value) { | |
var props = {}; | |
props[prop] = value; | |
this.register(props); | |
}; | |
/** | |
* Identify a user with a unique ID. All subsequent | |
* actions caused by this user will be tied to this unique ID. This | |
* property is used to track unique visitors. If the method is | |
* never called, then unique visitors will be identified by a UUID | |
* generated the first time they visit the site. | |
* | |
* ### Notes: | |
* | |
* You can call this function to overwrite a previously set | |
* unique ID for the current user. robotmia cannot translate | |
* between IDs at this time, so when you change a user's ID | |
* they will appear to be a new user. | |
* | |
* identify() should not be called to link anonymous activity to | |
* subsequent activity when a unique ID is first assigned. | |
* Use alias() when a unique ID is first assigned (registration), and | |
* use identify() to identify the user with that unique ID on an ongoing | |
* basis (e.g., each time a user logs in after registering). | |
* Do not call identify() at the same time as alias(). | |
* | |
* @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. | |
*/ | |
robotmiaLib.prototype.identify = function(unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback) { | |
// Optional Parameters | |
// _set_callback:function A callback to be run if and when the People set queue is flushed | |
// _add_callback:function A callback to be run if and when the People add queue is flushed | |
// _append_callback:function A callback to be run if and when the People append queue is flushed | |
// _set_once_callback:function A callback to be run if and when the People set_once queue is flushed | |
// _union_callback:function A callback to be run if and when the People union queue is flushed | |
// identify only changes the distinct id if it doesn't match either the existing or the alias; | |
// if it's new, blow away the alias as well. | |
if (unique_id !== this.get_distinct_id() && unique_id !== this.get_property(ALIAS_ID_KEY)) { | |
this.unregister(ALIAS_ID_KEY); | |
this._register_single('distinct_id', unique_id); | |
} | |
this._check_and_handle_notifications(this.get_distinct_id()); | |
this._flags.identify_called = true; | |
// Flush any queued up people requests | |
this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback); | |
}; | |
/** | |
* Clears super properties and generates a new random distinct_id for this instance. | |
* Useful for clearing data when a user logs out. | |
*/ | |
robotmiaLib.prototype.reset = function() { | |
this['persistence'].clear(); | |
this._flags.identify_called = false; | |
this.register_once({'distinct_id': _.UUID()}, ''); | |
}; | |
/** | |
* Returns the current distinct id of the user. This is either the id automatically | |
* generated by the library or the id that has been passed by a call to identify(). | |
* | |
* ### Notes: | |
* | |
* get_distinct_id() can only be called after the robotmia library has finished loading. | |
* init() has a loaded function available to handle this automatically. For example: | |
* | |
* // set distinct_id after the robotmia library has loaded | |
* robotmia.init('YOUR PROJECT TOKEN', { | |
* loaded: function(robotmia) { | |
* distinct_id = robotmia.get_distinct_id(); | |
* } | |
* }); | |
*/ | |
robotmiaLib.prototype.get_distinct_id = function() { | |
return this.get_property('distinct_id'); | |
}; | |
/** | |
* Create an alias, which robotmia will use to link two distinct_ids going forward (not retroactively). | |
* Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the | |
* following is a valid scenario: | |
* | |
* robotmia.alias('new_id', 'existing_id'); | |
* ... | |
* robotmia.alias('newer_id', 'new_id'); | |
* | |
* If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. | |
* | |
* ### Notes: | |
* | |
* The best practice is to call alias() when a unique ID is first created for a user | |
* (e.g., when a user first registers for an account and provides an email address). | |
* alias() should never be called more than once for a given user, except to | |
* chain a newer ID to a previously new ID, as described above. | |
* | |
* @param {String} alias A unique identifier that you want to use for this user in the future. | |
* @param {String} [original] The current identifier being used for this user. | |
*/ | |
robotmiaLib.prototype.alias = function(alias, original) { | |
// If the $people_distinct_id key exists in persistence, there has been a previous | |
// robotmia.people.identify() call made for this user. It is VERY BAD to make an alias with | |
// this ID, as it will duplicate users. | |
if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { | |
console.critical('Attempting to create alias for existing People user - aborting.'); | |
return -2; | |
} | |
var _this = this; | |
if (_.isUndefined(original)) { | |
original = this.get_distinct_id(); | |
} | |
if (alias !== original) { | |
this._register_single(ALIAS_ID_KEY, alias); | |
return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() { | |
// Flush the people queue | |
_this.identify(alias); | |
}); | |
} else { | |
console.error('alias matches current distinct_id - skipping api call.'); | |
this.identify(alias); | |
return -1; | |
} | |
}; | |
/** | |
* Provide a string to recognize the user by. The string passed to | |
* this method will appear in the robotmia Streams product rather | |
* than an automatically generated name. Name tags do not have to | |
* be unique. | |
* | |
* This value will only be included in Streams data. | |
* | |
* @param {String} name_tag A human readable name for the user | |
* @api private | |
*/ | |
robotmiaLib.prototype.name_tag = function(name_tag) { | |
this._register_single('mp_name_tag', name_tag); | |
}; | |
/** | |
* Update the configuration of a robotmia library instance. | |
* | |
* The default config is: | |
* | |
* { | |
* // super properties cookie expiration (in days) | |
* cookie_expiration: 365 | |
* | |
* // super properties span subdomains | |
* cross_subdomain_cookie: true | |
* | |
* // if this is true, the robotmia cookie or localStorage entry | |
* // will be deleted, and no user persistence will take place | |
* disable_persistence: false | |
* | |
* // type of persistent store for super properties (cookie/ | |
* // localStorage) if set to 'localStorage', any existing | |
* // robotmia cookie value with the same persistence_name | |
* // will be transferred to localStorage and deleted | |
* persistence: 'cookie' | |
* | |
* // name for super properties persistent store | |
* persistence_name: '' | |
* | |
* // names of properties/superproperties which should never | |
* // be sent with track() calls | |
* property_blacklist: [] | |
* | |
* // if this is true, robotmia cookies will be marked as | |
* // secure, meaning they will only be transmitted over https | |
* secure_cookie: false | |
* | |
* // the amount of time track_links will | |
* // wait for robotmia's servers to respond | |
* track_links_timeout: 300 | |
* | |
* // should we track a page view on page load | |
* track_pageview: true | |
* | |
* // if you set upgrade to be true, the library will check for | |
* // a cookie from our old js library and import super | |
* // properties from it, then the old cookie is deleted | |
* // The upgrade config option only works in the initialization, | |
* // so make sure you set it when you create the library. | |
* upgrade: false | |
* } | |
* | |
* | |
* @param {Object} config A dictionary of new configuration values to update | |
*/ | |
robotmiaLib.prototype.set_config = function(config) { | |
if (_.isObject(config)) { | |
_.extend(this['config'], config); | |
if (!this.get_config('persistence_name')) { | |
this['config']['persistence_name'] = this['config']['cookie_name']; | |
} | |
if (!this.get_config('disable_persistence')) { | |
this['config']['disable_persistence'] = this['config']['disable_cookie']; | |
} | |
if (this['persistence']) { | |
this['persistence'].update_config(this['config']); | |
} | |
Config.DEBUG = Config.DEBUG || this.get_config('debug'); | |
} | |
}; | |
/** | |
* returns the current config object for the library. | |
*/ | |
robotmiaLib.prototype.get_config = function(prop_name) { | |
return this['config'][prop_name]; | |
}; | |
/** | |
* Returns the value of the super property named property_name. If no such | |
* property is set, get_property() will return the undefined value. | |
* | |
* ### Notes: | |
* | |
* get_property() can only be called after the robotmia library has finished loading. | |
* init() has a loaded function available to handle this automatically. For example: | |
* | |
* // grab value for 'user_id' after the robotmia library has loaded | |
* robotmia.init('YOUR PROJECT TOKEN', { | |
* loaded: function(robotmia) { | |
* user_id = robotmia.get_property('user_id'); | |
* } | |
* }); | |
* | |
* @param {String} property_name The name of the super property you want to retrieve | |
*/ | |
robotmiaLib.prototype.get_property = function(property_name) { | |
return this['persistence']['props'][property_name]; | |
}; | |
robotmiaLib.prototype.toString = function() { | |
var name = this.get_config('name'); | |
if (name !== PRIMARY_INSTANCE_NAME) { | |
name = PRIMARY_INSTANCE_NAME + '.' + name; | |
} | |
return name; | |
}; | |
robotmiaLib.prototype._event_is_disabled = function(event_name) { | |
return _.isBlockedUA(userAgent) || | |
this._flags.disable_all_events || | |
_.include(this.__disabled_events, event_name); | |
}; | |
robotmiaLib.prototype._check_and_handle_notifications = function(distinct_id) { | |
if (!distinct_id || this._flags.identify_called || this.get_config('disable_notifications')) { | |
return; | |
} | |
console.log('robotmia NOTIFICATION CHECK'); | |
var data = { | |
'verbose': true, | |
'version': '2', | |
'lib': 'web', | |
'token': this.get_config('token'), | |
'distinct_id': distinct_id | |
}; | |
var self = this; | |
this._send_request( | |
this.get_config('api_host') + '/decide/', | |
data, | |
this._prepare_callback(function(r) { | |
if (r['notifications'] && r['notifications'].length > 0) { | |
self._show_notification.call(self, r['notifications'][0]); | |
} | |
}) | |
); | |
}; | |
robotmiaLib.prototype._show_notification = function(notification_data) { | |
var notification = new MPNotif(notification_data, this); | |
notification.show(); | |
}; | |
robotmiaPeople.prototype._init = function(robotmia_instance) { | |
this._robotmia = robotmia_instance; | |
}; | |
/* | |
* Set properties on a user record. | |
* | |
* ### Usage: | |
* | |
* robotmia.people.set('gender', 'm'); | |
* | |
* // or set multiple properties at once | |
* robotmia.people.set({ | |
* 'Company': 'Acme', | |
* 'Plan': 'Premium', | |
* 'Upgrade date': new Date() | |
* }); | |
* // properties can be strings, integers, dates, or lists | |
* | |
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. | |
* @param {*} [to] A value to set on the given property name | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.set = function(prop, to, callback) { | |
var data = {}; | |
var $set = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function(v, k) { | |
if (!this._is_reserved_property(k)) { | |
$set[k] = v; | |
} | |
}, this); | |
callback = to; | |
} else { | |
$set[prop] = to; | |
} | |
// make sure that the referrer info has been updated and saved | |
if (this._get_config('save_referrer')) { | |
this._robotmia['persistence'].update_referrer_info(document.referrer); | |
} | |
// update $set object with default people properties | |
$set = _.extend( | |
{}, | |
_.info.people_properties(), | |
this._robotmia['persistence'].get_referrer_info(), | |
$set | |
); | |
data[SET_ACTION] = $set; | |
return this._send_request(data, callback); | |
}; | |
/* | |
* Set properties on a user record, only if they do not yet exist. | |
* This will not overwrite previous people property values, unlike | |
* people.set(). | |
* | |
* ### Usage: | |
* | |
* robotmia.people.set_once('First Login Date', new Date()); | |
* | |
* // or set multiple properties at once | |
* robotmia.people.set_once({ | |
* 'First Login Date': new Date(), | |
* 'Starting Plan': 'Premium' | |
* }); | |
* | |
* // properties can be strings, integers or dates | |
* | |
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. | |
* @param {*} [to] A value to set on the given property name | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.set_once = function(prop, to, callback) { | |
var data = {}; | |
var $set_once = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function(v, k) { | |
if (!this._is_reserved_property(k)) { | |
$set_once[k] = v; | |
} | |
}, this); | |
callback = to; | |
} else { | |
$set_once[prop] = to; | |
} | |
data[SET_ONCE_ACTION] = $set_once; | |
return this._send_request(data, callback); | |
}; | |
/* | |
* Increment/decrement numeric people analytics properties. | |
* | |
* ### Usage: | |
* | |
* robotmia.people.increment('page_views', 1); | |
* | |
* // or, for convenience, if you're just incrementing a counter by | |
* // 1, you can simply do | |
* robotmia.people.increment('page_views'); | |
* | |
* // to decrement a counter, pass a negative number | |
* robotmia.people.increment('credits_left', -1); | |
* | |
* // like robotmia.people.set(), you can increment multiple | |
* // properties at once: | |
* robotmia.people.increment({ | |
* counter1: 1, | |
* counter2: 6 | |
* }); | |
* | |
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. | |
* @param {Number} [by] An amount to increment the given property | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.increment = function(prop, by, callback) { | |
var data = {}; | |
var $add = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function(v, k) { | |
if (!this._is_reserved_property(k)) { | |
if (isNaN(parseFloat(v))) { | |
console.error('Invalid increment value passed to robotmia.people.increment - must be a number'); | |
return; | |
} else { | |
$add[k] = v; | |
} | |
} | |
}, this); | |
callback = by; | |
} else { | |
// convenience: robotmia.people.increment('property'); will | |
// increment 'property' by 1 | |
if (_.isUndefined(by)) { | |
by = 1; | |
} | |
$add[prop] = by; | |
} | |
data[ADD_ACTION] = $add; | |
return this._send_request(data, callback); | |
}; | |
/* | |
* Append a value to a list-valued people analytics property. | |
* | |
* ### Usage: | |
* | |
* // append a value to a list, creating it if needed | |
* robotmia.people.append('pages_visited', 'homepage'); | |
* | |
* // like robotmia.people.set(), you can append multiple | |
* // properties at once: | |
* robotmia.people.append({ | |
* list1: 'bob', | |
* list2: 123 | |
* }); | |
* | |
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. | |
* @param {*} [value] An item to append to the list | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.append = function(list_name, value, callback) { | |
var data = {}; | |
var $append = {}; | |
if (_.isObject(list_name)) { | |
_.each(list_name, function(v, k) { | |
if (!this._is_reserved_property(k)) { | |
$append[k] = v; | |
} | |
}, this); | |
callback = value; | |
} else { | |
$append[list_name] = value; | |
} | |
data[APPEND_ACTION] = $append; | |
return this._send_request(data, callback); | |
}; | |
/* | |
* Merge a given list with a list-valued people analytics property, | |
* excluding duplicate values. | |
* | |
* ### Usage: | |
* | |
* // merge a value to a list, creating it if needed | |
* robotmia.people.union('pages_visited', 'homepage'); | |
* | |
* // like robotmia.people.set(), you can append multiple | |
* // properties at once: | |
* robotmia.people.union({ | |
* list1: 'bob', | |
* list2: 123 | |
* }); | |
* | |
* // like robotmia.people.append(), you can append multiple | |
* // values to the same list: | |
* robotmia.people.union({ | |
* list1: ['bob', 'billy'] | |
* }); | |
* | |
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. | |
* @param {*} [value] Value / values to merge with the given property | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.union = function(list_name, values, callback) { | |
var data = {}; | |
var $union = {}; | |
if (_.isObject(list_name)) { | |
_.each(list_name, function(v, k) { | |
if (!this._is_reserved_property(k)) { | |
$union[k] = _.isArray(v) ? v : [v]; | |
} | |
}, this); | |
callback = values; | |
} else { | |
$union[list_name] = _.isArray(values) ? values : [values]; | |
} | |
data[UNION_ACTION] = $union; | |
return this._send_request(data, callback); | |
}; | |
/* | |
* Record that you have charged the current user a certain amount | |
* of money. Charges recorded with track_charge() will appear in the | |
* robotmia revenue report. | |
* | |
* ### Usage: | |
* | |
* // charge a user $50 | |
* robotmia.people.track_charge(50); | |
* | |
* // charge a user $30.50 on the 2nd of january | |
* robotmia.people.track_charge(30.50, { | |
* '$time': new Date('jan 1 2012') | |
* }); | |
* | |
* @param {Number} amount The amount of money charged to the current user | |
* @param {Object} [properties] An associative array of properties associated with the charge | |
* @param {Function} [callback] If provided, the callback will be called when the server responds | |
*/ | |
robotmiaPeople.prototype.track_charge = function(amount, properties, callback) { | |
if (!_.isNumber(amount)) { | |
amount = parseFloat(amount); | |
if (isNaN(amount)) { | |
console.error('Invalid value passed to robotmia.people.track_charge - must be a number'); | |
return; | |
} | |
} | |
return this.append('$transactions', _.extend({ | |
'$amount': amount | |
}, properties), callback); | |
}; | |
/* | |
* Permanently clear all revenue report transactions from the | |
* current user's people analytics profile. | |
* | |
* ### Usage: | |
* | |
* robotmia.people.clear_charges(); | |
* | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
robotmiaPeople.prototype.clear_charges = function(callback) { | |
return this.set('$transactions', [], callback); | |
}; | |
/* | |
* Permanently deletes the current people analytics profile from | |
* robotmia (using the current distinct_id). | |
* | |
* ### Usage: | |
* | |
* // remove the all data you have stored about the current user | |
* robotmia.people.delete_user(); | |
* | |
*/ | |
robotmiaPeople.prototype.delete_user = function() { | |
if (!this._identify_called()) { | |
console.error('robotmia.people.delete_user() requires you to call identify() first'); | |
return; | |
} | |
var data = {'$delete': this._robotmia.get_distinct_id()}; | |
return this._send_request(data); | |
}; | |
robotmiaPeople.prototype.toString = function() { | |
return this._robotmia.toString() + '.people'; | |
}; | |
robotmiaPeople.prototype._send_request = function(data, callback) { | |
data['$token'] = this._get_config('token'); | |
data['$distinct_id'] = this._robotmia.get_distinct_id(); | |
var date_encoded_data = _.encodeDates(data); | |
var truncated_data = _.truncate(date_encoded_data, 255); | |
var json_data = _.JSONEncode(date_encoded_data); | |
var encoded_data = _.base64Encode(json_data); | |
if (!this._identify_called()) { | |
this._enqueue(data); | |
if (!_.isUndefined(callback)) { | |
if (this._get_config('verbose')) { | |
callback({status: -1, error: null}); | |
} else { | |
callback(-1); | |
} | |
} | |
return truncated_data; | |
} | |
console.log('robotmia PEOPLE REQUEST:'); | |
console.log(truncated_data); | |
this._robotmia._send_request( | |
this._get_config('api_host') + '/engage/', | |
{'data': encoded_data}, | |
this._robotmia._prepare_callback(callback, truncated_data) | |
); | |
return truncated_data; | |
}; | |
robotmiaPeople.prototype._get_config = function(conf_var) { | |
return this._robotmia.get_config(conf_var); | |
}; | |
robotmiaPeople.prototype._identify_called = function() { | |
return this._robotmia._flags.identify_called === true; | |
}; | |
// Queue up engage operations if identify hasn't been called yet. | |
robotmiaPeople.prototype._enqueue = function(data) { | |
if (SET_ACTION in data) { | |
this._robotmia['persistence']._add_to_people_queue(SET_ACTION, data); | |
} else if (SET_ONCE_ACTION in data) { | |
this._robotmia['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); | |
} else if (ADD_ACTION in data) { | |
this._robotmia['persistence']._add_to_people_queue(ADD_ACTION, data); | |
} else if (APPEND_ACTION in data) { | |
this._robotmia['persistence']._add_to_people_queue(APPEND_ACTION, data); | |
} else if (UNION_ACTION in data) { | |
this._robotmia['persistence']._add_to_people_queue(UNION_ACTION, data); | |
} else { | |
console.error('Invalid call to _enqueue():', data); | |
} | |
}; | |
// Flush queued engage operations - order does not matter, | |
// and there are network level race conditions anyway | |
robotmiaPeople.prototype._flush = function(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback) { | |
var _this = this; | |
var $set_queue = _.extend({}, this._robotmia['persistence']._get_queue(SET_ACTION)); | |
var $set_once_queue = _.extend({}, this._robotmia['persistence']._get_queue(SET_ONCE_ACTION)); | |
var $add_queue = _.extend({}, this._robotmia['persistence']._get_queue(ADD_ACTION)); | |
var $append_queue = this._robotmia['persistence']._get_queue(APPEND_ACTION); | |
var $union_queue = _.extend({}, this._robotmia['persistence']._get_queue(UNION_ACTION)); | |
if (!_.isUndefined($set_queue) && _.isObject($set_queue) && !_.isEmptyObject($set_queue)) { | |
_this._robotmia['persistence']._pop_from_people_queue(SET_ACTION, $set_queue); | |
this.set($set_queue, function(response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response === 0) { | |
_this._robotmia['persistence']._add_to_people_queue(SET_ACTION, $set_queue); | |
} | |
if (!_.isUndefined(_set_callback)) { | |
_set_callback(response, data); | |
} | |
}); | |
} | |
if (!_.isUndefined($set_once_queue) && _.isObject($set_once_queue) && !_.isEmptyObject($set_once_queue)) { | |
_this._robotmia['persistence']._pop_from_people_queue(SET_ONCE_ACTION, $set_once_queue); | |
this.set_once($set_once_queue, function(response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response === 0) { | |
_this._robotmia['persistence']._add_to_people_queue(SET_ONCE_ACTION, $set_once_queue); | |
} | |
if (!_.isUndefined(_set_once_callback)) { | |
_set_once_callback(response, data); | |
} | |
}); | |
} | |
if (!_.isUndefined($add_queue) && _.isObject($add_queue) && !_.isEmptyObject($add_queue)) { | |
_this._robotmia['persistence']._pop_from_people_queue(ADD_ACTION, $add_queue); | |
this.increment($add_queue, function(response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response === 0) { | |
_this._robotmia['persistence']._add_to_people_queue(ADD_ACTION, $add_queue); | |
} | |
if (!_.isUndefined(_add_callback)) { | |
_add_callback(response, data); | |
} | |
}); | |
} | |
if (!_.isUndefined($union_queue) && _.isObject($union_queue) && !_.isEmptyObject($union_queue)) { | |
_this._robotmia['persistence']._pop_from_people_queue(UNION_ACTION, $union_queue); | |
this.union($union_queue, function(response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response === 0) { | |
_this._robotmia['persistence']._add_to_people_queue(UNION_ACTION, $union_queue); | |
} | |
if (!_.isUndefined(_union_callback)) { | |
_union_callback(response, data); | |
} | |
}); | |
} | |
// we have to fire off each $append individually since there is | |
// no concat method server side | |
if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { | |
var $append_item; | |
var callback = function(response, data) { | |
if (response === 0) { | |
_this._robotmia['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); | |
} | |
if (!_.isUndefined(_append_callback)) { | |
_append_callback(response, data); | |
} | |
}; | |
for (var i = $append_queue.length - 1; i >= 0; i--) { | |
$append_item = $append_queue.pop(); | |
_this.append($append_item, callback); | |
} | |
// Save the shortened append queue | |
_this._robotmia['persistence'].save(); | |
} | |
}; | |
robotmiaPeople.prototype._is_reserved_property = function(prop) { | |
return prop === '$distinct_id' || prop === '$token'; | |
}; | |
// Internal class for notification display | |
robotmiaLib._Notification = function(notif_data, robotmia_instance) { | |
_.bind_instance_methods(this); | |
this.robotmia = robotmia_instance; | |
this.persistence = this.robotmia['persistence']; | |
this.campaign_id = _.escapeHTML(notif_data['id']); | |
this.message_id = _.escapeHTML(notif_data['message_id']); | |
this.body = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '<br/>'); | |
this.cta = _.escapeHTML(notif_data['cta']) || 'Close'; | |
this.notif_type = _.escapeHTML(notif_data['type']) || 'takeover'; | |
this.style = _.escapeHTML(notif_data['style']) || 'light'; | |
this.title = _.escapeHTML(notif_data['title']) || ''; | |
this.video_width = MPNotif.VIDEO_WIDTH; | |
this.video_height = MPNotif.VIDEO_HEIGHT; | |
// These fields are url-sanitized in the backend already. | |
this.dest_url = notif_data['cta_url'] || null; | |
this.image_url = notif_data['image_url'] || null; | |
this.thumb_image_url = notif_data['thumb_image_url'] || null; | |
this.video_url = notif_data['video_url'] || null; | |
this.clickthrough = true; | |
if (!this.dest_url) { | |
this.dest_url = '#dismiss'; | |
this.clickthrough = false; | |
} | |
this.mini = this.notif_type === 'mini'; | |
if (!this.mini) { | |
this.notif_type = 'takeover'; | |
} | |
this.notif_width = !this.mini ? MPNotif.NOTIF_WIDTH : MPNotif.NOTIF_WIDTH_MINI; | |
this._set_client_config(); | |
this.imgs_to_preload = this._init_image_html(); | |
this._init_video(); | |
}; | |
MPNotif = robotmiaLib._Notification; | |
MPNotif.ANIM_TIME = 200; | |
MPNotif.MARKUP_PREFIX = 'robotmia-notification'; | |
MPNotif.BG_OPACITY = 0.6; | |
MPNotif.NOTIF_TOP = 25; | |
MPNotif.NOTIF_START_TOP = 200; | |
MPNotif.NOTIF_WIDTH = 388; | |
MPNotif.NOTIF_WIDTH_MINI = 420; | |
MPNotif.NOTIF_HEIGHT_MINI = 85; | |
MPNotif.THUMB_BORDER_SIZE = 5; | |
MPNotif.THUMB_IMG_SIZE = 60; | |
MPNotif.THUMB_OFFSET = Math.round(MPNotif.THUMB_IMG_SIZE / 2); | |
MPNotif.VIDEO_WIDTH = 595; | |
MPNotif.VIDEO_HEIGHT = 334; | |
MPNotif.prototype.show = function() { | |
var self = this; | |
this._set_client_config(); | |
// don't display until HTML body exists | |
if (!this.body_el) { | |
setTimeout(function() { self.show(); }, 300); | |
return; | |
} | |
this._init_styles(); | |
this._init_notification_el(); | |
// wait for any images to load before showing notification | |
this._preload_images(this._attach_and_animate); | |
}; | |
MPNotif.prototype.dismiss = _.safewrap(function() { | |
if (!this.marked_as_shown) { | |
// unexpected condition: user interacted with notif even though we didn't consider it | |
// visible (see _mark_as_shown()); send tracking signals to mark delivery | |
this._mark_delivery({'invisible': true}); | |
} | |
var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el(); | |
if (this.use_transitions) { | |
this._remove_class('bg', 'visible'); | |
this._add_class(exiting_el, 'exiting'); | |
setTimeout(this._remove_notification_el, MPNotif.ANIM_TIME); | |
} else { | |
var notif_attr, notif_start, notif_goal; | |
if (this.mini) { | |
notif_attr = 'right'; | |
notif_start = 20; | |
notif_goal = -100; | |
} else { | |
notif_attr = 'top'; | |
notif_start = MPNotif.NOTIF_TOP; | |
notif_goal = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; | |
} | |
this._animate_els([ | |
{ | |
el: this._get_el('bg'), | |
attr: 'opacity', | |
start: MPNotif.BG_OPACITY, | |
goal: 0.0 | |
}, | |
{ | |
el: exiting_el, | |
attr: 'opacity', | |
start: 1.0, | |
goal: 0.0 | |
}, | |
{ | |
el: exiting_el, | |
attr: notif_attr, | |
start: notif_start, | |
goal: notif_goal | |
} | |
], MPNotif.ANIM_TIME, this._remove_notification_el); | |
} | |
}); | |
MPNotif.prototype._add_class = _.safewrap(function(el, class_name) { | |
class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; | |
if (typeof el === 'string') { | |
el = this._get_el(el); | |
} | |
if (!el.className) { | |
el.className = class_name; | |
} else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) { | |
el.className += ' ' + class_name; | |
} | |
}); | |
MPNotif.prototype._remove_class = _.safewrap(function(el, class_name) { | |
class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; | |
if (typeof el === 'string') { | |
el = this._get_el(el); | |
} | |
if (el.className) { | |
el.className = (' ' + el.className + ' ') | |
.replace(' ' + class_name + ' ', '') | |
.replace(/^[\s\xA0]+/, '') | |
.replace(/[\s\xA0]+$/, ''); | |
} | |
}); | |
MPNotif.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) { | |
var self = this, | |
in_progress = false, | |
ai, anim, | |
cur_time = 1 * new Date(), time_diff; | |
start_time = start_time || cur_time; | |
time_diff = cur_time - start_time; | |
for (ai = 0; ai < anims.length; ai++) { | |
anim = anims[ai]; | |
if (typeof anim.val === 'undefined') { | |
anim.val = anim.start; | |
} | |
if (anim.val !== anim.goal) { | |
in_progress = true; | |
var anim_diff = anim.goal - anim.start, | |
anim_dir = anim.goal >= anim.start ? 1 : -1; | |
anim.val = anim.start + anim_diff * time_diff / mss; | |
if (anim.attr !== 'opacity') { | |
anim.val = Math.round(anim.val); | |
} | |
if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) { | |
anim.val = anim.goal; | |
} | |
} | |
} | |
if (!in_progress) { | |
if (done_cb) { | |
done_cb(); | |
} | |
return; | |
} | |
for (ai = 0; ai < anims.length; ai++) { | |
anim = anims[ai]; | |
if (anim.el) { | |
var suffix = anim.attr === 'opacity' ? '' : 'px'; | |
anim.el.style[anim.attr] = String(anim.val) + suffix; | |
} | |
} | |
setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10); | |
}); | |
MPNotif.prototype._attach_and_animate = _.safewrap(function() { | |
var self = this; | |
// no possibility to double-display | |
if (this.shown || this._get_shown_campaigns()[this.campaign_id]) { | |
return; | |
} | |
this.shown = true; | |
this.body_el.appendChild(this.notification_el); | |
setTimeout(function() { | |
var notif_el = self._get_notification_display_el(); | |
if (self.use_transitions) { | |
if (!self.mini) { | |
self._add_class('bg', 'visible'); | |
} | |
self._add_class(notif_el, 'visible'); | |
self._mark_as_shown(); | |
} else { | |
var notif_attr, notif_start, notif_goal; | |
if (self.mini) { | |
notif_attr = 'right'; | |
notif_start = -100; | |
notif_goal = 20; | |
} else { | |
notif_attr = 'top'; | |
notif_start = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; | |
notif_goal = MPNotif.NOTIF_TOP; | |
} | |
self._animate_els([ | |
{ | |
el: self._get_el('bg'), | |
attr: 'opacity', | |
start: 0.0, | |
goal: MPNotif.BG_OPACITY | |
}, | |
{ | |
el: notif_el, | |
attr: 'opacity', | |
start: 0.0, | |
goal: 1.0 | |
}, | |
{ | |
el: notif_el, | |
attr: notif_attr, | |
start: notif_start, | |
goal: notif_goal | |
} | |
], MPNotif.ANIM_TIME, self._mark_as_shown); | |
} | |
}, 100); | |
_.register_event(self._get_el('cancel'), 'click', function(e) { | |
e.preventDefault(); | |
self.dismiss(); | |
}); | |
var click_el = self._get_el('button') || | |
self._get_el('mini-content'); | |
_.register_event(click_el, 'click', function(e) { | |
e.preventDefault(); | |
if (self.show_video) { | |
self._track_event('$campaign_open', {'$resource_type': 'video'}); | |
self._switch_to_video(); | |
} else { | |
self.dismiss(); | |
if (self.clickthrough) { | |
self._track_event('$campaign_open', {'$resource_type': 'link'}, function() { | |
window.location.href = self.dest_url; | |
}); | |
} | |
} | |
}); | |
}); | |
MPNotif.prototype._get_el = function(id) { | |
return document.getElementById(MPNotif.MARKUP_PREFIX + '-' + id); | |
}; | |
MPNotif.prototype._get_notification_display_el = function() { | |
return this._get_el(this.notif_type); | |
}; | |
MPNotif.prototype._get_shown_campaigns = function() { | |
return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {}); | |
}; | |
MPNotif.prototype._browser_lte = function(browser, version) { | |
return this.browser_versions[browser] && this.browser_versions[browser] <= version; | |
}; | |
MPNotif.prototype._init_image_html = function() { | |
var imgs_to_preload = []; | |
if (!this.mini) { | |
if (this.image_url) { | |
imgs_to_preload.push(this.image_url); | |
this.img_html = '<img id="img" src="' + this.image_url + '"/>'; | |
} else { | |
this.img_html = ''; | |
} | |
if (this.thumb_image_url) { | |
imgs_to_preload.push(this.thumb_image_url); | |
this.thumb_img_html = | |
'<div id="thumbborder-wrapper"><div id="thumbborder"></div></div>' + | |
'<img id="thumbnail"' + | |
' src="' + this.thumb_image_url + '"' + | |
' width="' + MPNotif.THUMB_IMG_SIZE + '"' + | |
' height="' + MPNotif.THUMB_IMG_SIZE + '"' + | |
'/>' + | |
'<div id="thumbspacer"></div>'; | |
} else { | |
this.thumb_img_html = ''; | |
} | |
} else { | |
this.thumb_image_url = this.thumb_image_url || '//cdn.mxpnl.com/site_media/images/icons/notifications/mini-news-dark.png'; | |
imgs_to_preload.push(this.thumb_image_url); | |
} | |
return imgs_to_preload; | |
}; | |
MPNotif.prototype._init_notification_el = function() { | |
var notification_html = ''; | |
var video_src = ''; | |
var video_html = ''; | |
var cancel_html = '<div id="cancel">' + | |
'<div id="cancel-icon"></div>' + | |
'</div>'; | |
this.notification_el = document.createElement('div'); | |
this.notification_el.id = MPNotif.MARKUP_PREFIX + '-wrapper'; | |
if (!this.mini) { | |
// TAKEOVER notification | |
var close_html = (this.clickthrough || this.show_video) ? '' : '<div id="button-close"></div>', | |
play_html = this.show_video ? '<div id="button-play"></div>' : ''; | |
if (this._browser_lte('ie', 7)) { | |
close_html = ''; | |
play_html = ''; | |
} | |
notification_html = | |
'<div id="takeover">' + | |
this.thumb_img_html + | |
'<div id="mainbox">' + | |
cancel_html + | |
'<div id="content">' + | |
this.img_html + | |
'<div id="title">' + this.title + '</div>' + | |
'<div id="body">' + this.body + '</div>' + | |
'<div id="tagline">' + | |
'<a href="http://robotmia.com?from=inapp" target="_blank">POWERED BY robotmia</a>' + | |
'</div>' + | |
'</div>' + | |
'<div id="button">' + | |
close_html + | |
'<a id="button-link" href="' + this.dest_url + '">' + this.cta + '</a>' + | |
play_html + | |
'</div>' + | |
'</div>' + | |
'</div>'; | |
} else { | |
// MINI notification | |
notification_html = | |
'<div id="mini">' + | |
'<div id="mainbox">' + | |
cancel_html + | |
'<div id="mini-content">' + | |
'<div id="mini-icon">' + | |
'<div id="mini-icon-img"></div>' + | |
'</div>' + | |
'<div id="body">' + | |
'<div id="body-text"><div>' + this.body + '</div></div>' + | |
'</div>' + | |
'</div>' + | |
'</div>' + | |
'<div id="mini-border"></div>' + | |
'</div>'; | |
} | |
if (this.youtube_video) { | |
video_src = '//www.youtube.com/embed/' + this.youtube_video + | |
'?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080'; | |
if (this.yt_custom) { | |
video_src += '&enablejsapi=1&html5=1&controls=0'; | |
video_html = | |
'<div id="video-controls">' + | |
'<div id="video-progress" class="video-progress-el">' + | |
'<div id="video-progress-total" class="video-progress-el"></div>' + | |
'<div id="video-elapsed" class="video-progress-el"></div>' + | |
'</div>' + | |
'<div id="video-time" class="video-progress-el"></div>' + | |
'</div>'; | |
} | |
} else if (this.vimeo_video) { | |
video_src = '//player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0'; | |
} | |
if (this.show_video) { | |
this.video_iframe = | |
'<iframe id="' + MPNotif.MARKUP_PREFIX + '-video-frame" ' + | |
'width="' + this.video_width + '" height="' + this.video_height + '" ' + | |
' src="' + video_src + '"' + | |
' frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen="1" scrolling="no"' + | |
'></iframe>'; | |
video_html = | |
'<div id="video-' + (this.flip_animate ? '' : 'no') + 'flip">' + | |
'<div id="video">' + | |
'<div id="video-holder"></div>' + | |
video_html + | |
'</div>' + | |
'</div>'; | |
} | |
var main_html = video_html + notification_html; | |
if (this.flip_animate) { | |
main_html = | |
(this.mini ? notification_html : '') + | |
'<div id="flipcontainer"><div id="flipper">' + | |
(this.mini ? video_html : main_html) + | |
'</div></div>'; | |
} | |
this.notification_el.innerHTML = | |
('<div id="overlay" class="' + this.notif_type + '">' + | |
'<div id="campaignid-' + this.campaign_id + '">' + | |
'<div id="bgwrapper">' + | |
'<div id="bg"></div>' + | |
main_html + | |
'</div>' + | |
'</div>' + | |
'</div>') | |
.replace(/class=\"/g, 'class="' + MPNotif.MARKUP_PREFIX + '-') | |
.replace(/id=\"/g, 'id="' + MPNotif.MARKUP_PREFIX + '-'); | |
}; | |
MPNotif.prototype._init_styles = function() { | |
if (this.style === 'dark') { | |
this.style_vals = { | |
bg: '#1d1f25', | |
bg_actions: '#282b32', | |
bg_hover: '#3a4147', | |
bg_light: '#4a5157', | |
border_gray: '#32353c', | |
cancel_opacity: '0.4', | |
mini_hover: '#2a3137', | |
text_title: '#fff', | |
text_main: '#9498a3', | |
text_tagline: '#464851', | |
text_hover: '#ddd' | |
}; | |
} else { | |
this.style_vals = { | |
bg: '#fff', | |
bg_actions: '#e7eaee', | |
bg_hover: '#eceff3', | |
bg_light: '#f5f5f5', | |
border_gray: '#e4ecf2', | |
cancel_opacity: '1.0', | |
mini_hover: '#fafafa', | |
text_title: '#5c6578', | |
text_main: '#8b949b', | |
text_tagline: '#ced9e6', | |
text_hover: '#7c8598' | |
}; | |
} | |
var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)', | |
video_shadow = shadow, | |
mini_shadow = shadow, | |
thumb_total_size = MPNotif.THUMB_IMG_SIZE + MPNotif.THUMB_BORDER_SIZE * 2, | |
anim_seconds = (MPNotif.ANIM_TIME / 1000) + 's'; | |
if (this.mini) { | |
shadow = 'none'; | |
} | |
// don't display on small viewports | |
var notif_media_queries = {}, | |
min_width = MPNotif.NOTIF_WIDTH_MINI + 20; | |
notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = { | |
'#overlay': { | |
'display': 'none' | |
} | |
}; | |
var notif_styles = { | |
'.flipped': { | |
'transform': 'rotateY(180deg)' | |
}, | |
'#overlay': { | |
'position': 'fixed', | |
'top': '0', | |
'left': '0', | |
'width': '100%', | |
'height': '100%', | |
'overflow': 'auto', | |
'text-align': 'center', | |
'z-index': '10000', | |
'font-family': '"Helvetica", "Arial", sans-serif', | |
'-webkit-font-smoothing': 'antialiased', | |
'-moz-osx-font-smoothing': 'grayscale' | |
}, | |
'#overlay.mini': { | |
'height': '0', | |
'overflow': 'visible' | |
}, | |
'#overlay a': { | |
'width': 'initial', | |
'padding': '0', | |
'text-decoration': 'none', | |
'text-transform': 'none', | |
'color': 'inherit' | |
}, | |
'#bgwrapper': { | |
'position': 'relative', | |
'width': '100%', | |
'height': '100%' | |
}, | |
'#bg': { | |
'position': 'fixed', | |
'top': '0', | |
'left': '0', | |
'width': '100%', | |
'height': '100%', | |
'min-width': this.doc_width * 4 + 'px', | |
'min-height': this.doc_height * 4 + 'px', | |
'background-color': 'black', | |
'opacity': '0.0', | |
'-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8 | |
'filter': 'alpha(opacity=60)', // IE5-7 | |
'transition': 'opacity ' + anim_seconds | |
}, | |
'#bg.visible': { | |
'opacity': MPNotif.BG_OPACITY | |
}, | |
'.mini #bg': { | |
'width': '0', | |
'height': '0', | |
'min-width': '0' | |
}, | |
'#flipcontainer': { | |
'perspective': '1000px', | |
'position': 'absolute', | |
'width': '100%' | |
}, | |
'#flipper': { | |
'position': 'relative', | |
'transform-style': 'preserve-3d', | |
'transition': '0.3s' | |
}, | |
'#takeover': { | |
'position': 'absolute', | |
'left': '50%', | |
'width': MPNotif.NOTIF_WIDTH + 'px', | |
'margin-left': Math.round(-MPNotif.NOTIF_WIDTH / 2) + 'px', | |
'backface-visibility': 'hidden', | |
'transform': 'rotateY(0deg)', | |
'opacity': '0.0', | |
'top': MPNotif.NOTIF_START_TOP + 'px', | |
'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds | |
}, | |
'#takeover.visible': { | |
'opacity': '1.0', | |
'top': MPNotif.NOTIF_TOP + 'px' | |
}, | |
'#takeover.exiting': { | |
'opacity': '0.0', | |
'top': MPNotif.NOTIF_START_TOP + 'px' | |
}, | |
'#thumbspacer': { | |
'height': MPNotif.THUMB_OFFSET + 'px' | |
}, | |
'#thumbborder-wrapper': { | |
'position': 'absolute', | |
'top': (-MPNotif.THUMB_BORDER_SIZE) + 'px', | |
'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET - MPNotif.THUMB_BORDER_SIZE) + 'px', | |
'width': thumb_total_size + 'px', | |
'height': (thumb_total_size / 2) + 'px', | |
'overflow': 'hidden' | |
}, | |
'#thumbborder': { | |
'position': 'absolute', | |
'width': thumb_total_size + 'px', | |
'height': thumb_total_size + 'px', | |
'border-radius': thumb_total_size + 'px', | |
'background-color': this.style_vals.bg_actions, | |
'opacity': '0.5' | |
}, | |
'#thumbnail': { | |
'position': 'absolute', | |
'top': '0px', | |
'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET) + 'px', | |
'width': MPNotif.THUMB_IMG_SIZE + 'px', | |
'height': MPNotif.THUMB_IMG_SIZE + 'px', | |
'overflow': 'hidden', | |
'z-index': '100', | |
'border-radius': MPNotif.THUMB_IMG_SIZE + 'px' | |
}, | |
'#mini': { | |
'position': 'absolute', | |
'right': '20px', | |
'top': MPNotif.NOTIF_TOP + 'px', | |
'width': this.notif_width + 'px', | |
'height': MPNotif.NOTIF_HEIGHT_MINI * 2 + 'px', | |
'margin-top': 20 - MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'backface-visibility': 'hidden', | |
'opacity': '0.0', | |
'transform': 'rotateX(90deg)', | |
'transition': 'opacity 0.3s, transform 0.3s, right 0.3s' | |
}, | |
'#mini.visible': { | |
'opacity': '1.0', | |
'transform': 'rotateX(0deg)' | |
}, | |
'#mini.exiting': { | |
'opacity': '0.0', | |
'right': '-150px' | |
}, | |
'#mainbox': { | |
'border-radius': '4px', | |
'box-shadow': shadow, | |
'text-align': 'center', | |
'background-color': this.style_vals.bg, | |
'font-size': '14px', | |
'color': this.style_vals.text_main | |
}, | |
'#mini #mainbox': { | |
'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'border-radius': '3px', | |
'transition': 'background-color ' + anim_seconds | |
}, | |
'#mini-border': { | |
'height': (MPNotif.NOTIF_HEIGHT_MINI + 6) + 'px', | |
'width': (MPNotif.NOTIF_WIDTH_MINI + 6) + 'px', | |
'position': 'absolute', | |
'top': '-3px', | |
'left': '-3px', | |
'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'border-radius': '6px', | |
'opacity': '0.25', | |
'background-color': '#fff', | |
'z-index': '-1', | |
'box-shadow': mini_shadow | |
}, | |
'#mini-icon': { | |
'position': 'relative', | |
'display': 'inline-block', | |
'width': '75px', | |
'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'border-radius': '3px 0 0 3px', | |
'background-color': this.style_vals.bg_actions, | |
'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)', | |
'transition': 'background-color ' + anim_seconds | |
}, | |
'#mini:hover #mini-icon': { | |
'background-color': this.style_vals.mini_hover | |
}, | |
'#mini:hover #mainbox': { | |
'background-color': this.style_vals.mini_hover | |
}, | |
'#mini-icon-img': { | |
'position': 'absolute', | |
'background-image': 'url(' + this.thumb_image_url + ')', | |
'width': '48px', | |
'height': '48px', | |
'top': '20px', | |
'left': '12px' | |
}, | |
'#content': { | |
'padding': '30px 20px 0px 20px' | |
}, | |
'#mini-content': { | |
'text-align': 'left', | |
'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'cursor': 'pointer' | |
}, | |
'#img': { | |
'width': '328px', | |
'margin-top': '30px', | |
'border-radius': '5px' | |
}, | |
'#title': { | |
'max-height': '600px', | |
'overflow': 'hidden', | |
'word-wrap': 'break-word', | |
'padding': '25px 0px 20px 0px', | |
'font-size': '19px', | |
'font-weight': 'bold', | |
'color': this.style_vals.text_title | |
}, | |
'#body': { | |
'max-height': '600px', | |
'margin-bottom': '25px', | |
'overflow': 'hidden', | |
'word-wrap': 'break-word', | |
'line-height': '21px', | |
'font-size': '15px', | |
'font-weight': 'normal', | |
'text-align': 'left' | |
}, | |
'#mini #body': { | |
'display': 'inline-block', | |
'max-width': '250px', | |
'margin': '0 0 0 30px', | |
'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', | |
'font-size': '16px', | |
'letter-spacing': '0.8px', | |
'color': this.style_vals.text_title | |
}, | |
'#mini #body-text': { | |
'display': 'table', | |
'height': MPNotif.NOTIF_HEIGHT_MINI + 'px' | |
}, | |
'#mini #body-text div': { | |
'display': 'table-cell', | |
'vertical-align': 'middle' | |
}, | |
'#tagline': { | |
'margin-bottom': '15px', | |
'font-size': '10px', | |
'font-weight': '600', | |
'letter-spacing': '0.8px', | |
'color': '#ccd7e0', | |
'text-align': 'left' | |
}, | |
'#tagline a': { | |
'color': this.style_vals.text_tagline, | |
'transition': 'color ' + anim_seconds | |
}, | |
'#tagline a:hover': { | |
'color': this.style_vals.text_hover | |
}, | |
'#cancel': { | |
'position': 'absolute', | |
'right': '0', | |
'width': '8px', | |
'height': '8px', | |
'padding': '10px', | |
'border-radius': '20px', | |
'margin': '12px 12px 0 0', | |
'box-sizing': 'content-box', | |
'cursor': 'pointer', | |
'transition': 'background-color ' + anim_seconds | |
}, | |
'#mini #cancel': { | |
'margin': '7px 7px 0 0' | |
}, | |
'#cancel-icon': { | |
'width': '8px', | |
'height': '8px', | |
'overflow': 'hidden', | |
'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)', | |
'opacity': this.style_vals.cancel_opacity | |
}, | |
'#cancel:hover': { | |
'background-color': this.style_vals.bg_hover | |
}, | |
'#button': { | |
'display': 'block', | |
'height': '60px', | |
'line-height': '60px', | |
'text-align': 'center', | |
'background-color': this.style_vals.bg_actions, | |
'border-radius': '0 0 4px 4px', | |
'overflow': 'hidden', | |
'cursor': 'pointer', | |
'transition': 'background-color ' + anim_seconds | |
}, | |
'#button-close': { | |
'display': 'inline-block', | |
'width': '9px', | |
'height': '60px', | |
'margin-right': '8px', | |
'vertical-align': 'top', | |
'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-' + this.style + '.png)', | |
'background-repeat': 'no-repeat', | |
'background-position': '0px 25px' | |
}, | |
'#button-play': { | |
'display': 'inline-block', | |
'width': '30px', | |
'height': '60px', | |
'margin-left': '15px', | |
'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-' + this.style + '-small.png)', | |
'background-repeat': 'no-repeat', | |
'background-position': '0px 15px' | |
}, | |
'a#button-link': { | |
'display': 'inline-block', | |
'vertical-align': 'top', | |
'text-align': 'center', | |
'font-size': '17px', | |
'font-weight': 'bold', | |
'overflow': 'hidden', | |
'word-wrap': 'break-word', | |
'color': this.style_vals.text_title, | |
'transition': 'color ' + anim_seconds | |
}, | |
'#button:hover': { | |
'background-color': this.style_vals.bg_hover, | |
'color': this.style_vals.text_hover | |
}, | |
'#button:hover a': { | |
'color': this.style_vals.text_hover | |
}, | |
'#video-noflip': { | |
'position': 'relative', | |
'top': (-this.video_height * 2) + 'px' | |
}, | |
'#video-flip': { | |
'backface-visibility': 'hidden', | |
'transform': 'rotateY(180deg)' | |
}, | |
'#video': { | |
'position': 'absolute', | |
'width': (this.video_width - 1) + 'px', | |
'height': this.video_height + 'px', | |
'top': MPNotif.NOTIF_TOP + 'px', | |
'margin-top': '100px', | |
'left': '50%', | |
'margin-left': Math.round(-this.video_width / 2) + 'px', | |
'overflow': 'hidden', | |
'border-radius': '5px', | |
'box-shadow': video_shadow, | |
'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform | |
'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds | |
}, | |
'#video.exiting': { | |
'opacity': '0.0', | |
'top': this.video_height + 'px' | |
}, | |
'#video-holder': { | |
'position': 'absolute', | |
'width': (this.video_width - 1) + 'px', | |
'height': this.video_height + 'px', | |
'overflow': 'hidden', | |
'border-radius': '5px' | |
}, | |
'#video-frame': { | |
'margin-left': '-1px', | |
'width': this.video_width + 'px' | |
}, | |
'#video-controls': { | |
'opacity': '0', | |
'transition': 'opacity 0.5s' | |
}, | |
'#video:hover #video-controls': { | |
'opacity': '1.0' | |
}, | |
'#video .video-progress-el': { | |
'position': 'absolute', | |
'bottom': '0', | |
'height': '25px', | |
'border-radius': '0 0 0 5px' | |
}, | |
'#video-progress': { | |
'width': '90%' | |
}, | |
'#video-progress-total': { | |
'width': '100%', | |
'background-color': this.style_vals.bg, | |
'opacity': '0.7' | |
}, | |
'#video-elapsed': { | |
'width': '0', | |
'background-color': '#6cb6f5', | |
'opacity': '0.9' | |
}, | |
'#video #video-time': { | |
'width': '10%', | |
'right': '0', | |
'font-size': '11px', | |
'line-height': '25px', | |
'color': this.style_vals.text_main, | |
'background-color': '#666', | |
'border-radius': '0 0 5px 0' | |
} | |
}; | |
// IE hacks | |
if (this._browser_lte('ie', 8)) { | |
_.extend(notif_styles, { | |
'* html #overlay': { | |
'position': 'absolute' | |
}, | |
'* html #bg': { | |
'position': 'absolute' | |
}, | |
'html, body': { | |
'height': '100%' | |
} | |
}); | |
} | |
if (this._browser_lte('ie', 7)) { | |
_.extend(notif_styles, { | |
'#mini #body': { | |
'display': 'inline', | |
'zoom': '1', | |
'border': '1px solid ' + this.style_vals.bg_hover | |
}, | |
'#mini #body-text': { | |
'padding': '20px' | |
}, | |
'#mini #mini-icon': { | |
'display': 'none' | |
} | |
}); | |
} | |
// add vendor-prefixed style rules | |
var VENDOR_STYLES = ['backface-visibility', 'border-radius', 'box-shadow', 'opacity', | |
'perspective', 'transform', 'transform-style', 'transition'], | |
VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit']; | |
for (var selector in notif_styles) { | |
for (var si = 0; si < VENDOR_STYLES.length; si++) { | |
var prop = VENDOR_STYLES[si]; | |
if (prop in notif_styles[selector]) { | |
var val = notif_styles[selector][prop]; | |
for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) { | |
notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val; | |
} | |
} | |
} | |
} | |
var inject_styles = function(styles, media_queries) { | |
var create_style_text = function(style_defs) { | |
var st = ''; | |
for (var selector in style_defs) { | |
var mp_selector = selector | |
.replace(/#/g, '#' + MPNotif.MARKUP_PREFIX + '-') | |
.replace(/\./g, '.' + MPNotif.MARKUP_PREFIX + '-'); | |
st += '\n' + mp_selector + ' {'; | |
var props = style_defs[selector]; | |
for (var k in props) { | |
st += k + ':' + props[k] + ';'; | |
} | |
st += '}'; | |
} | |
return st; | |
}; | |
var create_media_query_text = function(mq_defs) { | |
var mqt = ''; | |
for (var mq in mq_defs) { | |
mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}'; | |
} | |
return mqt; | |
}; | |
var style_text = create_style_text(styles) + create_media_query_text(media_queries), | |
head_el = document.head || document.getElementsByTagName('head')[0] || document.documentElement, | |
style_el = document.createElement('style'); | |
head_el.appendChild(style_el); | |
style_el.setAttribute('type', 'text/css'); | |
if (style_el.styleSheet) { // IE | |
style_el.styleSheet.cssText = style_text; | |
} else { | |
style_el.textContent = style_text; | |
} | |
}; | |
inject_styles(notif_styles, notif_media_queries); | |
}; | |
MPNotif.prototype._init_video = _.safewrap(function() { | |
if (!this.video_url) { | |
return; | |
} | |
var self = this; | |
// Youtube iframe API compatibility | |
self.yt_custom = 'postMessage' in window; | |
self.dest_url = self.video_url; | |
var youtube_match = self.video_url.match( | |
// http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match | |
/(?:youtube(?:-nocookie)?\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i | |
), | |
vimeo_match = self.video_url.match( | |
/vimeo\.com\/.*?(\d+)/i | |
); | |
if (youtube_match) { | |
self.show_video = true; | |
self.youtube_video = youtube_match[1]; | |
if (self.yt_custom) { | |
window['onYouTubeIframeAPIReady'] = function() { | |
if (self._get_el('video-frame')) { | |
self._yt_video_ready(); | |
} | |
}; | |
// load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference | |
var tag = document.createElement('script'); | |
tag.src = '//www.youtube.com/iframe_api'; | |
var firstScriptTag = document.getElementsByTagName('script')[0]; | |
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | |
} | |
} else if (vimeo_match) { | |
self.show_video = true; | |
self.vimeo_video = vimeo_match[1]; | |
} | |
// IE <= 7, FF <= 3: fall through to video link rather than embedded player | |
if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) { | |
self.show_video = false; | |
self.clickthrough = true; | |
} | |
}); | |
MPNotif.prototype._mark_as_shown = _.safewrap(function() { | |
// click on background to dismiss | |
var self = this; | |
_.register_event(self._get_el('bg'), 'click', function() { | |
self.dismiss(); | |
}); | |
var get_style = function(el, style_name) { | |
var styles = {}; | |
if (document.defaultView && document.defaultView.getComputedStyle) { | |
styles = document.defaultView.getComputedStyle(el, null); // FF3 requires both args | |
} else if (el.currentStyle) { // IE | |
styles = el.currentStyle; | |
} | |
return styles[style_name]; | |
}; | |
if (this.campaign_id) { | |
var notif_el = this._get_el('overlay'); | |
if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') { | |
this._mark_delivery(); | |
} | |
} | |
}); | |
MPNotif.prototype._mark_delivery = _.safewrap(function(extra_props) { | |
if (!this.marked_as_shown) { | |
this.marked_as_shown = true; | |
if (this.campaign_id) { | |
// mark notification shown (local cache) | |
this._get_shown_campaigns()[this.campaign_id] = 1 * new Date(); | |
this.persistence.save(); | |
} | |
// track delivery | |
this._track_event('$campaign_delivery', extra_props); | |
// mark notification shown (robotmia property) | |
this.robotmia['people']['append']({ | |
'$campaigns': this.campaign_id, | |
'$notifications': { | |
'campaign_id': this.campaign_id, | |
'message_id': this.message_id, | |
'type': 'web', | |
'time': new Date() | |
} | |
}); | |
} | |
}); | |
MPNotif.prototype._preload_images = function(all_loaded_cb) { | |
var self = this; | |
if (this.imgs_to_preload.length === 0) { | |
all_loaded_cb(); | |
return; | |
} | |
var preloaded_imgs = 0; | |
var img_objs = []; | |
var onload = function() { | |
preloaded_imgs++; | |
if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) { | |
all_loaded_cb(); | |
all_loaded_cb = null; | |
} | |
}; | |
for (var i = 0; i < this.imgs_to_preload.length; i++) { | |
var img = new Image(); | |
img.onload = onload; | |
img.src = this.imgs_to_preload[i]; | |
if (img.complete) { | |
onload(); | |
} | |
img_objs.push(img); | |
} | |
// IE6/7 doesn't fire onload reliably | |
if (this._browser_lte('ie', 7)) { | |
setTimeout(function() { | |
var imgs_loaded = true; | |
for (i = 0; i < img_objs.length; i++) { | |
if (!img_objs[i].complete) { | |
imgs_loaded = false; | |
} | |
} | |
if (imgs_loaded && all_loaded_cb) { | |
all_loaded_cb(); | |
all_loaded_cb = null; | |
} | |
}, 500); | |
} | |
}; | |
MPNotif.prototype._remove_notification_el = _.safewrap(function() { | |
window.clearInterval(this._video_progress_checker); | |
this.notification_el.style.visibility = 'hidden'; | |
this.body_el.removeChild(this.notification_el); | |
}); | |
MPNotif.prototype._set_client_config = function() { | |
var get_browser_version = function(browser_ex) { | |
var match = navigator.userAgent.match(browser_ex); | |
return match && match[1]; | |
}; | |
this.browser_versions = {}; | |
this.browser_versions['chrome'] = get_browser_version(/Chrome\/(\d+)/); | |
this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/); | |
this.browser_versions['ie'] = get_browser_version(/MSIE (\d+).+/); | |
if (!this.browser_versions['ie'] && !(window.ActiveXObject) && 'ActiveXObject' in window) { | |
this.browser_versions['ie'] = 11; | |
} | |
this.body_el = document.body || document.getElementsByTagName('body')[0]; | |
if (this.body_el) { | |
this.doc_width = Math.max( | |
this.body_el.scrollWidth, document.documentElement.scrollWidth, | |
this.body_el.offsetWidth, document.documentElement.offsetWidth, | |
this.body_el.clientWidth, document.documentElement.clientWidth | |
); | |
this.doc_height = Math.max( | |
this.body_el.scrollHeight, document.documentElement.scrollHeight, | |
this.body_el.offsetHeight, document.documentElement.offsetHeight, | |
this.body_el.clientHeight, document.documentElement.clientHeight | |
); | |
} | |
// detect CSS compatibility | |
var ie_ver = this.browser_versions['ie']; | |
var sample_styles = document.createElement('div').style, | |
is_css_compatible = function(rule) { | |
if (rule in sample_styles) { | |
return true; | |
} | |
if (!ie_ver) { | |
rule = rule[0].toUpperCase() + rule.slice(1); | |
var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule]; | |
for (var i = 0; i < props.length; i++) { | |
if (props[i] in sample_styles) { | |
return true; | |
} | |
} | |
} | |
return false; | |
}; | |
this.use_transitions = this.body_el && | |
is_css_compatible('transition') && | |
is_css_compatible('transform'); | |
this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) && | |
this.body_el && | |
is_css_compatible('backfaceVisibility') && | |
is_css_compatible('perspective') && | |
is_css_compatible('transform'); | |
}; | |
MPNotif.prototype._switch_to_video = _.safewrap(function() { | |
var self = this, | |
anims = [ | |
{ | |
el: self._get_notification_display_el(), | |
attr: 'opacity', | |
start: 1.0, | |
goal: 0.0 | |
}, | |
{ | |
el: self._get_notification_display_el(), | |
attr: 'top', | |
start: MPNotif.NOTIF_TOP, | |
goal: -500 | |
}, | |
{ | |
el: self._get_el('video-noflip'), | |
attr: 'opacity', | |
start: 0.0, | |
goal: 1.0 | |
}, | |
{ | |
el: self._get_el('video-noflip'), | |
attr: 'top', | |
start: -self.video_height * 2, | |
goal: 0 | |
} | |
]; | |
if (self.mini) { | |
var bg = self._get_el('bg'), | |
overlay = self._get_el('overlay'); | |
bg.style.width = '100%'; | |
bg.style.height = '100%'; | |
overlay.style.width = '100%'; | |
self._add_class(self._get_notification_display_el(), 'exiting'); | |
self._add_class(bg, 'visible'); | |
anims.push({ | |
el: self._get_el('bg'), | |
attr: 'opacity', | |
start: 0.0, | |
goal: MPNotif.BG_OPACITY | |
}); | |
} | |
var video_el = self._get_el('video-holder'); | |
video_el.innerHTML = self.video_iframe; | |
var video_ready = function() { | |
if (window['YT'] && window['YT']['loaded']) { | |
self._yt_video_ready(); | |
} | |
self.showing_video = true; | |
self._get_notification_display_el().style.visibility = 'hidden'; | |
}; | |
if (self.flip_animate) { | |
self._add_class('flipper', 'flipped'); | |
setTimeout(video_ready, MPNotif.ANIM_TIME); | |
} else { | |
self._animate_els(anims, MPNotif.ANIM_TIME, video_ready); | |
} | |
}); | |
MPNotif.prototype._track_event = function(event_name, properties, cb) { | |
if (this.campaign_id) { | |
properties = properties || {}; | |
properties = _.extend(properties, { | |
'campaign_id': this.campaign_id, | |
'message_id': this.message_id, | |
'message_type': 'web_inapp', | |
'message_subtype': this.notif_type | |
}); | |
this.robotmia['track'](event_name, properties, cb); | |
} else if (cb) { | |
cb.call(); | |
} | |
}; | |
MPNotif.prototype._yt_video_ready = _.safewrap(function() { | |
var self = this; | |
if (self.video_inited) { | |
return; | |
} | |
self.video_inited = true; | |
var progress_bar = self._get_el('video-elapsed'), | |
progress_time = self._get_el('video-time'), | |
progress_el = self._get_el('video-progress'); | |
new window['YT']['Player'](MPNotif.MARKUP_PREFIX + '-video-frame', { | |
'events': { | |
'onReady': function(event) { | |
var ytplayer = event['target'], | |
video_duration = ytplayer['getDuration'](), | |
pad = function(i) { | |
return ('00' + i).slice(-2); | |
}, | |
update_video_time = function(current_time) { | |
var secs = Math.round(video_duration - current_time), | |
mins = Math.floor(secs / 60), | |
hours = Math.floor(mins / 60); | |
secs -= mins * 60; | |
mins -= hours * 60; | |
progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs); | |
}; | |
update_video_time(0); | |
self._video_progress_checker = window.setInterval(function() { | |
var current_time = ytplayer['getCurrentTime'](); | |
progress_bar.style.width = (current_time / video_duration * 100) + '%'; | |
update_video_time(current_time); | |
}, 250); | |
_.register_event(progress_el, 'click', function(e) { | |
var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left); | |
ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true); | |
}); | |
} | |
} | |
}); | |
}); | |
// EXPORTS (for closure compiler) | |
// robotmiaLib Exports | |
robotmiaLib.prototype['init'] = robotmiaLib.prototype.init; | |
robotmiaLib.prototype['reset'] = robotmiaLib.prototype.reset; | |
robotmiaLib.prototype['disable'] = robotmiaLib.prototype.disable; | |
robotmiaLib.prototype['time_event'] = robotmiaLib.prototype.time_event; | |
robotmiaLib.prototype['track'] = robotmiaLib.prototype.track; | |
robotmiaLib.prototype['track_links'] = robotmiaLib.prototype.track_links; | |
robotmiaLib.prototype['track_forms'] = robotmiaLib.prototype.track_forms; | |
robotmiaLib.prototype['track_pageview'] = robotmiaLib.prototype.track_pageview; | |
robotmiaLib.prototype['register'] = robotmiaLib.prototype.register; | |
robotmiaLib.prototype['register_once'] = robotmiaLib.prototype.register_once; | |
robotmiaLib.prototype['unregister'] = robotmiaLib.prototype.unregister; | |
robotmiaLib.prototype['identify'] = robotmiaLib.prototype.identify; | |
robotmiaLib.prototype['alias'] = robotmiaLib.prototype.alias; | |
robotmiaLib.prototype['name_tag'] = robotmiaLib.prototype.name_tag; | |
robotmiaLib.prototype['set_config'] = robotmiaLib.prototype.set_config; | |
robotmiaLib.prototype['get_config'] = robotmiaLib.prototype.get_config; | |
robotmiaLib.prototype['get_property'] = robotmiaLib.prototype.get_property; | |
robotmiaLib.prototype['get_distinct_id'] = robotmiaLib.prototype.get_distinct_id; | |
robotmiaLib.prototype['toString'] = robotmiaLib.prototype.toString; | |
robotmiaLib.prototype['_check_and_handle_notifications'] = robotmiaLib.prototype._check_and_handle_notifications; | |
robotmiaLib.prototype['_show_notification'] = robotmiaLib.prototype._show_notification; | |
// robotmiaPersistence Exports | |
robotmiaPersistence.prototype['properties'] = robotmiaPersistence.prototype.properties; | |
robotmiaPersistence.prototype['update_search_keyword'] = robotmiaPersistence.prototype.update_search_keyword; | |
robotmiaPersistence.prototype['update_referrer_info'] = robotmiaPersistence.prototype.update_referrer_info; | |
robotmiaPersistence.prototype['get_cross_subdomain'] = robotmiaPersistence.prototype.get_cross_subdomain; | |
robotmiaPersistence.prototype['clear'] = robotmiaPersistence.prototype.clear; | |
// robotmiaPeople Exports | |
robotmiaPeople.prototype['set'] = robotmiaPeople.prototype.set; | |
robotmiaPeople.prototype['set_once'] = robotmiaPeople.prototype.set_once; | |
robotmiaPeople.prototype['increment'] = robotmiaPeople.prototype.increment; | |
robotmiaPeople.prototype['append'] = robotmiaPeople.prototype.append; | |
robotmiaPeople.prototype['union'] = robotmiaPeople.prototype.union; | |
robotmiaPeople.prototype['track_charge'] = robotmiaPeople.prototype.track_charge; | |
robotmiaPeople.prototype['clear_charges'] = robotmiaPeople.prototype.clear_charges; | |
robotmiaPeople.prototype['delete_user'] = robotmiaPeople.prototype.delete_user; | |
robotmiaPeople.prototype['toString'] = robotmiaPeople.prototype.toString; | |
_.safewrap_class(robotmiaLib, ['identify', '_check_and_handle_notifications', '_show_notification']); | |
var instances = {}; | |
var extend_mp = function() { | |
// add all the sub robotmia instances | |
_.each(instances, function(instance, name) { | |
if (name !== PRIMARY_INSTANCE_NAME) { robotmia_master[name] = instance; } | |
}); | |
// add private functions as _ | |
robotmia_master['_'] = _; | |
}; | |
var override_mp_init_func = function() { | |
// we override the snippets init function to handle the case where a | |
// user initializes the robotmia library after the script loads & runs | |
robotmia_master['init'] = function(token, config, name) { | |
if (name) { | |
// initialize a sub library | |
if (!robotmia_master[name]) { | |
robotmia_master[name] = instances[name] = create_mplib(token, config, name); | |
robotmia_master[name]._loaded(); | |
} | |
return robotmia_master[name]; | |
} else { | |
var instance = robotmia_master; | |
if (instances[PRIMARY_INSTANCE_NAME]) { | |
// main robotmia lib already initialized | |
instance = instances[PRIMARY_INSTANCE_NAME]; | |
} else if (token) { | |
// intialize the main robotmia lib | |
instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); | |
instance._loaded(); | |
instances[PRIMARY_INSTANCE_NAME] = instance; | |
} | |
robotmia_master = instance; | |
if (init_type === INIT_SNIPPET) { | |
window[PRIMARY_INSTANCE_NAME] = robotmia_master; | |
} | |
extend_mp(); | |
} | |
}; | |
}; | |
var add_dom_loaded_handler = function() { | |
// Cross browser DOM Loaded support | |
function dom_loaded_handler() { | |
// function flag since we only want to execute this once | |
if (dom_loaded_handler.done) { return; } | |
dom_loaded_handler.done = true; | |
DOM_LOADED = true; | |
ENQUEUE_REQUESTS = false; | |
_.each(instances, function(inst) { | |
inst._dom_loaded(); | |
}); | |
} | |
function do_scroll_check() { | |
try { | |
document.documentElement.doScroll('left'); | |
} catch(e) { | |
setTimeout(do_scroll_check, 1); | |
return; | |
} | |
dom_loaded_handler(); | |
} | |
if (document.addEventListener) { | |
if (document.readyState === 'complete') { | |
// safari 4 can fire the DOMContentLoaded event before loading all | |
// external JS (including this file). you will see some copypasta | |
// on the internet that checks for 'complete' and 'loaded', but | |
// 'loaded' is an IE thing | |
dom_loaded_handler(); | |
} else { | |
document.addEventListener('DOMContentLoaded', dom_loaded_handler, false); | |
} | |
} else if (document.attachEvent) { | |
// IE | |
document.attachEvent('onreadystatechange', dom_loaded_handler); | |
// check to make sure we arn't in a frame | |
var toplevel = false; | |
try { | |
toplevel = window.frameElement === null; | |
} catch(e) { | |
// noop | |
} | |
if (document.documentElement.doScroll && toplevel) { | |
do_scroll_check(); | |
} | |
} | |
// fallback handler, always will work | |
_.register_event(window, 'load', dom_loaded_handler, true); | |
}; | |
var add_dom_event_counting_handlers = function(instance) { | |
var name = instance.get_config('name'); | |
instance.mp_counts = instance.mp_counts || {}; | |
instance.mp_counts['$__c'] = parseInt(_.cookie.get('mp_' + name + '__c')) || 0; | |
var increment_count = function() { | |
instance.mp_counts['$__c'] = (instance.mp_counts['$__c'] || 0) + 1; | |
_.cookie.set('mp_' + name + '__c', instance.mp_counts['$__c'], 1, true); | |
}; | |
var evtCallback = function() { | |
try { | |
instance.mp_counts = instance.mp_counts || {}; | |
increment_count(); | |
} catch (e) { | |
console.error(e); | |
} | |
}; | |
_.register_event(document, 'submit', evtCallback); | |
_.register_event(document, 'change', evtCallback); | |
var mousedownTarget = null; | |
_.register_event(document, 'mousedown', function(e) { | |
mousedownTarget = e.target; | |
}); | |
_.register_event(document, 'mouseup', function(e) { | |
if (e.target === mousedownTarget) { | |
evtCallback(e); | |
} | |
}); | |
}; | |
export function init_from_snippet() { | |
init_type = INIT_SNIPPET; | |
robotmia_master = window[PRIMARY_INSTANCE_NAME]; | |
// Initialization | |
if (_.isUndefined(robotmia_master)) { | |
// robotmia wasn't initialized properly, report error and quit | |
console.critical('"robotmia" object not initialized. Ensure you are using the latest version of the robotmia JS Library along with the snippet we provide.'); | |
return; | |
} | |
if (robotmia_master['__loaded'] || (robotmia_master['config'] && robotmia_master['persistence'])) { | |
// lib has already been loaded at least once; we don't want to override the global object this time so bomb early | |
console.error('robotmia library has already been downloaded at least once.'); | |
return; | |
} | |
var snippet_version = robotmia_master['__SV'] || 0; | |
if (snippet_version < 1.1) { | |
// robotmia wasn't initialized properly, report error and quit | |
console.critical('Version mismatch; please ensure you\'re using the latest version of the robotmia code snippet.'); | |
return; | |
} | |
// Load instances of the robotmia Library | |
_.each(robotmia_master['_i'], function(item) { | |
if (item && _.isArray(item)) { | |
instances[item[item.length-1]] = create_mplib.apply(this, item); | |
} | |
}); | |
override_mp_init_func(); | |
robotmia_master['init'](); | |
// Fire loaded events after updating the window's robotmia object | |
_.each(instances, function(instance) { | |
instance._loaded(); | |
}); | |
add_dom_loaded_handler(); | |
} | |
export function init_as_module() { | |
init_type = INIT_MODULE; | |
robotmia_master = new robotmiaLib(); | |
override_mp_init_func(); | |
robotmia_master['init'](); | |
add_dom_loaded_handler(); | |
return robotmia_master; | |
} |