From 1e8881aad2ba3d7d1f50deea46f949d7baf14f80 Mon Sep 17 00:00:00 2001 From: danwilkerson Date: Mon, 28 Nov 2016 13:09:33 -0500 Subject: [PATCH] Bumped version for release, fixed license, adjusted Readme to list browser support --- Gruntfile.js | 14 +- LICENSE.MD | 2 + luna-scroll-tracking.json | 2 +- lunametrics-scroll-tracking.gtm.js | 207 ++++++++++++++++++++++--- lunametrics-scroll-tracking.gtm.min.js | 12 +- package.json | 2 +- readme.md | 48 +++++- src/lunametrics-scroll-tracking.gtm.js | 205 +++++++++++++++++++++--- 8 files changed, 441 insertions(+), 51 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9ec2429..61f4b43 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,12 +9,20 @@ module.exports = function(grunt) { ' * Written by @notdanwilkerson', ' * Documentation: https://github.com/lunametrics/gascroll/', ' * Licensed under the Creative Commons 4.0 Attribution Public License', - ' */'].join('\r\n'); + ' */' + ].join('\r\n'); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), jshint: { - files: ['./src/lunametrics-scroll-tracking.gtm.js'] + // files: ['./src/*.js'], + ignore_warning: { + options: { + '-W030': true, + '-W058': true + }, + src: ['./src/*.js'] + } }, uglify: { options: { @@ -109,7 +117,7 @@ module.exports = function(grunt) { oldContainer.containerVersion.tag[oldTag].parameter[oldParameter].value = '' + '\n'; fs.writeFileSync(options.build.dest, jsBeautify(JSON.stringify(oldContainer))); diff --git a/LICENSE.MD b/LICENSE.MD index 8015932..c58f7f3 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -1,3 +1,5 @@ +The MIT License (MIT) + Copyright (c) 2016 LunaMetrics, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/luna-scroll-tracking.json b/luna-scroll-tracking.json index ece514b..37b9d2a 100644 --- a/luna-scroll-tracking.json +++ b/luna-scroll-tracking.json @@ -32,7 +32,7 @@ }, { "type": "TEMPLATE", "key": "html", - "value": "" + "value": "" }], "fingerprint": "0", "firingTriggerId": ["2147479553"], diff --git a/lunametrics-scroll-tracking.gtm.js b/lunametrics-scroll-tracking.gtm.js index 0d0935c..44f8b8f 100644 --- a/lunametrics-scroll-tracking.gtm.js +++ b/lunametrics-scroll-tracking.gtm.js @@ -2,7 +2,133 @@ 'use strict'; - var cache = {}; + // Global cache we'll use to ensure no double-tracking occurs + var MarksAlreadyTracked = {}; + + // Backwards compatible with old every setting, which was single value + if (config.distances.percentages && config.distances.percentages.every) { + + if (!isArray_(config.distances.percentages.every)) { + + config.distances.percentages.every = [config.distances.percentages.every]; + + } + + } + + // Backwards compatible with old every setting, which was single value + if (config.distances.pixels && config.distances.pixels.every) { + + if (!isArray_(config.distances.pixels.every)) { + + config.distances.pixels.every = [config.distances.pixels.every]; + + } + + } + + // Get a hold of any relevant elements, if specified in config + var elementDistances = (function(selectors) { + + // If no selectors specified, short circuit here + if (!selectors) return; + + // Create a cache to store positions of elements temporarily + var cache = {}; + var counter = 0; + + // Fetch latest positions + _update(); + + // Return a function that can be called to get a map of element positions + return function () { + + // Clone here to prevent inheritance from getMarks step + var shallowClone = {}; + var key; + + counter++; + + // If temp cache counter is greater than 10, re-poll elements + if (counter > 10) { + + _update(); + counter = 0; + + } + + for (key in cache) { + + shallowClone[key] = cache[key]; + + } + + return shallowClone; + + }; + + function _update() { + + var selector, + markName, + els, + el, + y, + i; + // Clear current cache + cache = {}; + + if (selectors.each) { + + for (i = 0; i < selectors.each.length; i++) { + + selector = selectors.each[i]; + + if (!MarksAlreadyTracked[selector]) { + + el = document.querySelector(selector); + + if (el) cache[selector] = getNodeDistanceFromTop(el); + + } + + } + + } + + if (selectors.every) { + + for (i = 0; i < selectors.every.length; i++) { + + selector = selectors.every[i]; + els = document.querySelectorAll(selector); + + // If the last item in the selected group has been tracked, we skip it + if (els.length && !MarksAlreadyTracked[selector + ':' + (els.length - 1)]) { + + for (y = 0; y < els.length; y++) { + + markName = selector + ':' + y; + + // We also check at the individual element level + if (!MarksAlreadyTracked[markName]) { + + el = els[y]; + cache[markName] = getNodeDistanceFromTop(el); + + } + + } + + } + + } + + } + + } + + })(config.distances.elements); // If our document is ready to go, fire straight away if(document.readyState !== 'loading') { @@ -40,9 +166,12 @@ function getMarks(_docHeight, _offset) { - var marks = {}; + var marks = elementDistances() || {}; var percents = []; var pixels = []; + var everyPercent, + everyPixel, + i; if(config.distances.percentages) { @@ -54,8 +183,12 @@ if(config.distances.percentages.every) { - var _everyPercent = every_(config.distances.percentages.every, 100); - percents = percents.concat(_everyPercent); + for (i = 0; i < config.distances.percentages.every.length; i++) { + + everyPercent = every_(config.distances.percentages.every[i], 100); + percents = pixels.concat(everyPercent); + + } } @@ -71,8 +204,12 @@ if(config.distances.pixels.every) { - var _everyPixel = every_(config.distances.pixels.every, _docHeight); - pixels = pixels.concat(_everyPixel); + for (i = 0; i < config.distances.pixels.every.length; i++) { + + everyPixel = every_(config.distances.pixels.every[i], _docHeight); + pixels = pixels.concat(everyPixel); + + } } @@ -126,19 +263,27 @@ function checkDepth() { - var _bottom = parseBorder_(config.bottom); - var _top = parseBorder_(config.top); - + var _bottom = parseScrollBorder(config.bottom); + var _top = parseScrollBorder(config.top); var height = docHeight(_bottom, _top); var marks = getMarks(height, (_top || 0)); var _curr = currentPosition(); - var key; + var target, + key; for(key in marks) { - if(_curr > marks[key] && !cache[key]) { + target = marks[key]; + + // If we've scrolled past the mark, we haven't tracked it yet, and it's in range, track the mark + if( + _curr > target && + !MarksAlreadyTracked[key] && + target < (_bottom || Infinity) && + target > (_top || 0) + ) { - cache[key] = true; + MarksAlreadyTracked[key] = true; fireAnalyticsEvent(key); } @@ -176,7 +321,7 @@ } - function parseBorder_(border) { + function parseScrollBorder(border) { if(typeof border === 'number' || parseInt(border, 10)) { @@ -188,10 +333,8 @@ // If we have an element or a query selector, poll getBoundingClientRect var el = border.nodeType === 1 ? border : document.querySelector(border); - var docTop = document.body.getBoundingClientRect().top; - var _elTop = Math.floor(el.getBoundingClientRect().top - docTop); - return _elTop; + return getNodeDistanceFromTop(el); } catch (e) { @@ -252,6 +395,7 @@ } + /* * Throttle function borrowed from: * Underscore.js 1.5.2 @@ -315,6 +459,26 @@ } + // Helper for fetching top of element + function getNodeDistanceFromTop(node) { + + var nodeTop = node.getBoundingClientRect().top; + // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX + var docTop = (window.pageYOffset !== undefined) ? + window.pageYOffset : + (document.documentElement || document.body.parentNode || document.body).scrollTop; + + return nodeTop + docTop; + + } + + // Helper to check if something is an Array + function isArray_(thing) { + + return thing instanceof Array; + + } + })(document, window, { // Use 2 to force Classic Analytics hits and 1 for Universal hits 'forceSyntax': false, @@ -324,12 +488,17 @@ // Configure percentages of page you'd like to see if users scroll past 'percentages': { 'each': [10,90], - 'every': 25 + 'every': [25] }, // Configure for pixel measurements of page you'd like to see if users scroll past 'pixels': { 'each': [], - 'every': null + 'every': [] + }, + // Configure elements you'd like to see users scroll past (using CSS Selectors) + 'elements': { + 'each': [], + 'every': [] } }, // Accepts a number, DOM element, or query selector to determine the top of the scrolling area @@ -342,7 +511,7 @@ 'label': document.location.pathname }); /* - * v1.0.2 + * v1.1.0 * Created by the Google Analytics consultants at http://www.lunametrics.com/ * Written by @notdanwilkerson * Documentation: https://github.com/lunametrics/gascroll/ diff --git a/lunametrics-scroll-tracking.gtm.min.js b/lunametrics-scroll-tracking.gtm.min.js index 1b7d6ea..bb8b961 100644 --- a/lunametrics-scroll-tracking.gtm.min.js +++ b/lunametrics-scroll-tracking.gtm.min.js @@ -1,15 +1,19 @@ -!function(a,b,c){"use strict";function d(){return a.querySelector&&a.body.getBoundingClientRect?(c.dataLayerName=c.dataLayerName||"dataLayer",c.distances=c.distances||{},h(),void o(b,"scroll",n(h,500))):!1}function e(a,b){var d={},e=[],h=[];if(c.distances.percentages&&(c.distances.percentages.each&&(e=e.concat(c.distances.percentages.each)),c.distances.percentages.every)){var i=g(c.distances.percentages.every,100);e=e.concat(i)}if(c.distances.pixels&&(c.distances.pixels.each&&(h=h.concat(c.distances.pixels.each)),c.distances.pixels.every)){var j=g(c.distances.pixels.every,a);h=h.concat(j)}return d=f(d,e,"%",a,b),d=f(d,h,"px",a,b)}function f(a,b,c,d,e){var f;for(f=0;f=h&&(a[i]=h)}return a}function g(a,b){var c,d=parseInt(a,10),e=b/d,f=[];for(c=1;e+1>c;c++)f.push(c*d);return f}function h(){var a,b=j(c.bottom),d=j(c.top),f=m(b,d),g=e(f,d||0),h=k();for(a in g)h>g[a]&&!p[a]&&(p[a]=!0,i(a))}function i(a){var d=b.GoogleAnalyticsObject;"undefined"==typeof b[c.dataLayerName]||c.forceSyntax?"function"==typeof b[d]&&"function"==typeof b[d].getAll&&2!==c.forceSyntax?b[d]("send","event",c.category,a,c.label,{nonInteraction:1}):"undefined"!=typeof b._gaq&&1!==c.forceSyntax&&b._gaq.push(["_trackEvent",c.category,a,c.label,0,!0]):b[c.dataLayerName].push({event:"scrollTracking",attributes:{distance:a,label:c.label}})}function j(b){if("number"==typeof b||parseInt(b,10))return parseInt(b,10);try{var c=1===b.nodeType?b:a.querySelector(b),d=a.body.getBoundingClientRect().top,e=Math.floor(c.getBoundingClientRect().top-d);return e}catch(f){return void 0}}function k(){var c=void 0!==b.pageXOffset,d="CSS1Compat"===(a.compatMode||""),e=c?b.pageYOffset:d?a.documentElement.scrollTop:a.body.scrollTop;return parseInt(e,10)+parseInt(l(),10)}function l(){var b="CSS1Compat"===a.compatMode?a.documentElement:a.body;return b.clientHeight}function m(b,c){var d=a.body,e=a.documentElement,f=Math.max(d.scrollHeight,d.offsetHeight,e.clientHeight,e.scrollHeight,e.offsetHeight);return c&&(f-=c),b&&(f=b-(c||0)),f-5}function n(a,b){var c,d,e,f=null,g=0,h=function(){g=new Date,f=null,e=a.apply(c,d)};return function(){var i=new Date;g||(g=i);var j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(f),f=null,g=i,e=a.apply(c,d)):f||(f=setTimeout(h,j)),e}}function o(a,b,c){a.addEventListener?a.addEventListener(b,c):a.attachEvent?a.attachEvent("on"+b,function(b){c.call(a,b)}):("undefined"==typeof a["on"+b]||null===a["on"+b])&&(a["on"+b]=function(b){c.call(a,b)})}var p={};"loading"!==a.readyState?d():a.addEventListener?o(a,"DOMContentLoaded",d):o(b,"load",d)} +!function(a,b,c){"use strict";function d(){return a.querySelector&&a.body.getBoundingClientRect?(c.dataLayerName=c.dataLayerName||"dataLayer",c.distances=c.distances||{},h(),void o(b,"scroll",n(h,500))):!1}function e(a,b){var d,e,h,i=s()||{},j=[],k=[];if(c.distances.percentages&&(c.distances.percentages.each&&(j=j.concat(c.distances.percentages.each)),c.distances.percentages.every))for(h=0;h=h&&(a[i]=h)}return a}function g(a,b){var c,d=parseInt(a,10),e=b/d,f=[];for(c=1;e+1>c;c++)f.push(c*d);return f}function h(){var a,b,d=j(c.bottom),f=j(c.top),g=m(d,f),h=e(g,f||0),l=k();for(b in h)a=h[b],l>a&&!r[b]&&(d||1/0)>a&&a>(f||0)&&(r[b]=!0,i(b))}function i(a){var d=b.GoogleAnalyticsObject;"undefined"==typeof b[c.dataLayerName]||c.forceSyntax?"function"==typeof b[d]&&"function"==typeof b[d].getAll&&2!==c.forceSyntax?b[d]("send","event",c.category,a,c.label,{nonInteraction:1}):"undefined"!=typeof b._gaq&&1!==c.forceSyntax&&b._gaq.push(["_trackEvent",c.category,a,c.label,0,!0]):b[c.dataLayerName].push({event:"scrollTracking",attributes:{distance:a,label:c.label}})}function j(b){if("number"==typeof b||parseInt(b,10))return parseInt(b,10);try{var c=1===b.nodeType?b:a.querySelector(b);return p(c)}catch(d){return void 0}}function k(){var c=void 0!==b.pageXOffset,d="CSS1Compat"===(a.compatMode||""),e=c?b.pageYOffset:d?a.documentElement.scrollTop:a.body.scrollTop;return parseInt(e,10)+parseInt(l(),10)}function l(){var b="CSS1Compat"===a.compatMode?a.documentElement:a.body;return b.clientHeight}function m(b,c){var d=a.body,e=a.documentElement,f=Math.max(d.scrollHeight,d.offsetHeight,e.clientHeight,e.scrollHeight,e.offsetHeight);return c&&(f-=c),b&&(f=b-(c||0)),f-5}function n(a,b){var c,d,e,f=null,g=0,h=function(){g=new Date,f=null,e=a.apply(c,d)};return function(){var i=new Date;g||(g=i);var j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(f),f=null,g=i,e=a.apply(c,d)):f||(f=setTimeout(h,j)),e}}function o(a,b,c){a.addEventListener?a.addEventListener(b,c):a.attachEvent?a.attachEvent("on"+b,function(b){c.call(a,b)}):("undefined"==typeof a["on"+b]||null===a["on"+b])&&(a["on"+b]=function(b){c.call(a,b)})}function p(c){var d=c.getBoundingClientRect().top,e=void 0!==b.pageYOffset?b.pageYOffset:(a.documentElement||a.body.parentNode||a.body).scrollTop;return d+e}function q(a){return a instanceof Array}var r={};c.distances.percentages&&c.distances.percentages.every&&(q(c.distances.percentages.every)||(c.distances.percentages.every=[c.distances.percentages.every])),c.distances.pixels&&c.distances.pixels.every&&(q(c.distances.pixels.every)||(c.distances.pixels.every=[c.distances.pixels.every]));var s=function(b){function c(){var c,e,f,g,h,i;if(d={},b.each)for(i=0;i10&&(c(),e=0);for(a in d)b[a]=d[a];return b}}}(c.distances.elements);"loading"!==a.readyState?d():a.addEventListener?o(a,"DOMContentLoaded",d):o(b,"load",d)} (document, window, { forceSyntax: false, dataLayerName: false, distances: { percentages: { each: [10, 90], - every: 25 + every: [25] }, pixels: { each: [], - every: null + every: [] + }, + elements: { + each: [], + every: [] } }, top: null, @@ -17,7 +21,7 @@ category: "Scroll Tracking", label: document.location.pathname });/* - * v1.0.2 + * v1.1.0 * Created by the Google Analytics consultants at http://www.lunametrics.com/ * Written by @notdanwilkerson * Documentation: https://github.com/lunametrics/gascroll/ diff --git a/package.json b/package.json index 88f8b09..0166be7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "grunt": "^0.4.5", - "grunt-contrib-jshint": "~0.10.0", + "grunt-contrib-jshint": "~0.12.0", "grunt-contrib-uglify": "^0.9.1", "js-beautify": "1.5.7" }, diff --git a/readme.md b/readme.md index 9dac371..5a4cae8 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ #Scroll Tracking Google Analytics & GTM Plugin -Plug-and-play, dependency-free scroll tracking for Google Analytics or Google Tag Manager. Can be customized for custom percentages and custom pixel lengths. It will detect if GTM, Universal Analytics, or Classic Analytics is installed on the page, in that order, and use the first syntax it matches unless configured otherwise. It include support for delivering hits directly to Universal or Classic Google Analytics, or for pushing Data Layer events to be used by Google Tag Manager. +Plug-and-play, dependency-free scroll tracking for Google Analytics or Google Tag Manager. Can be customized for custom percentages, custom pixel lengths, and element-based tracking. It will detect if GTM, Universal Analytics, or Classic Analytics is installed on the page, in that order, and use the first syntax it matches unless configured otherwise. It include support for delivering hits directly to Universal or Classic Google Analytics, or for pushing Data Layer events to be used by Google Tag Manager. Once installed, the plugin will fire events with the following settings: @@ -8,6 +8,8 @@ Once installed, the plugin will fire events with the following settings: - Event Action: *<Scroll Percentage or Pixel Depth>* - Event Label: *<Page Path>* +Marker locations are refreshed every time the listener is called, so dynamic content should be trackable out of the box. Once a marker has been tracked, it is blocked from firing on subsequent checks. Tracking does not account for the starting position of the viewport; if the browser loads the viewport at the bottom of the page and the user triggers a scroll event, all percentages up to that point in the document will be tracked. + ##Installation This plugin is designed to be plug-and-play. By default, the plugin will try and detect if your site has Google Tag Manager, Universal Analytics, or Classic Analytics, and it will send the data to the first source it matches in that order. @@ -105,7 +107,7 @@ Fires an event every *n*% scrolled. The default setting fires every 25%. })(document, window, { 'distances': { 'percentages': { - 'every': 25 // Fires at the 25%, 50%, 75%, and 100% scroll marks + 'every': [10, 25] // Fires at the 10%, 20%, 25%, 30%, 40%, 50%, 60%, 70%, 75%, 80%, 90%, and 100% scroll marks } } }); @@ -125,7 +127,7 @@ Fires an event when the user scrolls past each percentage provided in the array. } }); -**NOTE**: Google Analytics has a 500 hit per-session limitation, as well as a 20 hit window that replenishes at 2 hits per second. For that reason, it is HIGHLY INADVISABLE to track every 1% of page scrolled. +**NOTE**: Google Analytics has a 500 hit per-session limitation, as well as a 20 hit window that replenishes at 2 hits per second. For that reason, it is HIGHLY INADVISABLE to track every 1% of page scrolled. #### distances.pixels.every Fires an event every *n* pixels scrolled vertically. @@ -137,7 +139,7 @@ Fires an event every *n* pixels scrolled vertically. })(document, window, { 'distances': { 'pixels': { - 'every': 250 // Fires at the 250px, 500px, 750px, ... scroll marks. + 'every': [250, 300] // Fires at the 250px, 300px, 500px, 600px, 750px, ... scroll marks. } } }); @@ -157,7 +159,43 @@ Fires an event when the user scrolls past each number of pixels provided in the } }); -**NOTE**: Google Analytics has a 500 hit per-session limitation, as well as a 20 hit window that replenishes at 2 hits per second. For that reason, it is HIGHLY INADVISABLE to track every pixel of the page scrolled. +Under the hood, the selector is passed to `document.querySelector`. The resulting locations are cached temporarily to prevent browser shudder. + +**NOTE**: Google Analytics has a 500 hit per-session limitation, as well as a 20 hit window that replenishes at 2 hits per second. For that reason, it is HIGHLY INADVISABLE to track every pixel of the page scrolled. + +#### distances.elements.every +Fires every time an element matching the given selector is scrolled past + + (function(document, window, config) { + + // ... the tracking code + + })(document, window, { + 'distances': { + 'elements': { + 'every': ['.hero-img', '.code-sample > pre'] // Fires when the user scrolls past any elements with the class 'base-img' and any pre elements that are the immediate children of an element with the class 'code-sample'. + } + } + }); + +Under the hood, the selector is passed to `document.querySelectorAll`. The resulting locations are cached temporarily to prevent browser shudder. + +#### distances.elements.each +Fires when the first element to match a given selector is scrolled past + + (function(document, window, config) { + + // ... the tracking code + + })(document, window, { + 'distances': { + 'elements': { + 'each': ['#content', '#footer'] // Fires when the #content and #footer elements are scrolled past + } + } + }); + +**NOTE**: Google Analytics has a 500 hit per-session limitation, as well as a 20 hit window that replenishes at 2 hits per second. For that reason, it is HIGHLY INADVISABLE to track every element of the page scrolled. ### Top/Bottom Of Scrollable Area This script allows you to specify where to begin and end tracking user scrolling. The default configuration is the entire page. diff --git a/src/lunametrics-scroll-tracking.gtm.js b/src/lunametrics-scroll-tracking.gtm.js index bfe8092..fe74752 100644 --- a/src/lunametrics-scroll-tracking.gtm.js +++ b/src/lunametrics-scroll-tracking.gtm.js @@ -2,7 +2,133 @@ 'use strict'; - var cache = {}; + // Global cache we'll use to ensure no double-tracking occurs + var MarksAlreadyTracked = {}; + + // Backwards compatible with old every setting, which was single value + if (config.distances.percentages && config.distances.percentages.every) { + + if (!isArray_(config.distances.percentages.every)) { + + config.distances.percentages.every = [config.distances.percentages.every]; + + } + + } + + // Backwards compatible with old every setting, which was single value + if (config.distances.pixels && config.distances.pixels.every) { + + if (!isArray_(config.distances.pixels.every)) { + + config.distances.pixels.every = [config.distances.pixels.every]; + + } + + } + + // Get a hold of any relevant elements, if specified in config + var elementDistances = (function(selectors) { + + // If no selectors specified, short circuit here + if (!selectors) return; + + // Create a cache to store positions of elements temporarily + var cache = {}; + var counter = 0; + + // Fetch latest positions + _update(); + + // Return a function that can be called to get a map of element positions + return function () { + + // Clone here to prevent inheritance from getMarks step + var shallowClone = {}; + var key; + + counter++; + + // If temp cache counter is greater than 10, re-poll elements + if (counter > 10) { + + _update(); + counter = 0; + + } + + for (key in cache) { + + shallowClone[key] = cache[key]; + + } + + return shallowClone; + + }; + + function _update() { + + var selector, + markName, + els, + el, + y, + i; + // Clear current cache + cache = {}; + + if (selectors.each) { + + for (i = 0; i < selectors.each.length; i++) { + + selector = selectors.each[i]; + + if (!MarksAlreadyTracked[selector]) { + + el = document.querySelector(selector); + + if (el) cache[selector] = getNodeDistanceFromTop(el); + + } + + } + + } + + if (selectors.every) { + + for (i = 0; i < selectors.every.length; i++) { + + selector = selectors.every[i]; + els = document.querySelectorAll(selector); + + // If the last item in the selected group has been tracked, we skip it + if (els.length && !MarksAlreadyTracked[selector + ':' + (els.length - 1)]) { + + for (y = 0; y < els.length; y++) { + + markName = selector + ':' + y; + + // We also check at the individual element level + if (!MarksAlreadyTracked[markName]) { + + el = els[y]; + cache[markName] = getNodeDistanceFromTop(el); + + } + + } + + } + + } + + } + + } + + })(config.distances.elements); // If our document is ready to go, fire straight away if(document.readyState !== 'loading') { @@ -40,9 +166,12 @@ function getMarks(_docHeight, _offset) { - var marks = {}; + var marks = elementDistances() || {}; var percents = []; var pixels = []; + var everyPercent, + everyPixel, + i; if(config.distances.percentages) { @@ -54,8 +183,12 @@ if(config.distances.percentages.every) { - var _everyPercent = every_(config.distances.percentages.every, 100); - percents = percents.concat(_everyPercent); + for (i = 0; i < config.distances.percentages.every.length; i++) { + + everyPercent = every_(config.distances.percentages.every[i], 100); + percents = pixels.concat(everyPercent); + + } } @@ -71,8 +204,12 @@ if(config.distances.pixels.every) { - var _everyPixel = every_(config.distances.pixels.every, _docHeight); - pixels = pixels.concat(_everyPixel); + for (i = 0; i < config.distances.pixels.every.length; i++) { + + everyPixel = every_(config.distances.pixels.every[i], _docHeight); + pixels = pixels.concat(everyPixel); + + } } @@ -126,19 +263,27 @@ function checkDepth() { - var _bottom = parseBorder_(config.bottom); - var _top = parseBorder_(config.top); - + var _bottom = parseScrollBorder(config.bottom); + var _top = parseScrollBorder(config.top); var height = docHeight(_bottom, _top); var marks = getMarks(height, (_top || 0)); var _curr = currentPosition(); - var key; + var target, + key; for(key in marks) { - if(_curr > marks[key] && !cache[key]) { + target = marks[key]; + + // If we've scrolled past the mark, we haven't tracked it yet, and it's in range, track the mark + if( + _curr > target && + !MarksAlreadyTracked[key] && + target < (_bottom || Infinity) && + target > (_top || 0) + ) { - cache[key] = true; + MarksAlreadyTracked[key] = true; fireAnalyticsEvent(key); } @@ -176,7 +321,7 @@ } - function parseBorder_(border) { + function parseScrollBorder(border) { if(typeof border === 'number' || parseInt(border, 10)) { @@ -188,10 +333,8 @@ // If we have an element or a query selector, poll getBoundingClientRect var el = border.nodeType === 1 ? border : document.querySelector(border); - var docTop = document.body.getBoundingClientRect().top; - var _elTop = Math.floor(el.getBoundingClientRect().top - docTop); - return _elTop; + return getNodeDistanceFromTop(el); } catch (e) { @@ -252,6 +395,7 @@ } + /* * Throttle function borrowed from: * Underscore.js 1.5.2 @@ -315,6 +459,26 @@ } + // Helper for fetching top of element + function getNodeDistanceFromTop(node) { + + var nodeTop = node.getBoundingClientRect().top; + // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX + var docTop = (window.pageYOffset !== undefined) ? + window.pageYOffset : + (document.documentElement || document.body.parentNode || document.body).scrollTop; + + return nodeTop + docTop; + + } + + // Helper to check if something is an Array + function isArray_(thing) { + + return thing instanceof Array; + + } + })(document, window, { // Use 2 to force Classic Analytics hits and 1 for Universal hits 'forceSyntax': false, @@ -324,12 +488,17 @@ // Configure percentages of page you'd like to see if users scroll past 'percentages': { 'each': [10,90], - 'every': 25 + 'every': [25] }, // Configure for pixel measurements of page you'd like to see if users scroll past 'pixels': { 'each': [], - 'every': null + 'every': [] + }, + // Configure elements you'd like to see users scroll past (using CSS Selectors) + 'elements': { + 'each': [], + 'every': [] } }, // Accepts a number, DOM element, or query selector to determine the top of the scrolling area