/*! * @preserve * jquery.scrolldepth.js | v1.2.0 * Copyright (c) 2020 Rob Flaherty (@robflaherty) * Licensed under the MIT and GPL licenses. */ /* Universal module definition */ (function(factory) { if (typeof define === 'function' && define.amd) { // AMD define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // CommonJS module.exports = factory(require('jquery')); } else { // Browser globals factory(jQuery); } }(function($) { /* Scroll Depth */ "use strict"; var defaults = { minHeight: 0, elements: [], percentage: true, userTiming: true, pixelDepth: true, nonInteraction: true, gaGlobal: false, gtmOverride: false, trackerName: false, dataLayer: 'dataLayer' }; var $window = $(window), cache = [], scrollEventBound = false, lastPixelDepth = 0, universalGA, classicGA, gaGlobal, globalSiteTag, standardEventHandler; /* * Plugin */ $.scrollDepth = function(options) { var startTime = +new Date; options = $.extend({}, defaults, options); // Return early if document height is too small if ( $(document).height() < options.minHeight ) { return; } /* * Determine which version of GA is being used * "ga", "__gaTracker", _gaq", and "dataLayer" are the possible globals */ if (options.gaGlobal) { universalGA = true; gaGlobal = options.gaGlobal; } else if (typeof gtag === "function") { globalSiteTag = true; gaGlobal = 'gtag'; } else if (typeof ga === "function") { universalGA = true; gaGlobal = 'ga'; } else if (typeof __gaTracker === "function") { universalGA = true; gaGlobal = '__gaTracker'; } if (typeof _gaq !== "undefined" && typeof _gaq.push === "function") { classicGA = true; } if (typeof options.eventHandler === "function") { standardEventHandler = options.eventHandler; } else if (typeof window[options.dataLayer] !== "undefined" && typeof window[options.dataLayer].push === "function" && !options.gtmOverride) { standardEventHandler = function(data) { window[options.dataLayer].push(data); }; } /* * Functions */ function sendEvent(action, label, scrollDistance, timing) { var command = options.trackerName ? (options.trackerName + '.send') : 'send'; if (standardEventHandler) { standardEventHandler({'event': 'ScrollDistance', 'eventCategory': 'Scroll Depth', 'eventAction': action, 'eventLabel': label, 'eventValue': 1, 'eventNonInteraction': options.nonInteraction}); if (options.pixelDepth && arguments.length > 2 && scrollDistance > lastPixelDepth) { lastPixelDepth = scrollDistance; standardEventHandler({'event': 'ScrollDistance', 'eventCategory': 'Scroll Depth', 'eventAction': 'Pixel Depth', 'eventLabel': rounded(scrollDistance), 'eventValue': 1, 'eventNonInteraction': options.nonInteraction}); } if (options.userTiming && arguments.length > 3) { standardEventHandler({'event': 'ScrollTiming', 'eventCategory': 'Scroll Depth', 'eventAction': action, 'eventLabel': label, 'eventTiming': timing}); } } else if (globalSiteTag) { gtag('event', action, { 'event_category': 'Scroll Depth', 'event_label': label, 'value': 1, 'non_interaction': options.nonInteraction }); if (options.pixelDepth && arguments.length > 2 && scrollDistance > lastPixelDepth) { lastPixelDepth = scrollDistance; gtag('event', 'Pixel Depth', { 'event_category': 'Scroll Depth', 'event_label': rounded(scrollDistance), 'value': 1, 'non_interaction': options.nonInteraction }); } if (options.userTiming && arguments.length > 3) { gtag('event', 'timing_complete', { 'event_category': 'Scroll Depth', 'name': action, 'event_label': label, 'value': timing }); } } else { if (universalGA) { window[gaGlobal](command, 'event', 'Scroll Depth', action, label, 1, {'nonInteraction': options.nonInteraction}); if (options.pixelDepth && arguments.length > 2 && scrollDistance > lastPixelDepth) { lastPixelDepth = scrollDistance; window[gaGlobal](command, 'event', 'Scroll Depth', 'Pixel Depth', rounded(scrollDistance), 1, {'nonInteraction': options.nonInteraction}); } if (options.userTiming && arguments.length > 3) { window[gaGlobal](command, 'timing', 'Scroll Depth', action, timing, label); } } if (classicGA) { _gaq.push(['_trackEvent', 'Scroll Depth', action, label, 1, options.nonInteraction]); if (options.pixelDepth && arguments.length > 2 && scrollDistance > lastPixelDepth) { lastPixelDepth = scrollDistance; _gaq.push(['_trackEvent', 'Scroll Depth', 'Pixel Depth', rounded(scrollDistance), 1, options.nonInteraction]); } if (options.userTiming && arguments.length > 3) { _gaq.push(['_trackTiming', 'Scroll Depth', action, timing, label, 100]); } } } } function calculateMarks(docHeight) { return { '25%' : parseInt(docHeight * 0.25, 10), '50%' : parseInt(docHeight * 0.50, 10), '75%' : parseInt(docHeight * 0.75, 10), // Cushion to trigger 100% event in iOS '100%': docHeight - 5 }; } function checkMarks(marks, scrollDistance, timing) { // Check each active mark $.each(marks, function(key, val) { if ( $.inArray(key, cache) === -1 && scrollDistance >= val ) { sendEvent('Percentage', key, scrollDistance, timing); cache.push(key); } }); } function checkElements(elements, scrollDistance, timing) { $.each(elements, function(index, elem) { if ( $.inArray(elem, cache) === -1 && $(elem).length ) { if ( scrollDistance >= $(elem).offset().top ) { sendEvent('Elements', elem, scrollDistance, timing); cache.push(elem); } } }); } function rounded(scrollDistance) { // Returns String return (Math.floor(scrollDistance/250) * 250).toString(); } function init() { bindScrollDepth(); } /* * Public Methods */ // Reset Scroll Depth with the originally initialized options $.scrollDepth.reset = function() { cache = []; lastPixelDepth = 0; $window.off('scroll.scrollDepth'); bindScrollDepth(); }; // Add DOM elements to be tracked $.scrollDepth.addElements = function(elems) { if (typeof elems == "undefined" || !$.isArray(elems)) { return; } $.merge(options.elements, elems); // If scroll event has been unbound from window, rebind if (!scrollEventBound) { bindScrollDepth(); } }; // Remove DOM elements currently tracked $.scrollDepth.removeElements = function(elems) { if (typeof elems == "undefined" || !$.isArray(elems)) { return; } $.each(elems, function(index, elem) { var inElementsArray = $.inArray(elem, options.elements); var inCacheArray = $.inArray(elem, cache); if (inElementsArray != -1) { options.elements.splice(inElementsArray, 1); } if (inCacheArray != -1) { cache.splice(inCacheArray, 1); } }); }; /* * Throttle function borrowed from: * Underscore.js 1.5.2 * http://underscorejs.org * (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Underscore may be freely distributed under the MIT license. */ function throttle(func, wait) { var context, args, result; var timeout = null; var previous = 0; var later = function() { previous = new Date; timeout = null; result = func.apply(context, args); }; return function() { var now = new Date; if (!previous) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); } else if (!timeout) { timeout = setTimeout(later, remaining); } return result; }; } /* * Scroll Event */ function bindScrollDepth() { scrollEventBound = true; $window.on('scroll.scrollDepth', throttle(function() { /* * We calculate document and window height on each scroll event to * account for dynamic DOM changes. */ var docHeight = $(document).height(), winHeight = window.innerHeight ? window.innerHeight : $window.height(), scrollDistance = $window.scrollTop() + winHeight, // Recalculate percentage marks marks = calculateMarks(docHeight), // Timing timing = +new Date - startTime; // If all marks already hit, unbind scroll event if (cache.length >= options.elements.length + (options.percentage ? 4:0)) { $window.off('scroll.scrollDepth'); scrollEventBound = false; return; } // Check specified DOM elements if (options.elements) { checkElements(options.elements, scrollDistance, timing); } // Check standard marks if (options.percentage) { checkMarks(marks, scrollDistance, timing); } }, 500)); } init(); }; // UMD export return $.scrollDepth; }));