/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2009-2012 (original work) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV); * 2009-2012 (update and modification) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV); * */ /** * TAO API events utilities. * * @author CRP Henri Tudor - TAO Team - {@link http://www.tao.lu} * @license GPLv2 http://www.opensource.org/licenses/gpl-2.0.php * @package taoItems * @requires jquery >= 1.4.0 {@link http://www.jquery.com} * * @see NewarX#Core */ /** * * @class EventTracer * @property {Object} [options] */ function EventTracer (options){ //keep the ref of the current instance for scopes traversing var _this = this; /** * array of events arrays * @fieldOf EventTracer * @type {Array} */ this.eventPool = new Array();// /** * array of strings * @fieldOf EventTracer * @type {Array} */ this.eventsToBeSend = new Array(); /** * The tracer common options * @fieldOf EventTracer * @type {Object} */ this.opts = { POOL_SIZE : 500, // number of events to cache before sending MIN_POOL_SIZE : 200, MAX_POOL_SIZE : 5000, time_limit_for_ajax_request : 2000, eventsToBeSendCursor : -1, ctrlPressed : false, altPressed : false }; //extends the options on the object construction if(options != null && options != undefined){ $.extend(this.opts, options); } /** * the list of events to be catched * @fieldOf EventTracer * @type {Object} */ this.EVENTS_TO_CATCH = new Object(); /** * the list of attributes to be catched * @fieldOf EventTracer * @type {Object} */ this.ATTRIBUTES_TO_CATCH = new Array(); /** * The parameters defining how and where to load the events list to catch * @fieldOf EventTracer * @type {Object} */ this.sourceService = { type: 'sync', // (sync | manual) data: null, //if type is manual, contains the data in JSON, else it should be null url: '/taoDelivery/ResultDelivery/getEvents', //the url sending the events list params: {}, //the common parameters to send to the service method: 'post', //sending method format: 'json' //the response format, now ONLY JSON is supported }; /** * The parameters defining how and where to send the events * @fieldOf EventTracer * @type {Object} */ this.destinationService = { url: '/taoDelivery/ResultDelivery/traceEvents', //the URL where to send the events params: {}, //the common parameters to send to the service method: 'post', //sending method format: 'json' //the response format, now ONLY JSON is supported }; /** * Initialize the service interface for the source service: * how and where we retrieve the events to catch * @methodOf EventTracer * @param {Object} environment */ this.initSourceService = function(environment){ //define the source service if($.isPlainObject(environment)){ if($.inArray(environment.type, ['manual','sync']) > -1){ this.sourceService.type = environment.type; //manual behaviour if(this.sourceService.type == 'manual' && $.isPlainObject(environment.data)){ this.sourceService.data = environment.data; } else{ //remote behaviour if(source.url){ if(/(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/.test(environment.url)){ //test url this.sourceService.url = environment.url; //set url } } //ADD parameters if($.isPlainObject(environment.params)){ for(key in environment.params){ if(isScalar(environment.params[key])){ this.sourceService.params[key] = environment.params[key]; } } } if(environment.method){ if(/^get|post$/i.test(environment.method)){ this.sourceService.method = environment.method; } } } } } //we load now the events to catch //we load it manually by calling directly the method with the data if(this.sourceService.type == 'manual' && this.sourceService.data != null){ this.EVENTS_TO_CATCH = this.setEventsToCatch(this.sourceService.data); } //we call the remote service if(this.sourceService.type == 'sync' && this.sourceService.url != ''){ received = $.parseJSON($.ajax({ async : false, url : this.sourceService.url, data : this.sourceService.params, type : this.sourceService.method }).responseText); if(received){ this.EVENTS_TO_CATCH = this.setEventsToCatch(received); } } //we bind the events to be observed in the item if(this.EVENTS_TO_CATCH.bubbling != undefined){ this.bind_platform(); } }; /** * Initialize the service interface forthe destination service: * how and where we send the catched events * @methodOf EventTracer * @param {Object} environment */ this.initDestinationService = function(environment){ if($.isPlainObject(environment)){ if(environment.url){ if(/(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/.test(environment.url)){ //test url this.destinationService.url = environment.url; //set url } } //ADD parameters if($.isPlainObject(environment.params)){ for(key in environment.params){ if(isScalar(environment.params[key])){ this.destinationService.params[key] = environment.params[key]; } } } if(environment.method){ if(/^get|post$/i.test(environment.method)){ this.destinationService.method = environment.method; } } } }; /** * @description record events of interaction between interviewee and the test * @methodOf EventTracer * @param {Object} data event type list * @returns {Object} the events to catch */ this.setEventsToCatch = function (data) { // retreive the list of events to catch or not to catch if (data.type.length > 0) { var EVENTS_TO_CATCH = {bubbling:[],nonBubbling:[]}; if (data.type == 'catch') { for (i in data.list) { if ($.inArray(i,['click', 'dblclick', 'change', 'submit', 'select', 'mousedown', 'mouseup', 'mouseenter', 'mousemove', 'mouseout']) > -1)//if is bubbling event { EVENTS_TO_CATCH.bubbling.push(i); } else { EVENTS_TO_CATCH.nonBubbling.push(i);// else non bubbling event } this.ATTRIBUTES_TO_CATCH[i] = data.list[i]; } } else { // no catch 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']}; for (i in data.list) { remove_array(data.list[i].event,EVENTS_TO_CATCH.bubbling); remove_array(data.list[i].event,EVENTS_TO_CATCH.nonBubbling); } } } else { 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']}; } return EVENTS_TO_CATCH; }; /** * @description bind platform events * @methodOf EventTracer */ this.bind_platform = function() { // for non bubbling events, link them to all the listened element // it is still useful to use delegation since it will remains much less listeners in the memory (just 1 instead of #numberOfElements) $('body').bindDom(this); // for bubbling events $('body').bind(this.EVENTS_TO_CATCH.bubbling.join(' ') , this.eventStation); }; /** * @description unbind platform events * @methodOf EventTracer */ this.unbind_platform = function() { $('body').unbind(EVENTS_TO_CATCH.bubbling.join(' ') , this.eventStation); $('body').unBindDom(this); }; /** * @description set all information from the event to the pLoad * @methodOf EventTracer * @param {event} e dom event triggered * @param {Object} pload callback function called when 'ok' clicked */ this.describeEvent = function(e,pload) { if (e.target && (typeof(e.target['value']) != 'undefined') && (e.target['value'] != -1) && (e.target['value'] != '')) { pload['value'] = e.target['value']; } // get everything about the event for (var i in e) { if ((typeof(e[i]) != 'undefined') && (typeof(e[i]) != 'object') && (typeof(e[i]) != 'function') && (e[i] != '')) { if ((i != 'cancelable') && (i != 'contentEditable') && (i != 'cancelable') && (i != 'bubbles') && (i.substr(0,6) != 'jQuery')) { pload[i] = e[i]; } } } }; /** * @description set all information from the target dom element to the pLoad * @methodOf EventTracer * @param {event} e dom event triggered * @param {Object} pload callback function called when 'ok' clicked */ this.describeElement = function(e,pload) { // take everything except useless attributes for (var i in e.target) { try { if (( (typeof(e.target[i]) == 'string') && (e.target[i] != '') ) | (typeof(e.target[i]) == 'number')) { if ( (!in_array(i,position_pload_array)) && (!in_array(i,ignored_pload_element_array)) && (i.substr(0,6) != 'jQuery') ) { pload[i] = ''+e.target[i]; } } } catch(e){} } if (typeof(e.target.nodeName) != 'undefined') { switch(e.target.nodeName.toLowerCase()) { case 'select': { pload['value'] = $(e.target).val(); if (typeof(pload['value']) == 'array') { pload['value'] = pload['value'].join('|'); } break; } case 'textarea': { pload['value'] = $(e.target).val(); break; } case 'input': { pload['value'] = $(e.target).val(); break; } case 'html':// case of iframe in design mode, equivalent of a textarea but with html { if (e.target.ownerDocument.designMode == 'on') { pload['text'] = $(e.target).contents('body').html(); } break; } } } }; /** * @description set wanted information from the event to the pLoad * @methodOf EventTracer * @param {event} e dom event triggered * @param {Object} pload callback function called when 'ok' clicked */ this.setEventParameters = function (e,pload) { for (var i in this.ATTRIBUTES_TO_CATCH[e.type]) { if (typeof(e[this.ATTRIBUTES_TO_CATCH[e.type][i]]) != 'undefined') { pload[this.ATTRIBUTES_TO_CATCH[e.type][i]] = e[this.ATTRIBUTES_TO_CATCH[e.type][i]]; } else { if (typeof(e.target[this.ATTRIBUTES_TO_CATCH[e.type][i]]) != 'undefined') { pload[this.ATTRIBUTES_TO_CATCH[e.type][i]] = e.target[this.ATTRIBUTES_TO_CATCH[e.type][i]]; } } } }; /** * @description return true if the event passed is a business event * @methodOf EventTracer * @param {event} e dom event triggered * @returns {boolean} */ this.hooks = function(e){ return (e.name == 'BUSINESS'); }; /** * @description controler that send events to feedtrace * @methodOf EventTracer * @param {event} e dom event triggered */ this.eventStation = function (e){ var keyCode = e.keyCode ? e.keyCode : e.charCode; 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 { try { 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) ) ) { e.preventDefault(); return false; } } catch(e){} } var target_tag = e.target.nodeName ? e.target.nodeName.toLowerCase():e.target.type; var idElement; if ((e.target.id) && (e.target.id.length > 0)) { idElement = e.target.id; } else { idElement = 'noID'; } var pload = {'id' : idElement}; if ((typeof(this.ATTRIBUTES_TO_CATCH)!= 'undefined') && (typeof(this.ATTRIBUTES_TO_CATCH[e.type])!= 'undefined') && (this.ATTRIBUTES_TO_CATCH[e.type].length > 0)) { this.setEventParameters(e,pload); } else { if (typeof(this.describeEvent) != 'undefined') { this.describeEvent(e,pload); } if (typeof(this.describeElement) != 'undefined') { this.describeElement(e,pload); } } _this.feedTrace(target_tag, e.type, e.timeStamp, pload); }; /** * @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 * @example feedTrace('BUSINESS','start_drawing',getGlobalTime(), {'unitTime':getUnitTime()}); * @methodOf EventTracer * @param {String} target_tag element type receiving the event. * @param {String} event_type type of event being catched * @param {Object} pLoad object containing various information about the event. you may put whatever you need in it. */ this.feedTrace = function (target_tag,event_type,time, pLoad) { var send_right_now = false; var event = '{"name":"'+target_tag+'","type":"'+event_type+'","time":"'+time+'"'; if (typeof(pLoad)=='string') { event = event+',"pLoad":"'+pLoad+'"'; } else { for (var prop_name in pLoad) { event = event+',"'+prop_name+'":"'+pLoad[prop_name]+'"'; } } event = event+'}'; if (typeof(this.hooks) != "undefined") { send_right_now = this.hooks($.parseJSON(event)); } this.eventPool.push(event); if ((this.eventPool.length > this.opts.POOL_SIZE) || (send_right_now)) { this.prepareFeedTrace(); } }; /** * @description prepare one block of stored traces for being sent * @methodOf EventTracer */ this.prepareFeedTrace = function() { var currentLength = this.eventsToBeSend.length; var temp_array = new Array(); for ( var i = 0 ; ((this.eventPool.length>0)&&(i < this.opts.POOL_SIZE )) ; i++ ) { temp_array.push(this.eventPool.shift()); } this.eventsToBeSend.push(temp_array); this.sendFeedTrace(); }; /** * @description send one block of traces (non blocking) * Does send the content of eventsToBeSend[0] to the server * @methodOf EventTracer */ this.sendFeedTrace = function () { var events = this.eventsToBeSend.pop(); var sent_timeStamp = new Date().getTime(); var params = $.extend({'events': events}, this.destinationService.params); $.ajax({ url : this.destinationService.url, data : params, type : this.destinationService.method, async :true, datatype: this.destinationService.format, success : function(data, textStatus){ _this.sendFeedTraceSucceed(data, textStatus, sent_timeStamp); }, error : function(xhr, errorString, exception){ _this.sendFeedTraceFail(xhr, errorString, exception, events); } }); }; /** * @description success callback after traces sent. does affinate the size of traces package sent * @methodOf EventTracer * @param {String} data response from server * @param {String} textStatus status of request * @param {int} sent_timeStamp time the request was sent */ this.sendFeedTraceSucceed = function (data, textStatus, sent_timeStamp)//callback for sendfeedtrace { // adaptation of the send frequence var request_time = (new Date()).getTime() - sent_timeStamp; if (request_time > this.opts.time_limit_for_ajax_request) { // it takes too long this.increaseEventsPoolSize(); } else { // we can increase the frequency of events storing this.reduceEventsPoolSize(); } if (data.saved) { this.eventsToBeSend.shift();// data send, we can delete at 0 index } }; /** * @description the request took too much time, we increase the size of traces package, to have less frequent requests * @methodOf EventTracer */ this.increaseEventsPoolSize = function () { if ( this.opts.POOL_SIZE < this.opts.MAX_POOL_SIZE) { this.opts.POOL_SIZE = Math.floor(this.opts.POOL_SIZE * 2); } }; /** * @description the request was fast enough, we increase the frequency of requests by reducing the size of traces package * @methodOf EventTracer */ this.reduceEventsPoolSize = function () { if ( this.opts.POOL_SIZE > this.opts.MIN_POOL_SIZE ) { this.opts.POOL_SIZE = Math.floor(this.opts.POOL_SIZE * 0.75); } }; /** * @description callback function after request failed (TODO) * @methodOf EventTracer * @param {ressource} xhr ajax request ressource * @param {String} errorString error message * @param {exception} [exception] exception object thrown */ this.sendFeedTraceFail = function (xhr, errorString, exception, events)//callback for sendfeedtrace { this.increaseEventsPoolSize(); this.eventsToBeSend.unshift(events); window.setInterval(this.sendAllFeedTrace_now, 2000); }; /* no callback on success used when business events catched*/ /** * @description send all traces with a blocking function * @methodOf EventTracer */ this.sendAllFeedTrace_now = function () { var currentLength = this.eventsToBeSend.length; this.eventsToBeSend[ currentLength ] = Array(); for ( ; this.eventPool.length > 0 ; )// empty the whole eventPool array { this.eventsToBeSend[ currentLength ].push( this.eventPool.pop() ); } var events = new Array(); for (var j in this.eventsToBeSend) { for (var i in this.eventsToBeSend[j]) { events.push(this.eventsToBeSend[j][i]); } } var params = $.extend({'events': events }, this.destinationService.params); var sent_timeStamp = new Date().getTime(); $.ajax({ url : this.destinationService.url, data : params, type : this.destinationService.method, async : false, datatype: this.destinationService.format, success : function(data, textStatus){ _this.sendFeedTraceSucceed(data, textStatus, sent_timeStamp); }, error : function(xhr, errorString, exception){ _this.sendFeedTraceFail(xhr, errorString, exception, events); } }); }; } /** * @description bind every non bubbling events to dom elements. * @methodOf EventTracer */ jQuery.fn.bindDom = function(eventTracer) { $(this).bind(eventTracer.EVENTS_TO_CATCH.nonBubbling.join(' ') , eventTracer.eventStation); var childrens = $(this).children(); if (childrens.length)// stop condition { childrens.bindDom(eventTracer); } }; /** * @description unbind platform events * @methodOf EventTracer */ jQuery.fn.unBindDom = function(eventTracer) { $(this).unbind( eventTracer.EVENTS_TO_CATCH.nonBubbling.join(' ') , eventTracer.eventStation); var childrens = $(this).children(); if (childrens.length)// stop condition { childrens.unBindDom(eventTracer); } }; // attributes set in the pos tag var ignored_pload_element_array = new Array('contentEditable','localName','tagname','textContent','namespaceURI','baseURI','innerHTML','defaultStatus','fullScreen','UNITSMAP','PROCESSURI','LANGID' ,'ITEMID','ACTIVITYID','DURATION','ELEMENT_NODE','ATTRIBUTE_NODE','TEXT_NODE','CDATA_SECTION_NODE','ENTITY_REFERENCE_NODE','ENTITY_NODE','PROCESSING_INSTRUCTION_NODE','COMMENT_NODE' ,'DOCUMENT_NODE','DOCUMENT_TYPE_NODE','DOCUMENT_FRAGMENT_NODE','NOTATION_NODE','DOCUMENT_POSITION_PRECEDING','DOCUMENT_POSITION_FOLLOWING','DOCUMENT_POSITION_CONTAINS','DOCUMENT_POSITION_CONTAINED_BY' ,'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC','DOCUMENT_POSITION_DISCONNECTED','childElementCount','LAYOUT_DIRECTION','CURRENTSTIMULUS','CURRENTITEMEXTENSION','CURRENTSTIMULUSEXTENSION','nodeType','tabIndex'); var ignored_pload_event_array = new Array('cancelable','contentEditable','bubbles','tagName','localName','timeStamp','type'); /* custom events definition */ /* changeCss */ jQuery.event.special.changeCss = {setup:function(){},teardown:function(){}}; /* reloadMapEvent order to reload the map */ jQuery.event.special.reloadMapEvent = {setup: function(){},teardown: function(){}};