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

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