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

Last change on this file since 2171 was 1823, checked in by pharms, 10 years ago
  • extended and corrected logging, e.g., of scrolling and submitting forms using the enter key
File size: 38.6 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": [ "onclick",
75                                  "onkeydown",
76                                  "onpagehide",
77                                  "onpageshow",
78                                  "onscroll",
79                                  "onundo" ] },
80    { "tag": "button", "actions": [ "onclick",
81                                    "onfocus" ] },
82    { "tag": "canvas", "actions": [ "onclick" ] },
83    //{ "tag": "caption", "actions": [  ] },
84    { "tag": "cite", "actions": [ "onclick" ] },
85    { "tag": "code", "actions": [ "onclick" ] },
86    //{ "tag": "col", "actions": [  ] },
87    //{ "tag": "colgroup", "actions": [  ] },
88    { "tag": "command", "actions": [ "onclick",
89                                     "onfocus" ] },
90    //{ "tag": "datalist", "actions": [  ] },
91    { "tag": "dd", "actions": [ "onclick" ] },
92    { "tag": "del", "actions": [ "onclick" ] },
93    //{ "tag": "details", "actions": [  ] },
94    { "tag": "dfn", "actions": [ "onclick" ] },
95    { "tag": "div", "actions": [ "onclick",
96                                 "onscroll" ] },
97    //{ "tag": "dl", "actions": [  ] },
98    { "tag": "dt", "actions": [ "onclick" ] },
99    { "tag": "em", "actions": [ "onclick" ] },
100    { "tag": "embed", "actions": [ "onclick" ] },
101    //{ "tag": "fieldset", "actions": [  ] },
102    //{ "tag": "figcaption", "actions": [  ] },
103    //{ "tag": "figure", "actions": [  ] },
104    //{ "tag": "footer", "actions": [  ] },
105    { "tag": "form", "actions": [ "onreset",
106                                  "onsubmit" ] },
107    //{ "tag": "header", "actions": [  ] },
108    //{ "tag": "hgroup", "actions": [  ] },
109    { "tag": "h1", "actions": [ "onclick" ] },
110    { "tag": "h2", "actions": [ "onclick" ] },
111    { "tag": "h3", "actions": [ "onclick" ] },
112    { "tag": "h4", "actions": [ "onclick" ] },
113    { "tag": "h5", "actions": [ "onclick" ] },
114    { "tag": "h6", "actions": [ "onclick" ] },
115    //{ "tag": "hr", "actions": [  ] },
116    { "tag": "i", "actions": [ "onclick" ] },
117    //{ "tag": "iframe", "actions": [  ] },
118    { "tag": "img", "actions": [ "onabort",
119                                 "onclick" ] },
120    { "tag": "input_button", "actions": [ "onclick",
121                                          "onfocus" ] },
122    { "tag": "input_checkbox", "actions": [ "onchange",
123                                            "onclick",
124                                            "onfocus" ] },
125    { "tag": "input_color", "actions": [ "onchange",
126                                         "onclick",
127                                         "onfocus",
128                                         "onselect" ] },
129    { "tag": "input_date", "actions": [ "onchange",
130                                        "onclick",
131                                        "onfocus",
132                                        "onselect" ] },
133    { "tag": "input_datetime", "actions": [ "onchange",
134                                            "onclick",
135                                            "onfocus",
136                                            "onselect" ] },
137    { "tag": "input_datetime-local", "actions": [ "onchange",
138                                                  "onclick",
139                                                  "onfocus",
140                                                  "onselect" ] },
141    { "tag": "input_email", "actions": [ "onchange",
142                                         "onclick",
143                                         "onfocus",
144                                         "onselect" ] },
145    { "tag": "input_file", "actions": [ "onchange",
146                                        "onclick",
147                                        "onfocus",
148                                        "onselect" ] },
149    { "tag": "input_image", "actions": [ "onclick",
150                                         "onfocus" ] },
151    { "tag": "input_month", "actions": [ "onchange",
152                                         "onclick",
153                                         "onfocus",
154                                         "onselect" ] },
155    { "tag": "input_number", "actions": [ "onchange",
156                                          "onclick",
157                                          "onfocus",
158                                          "onselect" ] },
159    { "tag": "input_password", "actions": [ "onchange",
160                                            "onclick",
161                                            "onfocus" ] },
162    { "tag": "input_radio", "actions": [ "onchange",
163                                         "onclick",
164                                         "onfocus" ] },
165    { "tag": "input_range", "actions": [ "onchange",
166                                         "onclick",
167                                         "onfocus",
168                                         "onselect" ] },
169    { "tag": "input_reset", "actions": [ "onclick",
170                                         "onfocus" ] },
171    { "tag": "input_search", "actions": [ "onchange",
172                                          "onclick",
173                                          "onfocus",
174                                          "onselect" ] },
175    { "tag": "input_submit", "actions": [ "onclick",
176                                          "onfocus" ] },
177    { "tag": "input_tel", "actions": [ "onchange",
178                                       "onclick",
179                                       "onfocus",
180                                       "onselect" ] },
181    { "tag": "input_text", "actions": [ "onchange",
182                                        "onclick",
183                                        "onfocus",
184                                        "onselect" ] },
185    { "tag": "input_time", "actions": [ "onchange",
186                                        "onclick",
187                                        "onfocus",
188                                        "onselect" ] },
189    { "tag": "input_url", "actions": [ "onchange",
190                                       "onclick",
191                                       "onfocus",
192                                       "onselect" ] },
193    { "tag": "input_week", "actions": [ "onchange",
194                                        "onclick",
195                                        "onfocus",
196                                        "onselect" ] },
197    { "tag": "input", "actions": [ "onchange",
198                                   "onclick",
199                                   "onfocus" ] },
200    { "tag": "ins", "actions": [ "onclick" ] },
201    { "tag": "kbd", "actions": [ "onclick" ] },
202    { "tag": "keygen", "actions": [ "onchange",
203                                    "onfocus" ] },
204    //{ "tag": "label", "actions": [  ] },
205    //{ "tag": "legend", "actions": [  ] },
206    { "tag": "li", "actions": [ "onclick" ] },
207    //{ "tag": "map", "actions": [  ] },
208    { "tag": "mark", "actions": [ "onclick" ] },
209    { "tag": "menu", "actions": [ "onclick" ] },
210    { "tag": "meter", "actions": [ "onclick" ] },
211    //{ "tag": "nav", "actions": [  ] },
212    //{ "tag": "noscript", "actions": [  ] },
213    { "tag": "object", "actions": [ "onclick" ] },
214    //{ "tag": "ol", "actions": [  ] },
215    //{ "tag": "optgroup", "actions": [  ] },
216    //{ "tag": "option", "actions": [  ] },
217    { "tag": "output", "actions": [ "onclick" ] },
218    { "tag": "p", "actions": [ "onclick" ] },
219    //{ "tag": "param", "actions": [  ] },
220    //{ "tag": "pre", "actions": [  ] },
221    { "tag": "progress", "actions": [ "onclick" ] },
222    { "tag": "q", "actions": [ "onclick" ] },
223    //{ "tag": "rp", "actions": [  ] },
224    //{ "tag": "rt", "actions": [  ] },
225    //{ "tag": "ruby", "actions": [  ] },
226    { "tag": "s", "actions": [ "onclick" ] },
227    { "tag": "samp", "actions": [ "onclick" ] },
228    //{ "tag": "section", "actions": [  ] },
229    { "tag": "select", "actions": [ "onchange",
230                                    "onfocus",
231                                    "onscroll" ] },
232    { "tag": "small", "actions": [ "onclick" ] },
233    //{ "tag": "source", "actions": [  ] },
234    { "tag": "span", "actions": [ "onclick" ] },
235    { "tag": "strong", "actions": [ "onclick" ] },
236    //{ "tag": "sub", "actions": [  ] },
237    //{ "tag": "summary", "actions": [  ] },
238    //{ "tag": "sup", "actions": [  ] },
239    //{ "tag": "table", "actions": [  ] },
240    //{ "tag": "tbody", "actions": [  ] },
241    { "tag": "td", "actions": [ "onclick" ] },
242    { "tag": "textarea", "actions": [ "onchange",
243                                      "onfocus",
244                                      "onselect",
245                                      "onscroll" ] },
246    //{ "tag": "tfoot", "actions": [  ] },
247    { "tag": "th", "actions": [ "onclick" ] },
248    //{ "tag": "thead", "actions": [  ] },
249    { "tag": "time", "actions": [ "onclick" ] },
250    //{ "tag": "tr", "actions": [  ] },
251    //{ "tag": "track", "actions": [  ] },
252    { "tag": "u", "actions": [ "onclick" ] },
253    //{ "tag": "ul", "actions": [  ] },
254    { "tag": "var", "actions": [ "onclick" ] },
255    { "tag": "video", "actions": [ "onplaying",
256                                   "onpause",
257                                   "ontimeupdate" ] }
258    //{ "tag": "wbr", "actions": [  ] },
259];
260
261/**
262 * a possibility to trace, what is going on
263 */
264var autoquestDoLog = false;
265
266/**
267 * stores the structure of the GUI of the current page
268 */
269var autoquestGUIModel;
270
271/**
272 * stores events, which were recorded but not sent to the server yet
273 */
274var autoquestRecordedEvents = [];
275
276/**
277 * stores the interval for sending data of inactive browser windows
278 */
279var autoquestSendInterval;
280
281/**
282 * automatically executed to initialize the event handling
283 */
284(function() {
285    initEventHandling();
286}());
287
288
289/**
290 * initializes the event handling after the document is loaded completely
291 */
292function initEventHandling() {
293    if (document.body) {
294        if (document.readyState !== "complete") {
295            // if the document is not loaded yet, try to add further event handling later
296            setTimeout(initEventHandling, 200);
297        }
298        else if (!autoquestSendInterval) {
299            log("adding event handling attributes");
300            determineDestination();
301            autoquestGUIModel =
302                addEventHandlingAndGetJSONRepresentation(document.documentElement, "");
303           
304            addDefaultEventHandling();
305           
306            // the onload event is already fired and lost. So pretend it to happen.
307            handleEvent(document.getElementsByTagName("body").item(0), "onload",
308                        "/html[0]/body[0]", null);
309           
310            // recall sending data each 100 seconds to ensure, that for browser windows staying
311            // open the data will be send, as well.
312            autoquestSendInterval = setTimeout(sendRequest, 100000);
313        }
314    }
315    else {
316        setTimeout(initEventHandling, 200);
317    }         
318}
319
320/**
321 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
322 * this URL, it calculates the destination to which the traced interactions must be sent
323 */
324function determineDestination() {
325    var scriptElements = document.getElementsByTagName("script");
326    var i;
327    var index;
328   
329    for (i = 0; i < scriptElements.length; i++) {
330        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
331            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
332            if (index > -1) {
333                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
334                log("using destination " + autoquestDestination);
335            }
336        }
337    }
338}
339
340/**
341 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
342 * relevant node. Furthermore returns a JSON representation of the node including the children
343 *
344 * @param node       the node of the DOM structure that shall be adapted and whose children shall
345 *                   be traversed
346 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
347 *                   the HTML-site
348 */
349function addEventHandlingAndGetJSONRepresentation(node, parentPath) {
350    var nodePath;
351    var i;
352    var jsonRepresentation = null;
353    var childRepresentation;
354    var childRepresentations = null;
355   
356    if (node.nodeType === Node.ELEMENT_NODE) {
357        jsonRepresentation = "{\"tagName\":\"" + getTagName(node) + "\",";
358       
359        if ((node.id) && (node.id !== "")) {
360            jsonRepresentation += "\"htmlId\":\"" + node.id + "\"";
361        }
362        else {
363            jsonRepresentation += "\"index\":\"" + getNodeIndex(node) + "\"";
364        }
365       
366        addEventHandling(node, parentPath);
367       
368        if (node.childNodes.length > 0) {
369            nodePath = getNodePath(node, parentPath);
370           
371            for (i = 0; i < node.childNodes.length; i++) {
372                childRepresentation =
373                    addEventHandlingAndGetJSONRepresentation(node.childNodes[i], nodePath);
374               
375                if (childRepresentation) {
376                    if (!childRepresentations) {
377                        childRepresentations = childRepresentation;
378                    }
379                    else {
380                        childRepresentations += "," + childRepresentation;
381                    }
382                }
383            }
384
385            if (childRepresentations) {
386                jsonRepresentation += ",\"children\":[" + childRepresentations + "]";
387            }
388        }
389       
390        jsonRepresentation += "}";
391    }
392   
393    return jsonRepresentation;
394}
395
396/**
397 * adds event handling functionality to the provided node. Calls
398 * {@link #addEventHandlingWithJQuery(node,parentPath)} or
399 * {@link #addEventHandlingWithoutJQuery(node,parentPath)} depending on the fact if jQuery is
400 * available and must therefore be used, or not.
401 *
402 * @param node       the node of the DOM structure that shall be equipped with event handling
403 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
404 *                   the HTML-site
405 */
406function addEventHandling(node, parentPath) {
407    if (typeof jQuery === 'undefined') {
408        addEventHandlingWithoutJQuery(node, parentPath);
409    }
410    else {
411        addEventHandlingWithJQuery(node, parentPath);
412    }
413}
414
415/**
416 * adds event handling functionality to the provided node using onxxx attributes
417 *
418 * @param node       the node of the DOM structure that shall be equipped with event handling
419 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
420 *                   the HTML-site
421 */
422function addEventHandlingWithoutJQuery(node, parentPath) {
423    var nodePath = getNodePath(node, parentPath);
424    var tagName = getTagName(node);
425    var i;
426    var k;
427   
428    for (i = 0; i < autoquestActionConfig.length; i++) {
429        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
430            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
431                adaptEventHandlingAttribute(node, nodePath, autoquestActionConfig[i].actions[k]);
432            }
433        }
434    }
435}
436
437/**
438 * adds event handling functionality to the provided node using jQuery attributes. If the node
439 * already used onxxx attributes, these are extended instead of using jQuery.
440 *
441 * @param node       the node of the DOM structure that shall be equipped with event handling
442 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
443 *                   the HTML-site
444 */
445function addEventHandlingWithJQuery(node, parentPath) {
446    var nodePath = getNodePath(node, parentPath);
447    var tagName = getTagName(node);
448    var action;
449    var i;
450    var k;
451   
452    for (i = 0; i < autoquestActionConfig.length; i++) {
453        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
454            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
455                action = autoquestActionConfig[i].actions[k];
456                if (jQuery(node).attr(action)) {
457                    // if there is an event handling attribute although jquery is present
458                    // edit this attribute accordingly
459                    adaptEventHandlingAttribute(node, nodePath, action);
460                }
461                else {
462                    registerEventHandler(node, nodePath, action);
463                }
464            }
465        }
466    }
467}
468
469/**
470 * adapts the event handling attributed provided by the action parameter so that it calls
471 * the {@link #handleEvent(node, action, path, event)} function in the case the event occurs.
472 * Either the method creates an appropriate onxxx attribute on the node if there is none, or it
473 * adds a call to the function as first thing called by the onxxx attribute.
474 *
475 * @param node     the node of the DOM structure that shall be equipped with event handling
476 * @param nodePath the path to the node within the DOM-structure of the HTML-site
477 * @param action   the event for which event handling shall be enabled
478 */
479function adaptEventHandlingAttribute(node, nodePath, action) {
480    var value = "handleEvent(this, '" + action + "', '" + nodePath + "', event);";
481    var oldValue;
482   
483    if (!node.getAttribute(action)) {
484        node.setAttribute(action, value);
485    }
486    else {
487        oldValue = node.getAttribute(action);
488        if (oldValue.indexOf(value) < 0) {
489            node.setAttribute(action, value + ' ' + oldValue);
490        }
491    }
492}
493
494/**
495 * registers an event handler using jQuery for the provided action on the given object so that it
496 * calls the {@link #handleJQueryEvent(event)} function in the case the event occurs.
497 *
498 * @param node     the node of the DOM structure that shall be equipped with event handling
499 * @param nodePath the path to the node within the DOM-structure of the HTML-site
500 * @param action   the event for which event handling shall be enabled
501 */
502function registerEventHandler(node, nodePath, action) {
503    var parameters = { action : action, path : nodePath};
504   
505    if ((action === "onscroll") && (node === document.body)) {
506        node = window;
507    }
508   
509    if (jQuery(node).on) {
510        jQuery(node).on(action.substring(2), parameters, handleJQueryEvent);
511    }
512    else {
513        jQuery(node).bind(action.substring(2), parameters, handleJQueryEvent);
514    }
515}
516
517/**
518 * adds default event handling functionality for receiving load, unload, and other document
519 * relevant events. The registration for events is done depending on the availability of jQuery.
520 * If jQuery is available and must therefore be used, then the registration is done on the window
521 * object. Otherwise, the appropriate attributes of the document's body tag are changed
522 */
523function addDefaultEventHandling() {
524    var body;
525
526    if (typeof jQuery === 'undefined') {
527        body = document.getElementsByTagName("body").item(0);
528        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onbeforeunload");
529        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onload");
530        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onunload");
531        //adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onerror");
532    }
533    else {
534        registerEventHandler(window, "/html[0]/body[0]", "onbeforeunload");
535        registerEventHandler(window, "/html[0]/body[0]", "onload");
536        registerEventHandler(window, "/html[0]/body[0]", "onunload");
537        //registerEventHandler(body, "/html[0]/body[0]", "onerror");
538    }
539}
540
541/**
542 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
543 * to its parent node. The result is the parent path plus a path element for the provided node.
544 * The first part of the path element generated for the node is the tag name returned by
545 * {@link #getTagName(node)}. If the node has an id, it becomes the second part of the path
546 * element. If the node does not have an id, the method calculates the index of the node within
547 * all children of the same type within the parent node. This index becomes then the second part
548 * of the path element generated for the node.
549 *
550 * @param node       the node of the DOM structure for which the path shall be created
551 * @param parentPath the path to the parent node of the provided node
552 *
553 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
554 *          element for the provided node
555 */
556function getNodePath(node, parentPath) {
557    var nodePath = parentPath + "/" + getTagName(node);
558   
559    if ((node.id) && (node.id !== "")) {
560        nodePath += "(htmlId=" + node.id + ")";
561    }
562    else {
563        nodePath += "[" + getNodeIndex(node) + "]";
564    }
565   
566    return nodePath;
567}
568
569/**
570 * called to handle events caused by the jQuery event handling mechanism. Forwards the event to
571 * {@link #handleEvent(node, eventName, nodePath, event)}
572 *
573 * @param the event to be handled
574 */
575function handleJQueryEvent(event) {
576    handleEvent(this, event.data.action, event.data.path, event);
577}
578
579/**
580 * handles an event that happened on a node. This method is called by the event handling attributes
581 * of the nodes. These attributes are generated by the
582 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
583 * adds it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
584 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
585 * through calling {@link #sendRequest()}.
586 *
587 * @param node      the node that fired the event
588 * @param eventName the name of the event, e.g. onscroll
589 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
590 * @param event     the HTML event that occured
591 */
592function handleEvent(node, eventName, nodePath, event) {
593    var eventType;
594    var eventObj = null;
595    var preceedingScrollEventObj = null;
596    var tagName;
597    var unmonitored;
598    var x1, x2;
599    var y1, y2;
600
601    if (!autoquestDestination) {
602        // do nothing if we have no destination to send data to
603        return;
604    }
605   
606    log("handling event " + eventName + " on " + node);
607   
608    eventType = eventName.toLowerCase();
609   
610    // filter onkeydown events, which are not of interest
611    if ((eventType === "onkeydown") && (node === document.body)) {
612        // do not handle key downs on the body which are other than tabs (keycode 9)
613        if ((event.keyCode !== 9) && (event.keyCode !== 13)) {
614            return;
615        }
616    }
617
618    // check, if the preceeding event is more concrete, i.e. the new event to be handled is only
619    // a propagation through the DOM and needs, therefore, not be handled
620    if (autoquestRecordedEvents.length > 0) {
621        eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1];
622
623        if ((eventObj.type === eventType) && (eventObj.nodePath.indexOf(nodePath) === 0)) {
624            // the event is of the same type.
625            if (eventObj.nodePath.length > nodePath.length) {
626                // the new event is the same event as the previous one, but fired for the parent
627                // GUI element of the previous event. This must not be handled. So ignore it
628                log("discarding event " + eventName + " on " + node +
629                    " as it is already handled by a child");
630                return;
631            }
632            else if (eventType !== "onscroll") {
633                // we have the same event on the same element. If it is an onscroll, we may
634                // reuse it. But it is not an onscroll. So we ignore the existing event.
635                eventObj = null;
636            }
637            else {
638                // we have a repeated onscroll. Check, if the scrolling direction can be determined
639                // and if it changed. If so created a new object. If not, stick to the existing one.
640                if (autoquestRecordedEvents.length > 1) {
641                    preceedingScrollEventObj =
642                        autoquestRecordedEvents[autoquestRecordedEvents.length - 2];
643                   
644                    if (preceedingScrollEventObj.type !== eventType) {
645                        preceedingScrollEventObj = null;
646                    }
647                }
648               
649                if (preceedingScrollEventObj != null) {
650                    x1 = preceedingScrollEventObj.scrollPosition[0] - eventObj.scrollPosition[0];
651                    x2 = eventObj.scrollPosition[0] - getScrollCoordinates(node)[0];
652                    y1 = preceedingScrollEventObj.scrollPosition[1] - eventObj.scrollPosition[1];
653                    y2 = eventObj.scrollPosition[1] - getScrollCoordinates(node)[1];
654                   
655                    if ((((x1 >= 0) && (x2 >= 0)) || ((x1 <= 0) && (x2 <= 0))) &&
656                        (((y1 >= 0) && (y2 >= 0)) || ((y1 <= 0) && (y2 <= 0))))
657                    {
658                        // The new scrolling is a continuation of the previous scrolling --> reuse
659                        // previous object
660                    }
661                    else {
662                        eventObj = null;
663                    }
664                }
665                else {
666                    // we cannot determine the preceeding scroll direction, so we have to store the
667                    // new event
668                    eventObj = null;
669                }
670            }
671        }
672        else {
673            // the event is not of an equal type as the previous one. So we will not reuse it
674            eventObj = null;
675        }
676    }
677   
678    if (!eventObj) {
679        // we can not reuse the previous event. So create a new event and add it to the list
680        eventObj = new Event(eventType, nodePath);
681        log("storing event " + eventName);
682        autoquestRecordedEvents.push(eventObj);
683    }
684
685    // now add further event parameters
686    if ((eventType === "onclick") || (eventType === "ondblclick")) {
687        eventObj.setClickCoordinates(getEventCoordinates(event));
688    }
689
690    tagName = getTagName(node);
691   
692    // ensure to not monitor details for GUI elements marked as unmonitored
693    if (node.getAttribute && node.getAttribute("class")) {
694        unmonitored = node.getAttribute("class").indexOf("autoquest-unmonitored") >= 0;
695    }
696    else {
697        unmonitored = false;
698    }
699   
700    if (!unmonitored && ("input_password" !== tagName)) {
701        // add further parameters for GUI elements which are monitored and which are no password
702        // fields
703        if ((eventType === "onkeypress") ||
704            (eventType === "onkeydown") ||
705            (eventType === "onkeyup"))
706        {
707            eventObj.setKey(event.keyCode);
708        }
709        else if (eventType === "onchange") {
710            if ((tagName.indexOf("input") === 0) ||
711                (tagName === "textarea") ||
712                (tagName === "keygen"))
713            {
714                eventObj.setSelectedValue(node.value);
715            }
716            else if (tagName === "select") {
717                eventObj.setSelectedValue(node.options.item(node.options.selectedIndex));
718            }
719        }
720    }
721   
722    if (eventType === "onscroll") {
723        eventObj.setScrollPosition(getScrollCoordinates(node));
724    }
725
726    // finally send the recorded data
727    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
728        log("initiating sending events");
729        setTimeout(sendRequest, 100);
730    }
731    else if ((eventType === "onunload") || (eventType === "onbeforeunload")) {
732        log("initiating sending events");
733        sendRequest();
734    }
735
736}
737
738/**
739 * determines a tag name of a node. If the node is an input element, the tag name includes the
740 * type of element. Otherwise the method simply returns the tag name.
741 *
742 * @param node the node for which the name must be determined
743 *
744 * @return the name as described
745 */
746function getTagName(node) {
747    var tagName = null;
748   
749    if (node.tagName) {
750        tagName = node.tagName.toLowerCase();
751        if ("input" === tagName) {
752            if (node.type && (node.type !== "")) {
753                tagName += "_" + node.type;
754            }
755            else {
756                tagName += "_text";
757            }
758        }
759    }
760
761    return tagName;
762}
763   
764/**
765 * determines the index of a node considering all nodes of the parent having the same name. If the
766 * node has no parent, the method returns index 0.
767 *
768 * @param node the node for which the index must be determined
769 *
770 * @return the index as described
771 */
772function getNodeIndex(node) {
773    var i;
774    var index = 0;
775   
776    if (node.parentNode) {
777        for (i = 0; i < node.parentNode.childNodes.length; i++) {
778            if (node.parentNode.childNodes[i].tagName === node.tagName) {
779                // if === also returns true if the nodes are not identical but only equal,
780                // this may fail.
781                if (node.parentNode.childNodes[i] === node) {
782                    break;
783                }
784                index++;
785            }
786        }
787       
788    }
789
790    return index;
791}
792   
793/**
794 * sends the collected data to the server, named in the destination-variable. For this it generates
795 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
796 * the server
797 */
798function sendRequest() {
799    var eventList = autoquestRecordedEvents;
800    var message;
801    var clientId;
802    var i = 0;
803    var request;
804   
805    if (eventList.length > 1) {
806        log("creating message");
807       
808        // put the last event into the new list to allow for checks for reoccurence of the same
809        // event
810        autoquestRecordedEvents = [ eventList.pop() ];
811       
812        message = "{\"message\":{\"clientInfos\":{";
813       
814        log("reading client id");
815        clientId = getClientId();
816        if ((clientId) && (clientId !== "")) {
817            message += "\"clientId\":\"" + clientId + "\",";
818        }
819       
820        log("adding other infos");
821        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
822        message += "\"title\":\"" + document.title + "\",";
823        message += "\"url\":\"" + document.URL + "\"},";
824       
825        message += "\"guiModel\":" + autoquestGUIModel + ",";
826       
827        message += "\"events\":[";
828       
829        for (i = 0; i < eventList.length; i++) {
830            if (i > 0) {
831                message += ",";
832            }
833            message += eventList[i].toJSON();
834        }
835       
836        message += "]}}";
837       
838        request = null;
839       
840        // Mozilla
841        if (window.XMLHttpRequest) {
842            request = new XMLHttpRequest();
843        }
844        // IE
845        else if (window.ActiveXObject) {
846            request = new ActiveXObject("Microsoft.XMLHTTP");
847        }
848       
849        request.open("POST", autoquestDestination, false);
850        request.setRequestHeader("Content-Type", "application/json");
851
852        log("sending " + message);
853        request.send(message);
854    }
855}
856
857/**
858 * determines the scroll coordinates of the scrolled element
859 *
860 * @param node the element that was scrolled
861 *
862 * @returns the coordinates of the scrolling as an array with x and y coordinate
863 */
864function getScrollCoordinates(node) {
865    if (typeof node.scrollLeft !== 'undefined') {
866        return [node.scrollLeft, node.scrollTop];
867    }
868    else if ((node === window) && (typeof window.pageXOffset !== 'undefined')) {
869        return [window.pageXOffset, window.pageYOffset];
870    }
871    else if ((node === document) || (node === document.body) ||
872             (node === document.documentElement))
873    {
874        if ((typeof document.body !== 'undefined') &&
875            (typeof document.body.scrollLeft !== 'undefined'))
876        {
877            return [document.body.scrollLeft, document.body.scrollTop];
878        }
879        else if ((typeof document.documentElement !== 'undefined') &&
880                 (typeof document.documentElement.scrollLeft !== 'undefined'))
881        {
882            return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
883        }
884    }
885
886    return [-1, -1];
887}
888
889/**
890 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
891 * with the provided event, they are determined based on the surrounding object
892 *
893 * @param event the event to extract the coordinates of
894 *
895 * @returns the coordinates of the event as an array with x and y coordinate
896 */
897function getEventCoordinates(event) {
898    if (event.layerX) {
899        return [event.layerX, event.layerY];
900    }
901    else if (event.offsetX) {
902        return [event.offsetX, event.offsetY];
903    }
904
905    var obj = event.target || event.srcElement;
906    var objOffset = getPageOffset(obj);
907
908    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
909}
910
911/**
912 * determines the page offset of an object using the parent objects offset
913 */
914function getPageOffset(object) {
915    var top = 0;
916    var left = 0;
917    var obj = object;
918
919    while (obj.offsetParent) {
920        left += obj.offsetLeft;
921        top += obj.offsetTop;
922        obj = obj.offsetParent;
923    }
924
925    return [left, top];
926}
927
928/**
929 * generates a client id based on several information retrieved from the environment. The client
930 * id is not always unique
931 *
932 * @returns the client id
933 */
934function getClientId() {
935    var clientIdStr;
936    var clientId;
937    var i = 0;
938   
939    if (!autoquestClientId) {
940        // create something like a more or less unique checksum.
941        clientIdStr =
942            navigator.appCodeName + navigator.appName + navigator.appVersion +
943            navigator.cookieEnabled + navigator.language + navigator.platform +
944            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
945            window.location.host + new Date().getTimezoneOffset();
946
947        clientId = clientIdStr.length;
948
949        for (i = 0; i < clientIdStr.length; i++) {
950            clientId += clientIdStr.charCodeAt(i);
951        }
952       
953        autoquestClientId = clientId;
954    }
955
956    return autoquestClientId;
957}
958
959/**
960 * performs a simple logging by adding a specific div to the HTML
961 *
962 * @param text the text to be logged
963 */
964function log(text) {
965    if (autoquestDoLog) {
966        var loggingInfo = document.getElementById("loggingInfoParagraph");
967       
968        if (!loggingInfo) {
969            var div = document.createElement("div");
970            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
971                             "border:1px black solid; position:absolute; top:10px; right:10px; " +
972                             "width:350px; height:auto; font-size:8pt; font-family:Helvetica");
973           
974            loggingInfo = document.createElement("div");
975            loggingInfo.id = "loggingInfoParagraph";
976            div.appendChild(loggingInfo);
977           
978            var body = document.getElementsByTagName("body")[0];
979            if (!body) {
980                alert("could not enable logging");
981            }
982            else {
983                body.appendChild(div);
984            }
985        }
986       
987        loggingInfo.appendChild(document.createTextNode(text));
988        loggingInfo.appendChild(document.createElement("br"));
989    }
990}
991
992/**
993 * this class represents a single event
994 *
995 * @param type     the type of the event
996 * @param nodePath the path through the HTML DOM to the event target
997 */
998function Event(type, nodePath) {
999    this.type = type;
1000    this.nodePath = nodePath;
1001    this.time = new Date().getTime();
1002   
1003    this.setClickCoordinates = function(coordinates) {
1004          this.clickCoordinates = coordinates;
1005    };
1006   
1007    this.setKey = function(key) {
1008          this.key = key;
1009    };
1010   
1011    this.setSelectedValue = function(value) {
1012        this.selectedValue = encodeText(value);
1013    };
1014 
1015    this.setScrollPosition = function(scrollPosition) {
1016        this.scrollPosition = scrollPosition;
1017    };
1018   
1019    this.toJSON = function() {
1020        var eventInJSON =
1021            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
1022            this.type + "\"";
1023
1024        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
1025            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
1026        }
1027
1028        if (this.key) {
1029            eventInJSON += ",\"key\":" + this.key;
1030        }
1031       
1032        if (this.selectedValue) {
1033            eventInJSON += ",\"selectedValue\":\"" + this.selectedValue + "\"";
1034        }
1035       
1036        if ("onscroll" === this.type) {
1037            eventInJSON += ",\"scrollPosition\":[" + this.scrollPosition + "]";
1038        }
1039
1040        eventInJSON += "}";
1041       
1042        return eventInJSON;
1043    };
1044   
1045    function encodeText(text) {
1046        return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"");
1047    };
1048}
Note: See TracBrowser for help on using the repository browser.