1 /** 2 * TAO API events utilities. 3 * 4 * @author CRP Henri Tudor - TAO Team - {@link http://www.tao.lu} 5 * @license GPLv2 http://www.opensource.org/licenses/gpl-2.0.php 6 * @package taoItems 7 * @requires jquery >= 1.4.0 {@link http://www.jquery.com} 8 * 9 * @see NewarX#Core 10 */ 11 12 /** 13 * 14 * @class EventTracer 15 * @property {Object} [options] 16 */ 17 function EventTracer (options){ 18 19 //keep the ref of the current instance for scopes traversing 20 var _this = this; 21 22 /** 23 * array of events arrays 24 * @fieldOf EventTracer 25 * @type {Array} 26 */ 27 this.eventPool = new Array();// 28 29 /** 30 * array of strings 31 * @fieldOf EventTracer 32 * @type {Array} 33 */ 34 this.eventsToBeSend = new Array(); 35 36 /** 37 * The tracer common options 38 * @fieldOf EventTracer 39 * @type {Object} 40 */ 41 this.opts = { 42 POOL_SIZE : 500, // number of events to cache before sending 43 MIN_POOL_SIZE : 200, 44 MAX_POOL_SIZE : 5000, 45 time_limit_for_ajax_request : 2000, 46 eventsToBeSendCursor : -1, 47 ctrlPressed : false, 48 altPressed : false 49 }; 50 51 //extends the options on the object construction 52 if(options != null && options != undefined){ 53 $.extend(this.opts, options); 54 } 55 56 57 /** 58 * the list of events to be catched 59 * @fieldOf EventTracer 60 * @type {Object} 61 */ 62 this.EVENTS_TO_CATCH = new Object(); 63 64 /** 65 * the list of attributes to be catched 66 * @fieldOf EventTracer 67 * @type {Object} 68 */ 69 this.ATTRIBUTES_TO_CATCH = new Array(); 70 71 /** 72 * The parameters defining how and where to load the events list to catch 73 * @fieldOf EventTracer 74 * @type {Object} 75 */ 76 this.sourceService = { 77 type: 'sync', // (sync | manual) 78 data: null, //if type is manual, contains the data in JSON, else it should be null 79 url: '/taoDelivery/ResultDelivery/getEvents', //the url sending the events list 80 params: {}, //the common parameters to send to the service 81 method: 'post', //sending method 82 format: 'json' //the response format, now ONLY JSON is supported 83 }; 84 85 /** 86 * The parameters defining how and where to send the events 87 * @fieldOf EventTracer 88 * @type {Object} 89 */ 90 this.destinationService = { 91 url: '/taoDelivery/ResultDelivery/traceEvents', //the URL where to send the events 92 params: {}, //the common parameters to send to the service 93 method: 'post', //sending method 94 format: 'json' //the response format, now ONLY JSON is supported 95 }; 96 97 /** 98 * Initialize the service interface for the source service: 99 * how and where we retrieve the events to catch 100 * @methodOf EventTracer 101 * @param {Object} environment 102 */ 103 this.initSourceService = function(environment){ 104 105 //define the source service 106 if($.isPlainObject(environment)){ 107 108 if($.inArray(environment.type, ['manual','sync']) > -1){ 109 110 this.sourceService.type = environment.type; 111 112 //manual behaviour 113 if(this.sourceService.type == 'manual' && $.isPlainObject(environment.data)){ 114 this.sourceService.data = environment.data; 115 } 116 else{ //remote behaviour 117 118 if(source.url){ 119 if(/(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/.test(environment.url)){ //test url 120 this.sourceService.url = environment.url; //set url 121 } 122 } 123 //ADD parameters 124 if($.isPlainObject(environment.params)){ 125 for(key in environment.params){ 126 if(isScalar(environment.params[key])){ 127 this.sourceService.params[key] = environment.params[key]; 128 } 129 } 130 } 131 if(environment.method){ 132 if(/^get|post$/i.test(environment.method)){ 133 this.sourceService.method = environment.method; 134 } 135 } 136 } 137 } 138 } 139 140 //we load now the events to catch 141 142 //we load it manually by calling directly the method with the data 143 if(this.sourceService.type == 'manual' && this.sourceService.data != null){ 144 this.EVENTS_TO_CATCH = this.setEventsToCatch(this.sourceService.data); 145 } 146 147 //we call the remote service 148 if(this.sourceService.type == 'sync' && this.sourceService.url != ''){ 149 received = $.parseJSON($.ajax({ 150 async : false, 151 url : this.sourceService.url, 152 data : this.sourceService.params, 153 type : this.sourceService.method 154 }).responseText); 155 if(received){ 156 this.EVENTS_TO_CATCH = this.setEventsToCatch(received); 157 } 158 } 159 160 //we bind the events to be observed in the item 161 if(this.EVENTS_TO_CATCH.bubbling != undefined){ 162 this.bind_platform(); 163 } 164 }; 165 166 /** 167 * Initialize the service interface forthe destination service: 168 * how and where we send the catched events 169 * @methodOf EventTracer 170 * @param {Object} environment 171 */ 172 this.initDestinationService = function(environment){ 173 if($.isPlainObject(environment)){ 174 if(environment.url){ 175 if(/(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/.test(environment.url)){ //test url 176 this.destinationService.url = environment.url; //set url 177 } 178 } 179 //ADD parameters 180 if($.isPlainObject(environment.params)){ 181 for(key in environment.params){ 182 if(isScalar(environment.params[key])){ 183 this.destinationService.params[key] = environment.params[key]; 184 } 185 } 186 } 187 if(environment.method){ 188 if(/^get|post$/i.test(environment.method)){ 189 this.destinationService.method = environment.method; 190 } 191 } 192 } 193 }; 194 195 /** 196 * @description record events of interaction between interviewee and the test 197 * @methodOf EventTracer 198 * @param {Object} data event type list 199 * @returns {Object} the events to catch 200 */ 201 this.setEventsToCatch = function (data) 202 { 203 // retreive the list of events to catch or not to catch 204 205 if (data.type.length > 0) 206 { 207 var EVENTS_TO_CATCH = {bubbling:[],nonBubbling:[]}; 208 if (data.type == 'catch') 209 { 210 for (i in data.list) 211 { 212 if ($.inArray(i,['click', 'dblclick', 'change', 'submit', 'select', 'mousedown', 'mouseup', 'mouseenter', 'mousemove', 'mouseout']) > -1)//if is bubbling event 213 { 214 EVENTS_TO_CATCH.bubbling.push(i); 215 } 216 else 217 { 218 EVENTS_TO_CATCH.nonBubbling.push(i);// else non bubbling event 219 } 220 this.ATTRIBUTES_TO_CATCH[i] = data.list[i]; 221 } 222 } 223 else 224 { 225 // no catch 226 EVENTS_TO_CATCH = {bubbling:['click', 'dblclick', 'change', 'submit', 'select', 'mousedown', 'mouseup', 'mouseenter', 'mousemove', 'mouseout'], nonBubbling:['blur', 'focus', 'load', 'resize', 'scroll', 'keyup', 'keydown', 'keypress', 'unload', 'beforeunload', 'select', 'submit']}; 227 for (i in data.list) 228 { 229 remove_array(data.list[i].event,EVENTS_TO_CATCH.bubbling); 230 remove_array(data.list[i].event,EVENTS_TO_CATCH.nonBubbling); 231 } 232 } 233 } 234 else 235 { 236 EVENTS_TO_CATCH = {bubbling:['click', 'dblclick', 'change', 'submit', 'select', 'mousedown', 'mouseup', 'mouseenter', 'mousemove', 'mouseout'], nonBubbling:['blur', 'focus', 'load', 'resize', 'scroll', 'keyup', 'keydown', 'keypress', 'unload', 'beforeunload', 'select', 'submit']}; 237 } 238 return EVENTS_TO_CATCH; 239 }; 240 241 /** 242 * @description bind platform events 243 * @methodOf EventTracer 244 */ 245 this.bind_platform = function() 246 { 247 // for non bubbling events, link them to all the listened element 248 // it is still useful to use delegation since it will remains much less listeners in the memory (just 1 instead of #numberOfElements) 249 $('body').bindDom(this); 250 251 // for bubbling events 252 $('body').bind(this.EVENTS_TO_CATCH.bubbling.join(' ') , this.eventStation); 253 }; 254 255 /** 256 * @description unbind platform events 257 * @methodOf EventTracer 258 */ 259 this.unbind_platform = function() 260 { 261 $('body').unbind(EVENTS_TO_CATCH.bubbling.join(' ') , this.eventStation); 262 $('body').unBindDom(this); 263 }; 264 265 266 /** 267 * @description set all information from the event to the pLoad 268 * @methodOf EventTracer 269 * @param {event} e dom event triggered 270 * @param {Object} pload callback function called when 'ok' clicked 271 */ 272 this.describeEvent = function(e,pload) 273 { 274 if (e.target && (typeof(e.target['value']) != 'undefined') && (e.target['value'] != -1) && (e.target['value'] != '')) 275 { 276 pload['value'] = e.target['value']; 277 } 278 // get everything about the event 279 for (var i in e) 280 { 281 if ((typeof(e[i]) != 'undefined') && (typeof(e[i]) != 'object') && (typeof(e[i]) != 'function') && (e[i] != '')) 282 { 283 if ((i != 'cancelable') && (i != 'contentEditable') && (i != 'cancelable') && (i != 'bubbles') && (i.substr(0,6) != 'jQuery')) 284 { 285 pload[i] = e[i]; 286 } 287 } 288 } 289 }; 290 291 292 /** 293 * @description set all information from the target dom element to the pLoad 294 * @methodOf EventTracer 295 * @param {event} e dom event triggered 296 * @param {Object} pload callback function called when 'ok' clicked 297 */ 298 this.describeElement = function(e,pload) 299 { 300 // take everything except useless attributes 301 for (var i in e.target) 302 { 303 try 304 { 305 if (( (typeof(e.target[i]) == 'string') && (e.target[i] != '') ) | (typeof(e.target[i]) == 'number')) 306 { 307 if ( (!in_array(i,position_pload_array)) && (!in_array(i,ignored_pload_element_array)) && (i.substr(0,6) != 'jQuery') ) 308 { 309 pload[i] = ''+e.target[i]; 310 } 311 } 312 } 313 catch(e){} 314 } 315 316 if (typeof(e.target.nodeName) != 'undefined') 317 { 318 switch(e.target.nodeName.toLowerCase()) 319 { 320 case 'select': 321 { 322 pload['value'] = $(e.target).val(); 323 if (typeof(pload['value']) == 'array') 324 { 325 pload['value'] = pload['value'].join('|'); 326 } 327 break; 328 } 329 case 'textarea': 330 { 331 pload['value'] = $(e.target).val(); 332 333 break; 334 } 335 case 'input': 336 { 337 pload['value'] = $(e.target).val(); 338 break; 339 } 340 case 'html':// case of iframe in design mode, equivalent of a textarea but with html 341 { 342 if (e.target.ownerDocument.designMode == 'on') 343 { 344 pload['text'] = $(e.target).contents('body').html(); 345 } 346 break; 347 } 348 } 349 } 350 }; 351 352 /** 353 * @description set wanted information from the event to the pLoad 354 * @methodOf EventTracer 355 * @param {event} e dom event triggered 356 * @param {Object} pload callback function called when 'ok' clicked 357 */ 358 this.setEventParameters = function (e,pload) 359 { 360 for (var i in this.ATTRIBUTES_TO_CATCH[e.type]) 361 { 362 if (typeof(e[this.ATTRIBUTES_TO_CATCH[e.type][i]]) != 'undefined') 363 { 364 pload[this.ATTRIBUTES_TO_CATCH[e.type][i]] = e[this.ATTRIBUTES_TO_CATCH[e.type][i]]; 365 } 366 else 367 { 368 if (typeof(e.target[this.ATTRIBUTES_TO_CATCH[e.type][i]]) != 'undefined') 369 { 370 pload[this.ATTRIBUTES_TO_CATCH[e.type][i]] = e.target[this.ATTRIBUTES_TO_CATCH[e.type][i]]; 371 } 372 } 373 } 374 }; 375 376 377 /** 378 * @description return true if the event passed is a business event 379 * @methodOf EventTracer 380 * @param {event} e dom event triggered 381 * @returns {boolean} 382 */ 383 this.hooks = function(e){ 384 return (e.name == 'BUSINESS'); 385 }; 386 387 /** 388 * @description controler that send events to feedtrace 389 * @methodOf EventTracer 390 * @param {event} e dom event triggered 391 */ 392 this.eventStation = function (e){ 393 var keyCode = e.keyCode ? e.keyCode : e.charCode; 394 if (e.type == 'keypress')// kill f4,f5,ctrl+r,s,t,n,u,p,o alt+tab,left and right arrow, right and left window key 395 { 396 try 397 { 398 if ( (typeof(keyCode) != 'undefined') && ((keyCode == 116) | (keyCode == 115) | ((e.ctrlKey)&&((keyCode == 114)|(keyCode == 115)|(keyCode == 116)|(keyCode == 112)|(keyCode == 110)|(keyCode == 111)|(keyCode == 79)) ) | ((e.altKey)&&(keyCode == 9 )) | (keyCode == 91) | (keyCode == 92)| (keyCode == 37)| (keyCode == 39) ) ) 399 { 400 e.preventDefault(); 401 return false; 402 } 403 } 404 catch(e){} 405 } 406 407 var target_tag = e.target.nodeName ? e.target.nodeName.toLowerCase():e.target.type; 408 var idElement; 409 410 if ((e.target.id) && (e.target.id.length > 0)) 411 { 412 idElement = e.target.id; 413 } 414 else 415 { 416 idElement = 'noID'; 417 } 418 var pload = {'id' : idElement}; 419 420 if ((typeof(this.ATTRIBUTES_TO_CATCH)!= 'undefined') && (typeof(this.ATTRIBUTES_TO_CATCH[e.type])!= 'undefined') && (this.ATTRIBUTES_TO_CATCH[e.type].length > 0)) 421 { 422 this.setEventParameters(e,pload); 423 } 424 else 425 { 426 if (typeof(this.describeEvent) != 'undefined') 427 { 428 this.describeEvent(e,pload); 429 } 430 if (typeof(this.describeElement) != 'undefined') 431 { 432 this.describeElement(e,pload); 433 } 434 } 435 436 _this.feedTrace(target_tag, e.type, e.timeStamp, pload); 437 }; 438 439 440 /** 441 * @description in the API to allow the unit creator to send events himself to the event log record events of interaction between interviewee and the test 442 * @example feedTrace('BUSINESS','start_drawing',getGlobalTime(), {'unitTime':getUnitTime()}); 443 * @methodOf EventTracer 444 * @param {String} target_tag element type receiving the event. 445 * @param {String} event_type type of event being catched 446 * @param {Object} pLoad object containing various information about the event. you may put whatever you need in it. 447 */ 448 this.feedTrace = function (target_tag,event_type,time, pLoad) 449 { 450 var send_right_now = false; 451 var event = '{"name":"'+target_tag+'","type":"'+event_type+'","time":"'+time+'"'; 452 453 454 if (typeof(pLoad)=='string') 455 { 456 event = event+',"pLoad":"'+pLoad+'"'; 457 } 458 else 459 { 460 for (var prop_name in pLoad) 461 { 462 event = event+',"'+prop_name+'":"'+pLoad[prop_name]+'"'; 463 } 464 } 465 event = event+'}'; 466 467 if (typeof(this.hooks) != "undefined") 468 { 469 send_right_now = this.hooks($.parseJSON(event)); 470 } 471 472 this.eventPool.push(event); 473 474 if ((this.eventPool.length > this.opts.POOL_SIZE) || (send_right_now)) 475 { 476 this.prepareFeedTrace(); 477 } 478 }; 479 480 481 /** 482 * @description prepare one block of stored traces for being sent 483 * @methodOf EventTracer 484 */ 485 this.prepareFeedTrace = function() 486 { 487 var currentLength = this.eventsToBeSend.length; 488 489 var temp_array = new Array(); 490 491 for ( var i = 0 ; ((this.eventPool.length>0)&&(i < this.opts.POOL_SIZE )) ; i++ ) 492 { 493 temp_array.push(this.eventPool.shift()); 494 } 495 this.eventsToBeSend.push(temp_array); 496 this.sendFeedTrace(); 497 }; 498 499 500 /** 501 * @description send one block of traces (non blocking) 502 * Does send the content of eventsToBeSend[0] to the server 503 * @methodOf EventTracer 504 */ 505 this.sendFeedTrace = function () 506 { 507 var events = this.eventsToBeSend.pop(); 508 var sent_timeStamp = new Date().getTime(); 509 var params = $.extend({'events': events}, this.destinationService.params); 510 511 $.ajax({ 512 url : this.destinationService.url, 513 data : params, 514 type : this.destinationService.method, 515 async :true, 516 datatype: this.destinationService.format, 517 success : function(data, textStatus){ 518 _this.sendFeedTraceSucceed(data, textStatus, sent_timeStamp); 519 }, 520 error : function(xhr, errorString, exception){ 521 _this.sendFeedTraceFail(xhr, errorString, exception, events); 522 } 523 }); 524 }; 525 526 /** 527 * @description success callback after traces sent. does affinate the size of traces package sent 528 * @methodOf EventTracer 529 * @param {String} data response from server 530 * @param {String} textStatus status of request 531 * @param {int} sent_timeStamp time the request was sent 532 */ 533 this.sendFeedTraceSucceed = function (data, textStatus, sent_timeStamp)//callback for sendfeedtrace 534 { 535 // adaptation of the send frequence 536 var request_time = (new Date()).getTime() - sent_timeStamp; 537 if (request_time > this.opts.time_limit_for_ajax_request) 538 { 539 // it takes too long 540 this.increaseEventsPoolSize(); 541 } 542 else 543 { 544 // we can increase the frequency of events storing 545 this.reduceEventsPoolSize(); 546 } 547 if (data.saved) 548 { 549 this.eventsToBeSend.shift();// data send, we can delete at 0 index 550 } 551 }; 552 553 /** 554 * @description the request took too much time, we increase the size of traces package, to have less frequent requests 555 * @methodOf EventTracer 556 */ 557 this.increaseEventsPoolSize = function () 558 { 559 if ( this.opts.POOL_SIZE < this.opts.MAX_POOL_SIZE) 560 { 561 this.opts.POOL_SIZE = Math.floor(this.opts.POOL_SIZE * 2); 562 } 563 }; 564 565 /** 566 * @description the request was fast enough, we increase the frequency of requests by reducing the size of traces package 567 * @methodOf EventTracer 568 */ 569 this.reduceEventsPoolSize = function () 570 { 571 if ( this.opts.POOL_SIZE > this.opts.MIN_POOL_SIZE ) 572 { 573 this.opts.POOL_SIZE = Math.floor(this.opts.POOL_SIZE * 0.75); 574 } 575 }; 576 577 /** 578 * @description callback function after request failed (TODO) 579 * @methodOf EventTracer 580 * @param {ressource} xhr ajax request ressource 581 * @param {String} errorString error message 582 * @param {exception} [exception] exception object thrown 583 */ 584 this.sendFeedTraceFail = function (xhr, errorString, exception, events)//callback for sendfeedtrace 585 { 586 this.increaseEventsPoolSize(); 587 588 this.eventsToBeSend.unshift(events); 589 590 window.setInterval(this.sendAllFeedTrace_now, 2000); 591 }; 592 593 594 /* no callback on success 595 used when business events catched*/ 596 /** 597 * @description send all traces with a blocking function 598 * @methodOf EventTracer 599 */ 600 this.sendAllFeedTrace_now = function () 601 { 602 var currentLength = this.eventsToBeSend.length; 603 604 this.eventsToBeSend[ currentLength ] = Array(); 605 for ( ; this.eventPool.length > 0 ; )// empty the whole eventPool array 606 { 607 this.eventsToBeSend[ currentLength ].push( this.eventPool.pop() ); 608 } 609 610 var events = new Array(); 611 for (var j in this.eventsToBeSend) 612 { 613 for (var i in this.eventsToBeSend[j]) 614 { 615 events.push(this.eventsToBeSend[j][i]); 616 } 617 } 618 619 var params = $.extend({'events': events }, this.destinationService.params); 620 var sent_timeStamp = new Date().getTime(); 621 622 $.ajax({ 623 url : this.destinationService.url, 624 data : params, 625 type : this.destinationService.method, 626 async : false, 627 datatype: this.destinationService.format, 628 success : function(data, textStatus){ 629 _this.sendFeedTraceSucceed(data, textStatus, sent_timeStamp); 630 }, 631 error : function(xhr, errorString, exception){ 632 _this.sendFeedTraceFail(xhr, errorString, exception, events); 633 } 634 }); 635 }; 636 637 } 638 639 640 /** 641 * @description bind every non bubbling events to dom elements. 642 * @methodOf EventTracer 643 */ 644 jQuery.fn.bindDom = function(eventTracer) 645 { 646 $(this).bind(eventTracer.EVENTS_TO_CATCH.nonBubbling.join(' ') , eventTracer.eventStation); 647 var childrens = $(this).children(); 648 if (childrens.length)// stop condition 649 { 650 childrens.bindDom(eventTracer); 651 } 652 }; 653 654 /** 655 * @description unbind platform events 656 * @methodOf EventTracer 657 */ 658 jQuery.fn.unBindDom = function(eventTracer) 659 { 660 661 $(this).unbind( eventTracer.EVENTS_TO_CATCH.nonBubbling.join(' ') , eventTracer.eventStation); 662 var childrens = $(this).children(); 663 if (childrens.length)// stop condition 664 { 665 childrens.unBindDom(eventTracer); 666 } 667 }; 668 669 // attributes set in the pos tag 670 var ignored_pload_element_array = new Array('contentEditable','localName','tagname','textContent','namespaceURI','baseURI','innerHTML','defaultStatus','fullScreen','UNITSMAP','PROCESSURI','LANGID' 671 ,'ITEMID','ACTIVITYID','DURATION','ELEMENT_NODE','ATTRIBUTE_NODE','TEXT_NODE','CDATA_SECTION_NODE','ENTITY_REFERENCE_NODE','ENTITY_NODE','PROCESSING_INSTRUCTION_NODE','COMMENT_NODE' 672 ,'DOCUMENT_NODE','DOCUMENT_TYPE_NODE','DOCUMENT_FRAGMENT_NODE','NOTATION_NODE','DOCUMENT_POSITION_PRECEDING','DOCUMENT_POSITION_FOLLOWING','DOCUMENT_POSITION_CONTAINS','DOCUMENT_POSITION_CONTAINED_BY' 673 ,'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC','DOCUMENT_POSITION_DISCONNECTED','childElementCount','LAYOUT_DIRECTION','CURRENTSTIMULUS','CURRENTITEMEXTENSION','CURRENTSTIMULUSEXTENSION','nodeType','tabIndex'); 674 var ignored_pload_event_array = new Array('cancelable','contentEditable','bubbles','tagName','localName','timeStamp','type'); 675 676 677 /* custom events definition */ 678 679 /* changeCss 680 */ 681 jQuery.event.special.changeCss = {setup:function(){},teardown:function(){}}; 682 /* reloadMapEvent 683 order to reload the map */ 684 jQuery.event.special.reloadMapEvent = {setup: function(){},teardown: function(){}}; 685