// Copyright 2012 Georg-August-Universität Göttingen, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* AutoQUEST - HTML Monitor
*
* Description: This script records the interactions done by an user on an
* HTML-Website and sends them to a server. It does not register actions on
* Flash, Java, or other special inputs. This script is tested on Firefox
* 15.0.1.
*
* To insert it on your HTML-side, you need to write in the
* head and change the src-attribute to the location, you have chosen for this
* script.
*
* To change the recorded events, edit the action config array. If you want to change
* the server where the data is send to, rewrite the destination variable. The
* records are send to the server, JSON-formatted, if there are 10 inputs or the
* user changes/closes the site.
*
* Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
*
* Version: 0.1
*/
/**
* the server to send the recorded data to
*/
var autoquestDestination;
/**
* an ID that is more or less unique for the client
*/
var autoquestClientId;
/**
* the maximum number of recorded events to be put into one package sent to the server
*/
var autoquestPackageSize = 10;
/**
* this variable defines the tags for which event handling shall be added, as well as the
* event handling action to be monitored
*/
var autoquestActionConfig = [
{ "tag": "a", "actions": [ "onclick",
"onfocus" ] },
//{ "tag": "abbr", "actions": [ ] },
//{ "tag": "address", "actions": [ ] },
//{ "tag": "applet", "actions": [ ] },
{ "tag": "area", "actions": [ "onclick",
"onfocus" ] },
//{ "tag": "article", "actions": [ ] },
//{ "tag": "aside", "actions": [ ] },
{ "tag": "audio", "actions": [ "onplaying",
"onpause",
"ontimeupdate" ] },
{ "tag": "b", "actions": [ "onclick" ] },
//{ "tag": "bdi", "actions": [ ] },
//{ "tag": "bdo", "actions": [ ] },
//{ "tag": "blockquote", "actions": [ ] },
{ "tag": "body", "actions": [ "onclick",
"onkeydown",
"onpagehide",
"onpageshow",
"onscroll",
"onundo" ] },
{ "tag": "button", "actions": [ "onclick",
"onfocus" ] },
{ "tag": "canvas", "actions": [ "onclick" ] },
//{ "tag": "caption", "actions": [ ] },
{ "tag": "cite", "actions": [ "onclick" ] },
{ "tag": "code", "actions": [ "onclick" ] },
//{ "tag": "col", "actions": [ ] },
//{ "tag": "colgroup", "actions": [ ] },
{ "tag": "command", "actions": [ "onclick",
"onfocus" ] },
//{ "tag": "datalist", "actions": [ ] },
{ "tag": "dd", "actions": [ "onclick" ] },
{ "tag": "del", "actions": [ "onclick" ] },
//{ "tag": "details", "actions": [ ] },
{ "tag": "dfn", "actions": [ "onclick" ] },
{ "tag": "div", "actions": [ "onclick",
"onscroll" ] },
//{ "tag": "dl", "actions": [ ] },
{ "tag": "dt", "actions": [ "onclick" ] },
{ "tag": "em", "actions": [ "onclick" ] },
{ "tag": "embed", "actions": [ "onclick" ] },
//{ "tag": "fieldset", "actions": [ ] },
//{ "tag": "figcaption", "actions": [ ] },
//{ "tag": "figure", "actions": [ ] },
//{ "tag": "footer", "actions": [ ] },
{ "tag": "form", "actions": [ "onreset",
"onsubmit" ] },
//{ "tag": "header", "actions": [ ] },
//{ "tag": "hgroup", "actions": [ ] },
{ "tag": "h1", "actions": [ "onclick" ] },
{ "tag": "h2", "actions": [ "onclick" ] },
{ "tag": "h3", "actions": [ "onclick" ] },
{ "tag": "h4", "actions": [ "onclick" ] },
{ "tag": "h5", "actions": [ "onclick" ] },
{ "tag": "h6", "actions": [ "onclick" ] },
//{ "tag": "hr", "actions": [ ] },
{ "tag": "i", "actions": [ "onclick" ] },
//{ "tag": "iframe", "actions": [ ] },
{ "tag": "img", "actions": [ "onabort",
"onclick" ] },
{ "tag": "input_button", "actions": [ "onclick",
"onfocus" ] },
{ "tag": "input_checkbox", "actions": [ "onchange",
"onclick",
"onfocus" ] },
{ "tag": "input_color", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_date", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_datetime", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_datetime-local", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_email", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_file", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_image", "actions": [ "onclick",
"onfocus" ] },
{ "tag": "input_month", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_number", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_password", "actions": [ "onchange",
"onclick",
"onfocus" ] },
{ "tag": "input_radio", "actions": [ "onchange",
"onclick",
"onfocus" ] },
{ "tag": "input_range", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_reset", "actions": [ "onclick",
"onfocus" ] },
{ "tag": "input_search", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_submit", "actions": [ "onclick",
"onfocus" ] },
{ "tag": "input_tel", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_text", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_time", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_url", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input_week", "actions": [ "onchange",
"onclick",
"onfocus",
"onselect" ] },
{ "tag": "input", "actions": [ "onchange",
"onclick",
"onfocus" ] },
{ "tag": "ins", "actions": [ "onclick" ] },
{ "tag": "kbd", "actions": [ "onclick" ] },
{ "tag": "keygen", "actions": [ "onchange",
"onfocus" ] },
//{ "tag": "label", "actions": [ ] },
//{ "tag": "legend", "actions": [ ] },
{ "tag": "li", "actions": [ "onclick" ] },
//{ "tag": "map", "actions": [ ] },
{ "tag": "mark", "actions": [ "onclick" ] },
{ "tag": "menu", "actions": [ "onclick" ] },
{ "tag": "meter", "actions": [ "onclick" ] },
//{ "tag": "nav", "actions": [ ] },
//{ "tag": "noscript", "actions": [ ] },
{ "tag": "object", "actions": [ "onclick" ] },
//{ "tag": "ol", "actions": [ ] },
//{ "tag": "optgroup", "actions": [ ] },
//{ "tag": "option", "actions": [ ] },
{ "tag": "output", "actions": [ "onclick" ] },
{ "tag": "p", "actions": [ "onclick" ] },
//{ "tag": "param", "actions": [ ] },
//{ "tag": "pre", "actions": [ ] },
{ "tag": "progress", "actions": [ "onclick" ] },
{ "tag": "q", "actions": [ "onclick" ] },
//{ "tag": "rp", "actions": [ ] },
//{ "tag": "rt", "actions": [ ] },
//{ "tag": "ruby", "actions": [ ] },
{ "tag": "s", "actions": [ "onclick" ] },
{ "tag": "samp", "actions": [ "onclick" ] },
//{ "tag": "section", "actions": [ ] },
{ "tag": "select", "actions": [ "onchange",
"onfocus",
"onscroll" ] },
{ "tag": "small", "actions": [ "onclick" ] },
//{ "tag": "source", "actions": [ ] },
{ "tag": "span", "actions": [ "onclick" ] },
{ "tag": "strong", "actions": [ "onclick" ] },
//{ "tag": "sub", "actions": [ ] },
//{ "tag": "summary", "actions": [ ] },
//{ "tag": "sup", "actions": [ ] },
//{ "tag": "table", "actions": [ ] },
//{ "tag": "tbody", "actions": [ ] },
{ "tag": "td", "actions": [ "onclick" ] },
{ "tag": "textarea", "actions": [ "onchange",
"onfocus",
"onselect",
"onscroll" ] },
//{ "tag": "tfoot", "actions": [ ] },
{ "tag": "th", "actions": [ "onclick" ] },
//{ "tag": "thead", "actions": [ ] },
{ "tag": "time", "actions": [ "onclick" ] },
//{ "tag": "tr", "actions": [ ] },
//{ "tag": "track", "actions": [ ] },
{ "tag": "u", "actions": [ "onclick" ] },
//{ "tag": "ul", "actions": [ ] },
{ "tag": "var", "actions": [ "onclick" ] },
{ "tag": "video", "actions": [ "onplaying",
"onpause",
"ontimeupdate" ] }
//{ "tag": "wbr", "actions": [ ] },
];
/**
* a possibility to trace, what is going on
*/
var autoquestDoLog = false;
/**
* stores the structure of the GUI of the current page
*/
var autoquestGUIModel;
/**
* stores events, which were recorded but not sent to the server yet
*/
var autoquestRecordedEvents = [];
/**
* stores the interval for sending data of inactive browser windows
*/
var autoquestSendInterval;
/**
* automatically executed to initialize the event handling
*/
(function() {
initEventHandling();
}());
/**
* initializes the event handling after the document is loaded completely
*/
function initEventHandling() {
if (document.body) {
if (document.readyState !== "complete") {
// if the document is not loaded yet, try to add further event handling later
setTimeout(initEventHandling, 200);
}
else if (!autoquestSendInterval) {
log("adding event handling attributes");
determineDestination();
autoquestGUIModel =
addEventHandlingAndGetJSONRepresentation(document.documentElement, "");
addDefaultEventHandling();
// the onload event is already fired and lost. So pretend it to happen.
handleEvent(document.getElementsByTagName("body").item(0), "onload",
"/html[0]/body[0]", null);
// recall sending data each 100 seconds to ensure, that for browser windows staying
// open the data will be send, as well.
autoquestSendInterval = setTimeout(sendRequest, 100000);
}
}
else {
setTimeout(initEventHandling, 200);
}
}
/**
* traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
* this URL, it calculates the destination to which the traced interactions must be sent
*/
function determineDestination() {
var scriptElements = document.getElementsByTagName("script");
var i;
var index;
for (i = 0; i < scriptElements.length; i++) {
if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
if (index > -1) {
autoquestDestination = scriptElements[i].src.substring(0, index - 1);
log("using destination " + autoquestDestination);
}
}
}
}
/**
* traverses the DOM-structure of the HTML-site and adds event handling attributes to each
* relevant node. Furthermore returns a JSON representation of the node including the children
*
* @param node the node of the DOM structure that shall be adapted and whose children shall
* be traversed
* @param parentPath the path to the parent node of the provided node within the DOM-structure of
* the HTML-site
*/
function addEventHandlingAndGetJSONRepresentation(node, parentPath) {
var nodePath;
var i;
var jsonRepresentation = null;
var childRepresentation;
var childRepresentations = null;
if (node.nodeType === Node.ELEMENT_NODE) {
jsonRepresentation = "{\"tagName\":\"" + getTagName(node) + "\",";
if ((node.id) && (node.id !== "")) {
jsonRepresentation += "\"htmlId\":\"" + node.id + "\"";
}
else {
jsonRepresentation += "\"index\":\"" + getNodeIndex(node) + "\"";
}
addEventHandling(node, parentPath);
if (node.childNodes.length > 0) {
nodePath = getNodePath(node, parentPath);
for (i = 0; i < node.childNodes.length; i++) {
childRepresentation =
addEventHandlingAndGetJSONRepresentation(node.childNodes[i], nodePath);
if (childRepresentation) {
if (!childRepresentations) {
childRepresentations = childRepresentation;
}
else {
childRepresentations += "," + childRepresentation;
}
}
}
if (childRepresentations) {
jsonRepresentation += ",\"children\":[" + childRepresentations + "]";
}
}
jsonRepresentation += "}";
}
return jsonRepresentation;
}
/**
* adds event handling functionality to the provided node. Calls
* {@link #addEventHandlingWithJQuery(node,parentPath)} or
* {@link #addEventHandlingWithoutJQuery(node,parentPath)} depending on the fact if jQuery is
* available and must therefore be used, or not.
*
* @param node the node of the DOM structure that shall be equipped with event handling
* @param parentPath the path to the parent node of the provided node within the DOM-structure of
* the HTML-site
*/
function addEventHandling(node, parentPath) {
if (typeof jQuery === 'undefined') {
addEventHandlingWithoutJQuery(node, parentPath);
}
else {
addEventHandlingWithJQuery(node, parentPath);
}
}
/**
* adds event handling functionality to the provided node using onxxx attributes
*
* @param node the node of the DOM structure that shall be equipped with event handling
* @param parentPath the path to the parent node of the provided node within the DOM-structure of
* the HTML-site
*/
function addEventHandlingWithoutJQuery(node, parentPath) {
var nodePath = getNodePath(node, parentPath);
var tagName = getTagName(node);
var i;
var k;
for (i = 0; i < autoquestActionConfig.length; i++) {
if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
adaptEventHandlingAttribute(node, nodePath, autoquestActionConfig[i].actions[k]);
}
}
}
}
/**
* adds event handling functionality to the provided node using jQuery attributes. If the node
* already used onxxx attributes, these are extended instead of using jQuery.
*
* @param node the node of the DOM structure that shall be equipped with event handling
* @param parentPath the path to the parent node of the provided node within the DOM-structure of
* the HTML-site
*/
function addEventHandlingWithJQuery(node, parentPath) {
var nodePath = getNodePath(node, parentPath);
var tagName = getTagName(node);
var action;
var i;
var k;
for (i = 0; i < autoquestActionConfig.length; i++) {
if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
action = autoquestActionConfig[i].actions[k];
if (jQuery(node).attr(action)) {
// if there is an event handling attribute although jquery is present
// edit this attribute accordingly
adaptEventHandlingAttribute(node, nodePath, action);
}
else {
registerEventHandler(node, nodePath, action);
}
}
}
}
}
/**
* adapts the event handling attributed provided by the action parameter so that it calls
* the {@link #handleEvent(node, action, path, event)} function in the case the event occurs.
* Either the method creates an appropriate onxxx attribute on the node if there is none, or it
* adds a call to the function as first thing called by the onxxx attribute.
*
* @param node the node of the DOM structure that shall be equipped with event handling
* @param nodePath the path to the node within the DOM-structure of the HTML-site
* @param action the event for which event handling shall be enabled
*/
function adaptEventHandlingAttribute(node, nodePath, action) {
var value = "handleEvent(this, '" + action + "', '" + nodePath + "', event);";
var oldValue;
if (!node.getAttribute(action)) {
node.setAttribute(action, value);
}
else {
oldValue = node.getAttribute(action);
if (oldValue.indexOf(value) < 0) {
node.setAttribute(action, value + ' ' + oldValue);
}
}
}
/**
* registers an event handler using jQuery for the provided action on the given object so that it
* calls the {@link #handleJQueryEvent(event)} function in the case the event occurs.
*
* @param node the node of the DOM structure that shall be equipped with event handling
* @param nodePath the path to the node within the DOM-structure of the HTML-site
* @param action the event for which event handling shall be enabled
*/
function registerEventHandler(node, nodePath, action) {
var parameters = { action : action, path : nodePath};
if ((action === "onscroll") && (node === document.body)) {
node = window;
}
if (jQuery(node).on) {
jQuery(node).on(action.substring(2), parameters, handleJQueryEvent);
}
else {
jQuery(node).bind(action.substring(2), parameters, handleJQueryEvent);
}
}
/**
* adds default event handling functionality for receiving load, unload, and other document
* relevant events. The registration for events is done depending on the availability of jQuery.
* If jQuery is available and must therefore be used, then the registration is done on the window
* object. Otherwise, the appropriate attributes of the document's body tag are changed
*/
function addDefaultEventHandling() {
var body;
if (typeof jQuery === 'undefined') {
body = document.getElementsByTagName("body").item(0);
adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onbeforeunload");
adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onload");
adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onunload");
//adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onerror");
}
else {
registerEventHandler(window, "/html[0]/body[0]", "onbeforeunload");
registerEventHandler(window, "/html[0]/body[0]", "onload");
registerEventHandler(window, "/html[0]/body[0]", "onunload");
//registerEventHandler(body, "/html[0]/body[0]", "onerror");
}
}
/**
* generates a path through the DOM-structure of the HTML-site depending on a node and the path
* to its parent node. The result is the parent path plus a path element for the provided node.
* The first part of the path element generated for the node is the tag name returned by
* {@link #getTagName(node)}. If the node has an id, it becomes the second part of the path
* element. If the node does not have an id, the method calculates the index of the node within
* all children of the same type within the parent node. This index becomes then the second part
* of the path element generated for the node.
*
* @param node the node of the DOM structure for which the path shall be created
* @param parentPath the path to the parent node of the provided node
*
* @returns a path in the DOM-structure of the HTML-site including the parent path an a path
* element for the provided node
*/
function getNodePath(node, parentPath) {
var nodePath = parentPath + "/" + getTagName(node);
if ((node.id) && (node.id !== "")) {
nodePath += "(htmlId=" + node.id + ")";
}
else {
nodePath += "[" + getNodeIndex(node) + "]";
}
return nodePath;
}
/**
* called to handle events caused by the jQuery event handling mechanism. Forwards the event to
* {@link #handleEvent(node, eventName, nodePath, event)}
*
* @param the event to be handled
*/
function handleJQueryEvent(event) {
handleEvent(this, event.data.action, event.data.path, event);
}
/**
* handles an event that happened on a node. This method is called by the event handling attributes
* of the nodes. These attributes are generated by the
* {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
* adds it to the list of autoquestRecordedEvents
. If this list achieves the maximum
* autoquestPackageSize
the events in the list are sent to the server asynchronously
* through calling {@link #sendRequest()}.
*
* @param node the node that fired the event
* @param eventName the name of the event, e.g. onscroll
* @param nodePath the path to the node in the HTML DOM on which the event occurred
* @param event the HTML event that occured
*/
function handleEvent(node, eventName, nodePath, event) {
var eventType;
var eventObj = null;
var preceedingScrollEventObj = null;
var tagName;
var unmonitored;
var x1, x2;
var y1, y2;
if (!autoquestDestination) {
// do nothing if we have no destination to send data to
return;
}
log("handling event " + eventName + " on " + node);
eventType = eventName.toLowerCase();
// filter onkeydown events, which are not of interest
if ((eventType === "onkeydown") && (node === document.body)) {
// do not handle key downs on the body which are other than tabs (keycode 9)
if ((event.keyCode !== 9) && (event.keyCode !== 13)) {
return;
}
}
// check, if the preceeding event is more concrete, i.e. the new event to be handled is only
// a propagation through the DOM and needs, therefore, not be handled
if (autoquestRecordedEvents.length > 0) {
eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1];
if ((eventObj.type === eventType) && (eventObj.nodePath.indexOf(nodePath) === 0)) {
// the event is of the same type.
if (eventObj.nodePath.length > nodePath.length) {
// the new event is the same event as the previous one, but fired for the parent
// GUI element of the previous event. This must not be handled. So ignore it
log("discarding event " + eventName + " on " + node +
" as it is already handled by a child");
return;
}
else if (eventType !== "onscroll") {
// we have the same event on the same element. If it is an onscroll, we may
// reuse it. But it is not an onscroll. So we ignore the existing event.
eventObj = null;
}
else {
// we have a repeated onscroll. Check, if the scrolling direction can be determined
// and if it changed. If so created a new object. If not, stick to the existing one.
if (autoquestRecordedEvents.length > 1) {
preceedingScrollEventObj =
autoquestRecordedEvents[autoquestRecordedEvents.length - 2];
if (preceedingScrollEventObj.type !== eventType) {
preceedingScrollEventObj = null;
}
}
if (preceedingScrollEventObj != null) {
x1 = preceedingScrollEventObj.scrollPosition[0] - eventObj.scrollPosition[0];
x2 = eventObj.scrollPosition[0] - getScrollCoordinates(node)[0];
y1 = preceedingScrollEventObj.scrollPosition[1] - eventObj.scrollPosition[1];
y2 = eventObj.scrollPosition[1] - getScrollCoordinates(node)[1];
if ((((x1 >= 0) && (x2 >= 0)) || ((x1 <= 0) && (x2 <= 0))) &&
(((y1 >= 0) && (y2 >= 0)) || ((y1 <= 0) && (y2 <= 0))))
{
// The new scrolling is a continuation of the previous scrolling --> reuse
// previous object
}
else {
eventObj = null;
}
}
else {
// we cannot determine the preceeding scroll direction, so we have to store the
// new event
eventObj = null;
}
}
}
else {
// the event is not of an equal type as the previous one. So we will not reuse it
eventObj = null;
}
}
if (!eventObj) {
// we can not reuse the previous event. So create a new event and add it to the list
eventObj = new Event(eventType, nodePath);
log("storing event " + eventName);
autoquestRecordedEvents.push(eventObj);
}
// now add further event parameters
if ((eventType === "onclick") || (eventType === "ondblclick")) {
eventObj.setClickCoordinates(getEventCoordinates(event));
}
tagName = getTagName(node);
// ensure to not monitor details for GUI elements marked as unmonitored
if (node.getAttribute && node.getAttribute("class")) {
unmonitored = node.getAttribute("class").indexOf("autoquest-unmonitored") >= 0;
}
else {
unmonitored = false;
}
if (!unmonitored && ("input_password" !== tagName)) {
// add further parameters for GUI elements which are monitored and which are no password
// fields
if ((eventType === "onkeypress") ||
(eventType === "onkeydown") ||
(eventType === "onkeyup"))
{
eventObj.setKey(event.keyCode);
}
else if (eventType === "onchange") {
if ((tagName.indexOf("input") === 0) ||
(tagName === "textarea") ||
(tagName === "keygen"))
{
eventObj.setSelectedValue(node.value);
}
else if (tagName === "select") {
eventObj.setSelectedValue(node.options.item(node.options.selectedIndex));
}
}
}
if (eventType === "onscroll") {
eventObj.setScrollPosition(getScrollCoordinates(node));
}
// finally send the recorded data
if (autoquestRecordedEvents.length >= autoquestPackageSize) {
log("initiating sending events");
setTimeout(sendRequest, 100);
}
else if ((eventType === "onunload") || (eventType === "onbeforeunload")) {
log("initiating sending events");
sendRequest();
}
}
/**
* determines a tag name of a node. If the node is an input element, the tag name includes the
* type of element. Otherwise the method simply returns the tag name.
*
* @param node the node for which the name must be determined
*
* @return the name as described
*/
function getTagName(node) {
var tagName = null;
if (node.tagName) {
tagName = node.tagName.toLowerCase();
if ("input" === tagName) {
if (node.type && (node.type !== "")) {
tagName += "_" + node.type;
}
else {
tagName += "_text";
}
}
}
return tagName;
}
/**
* determines the index of a node considering all nodes of the parent having the same name. If the
* node has no parent, the method returns index 0.
*
* @param node the node for which the index must be determined
*
* @return the index as described
*/
function getNodeIndex(node) {
var i;
var index = 0;
if (node.parentNode) {
for (i = 0; i < node.parentNode.childNodes.length; i++) {
if (node.parentNode.childNodes[i].tagName === node.tagName) {
// if === also returns true if the nodes are not identical but only equal,
// this may fail.
if (node.parentNode.childNodes[i] === node) {
break;
}
index++;
}
}
}
return index;
}
/**
* sends the collected data to the server, named in the destination-variable. For this it generates
* a JSON formatted message and uses Ajax and the autoquestDestination
to send it to
* the server
*/
function sendRequest() {
var eventList = autoquestRecordedEvents;
var message;
var clientId;
var i = 0;
var request;
if (eventList.length > 1) {
log("creating message");
// put the last event into the new list to allow for checks for reoccurence of the same
// event
autoquestRecordedEvents = [ eventList.pop() ];
message = "{\"message\":{\"clientInfos\":{";
log("reading client id");
clientId = getClientId();
if ((clientId) && (clientId !== "")) {
message += "\"clientId\":\"" + clientId + "\",";
}
log("adding other infos");
message += "\"userAgent\":\"" + navigator.userAgent + "\",";
message += "\"title\":\"" + document.title + "\",";
message += "\"url\":\"" + document.URL + "\"},";
message += "\"guiModel\":" + autoquestGUIModel + ",";
message += "\"events\":[";
for (i = 0; i < eventList.length; i++) {
if (i > 0) {
message += ",";
}
message += eventList[i].toJSON();
}
message += "]}}";
request = null;
// Mozilla
if (window.XMLHttpRequest) {
request = new XMLHttpRequest();
}
// IE
else if (window.ActiveXObject) {
request = new ActiveXObject("Microsoft.XMLHTTP");
}
request.open("POST", autoquestDestination, false);
request.setRequestHeader("Content-Type", "application/json");
log("sending " + message);
request.send(message);
}
}
/**
* determines the scroll coordinates of the scrolled element
*
* @param node the element that was scrolled
*
* @returns the coordinates of the scrolling as an array with x and y coordinate
*/
function getScrollCoordinates(node) {
if (typeof node.scrollLeft !== 'undefined') {
return [node.scrollLeft, node.scrollTop];
}
else if ((node === window) && (typeof window.pageXOffset !== 'undefined')) {
return [window.pageXOffset, window.pageYOffset];
}
else if ((node === document) || (node === document.body) ||
(node === document.documentElement))
{
if ((typeof document.body !== 'undefined') &&
(typeof document.body.scrollLeft !== 'undefined'))
{
return [document.body.scrollLeft, document.body.scrollTop];
}
else if ((typeof document.documentElement !== 'undefined') &&
(typeof document.documentElement.scrollLeft !== 'undefined'))
{
return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
}
}
return [-1, -1];
}
/**
* determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
* with the provided event, they are determined based on the surrounding object
*
* @param event the event to extract the coordinates of
*
* @returns the coordinates of the event as an array with x and y coordinate
*/
function getEventCoordinates(event) {
if (event.layerX) {
return [event.layerX, event.layerY];
}
else if (event.offsetX) {
return [event.offsetX, event.offsetY];
}
var obj = event.target || event.srcElement;
var objOffset = getPageOffset(obj);
return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
}
/**
* determines the page offset of an object using the parent objects offset
*/
function getPageOffset(object) {
var top = 0;
var left = 0;
var obj = object;
while (obj.offsetParent) {
left += obj.offsetLeft;
top += obj.offsetTop;
obj = obj.offsetParent;
}
return [left, top];
}
/**
* generates a client id based on several information retrieved from the environment. The client
* id is not always unique
*
* @returns the client id
*/
function getClientId() {
var clientIdStr;
var clientId;
var i = 0;
if (!autoquestClientId) {
// create something like a more or less unique checksum.
clientIdStr =
navigator.appCodeName + navigator.appName + navigator.appVersion +
navigator.cookieEnabled + navigator.language + navigator.platform +
navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
window.location.host + new Date().getTimezoneOffset();
clientId = clientIdStr.length;
for (i = 0; i < clientIdStr.length; i++) {
clientId += clientIdStr.charCodeAt(i);
}
autoquestClientId = clientId;
}
return autoquestClientId;
}
/**
* performs a simple logging by adding a specific div to the HTML
*
* @param text the text to be logged
*/
function log(text) {
if (autoquestDoLog) {
var loggingInfo = document.getElementById("loggingInfoParagraph");
if (!loggingInfo) {
var div = document.createElement("div");
div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
"border:1px black solid; position:absolute; top:10px; right:10px; " +
"width:350px; height:auto; font-size:8pt; font-family:Helvetica");
loggingInfo = document.createElement("div");
loggingInfo.id = "loggingInfoParagraph";
div.appendChild(loggingInfo);
var body = document.getElementsByTagName("body")[0];
if (!body) {
alert("could not enable logging");
}
else {
body.appendChild(div);
}
}
loggingInfo.appendChild(document.createTextNode(text));
loggingInfo.appendChild(document.createElement("br"));
}
}
/**
* this class represents a single event
*
* @param type the type of the event
* @param nodePath the path through the HTML DOM to the event target
*/
function Event(type, nodePath) {
this.type = type;
this.nodePath = nodePath;
this.time = new Date().getTime();
this.setClickCoordinates = function(coordinates) {
this.clickCoordinates = coordinates;
};
this.setKey = function(key) {
this.key = key;
};
this.setSelectedValue = function(value) {
this.selectedValue = encodeText(value);
};
this.setScrollPosition = function(scrollPosition) {
this.scrollPosition = scrollPosition;
};
this.toJSON = function() {
var eventInJSON =
"{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
this.type + "\"";
if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
}
if (this.key) {
eventInJSON += ",\"key\":" + this.key;
}
if (this.selectedValue) {
eventInJSON += ",\"selectedValue\":\"" + this.selectedValue + "\"";
}
if ("onscroll" === this.type) {
eventInJSON += ",\"scrollPosition\":[" + this.scrollPosition + "]";
}
eventInJSON += "}";
return eventInJSON;
};
function encodeText(text) {
return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"");
};
}