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

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