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

Last change on this file since 1021 was 1021, checked in by pharms, 12 years ago
  • changed to add event handling only if document is loaded completely. This is less likely to destroy other java scripts of the monitored application as observed with hudson
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        if (document.readyState !== "complete") {
248            // if the document is not loaded yet, try to add further event handling later
249            setTimeout(initEventHandling, 200);
250        }
251        else if (!autoquestSendInterval) {
252            log("adding event handling attributes");
253            determineDestination();
254            autoquestGUIModel =
255                addEventHandlingAndGetJSONRepresentation(document.documentElement, "");
256           
257            // recall sending data each 100 seconds to ensure, that for browser windows staying
258            // open the data will be send, as well.
259            autoquestSendInterval = setTimeout(sendRequest, 100000);
260        }
261    }
262    else {
263        setTimeout(initEventHandling, 200);
264    }         
265}
266
267/**
268 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
269 * this URL, it calculates the destination to which the traced interactions must be sent
270 */
271function determineDestination() {
272    var scriptElements = document.getElementsByTagName("script");
273    var i;
274    var index;
275   
276    for (i = 0; i < scriptElements.length; i++) {
277        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
278            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
279            if (index > -1) {
280                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
281                log("using destination " + autoquestDestination);
282            }
283        }
284    }
285}
286
287/**
288 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
289 * relevant node. Furthermore returns a JSON representation of the node including the children
290 *
291 * @param node       the node of the DOM structure that shall be adapted and whose children shall
292 *                   be traversed
293 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
294 *                   the HTML-site
295 */
296function addEventHandlingAndGetJSONRepresentation(node, parentPath) {
297    var nodePath;
298    var i;
299    var jsonRepresentation = null;
300    var childRepresentation;
301    var childRepresentations = null;
302   
303    if (node.nodeType === Node.ELEMENT_NODE) {
304        jsonRepresentation = "{\"tagName\":\"" + getTagName(node) + "\",";
305       
306        if ((node.id) && (node.id !== "")) {
307            jsonRepresentation += "\"id\":\"" + node.id + "\"";
308        }
309        else {
310            jsonRepresentation += "\"index\":\"" + getNodeIndex(node) + "\"";
311        }
312       
313        addEventHandling(node, parentPath);
314       
315        if (node.childNodes.length > 0) {
316            nodePath = getNodePath(node, parentPath);
317           
318            for (i = 0; i < node.childNodes.length; i++) {
319                childRepresentation =
320                    addEventHandlingAndGetJSONRepresentation(node.childNodes[i], nodePath);
321               
322                if (childRepresentation) {
323                    if (!childRepresentations) {
324                        childRepresentations = childRepresentation;
325                    }
326                    else {
327                        childRepresentations += "," + childRepresentation;
328                    }
329                }
330            }
331
332            if (childRepresentations) {
333                jsonRepresentation += ",\"children\":[" + childRepresentations + "]";
334            }
335        }
336       
337        jsonRepresentation += "}";
338    }
339   
340    return jsonRepresentation;
341}
342
343/**
344 * TODO comment
345 */
346function addEventHandling(node, parentPath) {
347    if (typeof jQuery === 'undefined') {
348        addEventHandlingWithoutJQuery(node, parentPath);
349    }
350    else {
351        addEventHandlingWithJQuery(node, parentPath);
352    }
353}
354
355/**
356 * TODO comment
357 */
358function addEventHandlingWithoutJQuery(node, parentPath) {
359    var nodePath = getNodePath(node, parentPath);
360    var tagName = getTagName(node);
361    var i;
362    var k;
363   
364    for (i = 0; i < autoquestActionConfig.length; i++) {
365        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
366            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
367                adaptEventHandlingAttribute(node, nodePath, autoquestActionConfig[i].actions[k]);
368            }
369        }
370    }
371}
372
373/**
374 * TODO comment
375 */
376function addEventHandlingWithJQuery(node, parentPath) {
377    var nodePath = getNodePath(node, parentPath);
378    var tagName = getTagName(node);
379    var action;
380    var parameters;
381    var i;
382    var k;
383   
384    for (i = 0; i < autoquestActionConfig.length; i++) {
385        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
386            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
387                action = autoquestActionConfig[i].actions[k];
388                if (jQuery(node).attr(action)) {
389                    // if there is an event handling attribute although jquery is present
390                    // edit this attribute accordingly
391                    adaptEventHandlingAttribute(node, nodePath, action);
392                }
393                else {
394                    parameters = { action : action, path : nodePath};
395                    if (jQuery(node).on) {
396                        jQuery(node).on(action.substring(2), parameters, handleJQueryEvent);
397                    }
398                    else {
399                        jQuery(node).bind(action.substring(2), parameters, handleJQueryEvent);
400                    }
401                }
402            }
403        }
404    }
405}
406
407/**
408 * TODO comment
409 */
410function adaptEventHandlingAttribute(node, nodePath, action) {
411    var value = "handleEvent(this, '" + action + "', '" + nodePath + "', event);";
412    var oldValue;
413   
414    if (!node.getAttribute(action)) {
415        node.setAttribute(action, value);
416    }
417    else {
418        oldValue = node.getAttribute(action);
419        if (oldValue.indexOf(value) < 0) {
420            node.setAttribute(action, value + ' ' + oldValue);
421        }
422    }
423}
424
425/**
426 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
427 * to its parent node. The result is the parent path plus a path element for the provided node.
428 * The first part of the path element generated for the node is the tag name returned by
429 * {@link #getTagName(node)}. If the node has an id, it becomes the second part of the path
430 * element. If the node does not have an id, the method calculates the index of the node within
431 * all children of the same type within the parent node. This index becomes then the second part
432 * of the path element generated for the node.
433 *
434 * @param node       the node of the DOM structure for which the path shall be created
435 * @param parentPath the path to the parent node of the provided node
436 *
437 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
438 *          element for the provided node
439 */
440function getNodePath(node, parentPath) {
441    var nodePath = parentPath + "/" + getTagName(node);
442   
443    if ((node.id) && (node.id !== "")) {
444        nodePath += "(id=" + node.id + ")";
445    }
446    else {
447        nodePath += "[" + getNodeIndex(node) + "]";
448    }
449   
450    return nodePath;
451}
452
453/**
454 * TODO comment
455 */
456function handleJQueryEvent(event) {
457    handleEvent(this, event.data.action, event.data.path, event);
458}
459
460/**
461 * handles an event that happened on a node. This method is called by the event handling attributes
462 * of the nodes. These attributes are generated by the
463 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
464 * adds it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
465 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
466 * through calling {@link #sendRequest()}.
467 *
468 * @param node      the node that fired the event
469 * @param eventName the name of the event, e.g. onscroll
470 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
471 * @param event     the HTML event that occured
472 */
473function handleEvent(node, eventName, nodePath, event) {
474    var eventType;
475    var eventObj = null;
476    var tagName;
477
478    if (!autoquestDestination) {
479        // do nothing if we have no destination to send data to
480        return;
481    }
482   
483    log("handling event " + eventName + " on " + node);
484   
485    eventType = eventName.toLowerCase();
486
487    if (autoquestRecordedEvents.length > 0) {
488        eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1];
489
490        // check if an event showed up several times either for the same or for a parent GUI element
491        if ((eventObj.type === eventName) && (eventObj.nodePath.indexOf(nodePath) === 0)) {
492            // the event is of the same type.
493            if (eventObj.nodePath.length > nodePath.length) {
494                // the same event showed up for the parent GUI element. This must not be handled.
495                // So ignore it
496                log("discarding event " + eventName + " on " + node +
497                    " as it is already handled by a child");
498                return;
499            }
500            else if (eventName !== "onscroll") {
501                // we have the same event on the same element. If it is an onscroll, we should
502                // reuse it. But it is not an onscroll. So we ignore the existing event.
503                eventObj = null;
504            }
505        }
506        else {
507            // the event is not of an equal type as the previous one. So we will not reuse it
508            eventObj = null;
509        }
510    }
511   
512    if (!eventObj) {
513        // create a new event and add it to the list
514        eventObj = new Event(eventType, nodePath);
515        log("storing event " + eventName);
516        autoquestRecordedEvents.push(eventObj);
517    }
518
519    // now add further event parameters
520    if ((eventType === "onclick") || (eventType === "ondblclick")) {
521        eventObj.setClickCoordinates(getEventCoordinates(event));
522    }
523
524    tagName = getTagName(node);
525   
526    if ("input_password" !== tagName) {
527        if ((eventType === "onkeypress") ||
528            (eventType === "onkeydown") ||
529            (eventType === "onkeyup"))
530        {
531            eventObj.setKey(event.keyCode);
532        }
533        else if (eventType === "onchange") {
534            if ((tagName.indexOf("input") === 0) ||
535                (tagName === "textarea") ||
536                (tagName === "keygen"))
537            {
538                eventObj.setSelectedValue(node.value);
539            }
540            else if (tagName === "select") {
541                eventObj.setSelectedValue(node.options.item(node.options.selectedIndex));
542            }
543        }
544    }
545   
546    if (eventType === "onscroll") {
547        eventObj.setScrollPosition(getScrollCoordinates(node));
548    }
549
550    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
551        log("initiating sending events");
552        setTimeout(sendRequest, 100);
553    }
554    else if ((eventType === "onunload") || (eventType === "onbeforeunload")) {
555        log("initiating sending events");
556        sendRequest();
557    }
558
559}
560
561/**
562 * determines a tag name of a node. If the node is an input element, the tag name includes the
563 * type of element. Otherwise the method simply returns the tag name.
564 *
565 * @param node the node for which the name must be determined
566 *
567 * @return the name as described
568 */
569function getTagName(node) {
570    var tagName = null;
571   
572    if (node.tagName) {
573        tagName = node.tagName.toLowerCase();
574        if ("input" === tagName) {
575            if (node.type && (node.type !== "")) {
576                tagName += "_" + node.type;
577            }
578            else {
579                tagName += "_text";
580            }
581        }
582    }
583
584    return tagName;
585}
586   
587/**
588 * determines the index of a node considering all nodes of the parent having the same name. If the
589 * node has no parent, the method returns index 0.
590 *
591 * @param node the node for which the index must be determined
592 *
593 * @return the index as described
594 */
595function getNodeIndex(node) {
596    var i;
597    var index = 0;
598   
599    if (node.parentNode) {
600        for (i = 0; i < node.parentNode.childNodes.length; i++) {
601            if (node.parentNode.childNodes[i].tagName === node.tagName) {
602                index++;
603                // if === also returns true if the nodes are not identical but only equal,
604                // this may fail.
605                if (node.parentNode.childNodes[i] === node) {
606                    break;
607                }
608            }
609        }
610       
611    }
612
613    return index;
614}
615   
616/**
617 * sends the collected data to the server, named in the destination-variable. For this it generates
618 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
619 * the server
620 */
621function sendRequest() {
622    var eventList = autoquestRecordedEvents;
623    var message;
624    var clientId;
625    var i = 0;
626    var request;
627   
628    if (eventList.length > 1) {
629        log("creating message");
630       
631        // put the last event into the new list to allow for checks for reoccurence of the same
632        // event
633        autoquestRecordedEvents = [ eventList.pop() ];
634       
635        message = "{\"message\":{\"clientInfos\":{";
636       
637        log("reading client id");
638        clientId = getClientId();
639        if ((clientId) && (clientId !== "")) {
640            message += "\"clientId\":\"" + clientId + "\",";
641        }
642       
643        log("adding other infos");
644        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
645        message += "\"title\":\"" + document.title + "\",";
646        message += "\"url\":\"" + document.URL + "\"},";
647       
648        message += "\"guiModel\":" + autoquestGUIModel + ",";
649       
650        message += "\"events\":[";
651       
652        for (i = 0; i < eventList.length; i++) {
653            if (i > 0) {
654                message += ",";
655            }
656            message += eventList[i].toJSON();
657        }
658       
659        message += "]}}";
660       
661        request = null;
662       
663        // Mozilla
664        if (window.XMLHttpRequest) {
665            request = new XMLHttpRequest();
666        }
667        // IE
668        else if (window.ActiveXObject) {
669            request = new ActiveXObject("Microsoft.XMLHTTP");
670        }
671       
672        request.open("POST", autoquestDestination, false);
673        request.setRequestHeader("Content-Type", "application/json");
674
675        log("sending " + message);
676        request.send(message);
677    }
678}
679
680/**
681 * determines the scroll coordinates of the scrolled element
682 *
683 * @param node the element that was scrolled
684 *
685 * @returns the coordinates of the scrolling as an array with x and y coordinate
686 */
687function getScrollCoordinates(node) {
688    if (node.scrollLeft || node.scrollTop) {
689        return [node.scrollLeft, node.scrollTop];
690    }
691    else if ((node === window) && window.pageYOffset) {
692        return [window.pageXOffset, window.pageYOffset];
693    }
694    else if ((node == document) || (node == document.body) || (node == document.documentElement)) {
695        if (document.body && (document.body.scrollLeft || document.body.scrollTop)) {
696            return [document.body.scrollLeft, document.body.scrollTop];
697        }
698        else if (document.documentElement &&
699                 (document.documentElement.scrollLeft || document.documentElement.scrollTop))
700        {
701            return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
702        }
703    }
704
705    return [-1, -1];
706}
707
708/**
709 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
710 * with the provided event, they are determined based on the surrounding object
711 *
712 * @param event the event to extract the coordinates of
713 *
714 * @returns the coordinates of the event as an array with x and y coordinate
715 */
716function getEventCoordinates(event) {
717    if (event.layerX) {
718        return [event.layerX, event.layerY];
719    }
720    else if (event.offsetX) {
721        return [event.offsetX, event.offsetY];
722    }
723
724    var obj = event.target || event.srcElement;
725    var objOffset = getPageOffset(obj);
726
727    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
728}
729
730/**
731 * determines the page offset of an object using the parent objects offset
732 */
733function getPageOffset(object) {
734    var top = 0;
735    var left = 0;
736    var obj = object;
737
738    while (obj.offsetParent) {
739        left += obj.offsetLeft;
740        top += obj.offsetTop;
741        obj = obj.offsetParent;
742    }
743
744    return [left, top];
745}
746
747/**
748 * generates a client id based on several information retrieved from the environment. The client
749 * id is not always unique
750 *
751 * @returns the client id
752 */
753function getClientId() {
754    var clientIdStr;
755    var clientId;
756    var i = 0;
757   
758    if (!autoquestClientId) {
759        // create something like a more or less unique checksum.
760        clientIdStr =
761            navigator.appCodeName + navigator.appName + navigator.appVersion +
762            navigator.cookieEnabled + navigator.language + navigator.platform +
763            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
764            window.location.host + new Date().getTimezoneOffset();
765
766        clientId = clientIdStr.length;
767
768        for (i = 0; i < clientIdStr.length; i++) {
769            clientId += clientIdStr.charCodeAt(i);
770        }
771       
772        autoquestClientId = clientId;
773    }
774
775    return autoquestClientId;
776}
777
778/**
779 * performs a simple logging by adding a specific div to the HTML
780 *
781 * @param text the text to be logged
782 */
783function log(text) {
784    if (autoquestDoLog) {
785        var loggingInfo = document.getElementById("loggingInfoParagraph");
786       
787        if (!loggingInfo) {
788            var div = document.createElement("div");
789            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
790                             "border:1px black solid; position:absolute; top:10px; right:10px; " +
791                             "width:350px; height:auto; font-size:8pt; font-family:Helvetica");
792           
793            loggingInfo = document.createElement("div");
794            loggingInfo.id = "loggingInfoParagraph";
795            div.appendChild(loggingInfo);
796           
797            var body = document.getElementsByTagName("body")[0];
798            if (!body) {
799                alert("could not enable logging");
800            }
801            else {
802                body.appendChild(div);
803            }
804        }
805       
806        loggingInfo.appendChild(document.createTextNode(text));
807        loggingInfo.appendChild(document.createElement("br"));
808    }
809}
810
811/**
812 * this class represents a single event
813 *
814 * @param type     the type of the event
815 * @param nodePath the path through the HTML DOM to the event target
816 */
817function Event(type, nodePath) {
818    this.type = type;
819    this.nodePath = nodePath;
820    this.time = new Date().getTime();
821   
822    this.setClickCoordinates = function(coordinates) {
823          this.clickCoordinates = coordinates;
824    };
825   
826    this.setKey = function(key) {
827          this.key = key;
828    };
829   
830    this.setSelectedValue = function(value) {
831        this.selectedValue = encodeText(value);
832    };
833 
834    this.setScrollPosition = function(scrollPosition) {
835        this.scrollPosition = scrollPosition;
836    };
837   
838    this.toJSON = function() {
839        var eventInJSON =
840            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
841            this.type + "\"";
842
843        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
844            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
845        }
846
847        if (this.key) {
848            eventInJSON += ",\"key\":" + this.key;
849        }
850       
851        if (this.selectedValue) {
852            eventInJSON += ",\"selectedValue\":\"" + this.selectedValue + "\"";
853        }
854       
855        if ("onscroll" === this.type) {
856            eventInJSON += ",\"scrollPosition\":[" + this.scrollPosition + "]";
857        }
858
859        eventInJSON += "}";
860       
861        return eventInJSON;
862    };
863   
864    function encodeText(text) {
865        return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"");
866    };
867}
Note: See TracBrowser for help on using the repository browser.