tmaiaroto/telepathic-black-panther

View on GitHub
src/main.js

Summary

Maintainability
F
1 wk
Test Coverage
(function() {
    // Make this available on the window for convenience and as $ki so it doesn't conflict with $
    window.$ki = require('./ki.ie8.js');

    Tbp = (function() {
        var defaults = {
            debug: false,
            autoDetect: true,
            // The default use case is to send events to Google Analytics, but that can be disabled...
            ga: true,
            // We also, by default, push events to the dataLayer (commonly used by GTM and many other things) 
            // pass an array in here, `dataLayer` || [] used by default if this is true
            dataLayer: true,
            // Visitor info
            clientId: null
        };

        /**
         * Telepathic Black Panther
         * 
         * @param {object} config Some configuration options used by Tbp
        */
        function Tbp(config) {
            // Tbp() or new Tbp() will work this way.
            if (!(this instanceof Tbp)) return new Tbp(config);

            // Shortcut Google Analytics, provide empty function if it doesn't exist so things don't bark at us elsewhere.
            if (typeof window.ga === "undefined") {
                if(console !== undefined && console.warn !== undefined) {
                    console.warn("Google Analytics not found.");
                }
                this.ga = function(){};
            } else {
                this.ga = window.ga;
            }

            this.ga(function(tracker) {
                defaults.clientId = tracker.get('clientId');
            });

            // Extend default config with passed config options.
            this.config = this.extend(defaults, config);
        
            // Load other core modules (kept separate for organization, still using require() for them).
            this.extend(this, require('./core.js'));
            this.extend(this, require('./engagement.js'));
            this.extend(this, require('./social.js'));
            this.extend(this, require('./auto_detect.js'));

            // Load some 3rd party modules.
            this.bus = require('../node_modules/minibus/minibus.js').create();
            this.analysis = require('./analysis.js');
            this.cookies = require('../node_modules/cookies-js/src/cookies.js');

            // Shortcut $ki.
            this.extend(window.$ki.prototype, require('./ki.plugins.js'));
            this.$ = window.$ki;

            // Setup auto detection for everything. If an array was passed then only on those defined methods (names of functions).
            if(this.config.autoDetect === true) {
                this.autoDetectEvents();
            } else if (typeof(this.config.autoDetect) === 'object') {
                this.autoDetectEvents(this.config.autoDetect);
            }

            // Cookie the user. Set the first time Telepathic Black Panther spotted them (trying to keep cookie names short, fv = first visit).
            if(!this.cookies.get("_tbp_fv")) {
                this.cookies.set("_tbp_fv", (new Date().getTime()), {expires: Infinity});
            }

            // There's going to be a few closures coming up here...
            var tbpContext = this;

            // Automatically submit to Google Analytics (unless configured otherwise) and log the event to console if debug was set true.
            // Also push on to the dataLayer if told to do so and emit an event for that through the bus too.
            // Anything else a user can handle via the bus.
            this.bus.on('event', function(event) {
                tbpContext.log("Emitted Event", event);

                // Push to the dataLayer
                if(typeof(tbpContext.config.dataLayer) === "object") {
                    tbpContext.config.dataLayer.push(event);
                } else if(tbpContext.config.dataLayer === true) {
                    if(typeof(window.dataLayer) === "object") {
                        window.dataLayer.push(event);
                    }
                }

                // Push to Google Analytics
                if(tbpContext.config.ga && event.label !== "" && event.label !== null) {
                    tbpContext.log("Sending event to Google Analytics", "info");
                    ga('send', {
                        'hitType': 'event',
                        'eventCategory': event.category,
                        'eventAction': event.action,
                        'eventLabel': event.label,
                        'hitCallback': event.hitCallback || null,
                        'nonInteraction': event.nonInteraction || false
                    });
                } else {
                    // Regardless of whether or not Google Analytics is in use, call "hitCallback" if it was defined.
                    // This is particularly important for the `linkOut` method as that one must stop the browser from navigating 
                    // in order to allow the event to be pushed. Then after the event has been pushed, it can continue.
                    if(typeof(event.hitCallback) === "function") {
                        event.hitCallback(event);
                    }
                }
            });

            // Here's how one could watch the dataLayer for anything Telepathic Black Panther pushes to it.
            // this.bus.on('dataLayer', function(event, theDataLayer) {
            //     console.dir(event);
            //     console.dir(theDataLayer);
            // });

            // Override push() on the dataLayer to catch changes to it.
            if(this.config.dataLayer) {
                var handleDataLayerPush = function () {
                    for (var i = 0, n = this.length, l = arguments.length; i < l; i++, n++) {
                        tbpContext.bus.emit('dataLayer', this[n] = arguments[i], this);
                        //RaiseMyEvent(this, n, this[n] = arguments[i]); // assign/raise your event
                    }
                    return n;
                };

                // Hopefully `dataLayer` will be an array already defined. However, some people may want to name it something different 
                // and that's ok too because `this.config.dataLayer` can be passed an array to use instead. If `dataLayer` doesn't exist yet, 
                // just make a new empty array to use.
                if(typeof(this.config.dataLayer) === "object") {
                    Object.defineProperty(this.config.dataLayer, "push", {
                        configurable: false,
                        enumerable: false, // hide from for...in
                        writable: false,
                        value: handleDataLayerPush
                    });
                } else if(this.config.dataLayer === true) {
                    if(typeof(window.dataLayer) === "object") {
                        Object.defineProperty(window.dataLayer, "push", {
                            configurable: false,
                            enumerable: false, // hide from for...in
                            writable: false,
                            value: handleDataLayerPush
                        });
                    }
                }
            }

        }

        Tbp.prototype = {
            /**
             * Simple extend to mimic jQuery's because we don't want a dep on jQuery for just this.
             * That'd be sillyness.
             * 
             * @return {Object} Returns an extended object
            */
            extend: function() {
                for(var i=1; i<arguments.length; i++) {
                    for(var key in arguments[i]) {
                        if(arguments[i].hasOwnProperty(key)) {
                            arguments[0][key] = arguments[i][key];
                        }
                    }
                }
                return arguments[0];
            },
            /**
             * A simple console log wrapper that first checks if debug mode is on.
             *
             * @var {mixed} message The string message to log
             * @var {mixed} obj     Either an object to log out or a string that will be matched for a log level that might change the color of the message
            */
            log: function(message, obj) {
                if(this.config.debug && console !== undefined) {
                    var style = "";
                    switch(obj) {
                        case "warn":
                            style = "color:orange;";
                            break;
                        case "error":
                            style = "color:red;font-weight:bold;";
                            break;
                        case "info":
                            style = "color:blue;";
                            break;
                        case "success":
                            style = "color:green;";
                            break;
                    }
                    if(style !== "") {
                        console.log("%c" + message, style);
                    } else {
                        if(obj) {
                            console.log(message, obj);
                        } else {
                            console.log(message);
                        }
                    }
                }
            },
            /**
             * Another simple wrapper for displaying an object as a collapsible tree via console.dir().
             * 
             * @param  {mixed} obj Object to print to the console
             */
            dir: function(obj) {
                if(this.config.debug && console !== undefined) {
                    console.dir(obj);
                }
            }
        };
        
        return Tbp;
    })();
    module.exports = Tbp;
})();