source: trunk/autoquest-htmlmonitor/src/main/js/autoquest-htmlmonitor.js @ 1020

Last change on this file since 1020 was 1020, checked in by pharms, 12 years ago
  • included logging of GUI structure
  • added support for web applications using jQuery
File size: 29.8 KB
Line 
1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15/**
16 * AutoQUEST - HTML Monitor
17 *
18 * Description: This script records the interactions done by an user on an
19 * HTML-Website and sends them to a server. It does not register actions on
20 * Flash, Java, or other special inputs. This script is tested on Firefox
21 * 15.0.1.
22 *
23 * To insert it on your HTML-side, you need to write <script
24 * language="JavaScript" type="text/javascript" src="autoquest-htmlmonitor.js"></script> in the
25 * head and change the src-attribute to the location, you have chosen for this
26 * script.
27 *
28 * To change the recorded events, edit the action config array. If you want to change
29 * the server where the data is send to, rewrite the destination variable. The
30 * records are send to the server, JSON-formatted, if there are 10 inputs or the
31 * user changes/closes the site.
32 *
33 * Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
34 *
35 * Version: 0.1
36 */
37
38/**
39 * the server to send the recorded data to
40 */
41var autoquestDestination;
42
43/**
44 * an ID that is more or less unique for the client
45 */
46var autoquestClientId;
47
48/**
49 * the maximum number of recorded events to be put into one package sent to the server
50 */
51var autoquestPackageSize = 10;
52
53/**
54 * this variable defines the tags for which event handling shall be added, as well as the
55 * event handling action to be monitored
56 */
57var autoquestActionConfig = [
58    { "tag": "a", "actions": [ "onclick",
59                               "onfocus" ] },
60    //{ "tag": "abbr", "actions": [  ] },
61    //{ "tag": "address", "actions": [  ] },
62    //{ "tag": "applet", "actions": [  ] },
63    { "tag": "area", "actions": [ "onclick",
64                                  "onfocus" ] },
65    //{ "tag": "article", "actions": [  ] },
66    //{ "tag": "aside", "actions": [  ] },
67    { "tag": "audio", "actions": [ "onplaying",
68                                   "onpause",
69                                   "ontimeupdate" ] },
70    { "tag": "b", "actions": [ "onclick" ] },
71    //{ "tag": "bdi", "actions": [  ] },
72    //{ "tag": "bdo", "actions": [  ] },
73    //{ "tag": "blockquote", "actions": [  ] },
74    { "tag": "body", "actions": [ "onbeforeunload",
75                                  "onload",
76                                  "onunload",
77                                  //"onerror",
78                                  "onscroll",
79                                  "onpagehide",
80                                  "onpageshow",
81                                  "onundo" ] },
82    { "tag": "button", "actions": [ "onclick",
83                                    "onfocus" ] },
84    { "tag": "canvas", "actions": [ "onclick" ] },
85    //{ "tag": "caption", "actions": [  ] },
86    { "tag": "cite", "actions": [ "onclick" ] },
87    { "tag": "code", "actions": [ "onclick" ] },
88    //{ "tag": "col", "actions": [  ] },
89    //{ "tag": "colgroup", "actions": [  ] },
90    { "tag": "command", "actions": [ "onclick",
91                                     "onfocus" ] },
92    //{ "tag": "datalist", "actions": [  ] },
93    { "tag": "dd", "actions": [ "onclick" ] },
94    { "tag": "del", "actions": [ "onclick" ] },
95    //{ "tag": "details", "actions": [  ] },
96    { "tag": "dfn", "actions": [ "onclick" ] },
97    { "tag": "div", "actions": [ "onclick" ] },
98    //{ "tag": "dl", "actions": [  ] },
99    { "tag": "dt", "actions": [ "onclick" ] },
100    { "tag": "em", "actions": [ "onclick" ] },
101    { "tag": "embed", "actions": [ "onclick" ] },
102    //{ "tag": "fieldset", "actions": [  ] },
103    //{ "tag": "figcaption", "actions": [  ] },
104    //{ "tag": "figure", "actions": [  ] },
105    //{ "tag": "footer", "actions": [  ] },
106    { "tag": "form", "actions": [ "onreset",
107                                  "onsubmit" ] },
108    //{ "tag": "header", "actions": [  ] },
109    //{ "tag": "hgroup", "actions": [  ] },
110    { "tag": "h1", "actions": [ "onclick" ] },
111    { "tag": "h2", "actions": [ "onclick" ] },
112    { "tag": "h3", "actions": [ "onclick" ] },
113    { "tag": "h4", "actions": [ "onclick" ] },
114    { "tag": "h5", "actions": [ "onclick" ] },
115    { "tag": "h6", "actions": [ "onclick" ] },
116    //{ "tag": "hr", "actions": [  ] },
117    { "tag": "i", "actions": [ "onclick" ] },
118    //{ "tag": "iframe", "actions": [  ] },
119    { "tag": "img", "actions": [ "onabort",
120                                 "onclick" ] },
121    { "tag": "input_text", "actions": [ "onchange",
122                                        "onfocus",
123                                        "onselect" ] },
124    { "tag": "input_password", "actions": [ "onchange",
125                                            "onfocus" ] },
126    { "tag": "input_checkbox", "actions": [ "onchange",
127                                            "onclick",
128                                            "onfocus" ] },
129    { "tag": "input_radio", "actions": [ "onchange",
130                                         "onclick",
131                                         "onfocus" ] },
132    { "tag": "input_submit", "actions": [ "onclick",
133                                          "onfocus" ] },
134    { "tag": "input_reset", "actions": [ "onclick",
135                                         "onfocus" ] },
136    { "tag": "input_file", "actions": [ "onclick",
137                                        "onfocus" ] },
138    { "tag": "input_image", "actions": [ "onclick",
139                                         "onfocus" ] },
140    { "tag": "input_button", "actions": [ "onclick",
141                                          "onfocus" ] },
142    { "tag": "input", "actions": [ "onchange",
143                                   "onfocus" ] },
144    { "tag": "ins", "actions": [ "onclick" ] },
145    { "tag": "kbd", "actions": [ "onclick" ] },
146    { "tag": "keygen", "actions": [ "onchange",
147                                    "onfocus" ] },
148    //{ "tag": "label", "actions": [  ] },
149    //{ "tag": "legend", "actions": [  ] },
150    { "tag": "li", "actions": [ "onclick" ] },
151    //{ "tag": "map", "actions": [  ] },
152    { "tag": "mark", "actions": [ "onclick" ] },
153    { "tag": "menu", "actions": [ "onclick" ] },
154    { "tag": "meter", "actions": [ "onclick" ] },
155    //{ "tag": "nav", "actions": [  ] },
156    //{ "tag": "noscript", "actions": [  ] },
157    { "tag": "object", "actions": [ "onclick" ] },
158    //{ "tag": "ol", "actions": [  ] },
159    //{ "tag": "optgroup", "actions": [  ] },
160    //{ "tag": "option", "actions": [  ] },
161    { "tag": "output", "actions": [ "onclick" ] },
162    { "tag": "p", "actions": [ "onclick" ] },
163    //{ "tag": "param", "actions": [  ] },
164    //{ "tag": "pre", "actions": [  ] },
165    { "tag": "progress", "actions": [ "onclick" ] },
166    { "tag": "q", "actions": [ "onclick" ] },
167    //{ "tag": "rp", "actions": [  ] },
168    //{ "tag": "rt", "actions": [  ] },
169    //{ "tag": "ruby", "actions": [  ] },
170    { "tag": "s", "actions": [ "onclick" ] },
171    { "tag": "samp", "actions": [ "onclick" ] },
172    //{ "tag": "section", "actions": [  ] },
173    { "tag": "select", "actions": [ "onchange",
174                                    "onfocus" ] },
175    { "tag": "small", "actions": [ "onclick" ] },
176    //{ "tag": "source", "actions": [  ] },
177    { "tag": "span", "actions": [ "onclick" ] },
178    { "tag": "strong", "actions": [ "onclick" ] },
179    //{ "tag": "sub", "actions": [  ] },
180    //{ "tag": "summary", "actions": [  ] },
181    //{ "tag": "sup", "actions": [  ] },
182    //{ "tag": "table", "actions": [  ] },
183    //{ "tag": "tbody", "actions": [  ] },
184    { "tag": "td", "actions": [ "onclick" ] },
185    { "tag": "textarea", "actions": [ "onchange",
186                                      "onfocus",
187                                      "onselect" ] },
188    //{ "tag": "tfoot", "actions": [  ] },
189    { "tag": "th", "actions": [ "onclick" ] },
190    //{ "tag": "thead", "actions": [  ] },
191    { "tag": "time", "actions": [ "onclick" ] },
192    //{ "tag": "tr", "actions": [  ] },
193    //{ "tag": "track", "actions": [  ] },
194    { "tag": "u", "actions": [ "onclick" ] },
195    //{ "tag": "ul", "actions": [  ] },
196    { "tag": "var", "actions": [ "onclick" ] },
197    { "tag": "video", "actions": [ "onplaying",
198                                   "onpause",
199                                   "ontimeupdate" ] }
200    //{ "tag": "wbr", "actions": [  ] },
201];
202
203/**
204 * a possibility to trace, what is going on
205 */
206var autoquestDoLog = false;
207
208/*var matchedTags = ["A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "B", "BIG", "BLOCKQUOTE", "BODY",
209                   "BUTTON", "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD", "DEL",
210                   "DFN", "DIR", "DIV", "DL", "DT", "EM", "FIELDSET", "FORM", "H1", "H2", "H3",
211                   "H4", "H5", "H6", "HR", "I", "IMG", "INPUT", "INS", "KBD", "LABEL", "LEGEND",
212                   "LI", "LINK", "MAP", "MENU", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL",
213                   "OPTGROUP", "OPTION", "P", "PRE", "Q", "S", "SAMP", "SELECT", "SMALL", "SPAN",
214                   "STRIKE", "STRONG", "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT",
215                   "TH", "THEAD", "TR", "TT", "U", "UL", "VAR"];*/
216/*var actions = ['onclick', 'ondblclick', 'onkeypress', 'onkeydown', 'onkeyup',
217'onmouseout' , 'onmousemove' ,'onfocus','onscroll'];  // edit*/
218
219/**
220 * stores the structure of the GUI of the current page
221 */
222var autoquestGUIModel;
223
224/**
225 * stores events, which were recorded but not sent to the server yet
226 */
227var autoquestRecordedEvents = [];
228
229/**
230 * stores the interval for sending data of inactive browser windows
231 */
232var autoquestSendInterval;
233
234/**
235 * automatically executed to initialize the event handling
236 */
237(function() {
238    initEventHandling();
239}());
240
241
242/**
243 * initializes the event handling after the document is loaded completely
244 */
245function initEventHandling() {
246    if (document.body) {
247        log("adding event handling attributes");
248        determineDestination();
249        autoquestGUIModel = addEventHandlingAndGetJSONRepresentation(document.documentElement, "");
250       
251        if (document.readyState !== "complete") {
252            // if the document is not loaded yet, try to add further event handling later
253            setTimeout(initEventHandling, 200);
254        }
255        else if (!autoquestSendInterval) {
256            // recall sending data each 100 seconds to ensure, that for browser windows staying
257            // open the data will be send, as well.
258            autoquestSendInterval = setTimeout(sendRequest, 100000);
259        }
260    }
261    else {
262        setTimeout(initEventHandling, 200);
263    }         
264}
265
266/**
267 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
268 * this URL, it calculates the destination to which the traced interactions must be sent
269 */
270function determineDestination() {
271    var scriptElements = document.getElementsByTagName("script");
272    var i;
273    var index;
274   
275    for (i = 0; i < scriptElements.length; i++) {
276        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
277            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
278            if (index > -1) {
279                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
280                log("using destination " + autoquestDestination);
281            }
282        }
283    }
284}
285
286/**
287 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
288 * relevant node. Furthermore returns a JSON representation of the node including the children
289 *
290 * @param node       the node of the DOM structure that shall be adapted and whose children shall
291 *                   be traversed
292 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
293 *                   the HTML-site
294 */
295function addEventHandlingAndGetJSONRepresentation(node, parentPath) {
296    var nodePath;
297    var i;
298    var jsonRepresentation = null;
299    var childRepresentation;
300    var childRepresentations = null;
301   
302    if (node.nodeType === Node.ELEMENT_NODE) {
303        jsonRepresentation = "{\"tagName\":\"" + getTagName(node) + "\",";
304       
305        if ((node.id) && (node.id !== "")) {
306            jsonRepresentation += "\"id\":\"" + node.id + "\"";
307        }
308        else {
309            jsonRepresentation += "\"index\":\"" + getNodeIndex(node) + "\"";
310        }
311       
312        addEventHandling(node, parentPath);
313       
314        if (node.childNodes.length > 0) {
315            nodePath = getNodePath(node, parentPath);
316           
317            for (i = 0; i < node.childNodes.length; i++) {
318                childRepresentation =
319                    addEventHandlingAndGetJSONRepresentation(node.childNodes[i], nodePath);
320               
321                if (childRepresentation) {
322                    if (!childRepresentations) {
323                        childRepresentations = childRepresentation;
324                    }
325                    else {
326                        childRepresentations += "," + childRepresentation;
327                    }
328                }
329            }
330
331            if (childRepresentations) {
332                jsonRepresentation += ",\"children\":[" + childRepresentations + "]";
333            }
334        }
335       
336        jsonRepresentation += "}";
337    }
338   
339    return jsonRepresentation;
340}
341
342/**
343 * TODO comment
344 */
345function addEventHandling(node, parentPath) {
346    if (typeof jQuery === 'undefined') {
347        addEventHandlingWithoutJQuery(node, parentPath);
348    }
349    else {
350        addEventHandlingWithJQuery(node, parentPath);
351    }
352}
353
354/**
355 * TODO comment
356 */
357function addEventHandlingWithoutJQuery(node, parentPath) {
358    var nodePath = getNodePath(node, parentPath);
359    var tagName = getTagName(node);
360    var i;
361    var k;
362   
363    for (i = 0; i < autoquestActionConfig.length; i++) {
364        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
365            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
366                adaptEventHandlingAttribute(node, nodePath, autoquestActionConfig[i].actions[k]);
367            }
368        }
369    }
370}
371
372/**
373 * TODO comment
374 */
375function addEventHandlingWithJQuery(node, parentPath) {
376    var nodePath = getNodePath(node, parentPath);
377    var tagName = getTagName(node);
378    var action;
379    var parameters;
380    var i;
381    var k;
382   
383    for (i = 0; i < autoquestActionConfig.length; i++) {
384        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
385            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
386                action = autoquestActionConfig[i].actions[k];
387                if (jQuery(node).attr(action)) {
388                    // if there is an event handling attribute although jquery is present
389                    // edit this attribute accordingly
390                    adaptEventHandlingAttribute(node, nodePath, action);
391                }
392                else {
393                    parameters = { action : action, path : nodePath};
394                    if (jQuery(node).on) {
395                        jQuery(node).on(action.substring(2), parameters, handleJQueryEvent);
396                    }
397                    else {
398                        jQuery(node).bind(action.substring(2), parameters, handleJQueryEvent);
399                    }
400                }
401            }
402        }
403    }
404}
405
406/**
407 * TODO comment
408 */
409function adaptEventHandlingAttribute(node, nodePath, action) {
410    var value = "handleEvent(this, '" + action + "', '" + nodePath + "', event);";
411    var oldValue;
412   
413    if (!node.getAttribute(action)) {
414        node.setAttribute(action, value);
415    }
416    else {
417        oldValue = node.getAttribute(action);
418        if (oldValue.indexOf(value) < 0) {
419            node.setAttribute(action, value + ' ' + oldValue);
420        }
421    }
422}
423
424/**
425 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
426 * to its parent node. The result is the parent path plus a path element for the provided node.
427 * The first part of the path element generated for the node is the tag name returned by
428 * {@link #getTagName(node)}. If the node has an id, it becomes the second part of the path
429 * element. If the node does not have an id, the method calculates the index of the node within
430 * all children of the same type within the parent node. This index becomes then the second part
431 * of the path element generated for the node.
432 *
433 * @param node       the node of the DOM structure for which the path shall be created
434 * @param parentPath the path to the parent node of the provided node
435 *
436 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
437 *          element for the provided node
438 */
439function getNodePath(node, parentPath) {
440    var nodePath = parentPath + "/" + getTagName(node);
441   
442    if ((node.id) && (node.id !== "")) {
443        nodePath += "(id=" + node.id + ")";
444    }
445    else {
446        nodePath += "[" + getNodeIndex(node) + "]";
447    }
448   
449    return nodePath;
450}
451
452/**
453 * TODO comment
454 */
455function handleJQueryEvent(event) {
456    handleEvent(this, event.data.action, event.data.path, event);
457}
458
459/**
460 * handles an event that happened on a node. This method is called by the event handling attributes
461 * of the nodes. These attributes are generated by the
462 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
463 * adds it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
464 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
465 * through calling {@link #sendRequest()}.
466 *
467 * @param node      the node that fired the event
468 * @param eventName the name of the event, e.g. onscroll
469 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
470 * @param event     the HTML event that occured
471 */
472function handleEvent(node, eventName, nodePath, event) {
473    var eventType;
474    var eventObj = null;
475    var tagName;
476
477    if (!autoquestDestination) {
478        // do nothing if we have no destination to send data to
479        return;
480    }
481   
482    log("handling event " + eventName + " on " + node);
483   
484    eventType = eventName.toLowerCase();
485
486    if (autoquestRecordedEvents.length > 0) {
487        eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1];
488
489        // check if an event showed up several times either for the same or for a parent GUI element
490        if ((eventObj.type === eventName) && (eventObj.nodePath.indexOf(nodePath) === 0)) {
491            // the event is of the same type.
492            if (eventObj.nodePath.length > nodePath.length) {
493                // the same event showed up for the parent GUI element. This must not be handled.
494                // So ignore it
495                log("discarding event " + eventName + " on " + node +
496                    " as it is already handled by a child");
497                return;
498            }
499            else if (eventName !== "onscroll") {
500                // we have the same event on the same element. If it is an onscroll, we should
501                // reuse it. But it is not an onscroll. So we ignore the existing event.
502                eventObj = null;
503            }
504        }
505        else {
506            // the event is not of an equal type as the previous one. So we will not reuse it
507            eventObj = null;
508        }
509    }
510   
511    if (!eventObj) {
512        // create a new event and add it to the list
513        eventObj = new Event(eventType, nodePath);
514        log("storing event " + eventName);
515        autoquestRecordedEvents.push(eventObj);
516    }
517
518    // now add further event parameters
519    if ((eventType === "onclick") || (eventType === "ondblclick")) {
520        eventObj.setClickCoordinates(getEventCoordinates(event));
521    }
522
523    tagName = getTagName(node);
524   
525    if ("input_password" !== tagName) {
526        if ((eventType === "onkeypress") ||
527            (eventType === "onkeydown") ||
528            (eventType === "onkeyup"))
529        {
530            eventObj.setKey(event.keyCode);
531        }
532        else if (eventType === "onchange") {
533            if ((tagName.indexOf("input") === 0) ||
534                (tagName === "textarea") ||
535                (tagName === "keygen"))
536            {
537                eventObj.setSelectedValue(node.value);
538            }
539            else if (tagName === "select") {
540                eventObj.setSelectedValue(node.options.item(node.options.selectedIndex));
541            }
542        }
543    }
544   
545    if (eventType === "onscroll") {
546        eventObj.setScrollPosition(getScrollCoordinates(node));
547    }
548
549    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
550        log("initiating sending events");
551        setTimeout(sendRequest, 100);
552    }
553    else if ((eventType === "onunload") || (eventType === "onbeforeunload")) {
554        log("initiating sending events");
555        sendRequest();
556    }
557
558}
559
560/**
561 * determines a tag name of a node. If the node is an input element, the tag name includes the
562 * type of element. Otherwise the method simply returns the tag name.
563 *
564 * @param node the node for which the name must be determined
565 *
566 * @return the name as described
567 */
568function getTagName(node) {
569    var tagName = null;
570   
571    if (node.tagName) {
572        tagName = node.tagName.toLowerCase();
573        if ("input" === tagName) {
574            if (node.type && (node.type !== "")) {
575                tagName += "_" + node.type;
576            }
577            else {
578                tagName += "_text";
579            }
580        }
581    }
582
583    return tagName;
584}
585   
586/**
587 * determines the index of a node considering all nodes of the parent having the same name. If the
588 * node has no parent, the method returns index 0.
589 *
590 * @param node the node for which the index must be determined
591 *
592 * @return the index as described
593 */
594function getNodeIndex(node) {
595    var i;
596    var index = 0;
597   
598    if (node.parentNode) {
599        for (i = 0; i < node.parentNode.childNodes.length; i++) {
600            if (node.parentNode.childNodes[i].tagName === node.tagName) {
601                index++;
602                // if === also returns true if the nodes are not identical but only equal,
603                // this may fail.
604                if (node.parentNode.childNodes[i] === node) {
605                    break;
606                }
607            }
608        }
609       
610    }
611
612    return index;
613}
614   
615/**
616 * sends the collected data to the server, named in the destination-variable. For this it generates
617 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
618 * the server
619 */
620function sendRequest() {
621    var eventList = autoquestRecordedEvents;
622    var message;
623    var clientId;
624    var i = 0;
625    var request;
626   
627    if (eventList.length > 1) {
628        log("creating message");
629       
630        // put the last event into the new list to allow for checks for reoccurence of the same
631        // event
632        autoquestRecordedEvents = [ eventList.pop() ];
633       
634        message = "{\"message\":{\"clientInfos\":{";
635       
636        log("reading client id");
637        clientId = getClientId();
638        if ((clientId) && (clientId !== "")) {
639            message += "\"clientId\":\"" + clientId + "\",";
640        }
641       
642        log("adding other infos");
643        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
644        message += "\"title\":\"" + document.title + "\",";
645        message += "\"url\":\"" + document.URL + "\"},";
646       
647        message += "\"guiModel\":" + autoquestGUIModel + ",";
648       
649        message += "\"events\":[";
650       
651        for (i = 0; i < eventList.length; i++) {
652            if (i > 0) {
653                message += ",";
654            }
655            message += eventList[i].toJSON();
656        }
657       
658        message += "]}}";
659       
660        request = null;
661       
662        // Mozilla
663        if (window.XMLHttpRequest) {
664            request = new XMLHttpRequest();
665        }
666        // IE
667        else if (window.ActiveXObject) {
668            request = new ActiveXObject("Microsoft.XMLHTTP");
669        }
670       
671        request.open("POST", autoquestDestination, false);
672        request.setRequestHeader("Content-Type", "application/json");
673
674        log("sending " + message);
675        request.send(message);
676    }
677}
678
679/**
680 * determines the scroll coordinates of the scrolled element
681 *
682 * @param node the element that was scrolled
683 *
684 * @returns the coordinates of the scrolling as an array with x and y coordinate
685 */
686function getScrollCoordinates(node) {
687    if (node.scrollLeft || node.scrollTop) {
688        return [node.scrollLeft, node.scrollTop];
689    }
690    else if ((node === window) && window.pageYOffset) {
691        return [window.pageXOffset, window.pageYOffset];
692    }
693    else if ((node == document) || (node == document.body) || (node == document.documentElement)) {
694        if (document.body && (document.body.scrollLeft || document.body.scrollTop)) {
695            return [document.body.scrollLeft, document.body.scrollTop];
696        }
697        else if (document.documentElement &&
698                 (document.documentElement.scrollLeft || document.documentElement.scrollTop))
699        {
700            return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
701        }
702    }
703
704    return [-1, -1];
705}
706
707/**
708 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
709 * with the provided event, they are determined based on the surrounding object
710 *
711 * @param event the event to extract the coordinates of
712 *
713 * @returns the coordinates of the event as an array with x and y coordinate
714 */
715function getEventCoordinates(event) {
716    if (event.layerX) {
717        return [event.layerX, event.layerY];
718    }
719    else if (event.offsetX) {
720        return [event.offsetX, event.offsetY];
721    }
722
723    var obj = event.target || event.srcElement;
724    var objOffset = getPageOffset(obj);
725
726    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
727}
728
729/**
730 * determines the page offset of an object using the parent objects offset
731 */
732function getPageOffset(object) {
733    var top = 0;
734    var left = 0;
735    var obj = object;
736
737    while (obj.offsetParent) {
738        left += obj.offsetLeft;
739        top += obj.offsetTop;
740        obj = obj.offsetParent;
741    }
742
743    return [left, top];
744}
745
746/**
747 * generates a client id based on several information retrieved from the environment. The client
748 * id is not always unique
749 *
750 * @returns the client id
751 */
752function getClientId() {
753    var clientIdStr;
754    var clientId;
755    var i = 0;
756   
757    if (!autoquestClientId) {
758        // create something like a more or less unique checksum.
759        clientIdStr =
760            navigator.appCodeName + navigator.appName + navigator.appVersion +
761            navigator.cookieEnabled + navigator.language + navigator.platform +
762            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
763            window.location.host + new Date().getTimezoneOffset();
764
765        clientId = clientIdStr.length;
766
767        for (i = 0; i < clientIdStr.length; i++) {
768            clientId += clientIdStr.charCodeAt(i);
769        }
770       
771        autoquestClientId = clientId;
772    }
773
774    return autoquestClientId;
775}
776
777/**
778 * performs a simple logging by adding a specific div to the HTML
779 *
780 * @param text the text to be logged
781 */
782function log(text) {
783    if (autoquestDoLog) {
784        var loggingInfo = document.getElementById("loggingInfoParagraph");
785       
786        if (!loggingInfo) {
787            var div = document.createElement("div");
788            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
789                             "border:1px black solid; position:absolute; top:10px; right:10px; " +
790                             "width:350px; height:auto; font-size:8pt; font-family:Helvetica");
791           
792            loggingInfo = document.createElement("div");
793            loggingInfo.id = "loggingInfoParagraph";
794            div.appendChild(loggingInfo);
795           
796            var body = document.getElementsByTagName("body")[0];
797            if (!body) {
798                alert("could not enable logging");
799            }
800            else {
801                body.appendChild(div);
802            }
803        }
804       
805        loggingInfo.appendChild(document.createTextNode(text));
806        loggingInfo.appendChild(document.createElement("br"));
807    }
808}
809
810/**
811 * this class represents a single event
812 *
813 * @param type     the type of the event
814 * @param nodePath the path through the HTML DOM to the event target
815 */
816function Event(type, nodePath) {
817    this.type = type;
818    this.nodePath = nodePath;
819    this.time = new Date().getTime();
820   
821    this.setClickCoordinates = function(coordinates) {
822          this.clickCoordinates = coordinates;
823    };
824   
825    this.setKey = function(key) {
826          this.key = key;
827    };
828   
829    this.setSelectedValue = function(value) {
830        this.selectedValue = encodeText(value);
831    };
832 
833    this.setScrollPosition = function(scrollPosition) {
834        this.scrollPosition = scrollPosition;
835    };
836   
837    this.toJSON = function() {
838        var eventInJSON =
839            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
840            this.type + "\"";
841
842        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
843            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
844        }
845
846        if (this.key) {
847            eventInJSON += ",\"key\":" + this.key;
848        }
849       
850        if (this.selectedValue) {
851            eventInJSON += ",\"selectedValue\":\"" + this.selectedValue + "\"";
852        }
853       
854        if ("onscroll" === this.type) {
855            eventInJSON += ",\"scrollPosition\":[" + this.scrollPosition + "]";
856        }
857
858        eventInJSON += "}";
859       
860        return eventInJSON;
861    };
862   
863    function encodeText(text) {
864        return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"");
865    };
866}
Note: See TracBrowser for help on using the repository browser.