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

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