Skip to content

Commit

Permalink
Merge pull request #138 from driveback/feature/trackers
Browse files Browse the repository at this point in the history
Feature/trackers
  • Loading branch information
ConstantineYurevich authored Apr 20, 2017
2 parents 793e77d + 0678488 commit d0b406f
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "digital-data-manager",
"description": "The hassle-free way to integrate Digital Data Layer on your website.",
"author": "Driveback LLC <[email protected]>",
"version": "1.2.31",
"version": "1.2.32",
"license": "MIT",
"main": "dist/dd-manager.js",
"scripts": {
Expand Down
11 changes: 10 additions & 1 deletion src/ddManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { VIEWED_PAGE, mapEvent } from './events';
import { validateIntegrationEvent, trackValidationErrors } from './EventValidator';
import { enableErrorTracking } from './ErrorTracker';
import { warn, error as errorLog } from './functions/safeConsole';
import { trackLink, trackImpression } from './trackers';

let ddManager;

Expand Down Expand Up @@ -240,7 +241,7 @@ function _initializeIntegrations(settings) {

ddManager = {

VERSION: '1.2.31',
VERSION: '1.2.32',

setAvailableIntegrations: (availableIntegrations) => {
_availableIntegrations = availableIntegrations;
Expand Down Expand Up @@ -382,6 +383,14 @@ ddManager = {
return _eventManager;
},

trackLink: (elements, handler) => {
trackLink(elements, handler);
},

trackImpression: (elements, handler) => {
trackImpression(elements, handler);
},

reset: () => {
if (_ddStorage) {
_ddStorage.clear();
Expand Down
22 changes: 22 additions & 0 deletions src/functions/domQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function domQuery(selector, context) {
context = context || window.document;
// Redirect simple selectors to the more performant function
if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) {
switch (selector.charAt(0)) {
case '#':
// Handle ID-based selectors
return [context.getElementById(selector.substr(1))];
case '.':
// Handle class-based selectors
// Query by multiple classes by converting the selector
// string into single spaced class names
var classes = selector.substr(1).replace(/\./g, ' ');
return [].slice.call(context.getElementsByClassName(classes));
default:
// Handle tag-based selectors
return [].slice.call(context.getElementsByTagName(selector));
}
}
// Default to `querySelectorAll`
return [].slice.call(context.querySelectorAll(selector));
}
35 changes: 35 additions & 0 deletions src/functions/eventListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const bindName = window.addEventListener ? 'addEventListener' : 'attachEvent';
const unbindName = window.removeEventListener ? 'removeEventListener' : 'detachEvent';
const prefix = bindName !== 'addEventListener' ? 'on' : '';

/**
* Bind `el` event `type` to `fn`.
*
* @param {Element} el
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @return {Function}
* @api public
*/

export function bind(el, type, fn, capture) {
el[bindName](prefix + type, fn, capture || false);
return fn;
}

/**
* Unbind `el` event `type`'s callback `fn`.
*
* @param {Element} el
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @return {Function}
* @api public
*/

export function undbin(el, type, fn, capture) {
el[unbindName](prefix + type, fn, capture || false);
return fn;
}
8 changes: 8 additions & 0 deletions src/functions/getStyle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function getStyle(el, prop) {
if (window.getComputedStyle) {
return window.getComputedStyle(el)[prop];
} else if (el.currentStyle) {
return el.currentStyle[prop];
}
return undefined;
}
22 changes: 22 additions & 0 deletions src/functions/isMeta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Checks whether a DOM click event should open a link in a new tab.
*/

export default function isMeta(e) {
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
return true;
}

// Logic that handles checks for the middle mouse button, based
// on [jQuery](https://github.com/jquery/jquery/blob/master/src/event.js#L466).
const which = e.which;
const button = e.button;
if (!which && button !== undefined) {
// eslint-disable-next-line no-bitwise, no-extra-parens
return (!button & 1) && (!button & 2) && (button & 4);
} else if (which === 2) {
return true;
}

return false;
}
4 changes: 4 additions & 0 deletions src/functions/preventDefault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function preventDefault(e) {
e = e || window.event;
return e.preventDefault ? e.preventDefault() : e.returnValue = false;
}
2 changes: 2 additions & 0 deletions src/trackers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as trackLink } from './trackLink.js';
export { default as trackImpression } from './trackImpression.js';
189 changes: 189 additions & 0 deletions src/trackers/trackImpression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { bind } from './../functions/eventListener';
import getStyle from './../functions/getStyle';
import domQuery from './../functions/domQuery';

class Batch {
constructor(handler) {
this.blocks = [];
this.viewedBlocks = [];
this.handler = handler;
}

addViewedBlock(block) {
this.viewedBlocks.push(block);
}

isViewedBlock(block) {
return !(this.viewedBlocks.indexOf(block) < 0);
}

setBlocks(blocks) {
this.blocks = blocks;
}
}

class BatchTable {
constructor() {
this.selectors = [];
this.batches = {};
}

add(selector, handler) {
if (this.selectors.indexOf(selector) < 0) {
this.selectors.push(selector);
this.batches[selector] = [];
}

const batch = new Batch(handler);
this.batches[selector].push(batch)
}

update() {
for (const selector of this.selectors) {
const batches = this.batches[selector];
const blocks = domQuery(selector);
for (const batch of batches) {
batch.setBlocks(blocks);
}
}
}

getAll() {
let allBatches = [];
for (const selector of this.selectors) {
const batches = this.batches[selector];
allBatches = [...allBatches, ...batches];
}
return allBatches;
}
}

const batchTable = new BatchTable();

let isStarted = false;

let docViewTop;
let docViewBottom;
let docViewLeft;
let docViewRight;

function defineDocBoundaries(maxWebsiteWidth) {
const _defineDocBoundaries = () => {
const body = window.document.body;
const docEl = window.document.documentElement;

docViewTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;;
docViewBottom = docViewTop + docEl.clientHeight;
docViewLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
docViewRight = docViewLeft + docEl.clientWidth;

if (maxWebsiteWidth && maxWebsiteWidth < this.docViewRight && this.docViewLeft === 0) {
docViewLeft = (docViewRight - maxWebsiteWidth) / 2;
docViewRight = docViewLeft + maxWebsiteWidth;
}
};

_defineDocBoundaries();
bind(window, 'resize', () => {
_defineDocBoundaries();
});
bind(window, 'scroll', () => {
_defineDocBoundaries();
});
}

/**
* Returns true if element is visible by css
* and at least 3/4 of the element fit user viewport
*
* @param el DOMElement
* @returns boolean
*/
function isVisible(el) {
const docEl = window.document.documentElement;

const elemWidth = el.clientWidth;
const elemHeight = el.clientHeight;

const elemTop = el.getBoundingClientRect().top;
const elemBottom = elemTop + elemHeight;
const elemLeft = el.getBoundingClientRect().left;
const elemRight = elemLeft + elemWidth;

const visible = !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length) && Number(getStyle(el, 'opacity')) > 0 && getStyle(el, 'visibility') !== 'hidden';
if (!visible) {
return false;
}

const fitsVertical = (
((elemBottom - elemHeight / 4) <= docEl.clientHeight) &&
((elemTop + elemHeight / 4) >= 0)
);

const fitsHorizontal = (
(elemLeft + elemWidth / 4 >= 0) &&
(elemRight - elemWidth / 4 <= docEl.clientWidth)
);

if (!fitsVertical || !fitsHorizontal) {
return false;
}

let elementFromPoint = document.elementFromPoint(
elemLeft + elemWidth / 2,
elemTop + elemHeight / 2
);

while (elementFromPoint && elementFromPoint !== el && elementFromPoint.parentNode !== document) {
elementFromPoint = elementFromPoint.parentNode;
}
return (!!elementFromPoint && elementFromPoint === el);
}

function trackViews() {
batchTable.update();

const batches = batchTable.getAll();
for (const batch of batches) {
const newViewedBlocks = [];

const blocks = batch.blocks;
for (const block of blocks) {
if (isVisible(block) && !batch.isViewedBlock(block)) {
newViewedBlocks.push(block);
batch.addViewedBlock(block);
}
}

if (newViewedBlocks.length > 0) {
try {
batch.handler(newViewedBlocks);
} catch (error) {
// TODO
}
}
}
}

function startTracking() {
defineDocBoundaries();
trackViews();
setInterval(() => {
trackViews();
}, 500);
}

export default function trackImpression(selector, handler) {
if (!selector) return;

if (typeof handler !== 'function') {
throw new TypeError('Must pass function handler to `ddManager.trackImpression`.');
}

batchTable.add(selector, handler);

if (!isStarted) {
isStarted = true;
startTracking();
}
}
53 changes: 53 additions & 0 deletions src/trackers/trackLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { bind } from './../functions/eventListener';
import isMeta from './../functions/isMeta';
import preventDefault from './../functions/preventDefault';
import domQuery from './../functions/domQuery';

function isElement(el) {
return (el && el.nodeType === 1);
}

function onClick(el, handler) {
return (e) => {
const href = el.getAttribute('href')
|| el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
|| el.getAttribute('xlink:href');

try {
handler(el);
} catch (error) {
// TODO
}

if (href && el.target !== '_blank' && !isMeta(e)) {
preventDefault(e);
setTimeout(() => {
window.location.href = href;
}, 500);
}
};
}

export default function trackLink(links, handler) {
if (!links) return;
if (typeof links === 'string') {
links = domQuery(links);
} else if (isElement(links)) {
links = [links];
} else if (links.toArray) { // handles jquery
links = links.toArray();
}

if (typeof handler !== 'function') {
throw new TypeError('Must pass function handler to `ddManager.trackLink`.');
}

for (const el of links) {
if (!isElement(el)) {
throw new TypeError('Must pass HTMLElement to `ddManager.trackLink`.');
}
bind(el, 'click', onClick(el, handler));
}

return this;
}
Loading

0 comments on commit d0b406f

Please sign in to comment.