Commit 5b066770 by James Ooi

Refactor Foresight into TypeScript

parent 6ebb1c92
...@@ -18,7 +18,10 @@ ...@@ -18,7 +18,10 @@
</head> </head>
<body> <body>
<div style="background-color: #FAFAFA; min-height: 100vh; display: flex; justify-content: center; align-items: center"> <div
style="background-color: #FAFAFA; min-height: 100vh; display: flex; justify-content: center; align-items: center"
data-track-view="views;landing : view : google"
>
<a href="https://google.com" class="btn btn-primary" data-track="clicks;landing : all models : series select"> <a href="https://google.com" class="btn btn-primary" data-track="clicks;landing : all models : series select">
Google Google
</a> </a>
...@@ -34,12 +37,23 @@ ...@@ -34,12 +37,23 @@
</a> </a>
</div> </div>
<div
style="background-color: #FFFFFF; min-height: 100vh; display: flex; justify-content: center; align-items: center"
data-track="landing : section : twitter"
data-track-view="views;landing : view : twitter"
data-track-view:interaction
>
<a href="https://twitter.com" class="btn btn-secondary" data-track="clicks;landing" data-track:non-interaction>
Twitter
</a>
</div>
</body> </body>
<script src="build/foresight.js"></script> <script src="build/foresight.js"></script>
<script> <script>
Foresight.track(); new Foresight().start();
</script> </script>
</html> </html>
\ No newline at end of file
...@@ -11,10 +11,11 @@ ...@@ -11,10 +11,11 @@
"webpack-cli": "^3.1.0" "webpack-cli": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"intersection-observer": "^0.5.0",
"scrollmonitor": "^1.2.4" "scrollmonitor": "^1.2.4"
}, },
"scripts": { "scripts": {
"start": "webpack --watch", "start": "webpack --watch & browser-sync start -s . -f .",
"dist": "webpack -p --progress" "dist": "webpack -p --progress"
} }
} }
/** /**
* Foresight Event Tracking Library * Foresight
*
* @author James Ooi <james.ooi@forefront.com.my> * @author James Ooi <james.ooi@forefront.com.my>
* @license MIT * @license MIT
* @copyright 2018 (c) FOREFRONT International Sdn Bhd
*/ */
// import ScrollMonitor from 'scrollmonitor'; import 'intersection-observer';
import * as Utils from './utils';
declare var gtag: (action: string, ...args: any[]) => any;
/**
* Available options for configuring Foresight.
*/
interface ForesightConfig {
observerOptions?: IntersectionObserverInit
nonInteractionClicks?: boolean
nonInteractionViews?: boolean
}
/**
* Represents a particular event's data.
*/
interface EventData {
category: string
action: string
label: string
interaction: boolean
}
/** /**
* Foresight is an event analytics tracking library that allows for declarative * Foresight is an analytics library that allows for declarative event tracking
* and elegant event tracking. * in your websites.
*/ */
class Foresight { class Foresight {
static track() {
console.log('hellu!'); /**
} * Default Options
// static track() { * @static
// if (gtag === undefined) { */
// throw 'The `gtag` function is undefined. Has Google Analytics been loaded yet?'; static defaultOptions: Partial<ForesightConfig> = {
// } nonInteractionClicks: false,
// Foresight._setupClickTrackers(); nonInteractionViews: true,
// Foresight._setupViewTrackers(); observerOptions: {},
// } }
// static _setupClickTrackers() { /**
// const elements = [].slice * Stores the options of the current Foresight instance.
// .call(document.querySelectorAll('[data-track]')) * @public
// .filter(el => el.getAttribute('data-track-ready') === null) */
// .forEach(el => { options: ForesightConfig;
// el.addEventListener('click', e => Foresight._onClick(el, e));
// el.addEventListener('auxclick', e => Foresight._onClick(el, e)); // also track middle clicks /**
// el.setAttribute('data-track-ready', ''); * Stores a mapping of elements with is respective functions to de-register
// }); * listeners.
// } * @private
*/
// static _setupViewTrackers() { private _untrackFns: Map<Element, { click: Function, view: Function }> = new Map();
// const watchers = [].slice
// .call(document.querySelectorAll('[data-track-view]')) /**
// .filter(el => el.getAttribute('data-track-view-ready') === null) * Stores an instance of an IntersectionObserver.
// .map(el => { * @private
// const watcher = ScrollMonitor.create(el); */
// watcher.enterViewport(() => { private _observer: IntersectionObserver = null;
// Foresight._onView(el);
// watcher.destroy(); /**
// }); * @constructor Foresight
// el.setAttribute('data-track-view-ready', ''); */
// return watcher; constructor(config: ForesightConfig) {
// }); this.options = { ...Foresight.defaultOptions, ...config };
// }
// Initialise IntersectionObserver
// static _onClick(element) { this._observer = new IntersectionObserver((entries, observer) => {
// const eventString = element.getAttribute('data-track'); entries.forEach(entry => {
// if (eventString === null || eventString === '') { if (entry.isIntersecting) {
// console.warn('Insufficient tracking arguments provided.', element); this._onTrackedView(entry.target, observer);
// return; observer.unobserve(entry.target);
// } }
});
// const eventStringArr = eventString.split(';'); }, this.options.observerOptions);
// let [ eventCategory, eventName, eventLabel ] = eventStringArr; }
// let isNonInteraction = element.getAttribute('data-track:non-interaction') !== null;
/**
// // If only one argument is provided, then the argument is the action * Start event tracking for all DOM elements with event tracking attributes.
// if (eventStringArr.length === 1) { */
// eventName = eventCategory; start(root: Element = document.body) {
// eventCategory = undefined; if (gtag === undefined) {
// } throw `The 'gtag' function is undefined. Has Google Analytics been loaded yet?`;
}
// gtag('event', eventName, {
// 'event_label': eventLabel, Utils
// 'event_category': eventCategory, .toArray<Element>(root.querySelectorAll('[data-track], [data-track-view]'))
// 'non_interaction': isNonInteraction, .map(element => this.track(element));
// }); }
// }
/**
// static _onView(element) { * Enables event tracking for a DOM element with event tracking attribute.
// const eventString = element.getAttribute('data-track-view'); */
// if (eventString === null || eventString === '') { track(element: Element) {
// console.warn('Insufficient tracking arguments provided.', element); if (!this._untrackFns.has(element)) {
// return; this._untrackFns.set(element, { click: null, view: null });
// } }
// const eventStringArr = eventString.split(';'); const untrackFn = this._untrackFns.get(element);
// let [ eventCategory, eventName, eventLabel ] = eventStringArr;
// let isNonInteraction = !(element.getAttribute('data-track-view:is-interaction') !== null); // Track clicks
if (element.getAttribute('data-track') !== null) {
// // If only one argument is provided, then the argument is the action if (untrackFn.click == null) {
// if (eventStringArr.length === 1) { untrackFn.click = this._trackClicks(element);
// eventName = eventCategory; }
// eventCategory = undefined; }
// }
// Track views
// gtag('event', eventName, { if (element.getAttribute('data-track-view') !== null) {
// 'event_label': eventLabel, if (untrackFn.view == null) {
// 'event_category': eventCategory, untrackFn.view = this._trackViews(element);
// 'non_interaction': isNonInteraction, }
// }); }
// }
this._untrackFns.set(element, untrackFn);
}
/**
* Disable event tracking for a DOM element.
*/
untrack(element: Element) {
const untrackFn = this._untrackFns.get(element);
if (untrackFn === undefined) {
return;
}
if (untrackFn.click !== null) {
untrackFn.click();
}
if (untrackFn.view !== null) {
untrackFn.view();
}
this._untrackFns.delete(element);
}
/**
* Parse an event string and returns a `EventData` object.
*/
parseEventString(eventString: string): EventData {
const split = eventString.split(';');
let [ category, action, label ] = split;
// If only one argument is provided, then the argument is the action
if (split.length === 1) {
action = category;
category = undefined;
}
return { category, action, label, interaction: true }
}
/**
* Registers click listeners that triggers an analytics event when the element
* is clicked or middle clicked.
*
* @returns Returns a function to remove the event listener.
*/
private _trackClicks(element: Element): Function {
// Define listen fucntion
const listener = (e) => {
this._onTrackedClick(element, e);
};
element.addEventListener('click', listener);
element.addEventListener('auxclick', listener);
// Define unlisten function
const unlisten = () => {
element.removeEventListener('click', listener);
element.removeEventListener('auxclick', listener);
}
return unlisten;
}
/**
* Registers a view observer that triggers an analytics event when the element
* is in view.
*
* @returns Returns a function that disconnects the view observer.
*/
private _trackViews(element: Element): Function {
this._observer.observe(element);
return () => this._observer.unobserve(element);
}
/**
* @private
* Handles a click event on an element that is being tracked by Foresight.
*/
private _onTrackedClick(element: Element, event: Event) {
const s = element.getAttribute('data-track');
const data = this.parseEventString(s);
if (element.getAttribute('data-track:non-interaction') !== null) {
data.interaction = false;
}
gtag('event', data.action, {
'event_label': data.label,
'event_category': data.category,
'non_interaction': !data.interaction,
});
}
/**
* @private
* Handles a view event on an element that is being tracked by Foresight.
*/
private _onTrackedView(element: Element, observer) {
const s = element.getAttribute('data-track-view');
const data = this.parseEventString(s);
if (element.getAttribute('data-track-view:interaction') !== null) {
data.interaction = true;
} else {
data.interaction = false;
}
gtag('event', data.action, {
'event_label': data.label,
'event_category': data.category,
'non_interaction': !data.interaction,
});
}
} }
export = Foresight; export = Foresight;
export { default as toArray } from './toArray';
\ No newline at end of file
/**
* Creates a new `Array` instance from an array-like or iterable object.
*/
export default function toArray<T>(arrayLike: any): T[] {
return [].slice.call(arrayLike);
}
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": false,
"removeComments": true "removeComments": true,
"lib": ["es6", "dom"]
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",
......
...@@ -1187,6 +1187,10 @@ interpret@^1.1.0: ...@@ -1187,6 +1187,10 @@ interpret@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
intersection-observer@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.0.tgz#9fe8bee3953c755b1485c38efd9633d535775ea6"
invert-kv@^1.0.0: invert-kv@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment