Commit 617ec396 by James Ooi

Merge branch 'feat/typescript'

parents 69ef6e42 80233250
{
"presets": [
"env",
"stage-2"
]
}
\ No newline at end of file
......@@ -18,7 +18,10 @@
</head>
<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">
Google
</a>
......@@ -34,12 +37,23 @@
</a>
</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>
<script src="build/foresight.js"></script>
<script>
Foresight.track();
new Foresight().start();
</script>
</html>
\ No newline at end of file
......@@ -5,14 +5,17 @@
"author": "James Ooi <wengteikooi@gmail.com>",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-2": "^6.24.1",
"awesome-typescript-loader": "^5.2.0",
"typescript": "^3.0.1",
"webpack": "^4.16.3",
"webpack-cli": "^3.1.0"
},
"dependencies": {
"intersection-observer": "^0.5.0",
"scrollmonitor": "^1.2.4"
},
"scripts": {
"start": "webpack --watch & browser-sync start -s . -f .",
"dist": "webpack -p --progress"
}
}
/**
* Foresight Event Tracking Library
* @author James Ooi <james.ooi@forefront.com.my>
* @license MIT
*/
import ScrollMonitor from 'scrollmonitor';
/**
* Foresight is an event analytics tracking library that allows for declarative
* and elegant event tracking.
*/
class Foresight {
static track() {
if (gtag === undefined) {
throw 'The `gtag` function is undefined. Has Google Analytics been loaded yet?';
}
Foresight._setupClickTrackers();
Foresight._setupViewTrackers();
}
static _setupClickTrackers() {
const elements = [].slice
.call(document.querySelectorAll('[data-track]'))
.filter(el => el.getAttribute('data-track-ready') === null)
.forEach(el => {
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', '');
});
}
static _setupViewTrackers() {
const watchers = [].slice
.call(document.querySelectorAll('[data-track-view]'))
.filter(el => el.getAttribute('data-track-view-ready') === null)
.map(el => {
const watcher = ScrollMonitor.create(el);
watcher.enterViewport(() => {
Foresight._onView(el);
watcher.destroy();
});
el.setAttribute('data-track-view-ready', '');
return watcher;
});
}
static _onClick(element) {
const eventString = element.getAttribute('data-track');
if (eventString === null || eventString === '') {
console.warn('Insufficient tracking arguments provided.', element);
return;
}
const eventStringArr = eventString.split(';');
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
if (eventStringArr.length === 1) {
eventName = eventCategory;
eventCategory = undefined;
}
gtag('event', eventName, {
'event_label': eventLabel,
'event_category': eventCategory,
'non_interaction': isNonInteraction,
});
}
static _onView(element) {
const eventString = element.getAttribute('data-track-view');
if (eventString === null || eventString === '') {
console.warn('Insufficient tracking arguments provided.', element);
return;
}
const eventStringArr = eventString.split(';');
let [ eventCategory, eventName, eventLabel ] = eventStringArr;
let isNonInteraction = !(element.getAttribute('data-track-view:is-interaction') !== null);
// If only one argument is provided, then the argument is the action
if (eventStringArr.length === 1) {
eventName = eventCategory;
eventCategory = undefined;
}
gtag('event', eventName, {
'event_label': eventLabel,
'event_category': eventCategory,
'non_interaction': isNonInteraction,
});
}
}
module.exports = Foresight;
/**
* Foresight
*
* @author James Ooi <james.ooi@forefront.com.my>
* @license MIT
* @copyright 2018 (c) FOREFRONT International Sdn Bhd
*/
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 analytics library that allows for declarative event tracking
* in your websites.
* @class
*/
class Foresight {
/**
* Default Options
* @static
*/
static defaultOptions: Partial<ForesightConfig> = {
nonInteractionClicks: false,
nonInteractionViews: true,
observerOptions: {},
}
/**
* Stores the options of the current Foresight instance.
* @public
*/
options: ForesightConfig;
/**
* Stores a mapping of elements with is respective functions to de-register
* listeners.
* @private
*/
private _untrackFns: Map<Element, { click: Function, view: Function }> = new Map();
/**
* Stores an instance of an IntersectionObserver.
* @private
*/
private _observer: IntersectionObserver = null;
/**
* @constructor
*/
constructor(config: ForesightConfig) {
this.options = { ...Foresight.defaultOptions, ...config };
// Initialise IntersectionObserver
this._observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this._onTrackedView(entry.target, observer);
observer.unobserve(entry.target);
}
});
}, this.options.observerOptions);
}
/**
* Start event tracking for all DOM elements with event tracking attributes.
*/
start(root: Element = document.body) {
if (gtag === undefined) {
throw `The 'gtag' function is undefined. Has Google Analytics been loaded yet?`;
}
Utils
.toArray<Element>(root.querySelectorAll('[data-track], [data-track-view]'))
.map(element => this.track(element));
}
/**
* Enables event tracking for a DOM element with event tracking attribute.
*/
track(element: Element) {
if (!this._untrackFns.has(element)) {
this._untrackFns.set(element, { click: null, view: null });
}
const untrackFn = this._untrackFns.get(element);
// Track clicks
if (element.getAttribute('data-track') !== null) {
if (untrackFn.click == null) {
untrackFn.click = this._trackClicks(element);
}
}
// Track views
if (element.getAttribute('data-track-view') !== null) {
if (untrackFn.view == null) {
untrackFn.view = this._trackViews(element);
}
}
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.
* @private
*/
private _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
*/
private _trackClicks(element: Element): Function {
// Define listen fucntion
const listener = (e) => {
this._onTrackedClick(element, e);
};
element.addEventListener('click', listener);
element.addEventListener('auxclick', listener);
return () => {
element.removeEventListener('click', listener);
element.removeEventListener('auxclick', listener);
};
}
/**
* 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
*/
private _trackViews(element: Element): Function {
this._observer.observe(element);
return () => {
this._observer.unobserve(element);
};
}
/**
* Handles a click event on an element that is being tracked by Foresight.
* @private
*/
private _onTrackedClick(element: Element, event: Event) {
const s = element.getAttribute('data-track');
const data = this._parseEventString(s);
data.interaction = !this.options.nonInteractionClicks;
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,
});
}
/**
* Handles a view event on an element that is being tracked by Foresight.
* @private
*/
private _onTrackedView(element: Element, observer) {
const s = element.getAttribute('data-track-view');
const data = this._parseEventString(s);
data.interaction = !this.options.nonInteractionViews;
if (element.getAttribute('data-track-view:interaction') !== null) {
data.interaction = true;
}
gtag('event', data.action, {
'event_label': data.label,
'event_category': data.category,
'non_interaction': !data.interaction,
});
}
}
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);
}
{
"compilerOptions": {
"target": "es5",
"allowJs": true,
"noImplicitAny": false,
"removeComments": true,
"lib": ["es6", "dom"]
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}
\ No newline at end of file
......@@ -13,8 +13,8 @@ const isProduction = process.env.NODE_ENV === 'production' || process.argv.index
const config = {
mode: isProduction ? 'production' : 'development',
entry: {
'foresight': './src/foresight.js',
'foresight.min': './src/foresight.js',
'foresight': './src/foresight.ts',
'foresight.min': './src/foresight.ts',
},
output: {
path: path.join(__dirname, isProduction ? 'dist' : 'build'),
......@@ -26,12 +26,15 @@ const config = {
module: {
rules: [
{
test: /\.jsx?$/,
test: /\.tsx?$/,
exclude: /node_modules/,
use: ['babel-loader'],
use: ['awesome-typescript-loader'],
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
optimization: {
minimize: isProduction,
minimizer: [new UglifyJsPlugin({
......
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