Source: Offline/offline.js

/**
 * @class
 * @name Offline
 * @memberof ATInternet.Tracker.Plugins
 * @type {function}
 * @param tag {object} Instance of the Tag used
 * @description
 * Plugin to save hits that could not be sent, following a loss of Internet connection, to the user’s browser.
 * <br />To do this, the plugin uses the localStorage feature available in recent browsers.
 * The status of the Internet connection (whether the browser is online or not) is determined by the browser’s onLine property available in HTML5.
 * @public
 */
ATInternet.Tracker.Plugins.Offline = function (tag) {

    'use strict';

    var _config = {};
    var _debug = {
        level: 'DEBUG',
        messageEnd: 'method ended',
        messageStart: 'method started'
    };
    var _triggersAdded = false;

    // Set specific plugin configuration.
    // If global configuration already exists, set only undefined properties.
    tag.configPlugin('Offline', dfltPluginCfg || {}, function (newConf) {
        _config = newConf;
    });

    /**
     * Get data from local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @return {object}
     * @private
     */
    var _getStorageData = function () {
        var storage = localStorage.getItem('ATOffline');
        var data = {
            hits: [],
            length: 0
        };
        if (storage) {
            var storageObject = ATInternet.Utils.jsonParse(storage) || {hits: []};
            data.hits = storageObject.hits;
            data.length = storage.length;
        }
        return data;
    };

    /**
     * Set data into local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param value {object} Object containing array of hits
     * @return {object}
     * @private
     */
    var _setStorageData = function (value) {
        try {
            localStorage.setItem('ATOffline', ATInternet.Utils.jsonSerialize(value));
        } catch (e) {
            /* @if debug */
            tag.debug('Offline:localStorage:setItem:Error', 'ERROR', 'Cannot set value in local storage', {
                except: e,
                value: ATInternet.Utils.jsonSerialize(value)
            });
            /* @endif */
        }
    };

    /**
     * Get estimated storage data length (max bytes).
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @return {number}
     * @private
     */
    var _getStorageDataLength = function () {
        /* @if debug */
        tag.debug('Offline:offline:getLength', _debug.level, _debug.messageStart);
        /* @endif */
        return _getStorageData().length * 4;
    };

    /**
     * Remove hits from local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @private
     */
    var _removeHitsFromStorage = function () {
        var value = {hits: []};
        _setStorageData(value);
        /* @if debug */
        tag.debug('Offline:offline:remove', _debug.level, _debug.messageEnd);
        /* @endif */
    };

    /**
     * Get hits array from local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @return {Array}
     * @private
     */
    var _getHitsFromStorage = function () {
        /* @if debug */
        tag.debug('Offline:offline:get', _debug.level, _debug.messageStart);
        /* @endif */
        return _getStorageData().hits;
    };

    /**
     * Add offline parameter to hit in order to store it.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param hit {string} Hit value
     * @private
     */
    var _processSingleHit = function (hit) {
        if (hit) {
            var str = hit.split(/&ref=\.*/i);
            var params = '&cn=offline&olt=' + String(Math.floor(new Date().getTime() / 1000));
            hit = str[0] + params + (str[1] ? '&ref=' + str[1] : '');
        }
        return hit;
    };

    /**
     * Store a new hit in local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param hit {string} Hit value
     * @private
     */
    var _storeSingleHit = function (hit) {
        var storageData = _getStorageData();
        var storageLength = storageData.length || '{"hits":[]}'.length;
        var hitLength = hit.length;
        var processHit = true;
        // Remove old hits if necessary
        if (((storageLength + hitLength) * 4) > (_config.storageCapacity * 1024 * 1024)) {
            processHit = false;
            var storageHit = storageData.hits.shift();
            if (typeof storageHit !== 'undefined') {
                processHit = true;
                var storageHitLength = storageHit.length;
                while (storageHitLength < hitLength) {
                    storageHit = storageData.hits.shift();
                    if (typeof storageHit !== 'undefined') {
                        storageHitLength += storageHit.length;
                    } else {
                        processHit = false;
                        break;
                    }
                }
            }
        }
        if (processHit) {
            // Push new hit
            storageData.hits.push(hit);
            // Update local storage
            _setStorageData({hits: storageData.hits});
        }
    };

    /**
     * Send a new hit if it exists after the Tracker has been triggered.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param hitSent {string} Hit sent
     * @param newHit {string} New hit to send
     * @param callback {function} Callback to execute
     * @param requestMethod {string} Overloading the global method of sending hits (GET|POST)
     * @param elementType {string} Element type (mailto, form, redirection)
     * @private
     */
    function _callback(hitSent, newHit, callback, requestMethod, elementType) {
        if (!newHit || (hitSent !== newHit)) {
            _sendHitsFromStorage(newHit, callback, requestMethod, elementType);
        }
    }

    /**
     * Fire all hits from local storage.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param newHit {string} New hit to send
     * @param callback {function} Callback to execute
     * @param requestMethod {string} Overloading the global method of sending hits (GET|POST)
     * @param elementType {string} Element type (mailto, form, redirection)
     * @private
     */
    function _sendHitsFromStorage(newHit, callback, requestMethod, elementType) {
        if (window.navigator && window.navigator.onLine) {
            var hits = _getHitsFromStorage();
            if (hits.length > 0) {
                // Get first hit
                var storageHit = hits.shift();
                // Update local storage
                _setStorageData({hits: hits});
                // Process next hit from local storage after triggers
                if (!_triggersAdded) {
                    tag.onTrigger('Tracker:Hit:Sent:Ok', function (event, data) {
                        _callback(data.details.hit, newHit, callback, requestMethod, elementType);
                    }, false);
                    tag.onTrigger('Tracker:Hit:Sent:Error', function (event, data) {
                        _callback(data.details.hit, newHit, callback, requestMethod, elementType);
                    }, false);
                    _triggersAdded = true;
                }
                // Forcing the use of the 'GET' method for sending to keep the RichMedia scheduling management, in particular
                tag.sendUrl(storageHit, null, 'GET', elementType);
            } else if (newHit) {
                // Send new hit if present
                var action = tag.utils.getQueryStringValue('a', newHit);
                if (action) {
                    // Add timeout for Rich Media in order to let enough time to
                    // possible last storage hit currently being sent
                    setTimeout(function () {
                        // Keep the default sending method for non-storage cases
                        tag.sendUrl(newHit, null, null, null);
                    }, _config.timeout);
                } else {
                    // Keep the default sending method for non-storage cases
                    tag.sendUrl(newHit, callback, requestMethod, elementType);
                }
            }
        } else if (newHit) {
            // Store new hit if present
            _storeSingleHit(_processSingleHit(newHit));
            callback && callback();
        }
    }

    /**
     * Process all hits.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @param store {boolean} Store new hits depending on configuration
     * @private
     */
    var _processHits = function (store) {
        // Overload sending method of Tracker
        tag.builder.sendUrl = function (hit, callback, requestMethod, elementType) {
            // Store current hit if necessary
            if (store || (window.navigator && !window.navigator.onLine)) {
                _storeSingleHit(_processSingleHit(hit));
                callback && callback();
            } else {
                _sendHitsFromStorage(hit, callback, requestMethod, elementType);
            }
        };

    };

    /**
     * Run global process.
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @private
     */
    var _run = function () {
        var isLocalStorageAvailable = ATInternet.Utils.isLocalStorageAvailable();
        var isOnLine;
        if (window.navigator) {
            isOnLine = window.navigator.onLine;
        }
        if (isLocalStorageAvailable && (typeof isOnLine !== 'undefined')) {
            if (_config.storageMode === 'required') {
                _processHits(false);
            } else if (_config.storageMode === 'always') {
                _processHits(true);
            }
        }
        tag.emit('Offline:Ready', {
            lvl: 'INFO',
            details: {
                isLocalStorageAvailable: isLocalStorageAvailable,
                storageMode: _config.storageMode,
                isOnline: isOnLine
            }
        });
    };

    /**
     * Init method, check dependencies and launch the plugin when all dependencies are present
     * @memberof ATInternet.Tracker.Plugins.Offline#
     * @function
     * @private
     */
    var _init = function () {
        var dependencies = ['Utils'];
        tag.plugins.waitForDependencies(dependencies, _run);
    };

    /**
     * [Object added by plugin {@link ATInternet.Tracker.Plugins.Offline Offline}] Tags to manage local storage.
     * @name offline
     * @memberof ATInternet.Tracker.Tag
     * @inner
     * @type {object}
     * @property {function} getLength Tag helper, see details here {@link ATInternet.Tracker.Tag#offline.getLength}
     * @property {function} remove Tag helper, see details here {@link ATInternet.Tracker.Tag#offline.remove}
     * @property {function} get Tag helper, see details here {@link ATInternet.Tracker.Tag#offline.get}
     * @property {function} send Tag helper, see details here {@link ATInternet.Tracker.Tag#offline.send}
     * @public
     */
    tag.offline = {};

    /**
     * [Helper added by plugin {@link ATInternet.Tracker.Plugins.Offline Offline}] Get estimated storage data length.
     * @alias offline.getLength
     * @memberof! ATInternet.Tracker.Tag#
     * @function
     * @returns {number}
     * @example
     * <pre><code class="javascript">var len = tag.offline.getLength();
     * </code></pre>
     * @public
     */
    tag.offline.getLength = _getStorageDataLength;

    /**
     * [Helper added by plugin {@link ATInternet.Tracker.Plugins.Offline Offline}] Remove hits from local storage.
     * @alias offline.remove
     * @memberof! ATInternet.Tracker.Tag#
     * @function
     * @example
     * <pre><code class="javascript">tag.offline.remove();
     * </code></pre>
     * @public
     */
    tag.offline.remove = _removeHitsFromStorage;

    /**
     * [Helper added by plugin {@link ATInternet.Tracker.Plugins.Offline Offline}] Get hits array from local storage.
     * @alias offline.get
     * @memberof! ATInternet.Tracker.Tag#
     * @function
     * @returns {Array}
     * @example
     * <pre><code class="javascript">var hitArr = tag.offline.get();
     * </code></pre>
     * @public
     */
    tag.offline.get = _getHitsFromStorage;

    /**
     * [Helper added by plugin {@link ATInternet.Tracker.Plugins.Offline Offline}] Send hits from local storage.
     * @alias offline.send
     * @memberof! ATInternet.Tracker.Tag#
     * @function
     * @example
     * <pre><code class="javascript">tag.offline.send();
     * </code></pre>
     * @public
     */
    tag.offline.send = function () {
        _sendHitsFromStorage(null, null, 'GET');
        /* @if debug */
        tag.debug('Offline:offline:send', _debug.level, _debug.messageEnd);
        /* @endif */
    };

    // Init global process.
    _init();

    // For unit tests on private elements !!!
    /* @if test */
    var _this = this;
    _this._getStorageData = _getStorageData;
    _this._setStorageData = _setStorageData;
    _this._getStorageDataLength = _getStorageDataLength;
    _this._getHitsFromStorage = _getHitsFromStorage;
    _this._removeHitsFromStorage = _removeHitsFromStorage;
    _this._processSingleHit = _processSingleHit;
    _this._callback = _callback;
    _this._sendHitsFromStorage = _sendHitsFromStorage;
    _this._processHits = _processHits;
    _this._storeSingleHit = _storeSingleHit;
    _this._run = _run;
    _this._init = _init;
    /* @endif */

};
window['ATInternet']['Tracker']['addPlugin']('Offline');