// 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", "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" ] }, //{ "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_text", "actions": [ "onchange", "onfocus", "onselect" ] }, { "tag": "input_password", "actions": [ "onchange", "onfocus" ] }, { "tag": "input_checkbox", "actions": [ "onchange", "onclick", "onfocus" ] }, { "tag": "input_radio", "actions": [ "onchange", "onclick", "onfocus" ] }, { "tag": "input_submit", "actions": [ "onclick", "onfocus" ] }, { "tag": "input_reset", "actions": [ "onclick", "onfocus" ] }, { "tag": "input_file", "actions": [ "onclick", "onfocus" ] }, { "tag": "input_image", "actions": [ "onclick", "onfocus" ] }, { "tag": "input_button", "actions": [ "onclick", "onfocus" ] }, { "tag": "input", "actions": [ "onchange", "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" ] }, { "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" ] }, //{ "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(); // 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 (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 += "(id=" + 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 tagName; if (!autoquestDestination) { // do nothing if we have no destination to send data to return; } log("handling event " + eventName + " on " + node); eventType = eventName.toLowerCase(); if (autoquestRecordedEvents.length > 0) { eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1]; // check if an event showed up several times either for the same or for a parent GUI element if ((eventObj.type === eventName) && (eventObj.nodePath.indexOf(nodePath) === 0)) { // the event is of the same type. if (eventObj.nodePath.length > nodePath.length) { // the same event showed up for the parent GUI element. 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 (eventName !== "onscroll") { // we have the same event on the same element. If it is an onscroll, we should // reuse it. But it is not an onscroll. So we ignore the existing 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) { // 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); if ("input_password" !== tagName) { 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)); } 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 (node.scrollLeft || node.scrollTop) { return [node.scrollLeft, node.scrollTop]; } else if ((node === window) && window.pageYOffset) { return [window.pageXOffset, window.pageYOffset]; } else if ((node == document) || (node == document.body) || (node == document.documentElement)) { if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { return [document.body.scrollLeft, document.body.scrollTop]; } else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) { 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, "\\\""); }; }