190 lines
7 KiB
JavaScript
190 lines
7 KiB
JavaScript
|
/*
|
||
|
* JavaScript tracker for Snowplow: forms.js
|
||
|
*
|
||
|
* Significant portions copyright 2010 Anthon Pang. Remainder copyright
|
||
|
* 2012-2014 Snowplow Analytics Ltd. All rights reserved.
|
||
|
*
|
||
|
* Redistribution and use in source and binary forms, with or without
|
||
|
* modification, are permitted provided that the following conditions are
|
||
|
* met:
|
||
|
*
|
||
|
* * Redistributions of source code must retain the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer.
|
||
|
*
|
||
|
* * Redistributions in binary form must reproduce the above copyright
|
||
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
* documentation and/or other materials provided with the distribution.
|
||
|
*
|
||
|
* * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the
|
||
|
* names of their contributors may be used to endorse or promote products
|
||
|
* derived from this software without specific prior written permission.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
*/
|
||
|
|
||
|
var forEach = require('lodash/forEach'),
|
||
|
filter = require('lodash/filter'),
|
||
|
find = require('lodash/find'),
|
||
|
helpers = require('./lib/helpers'),
|
||
|
object = typeof exports !== 'undefined' ? exports : this;
|
||
|
|
||
|
/**
|
||
|
* Object for handling automatic form tracking
|
||
|
*
|
||
|
* @param object core The tracker core
|
||
|
* @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements
|
||
|
* @param function contextAdder Function to add common contexts like PerformanceTiming to all events
|
||
|
* @return object formTrackingManager instance
|
||
|
*/
|
||
|
object.getFormTrackingManager = function (core, trackerId, contextAdder) {
|
||
|
|
||
|
// Tag names of mutable elements inside a form
|
||
|
var innerElementTags = ['textarea', 'input', 'select'];
|
||
|
|
||
|
// Used to mark elements with event listeners
|
||
|
var trackingMarker = trackerId + 'form';
|
||
|
|
||
|
// Filter to determine which forms should be tracked
|
||
|
var formFilter = function () { return true };
|
||
|
|
||
|
// Filter to determine which form fields should be tracked
|
||
|
var fieldFilter = function () { return true };
|
||
|
|
||
|
// Default function applied to all elements, optionally overridden by transform field
|
||
|
var fieldTransform = function (x) { return x };
|
||
|
|
||
|
/*
|
||
|
* Get an identifier for a form, input, textarea, or select element
|
||
|
*/
|
||
|
function getFormElementName(elt) {
|
||
|
return elt[find(['name', 'id', 'type', 'nodeName'], function (propName) {
|
||
|
|
||
|
// If elt has a child whose name is "id", that element will be returned
|
||
|
// instead of the actual id of elt unless we ensure that a string is returned
|
||
|
return elt[propName] && typeof elt[propName] === 'string';
|
||
|
})];
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Identifies the parent form in which an element is contained
|
||
|
*/
|
||
|
function getParentFormName(elt) {
|
||
|
while (elt && elt.nodeName && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') {
|
||
|
elt = elt.parentNode;
|
||
|
}
|
||
|
if (elt && elt.nodeName && elt.nodeName.toUpperCase() === 'FORM') {
|
||
|
return getFormElementName(elt);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Returns a list of the input, textarea, and select elements inside a form along with their values
|
||
|
*/
|
||
|
function getInnerFormElements(elt) {
|
||
|
var innerElements = [];
|
||
|
forEach(innerElementTags, function (tagname) {
|
||
|
|
||
|
var trackedChildren = filter(elt.getElementsByTagName(tagname), function (child) {
|
||
|
return child.hasOwnProperty(trackingMarker);
|
||
|
});
|
||
|
|
||
|
forEach(trackedChildren, function (child) {
|
||
|
if (child.type === 'submit') {
|
||
|
return;
|
||
|
}
|
||
|
var elementJson = {
|
||
|
name: getFormElementName(child),
|
||
|
value: child.value,
|
||
|
nodeName: child.nodeName
|
||
|
};
|
||
|
if (child.type && child.nodeName.toUpperCase() === 'INPUT') {
|
||
|
elementJson.type = child.type;
|
||
|
}
|
||
|
|
||
|
if ((child.type === 'checkbox' || child.type === 'radio') && !child.checked) {
|
||
|
elementJson.value = null;
|
||
|
}
|
||
|
innerElements.push(elementJson);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return innerElements;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Return function to handle form field change event
|
||
|
*/
|
||
|
function getFormChangeListener(event_type, context) {
|
||
|
return function (e) {
|
||
|
var elt = e.target;
|
||
|
var type = (elt.nodeName && elt.nodeName.toUpperCase() === 'INPUT') ? elt.type : null;
|
||
|
var value = (elt.type === 'checkbox' && !elt.checked) ? null : fieldTransform(elt.value);
|
||
|
if (event_type === 'change_form' || (type !== 'checkbox' && type !== 'radio')) {
|
||
|
core.trackFormFocusOrChange(event_type, getParentFormName(elt), getFormElementName(elt), elt.nodeName, type, helpers.getCssClasses(elt), value, contextAdder(helpers.resolveDynamicContexts(context, elt, type, value)));
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Return function to handle form submission event
|
||
|
*/
|
||
|
function getFormSubmissionListener(context) {
|
||
|
return function (e) {
|
||
|
var elt = e.target;
|
||
|
var innerElements = getInnerFormElements(elt);
|
||
|
forEach(innerElements, function (innerElement) {
|
||
|
innerElement.value = fieldTransform(innerElement.value);
|
||
|
});
|
||
|
core.trackFormSubmission(getFormElementName(elt), helpers.getCssClasses(elt), innerElements, contextAdder(helpers.resolveDynamicContexts(context, elt, innerElements)));
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
|
||
|
/*
|
||
|
* Configures form tracking: which forms and fields will be tracked, and the context to attach
|
||
|
*/
|
||
|
configureFormTracking: function (config) {
|
||
|
if (config) {
|
||
|
formFilter = helpers.getFilter(config.forms, true);
|
||
|
fieldFilter = helpers.getFilter(config.fields, false);
|
||
|
fieldTransform = helpers.getTransform(config.fields);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* Add submission event listeners to all form elements
|
||
|
* Add value change event listeners to all mutable inner form elements
|
||
|
*/
|
||
|
addFormListeners: function (context) {
|
||
|
forEach(document.getElementsByTagName('form'), function (form) {
|
||
|
if (formFilter(form) && !form[trackingMarker]) {
|
||
|
|
||
|
forEach(innerElementTags, function (tagname) {
|
||
|
forEach(form.getElementsByTagName(tagname), function (innerElement) {
|
||
|
if (fieldFilter(innerElement) && !innerElement[trackingMarker] && innerElement.type.toLowerCase() !== 'password') {
|
||
|
helpers.addEventListener(innerElement, 'focus', getFormChangeListener('focus_form', context), false);
|
||
|
helpers.addEventListener(innerElement, 'change', getFormChangeListener('change_form', context), false);
|
||
|
innerElement[trackingMarker] = true;
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
helpers.addEventListener(form, 'submit', getFormSubmissionListener(context));
|
||
|
form[trackingMarker] = true;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
};
|