UNPKG

ifsm

Version:

a jQuery State Machine (FSM / HSM) to design and manage javascript web user interfaces, simulators, games...

1,219 lines (1,119 loc) 61.9 kB
/** * ----------------------------------------------------------------------------------------- * INTERSEL - 4 cité d'Hauteville - 75010 PARIS * RCS PARIS 488 379 660 - NAF 721Z * * File : iFSM.js * iFSM : a finite state machine with jQuery * * ----------------------------------------------------------------------------------------- * Modifications : * - 2013/10/23 - E.Podvin - V1.0 - Creation * - 2013/11/03 - E.Podvin - V1.1 - add * - 2013/11/04 - E.Podvin - V1.2 - add process_event_if condition * - 2013/11/05 - E.Podvin - V1.3 - add sub machine to manage hierarchical state machines (HSM) * - 2013/11/12 - E.Podvin - V1.4 - debug on submachine management * - 2013/11/22 - E.Podvin - V1.5 - add 'next_state_on_target' to change a state according to the submachine states * - 2014/03/19 - E.Podvin - V1.6.1 - add options.initState in the jquery call to be able to define the initial state * - 2014/06/06 - E.Podvin - 1.6.8 - add synonymous events * - 2014/06/16 - E.Podvin - 1.6.9 - patch to correctly process process_on_UItarget option * - 2014/06/16 - E.Podvin - 1.6.10 - copy state definition in order to be able to have it dynamic * - 2014/07/01 - E.Podvin - 1.6.11 - patch on the reinitialisation of the event iterations when changing state * - 2014/07/08 - E.Podvin - 1.6.13 - propagate_event may be an array of events to propagate * - 2014/07/14 - E.Podvin - 1.6.14 - improve performance * - 2014/09/04 - E.Podvin - 1.6.15 - fix on the 'exitMachine' message when it was sent * - 2014/09/11 - E.Podvin - 1.6.16 - fix on synonymous event that was not set when still defined in a previous state * - 2016/04/26 - E.Podvin - 1.6.17 - fix on delayed events * - 2016/04/26 - E.Podvin - 1.6.18 - fix on delayed events on "DefaultState" * - 2017/03/08 - E.Podvin - 1.7.0 - myFSM.eventCalled available for script + 'propagate_event_on_localmachine' directive + catchEvent * - 2017/03/13 - E.Podvin - 1.7.1 - add myFSM.lastState * - 2017/03/20 - E.Podvin - 1.7.2 - fixes to be compliant with jquery 3.2.0 * - 2017/10/14 - E.Podvin - 1.7.3 - small fix in case event is triggered with multiple parameters * - 2017/10/17 - E.POdvin - 1.7.4 - change log level - by default, displays the state&event processing step in the console * - 2018/03/08 - E.POdvin - 1.7.5 - transfer data transmitted by trigger to enterEvent and exitEvent * - 2018/03/13 - E.Podvin - 1.7.6 - add some controls when starting the state machines: can't start if no id on the object or if still exists * - 2019/11/22 - E.Podvin - 1.7.7 - prevent preventCancel event to stack if delayed again (see preventCancelSet) * add console.warn (instead of log) for errors message * ----------------------------------------------------------------------------------------- * * @copyright Intersel 2013-2018 * @fileoverview : iFSM : a finite state machine with jQuery * @see {@link https://github.com/intersel/iFSM} * @author : Emmanuel Podvin - emmanuel.podvin@intersel.fr * @version : 1.7.6 * ----------------------------------------------------------------------------------------- */ /** * How to use it : * =============== * <code> * <script type="text/javascript" src="jquery-3.2.0.min.js"></script> * <script type="text/javascript" src="jquery.dorequesttimeout.js"></script> * <script type="text/javascript" src="jquery.attrchange.js"></script> * <script type="text/javascript" src="ifsm.js"></script> * //Example of use : * <script type="text/javascript"> * var aStateDefinition = { * FirstState : * { * enterState: * { * init_function: function(){alert("First State");} * }, * click: * { * next_state: 'NextState' * } * }, * NextState : * { * enterState: * { * init_function: function(){alert("Next State");} * }, * click: * { * next_state: 'FirstState' * } * }, * DefaultState : * { * start : * { * next_state : 'FirstState' * } * } * }; * * $('#myButton').iFSM(aStateDefinition); * myFSM = $('#myButton').getFSM(aStateDefinition); //get the linked FSM object if needed * * </script> * </code> * */ (function($){ /** * @param integer nb_FSM - number of active FSM */ var nb_FSM = 0; /** * * fsm_manager - class constructor * @param {object} aStateDefinition - states definition * { * <aStateName1> : * { * delegate_machines : * { * <aSubMachine name 1> : * { * submachine : <a State definition>, * no_reinitialisation : <boolean, default:false> * }, * <aSubMachine name i> : * { * submachine : <a State definition> * }, * ... * }, * * <aEventName1>: * { * how_process_event: <immediate||push (default)||{delay:<adelay>,preventcancel:<false(default)|true>}>, * init_function: <a function(parameters, event, data)>, * properties_init_function: <parameters for init_function>, * next_state: <aStateName>, * pushpop_state: <'PushState'||'PopState'>, * next_state_when: <a statement that returns boolean>, * next_state_on_target : * { * condition : <'||'||'&&'> * submachines : * { * <submachineName1> : * { * condition : <''(default)||'not'> * target_list : [<targetState1>,...,<targetStaten>], * } * ... * <submachineNamen> : ... * } * } * next_state_if_error: <aStateName>, * pushpop_state_if_error: <'PushState'||'PopState'>, * propagate_event: <true||anEventName||[aEVN1,aEVN2,...]> * process_event_if : <a statement that returns boolean>, * propagate_event_on_refused : <anEventName> * out_function: <a function(parameters, event, data)>, * properties_out_function: <parameters for out_function>, * prevent_bubble: <true|false(default)> * process_on_UItarget: <true|false(default)> * UI_event_bubble: <true|false(default)> * }, * <aEventName....>: <anOtherEventName>, * <aEventName....>: * { * .... * }, * enterState : ... * exitState : ... * }, * <aStateName...> : <anAnotherStateName>, * <aStateName...> : * { * .... * }, * DefaultState : * { * start: //a default start event received at the FSM start * { * }, * <aEventName....>: * { * } * } * } * * - <aStateName> : * <aStateName> is the name of a state. * - delegate_machines : sub machines list to delegate the events on the state * - submachine : the variable name of a state definition or a state definition description * - <aEventName> : * '<aEventName>' is the name of an event. It may be any event name supported by javascript or JQuery. * It defines an event we want to be alerted when it occurs on the object * Specific events : * - 'start' : this event is automatically sent when the FSM starts. should be defined in the initial state (or 'DefaultState') * - 'enterState' : this event is automatically sent and immediatly executed when the FSM enter the state * - 'exitState' : this event is automatically sent and immediatly executed when the FSM exit the state * - 'exitMachine' : this event is automatically sent when the (sub)machine will exit/close * - 'attrchange' : received if any attribute of the jquery object (myUIObject) changed * - data sent : event - event object * * event.attributeName - Name of the attribute modified * * event.oldValue - Previous value of the modified attribute * * event.newValue - New value of the modified attribute * - 'attrchange_<attributename>' (ex: 'attrchange_class') : received if the attribute of the jquery object changed * - data sent : * * newValue - New value of the modified attribute * * oldValue - Previous value of the modified attribute * - 'attrchange_style_<cssattributename_in_camelcase>' (ex:'attrchange_style_width') : received if the css attribute of the jquery object changed * - data sent : * * newValue - New value of the modified attribute * * oldValue - Previous value of the modified attribute * * its description is using an object with the following properties: * - how_process_event [default:{push}] : {immediate}||{push}||{delay:delay_value,preventcancel:<false(default)|true>} * if delay is defined, the processing of the event is delayed and activated at 'delay' * by default, any event delayed will be cancelled if the state changes * if preventcancel is defined, the delayed event won't be cancelled * - process_event_if : * Definition of condition test that will be evaluated, and if result is true then event will be processed * if not, see if a propagate_event_on_refused in order to trigger it... and exit... * - propagate_event_on_refused : an event name to trigger if process_event_if is false * - init_function : function name or a function statement * - properties_init_function : parameters to send to init_function * - next_state : next state once init_function done. If not defined, there is no state change. * - pushpop_state : * If 'PushState', then current state is pushed in the StateStack then next_state takes place. * If 'PopState', then the next state will be the one on top of the StateStack which is poped. next_state is so overwritten... If the stack is void, there is no state change. * - next_state_when : * Definition of condition test that will be evaluated, and if result is true then state will change * Following variables may be used for the test * this : the FSM object * this.EventIteration : variable that gives the iteration of the number of calls of the current event. * EventIteration is reset when the state changes * - next_state_on_target : * Definition of condition test based on the current states of the defined submachines * the test consist to : * - get the current states of each defined sub-machines, * - match the current state to the given array, resulting to true if found * - apply the defined operator between the results * - next_state_if_error (default: does not change state) : state set if init_function returns false. . * - pushpop_state_if_error : * If 'PushState', then current state is pushed in the StateStack then next_state_if_error takes place. * If 'PopState', then the next state will be the one on top of the StateStack which is poped. If the stack is void, there is no state change. next_state_if_error is so overwritten... * - out_function : function name to do once next_state changed * - properties_out_function : parameters to send to out_function * - propagate_event : if == 'true', the current event is propagated to the next state * if it's the name of an event, triggers the event... * if an array of events, events are triggered in sequence * - prevent_bubble : if defined and true, the current event will not bubble to its parent machine * - UI_event_bubble : if defined and true, the current event will bubble. By default, no UI event bubbling... * - process_on_UItarget : if defined and true, the current event will be processed only if the event was directly targeting * the UI jQuery object linked to the machine * An event may be synonymous with another event. In this case, we simply give the name of the synonymous event. * * @remarks * - any FSM is automatically initialised with a 'start' event * - state function should return a boolean : true: ok works fine; false: error * - state function should have the following input : * - parameters : the properties_<init/out>_function * - event : the event * - data : the data sent with the event * - a default statename 'DefaultState' can be defined to define the default behaviour of some events... * - an event is first search in the current state, then if not found in the 'DefaultState' * - if an event is not found, nothing is done... * - a 'start' event is triggered when the FSM is started with InitManager * - when there are sub machines defined for a state : * - the events are sent to each defined submachines in the order * - once the event is processed by the submachines, it is bubbled to the upper machines * - it is possible to prevent the bubbling of events with the directive 'prevent_bubble' to true * - a submachine works as the main one : * - if no_reinitialisation == false (default) it is initialised * - then starts once entering in the state * - a start event is triggered to it * * - to trigger an event to the machine itself, use can use the 'trigger' function * ex: myFSM.trigger('myevent'); * - it is possible to trigger any event to a machine with the jquery trigger function : * ex: $('#myButton1').trigger('start',{targetFSM:myFsm}); * - within a state function, it is possible to trigger event to any machine using its linked jQuery object : myFSM.myUIObject * ex : this.myUIObject.trigger('aEventName') * - if multiple machines are assigned to the same jQuery Object, it also possible to specify the FSM in the parameter : * ex : this.myUIObject.trigger('aEventName',{targetFSM:this}) * - beware that if the submachine is no more accessible, it won't perhaps receive the message you triggered from it. * a workaround is to directly "push" an event in order that it will be processed within the flow of the current event processing. * the function to use is myFSM.pushEvent(anEventName, data) (or this.pushEvent in a FSM function) * ex: init_function : function(){this.pushEvent('setText','I push an event');}, * - if a delayed event is sent again before a previous one was processed, the previous event is cancelled and the new one re-started * - a sub machine can manage its first state by handling the 'start' event in the DefaultState * - as the structure definition of the states of a machine is a javascript object, * it is then possible to define generic states or generic events as this example : * <code> * var myGenericEvent = { * next_state : aState, * propagate_event : anEvent, * .... * } * var myMachine = { * aState1 : * firstEvent : $.extend(true, {}, myGenericEvent), //copy of the myGenericEvent object * secondEvent : {...eventdefinition...}, * thirdEvent : myGenericEvent, * .... * } * </code> * - if you stay coherent, it is possible to define and to change dynamically the states definition through myFSM._stateDefinition * - enterState and exitState can't be delayed * - all events in a submachine should have the prevent_bubble to true, except for those you'd like to be processed in other parts of the machine * - delegate machines should not be created on the DefaultState state * - if UI_event_bubble for an UI event is set to false by any part of the machine or submachine it will stay to false * * * The public available variables : * - myFSM.currentState : current processed state name * - myFSM.currentEvent : current processed event * - myFSM.myUIObject : the jQuery object associated to the FSM * - myFSM._stateDefinition : the definition of the states and events * - myFSM._stateDefinition.<statename>.<eventname>.EventIteration - the number of times an event has been called * - myFSM.opts - the defined options. Generally used to store local data * - myFSM.rootMachine : the root machine * - myFSM.parentMachine : the parent machine if it's in a sub machine (null if none) * * Within the call of FSM function, you can refer to the FSM by 'this', examples : * - this.currentState * - this.currentEvent * - this.currentEvent.type (name of the current processed event) * - this.myUIObject * - ... * plus about event: * - this.EventIteration : the current event iteration * - this.actualTarget : the jQuery object that is currently targetted by an event * * @param anObject - a jquery object on which the FSM applies. * ATTENTION : the property 'id' needs to be defined to have the machine working properly * @param options - an object defining the options: * - boolean debug * - integer LogLevel - * - 1 only errors displayed * - 2 - errors and warnings * - 3 - all * - boolean AlertError - send an alert box when error * - boolean startEvent - name of the starting event * - integer maxPushEvent - size of the push events array * - string logFSM - list of FSM names to follow on debug (ex: "FSM_1 FSM_4"). If void, then displays all machine messages */ var fsm_manager = window.fsm_manager = function (anObject, aStateDefinition, options) { var $defaults = { debug : true, LogLevel : 1, AlertError : false, maxPushEvent : 100, startEvent : 'start', prefixFsmName : 'FSM_', logFSM : "" }; nb_FSM = nb_FSM+1; // on charge les options passées en paramètre if (options == undefined) options=null; this.opts = jQuery.extend( {}, $defaults, options || {}); /** * @param string FSMName - name of the FSM */ this.FSMName = this.opts.prefixFsmName+nb_FSM; /** * @param Object _stateDefinition - the definition of the states of the FSM */ this._stateDefinition = jQuery.extend(true, {}, aStateDefinition); this._originalstateDefinition = aStateDefinition;//keep the original definition here /** * currentState - current state of the fsm * */ this.currentState = ''; /** * lastState - previous state of the current state */ this.lastState=this.currentState; /** * @param currentEvent - current event processed by the fsm * */ this.currentEvent = ''; /** * @param pushStateList array - a state list pushed that can be poped * */ this.pushStateList = new Array(); /** * @param processEventStatus - status of the process event execution * - idle : not working * - processing : is processing an event * */ this.processEventStatus = 'idle'; /** * @param pushEventList array - an event list waiting to be processed * */ this.pushEventList = new Array(); /** * @param myUIObject - Target object of the FSM */ this.myUIObject = anObject; /** * @param listEvents - array of the events subscribed */ this.listEvents ={}; /** * @param currentDataEvent - the data of the event currently processed */ this.currentDataEvent ={}; /** * @param returnGeneralEventStatus - status to send back to the event triggering (if false, generally will prevent event propagation) */ this.returnGeneralEventStatus = true; /** * @param rootMachine - root FSM machine of this current FSM */ if (this.opts.rootMachine == undefined) this.opts.rootMachine = this; this.rootMachine = this.opts.rootMachine; /** * @param parentMachine - parent machine of this current one */ if (this.opts.nextParent == undefined) this.parentMachine = null; else this.parentMachine = this.opts.nextParent; this.opts.nextParent = this; // /** * @param childrenMachine - list of children of the current machine */ this.childrenMachine = new Array(); if (this.parentMachine) this.parentMachine.childrenMachine.push(this);//update the children of the parent if any /** * preventCancelId to manage the preventcancel by creating a unique id for doTimeout * @type {Number} */ this.preventCancelId=0; var aState; var aEvent; var theEvents =''; var space =''; var theTarget=$(document); var attrChangeRequested = false; var attrStyleChangeRequested = false; var attrChangeEvents = new Array(); var setStart = false; // look for all the defined and unique events // as we're on it, copy the synonym states and events for(aState in this._stateDefinition) { if (typeof this._stateDefinition[aState] == 'string') //the state is synomym to another... so copy it this._stateDefinition[aState]=this._stateDefinition[this._stateDefinition[aState]]; for(aEvent in this._stateDefinition[aState]) { //to process synonymous events if (typeof this._stateDefinition[aState][aEvent] == 'string') this._stateDefinition[aState][aEvent] = this._stateDefinition[aState][this._stateDefinition[aState][aEvent]]; // filter to the events list not subscribed by the root machine // start is always sent to the current machine if (!this.rootMachine.listEvents[aEvent] && aEvent != 'delegate_machines' && aEvent != this.opts.startEvent) { this.listEvents[aEvent]=aEvent; if (this != this.rootMachine) this.rootMachine.listEvents[aEvent]=aEvent; } else if (aEvent == this.opts.startEvent) setStart= true; } } //list all defined events in the FSM in a $.on format var splitevent=''; for(aEvent in this.listEvents) { splitevent = aEvent.split('_'); if ( splitevent[0] == 'attrchange' ) { attrChangeRequested=true; attrChangeEvents.push(aEvent); if ( (splitevent[1] == 'style') && (splitevent.length > 2) ) attrStyleChangeRequested=true; } theEvents=theEvents+space+aEvent; space=' '; } //define a selector object if none defined if ( (!anObject.selector) && (anObject.attr('id') && (anObject.attr('id') != 'iFSMDocumentRoot')) ) anObject.selector='#'+anObject.attr('id');// set to the #id else anObject.selector=''; //define the triggers for attrchange if ( attrChangeRequested && (anObject.selector) ) { var aStyleListNew; var aStyleCssListNew={}; var aStyleListOld; var aStyleCssListOld={}; var splitres; var i; var aCssStyle; $(anObject.selector).attrchange({ trackValues: true, // Default to false, if set to true the event object is updated with old and new value. callback: function (event) { //event - event object //event.attributeName - Name of the attribute modified //event.oldValue - Previous value of the modified attribute //event.newValue - New value of the modified attribute //Triggered when the selected elements attribute is added/updated/removed //send trigger event if an attribute change... if (jQuery.inArray('attrchange', attrChangeEvents)>=0) $(this).trigger('attrchange',event); //send trigger event on attribute changes if (jQuery.inArray('attrchange'+'_'+event.attributeName, attrChangeEvents)>=0) $(this).trigger('attrchange'+'_'+event.attributeName,{newValue:event.newValue,oldValue:event.oldValue}); //send trigger for the style changes if ( (event.attributeName == 'style') && attrStyleChangeRequested) { if (event.newValue) aStyleListNew=event.newValue.split(';'); else aStyleListNew = []; if (event.oldValue) aStyleListOld=event.oldValue.split(';'); else aStyleListOld = []; for(i= 0; i < aStyleListNew.length; i++) { splitres=aStyleListNew[i].split(':'); if (splitres.length==2) aStyleCssListNew[splitres[0]]=splitres[1].replace(/^\s+/g,'').replace(/\s+$/g,''); } for(i= 0; i < aStyleListOld.length; i++) { splitres=aStyleListOld[i].split(':'); if (splitres.length==2) aStyleCssListOld[splitres[0]]=splitres[1].replace(/^\s+/g,'').replace(/\s+$/g,''); } for(aCssStyle in aStyleCssListNew) { if (aStyleCssListOld[aCssStyle] == undefined || (aStyleCssListOld[aCssStyle] && aStyleCssListOld[aCssStyle] != aStyleCssListNew[aCssStyle]) ) $(this).trigger('attrchange_style_'+fsm_manager_getcss3prop(aCssStyle),{newValue:aStyleCssListNew[aCssStyle],oldValue:aStyleCssListOld[aCssStyle]}); } }//end if }//end callback }); //end attrchange selector }//end if //if target object not a document one if ($.isWindow(anObject[0])) theTarget = $(window); //activate the listening of events on object/FSM var myFSM=this.rootMachine; if (theEvents!='') theTarget.on(theEvents, anObject.selector, function( event, dataevent ) { myFSM.returnGeneralEventStatus=true; myFSM.processEvent(event.type,arguments); return myFSM.returnGeneralEventStatus; }); //activate the start event if defined if (setStart) { var myLocalFSM = this; theTarget.on(this.opts.startEvent, anObject.selector, function( event, dataevent ) { myLocalFSM.processEvent(event.type,arguments); }); } this._log('new fsm_manager:'+this.FSMName+'-'+anObject.selector,2); };//fsm_manager /*available functions*/ /** * InitManager - init the One Page Fonction State machine * - a 'start' event is triggered. * public method * @aInitState - the init state at start */ fsm_manager.prototype.InitManager = function(aInitState) { this._log('InitManager'); if (aInitState==undefined) this.currentState = 'DefaultState'; else this.currentState = aInitState; if (this._stateDefinition['DefaultState']==undefined) this._stateDefinition.DefaultState={}; //send 'start' event if (!this.parentMachine) this.trigger(this.opts.startEvent); else //directly talk to the sub machine to process the start { var anEv = new Array(); anEv[0] = fsm_manager_create_event(this.myUIObject,this.opts.startEvent,null); this.processEvent(this.opts.startEvent,anEv,true); } };// /** * procesEvent - process an event according to the current state * public Method * @param string anEvent : an event name * @param Array data : arguments sent by the triggering event [event, dataevent] * @param boolean forceProcess: force event to be processed * @return false to stop propagation * @comment * it is possible to pass the FSM Object to test the target through 'data' : data[data.length-1].targetFSM * ex : the function this.trigger always send the FSM through this way; */ fsm_manager.prototype.processEvent= function(anEvent,data,forceProcess) { var thisFSM = this; var currentState = this.currentState; var currentEvent = this.currentUIEvent = data[0]; this.receivedEvent = anEvent; this.currentDataEvent = data; this.currentEvent = currentEvent; var currentStateEvent = this.currentState; var doForceProcess = (forceProcess==undefined?false:true); var anEv = new Array();//dummy event to be used in 'processEvent' function (see processing of enterEvent and exitEvent) anEv[0] = fsm_manager_create_event(this.myUIObject,'',null); // to use it, just change anEv[0].type='an_event_name'; anEv[1] = data[1]; anEv[2]= data[2]; this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> START Processing Event',3,1); //targetFSM is sent through the this.trigger function //we consider the sub FSMs of a machine as the same machine... //except if the event was triggered as addressing only the "localmachine" if (data.length > 1 && data[(data.length - 1)] != null && data[data.length-1].targetFSM && (data[data.length-1].targetFSM != this) && ( (data[data.length-1].localMachine) || (data[data.length-1].targetFSM.rootMachine != this.rootMachine) ) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> not for the current machine ---> exit',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; //not for this machine... } if (data.length > 1 && data[(data.length - 1)] != null && data[data.length-1].localMachine) this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> process event locally',3); // submachine needs to be active only if in the correct parent state where it was defined if (this.subMachineName) { if ( (!this.parentMachine._stateDefinition[this.parentMachine.currentState].delegate_machines) || ( (this.parentMachine._stateDefinition[this.parentMachine.currentState].delegate_machines) && (!this.parentMachine._stateDefinition[this.parentMachine.currentState].delegate_machines[this.subMachineName]) ) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> submachine can\'t run if the parent state is not active for the submachine ---> exit',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; //not in the correct parent state } } if (this._stateDefinition[currentState]==undefined) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> currentState does not exist!',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } if ( ( anEvent == 'enterState' ) || ( anEvent == 'exitState' ) ) doForceProcess = true; //element is not a right target...? if ( !this.myUIObject.is(currentEvent.currentTarget) && !this.myUIObject.is(currentEvent.target) && (this.myUIObject[0] != document) && !$.isWindow(currentEvent.currentTarget) && !$.isWindow(currentEvent.target) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> object not a good target ---> '+$(currentEvent.currentTarget).attr('id')+'---> exit',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } else { if (this.myUIObject[0] == document) this.actualTarget=$(document); else this.actualTarget = $(currentEvent.currentTarget); } var currentEventConfiguration = null; currentEventConfiguration = this._stateDefinition[currentState][anEvent]; //if we are still processing we push the event, except if explicitly asked otherwise //see if we should push the event if ( doForceProcess == false && this.processEventStatus != 'idle' && ( currentEventConfiguration == undefined || ( currentEventConfiguration && currentEventConfiguration.how_process_event == undefined ) || ( currentEventConfiguration && currentEventConfiguration.how_process_event && currentEventConfiguration.how_process_event.immediate == undefined && currentEventConfiguration.how_process_event.delay == undefined ) ) ) { this.pushEvent(anEvent,data); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event pushed (lastevent)---> '+this.lastevent+' --> exit',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } this.lastevent=currentState+'-'+anEvent;//unused...mainly for debug... /* * Processing of the sub machines */ if ( ( this._stateDefinition[currentState].delegate_machines ) ) { var aSubMachineDefinition; for(aSubMachine in this._stateDefinition[currentState].delegate_machines) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> delegate to submachine---> '+aSubMachine,3); aSubMachineDefinition = this._stateDefinition[currentState].delegate_machines[aSubMachine]; //initialize the sub machines if needed if (aSubMachineDefinition.myFSM == undefined) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> process submachine ---> create FSM for the submachine '+aSubMachine,3); this._stateDefinition[currentState].delegate_machines[aSubMachine].myFSM = new fsm_manager(this.myUIObject,aSubMachineDefinition.submachine,this.opts); //create the machine this._stateDefinition[currentState].delegate_machines[aSubMachine].myFSM.opts.FSMParent=this; this._stateDefinition[currentState].delegate_machines[aSubMachine].myFSM.subMachineName=aSubMachine; } if ( anEvent == 'enterState' ) { if ( (aSubMachineDefinition.myFSM.currentState =='')//never initialised if =='' || (aSubMachineDefinition.no_reinitialisation == undefined) || (aSubMachineDefinition.no_reinitialisation == false) ) { aSubMachineDefinition.myFSM.InitManager();//reinit the sub machine } } else if ( anEvent == 'exitState' ) { anEv[0].type='exitMachine'; aSubMachineDefinition.myFSM.processEvent('exitMachine',anEv,true);//stop the sub machine //we cancel any waiting events on the state aSubMachineDefinition.myFSM.cancelDelayedProcess(); } // process event except on the enterState and exitState events that are not to be delegated... or if we dont propagate the event amongst the different machines else { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> process submachine (event)---> '+aSubMachine+'('+anEvent+') Start',3); aSubMachineDefinition.myFSM.processEvent(anEvent,data); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> process submachine (event)---> '+aSubMachine+'('+anEvent+') Processed',3); //current state changed during the process of the submachine...? //not sure it is a normal behaviour... if it occurs, we exit... if (currentState != this.currentState) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> process submachine changed the current environment (event)',3); this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } if ( ( aSubMachineDefinition.myFSM._stateDefinition[aSubMachineDefinition.myFSM.currentState][anEvent] && aSubMachineDefinition.myFSM._stateDefinition[aSubMachineDefinition.myFSM.currentState][anEvent].prevent_bubble ) || ( aSubMachineDefinition.myFSM._stateDefinition.DefaultState[anEvent] && aSubMachineDefinition.myFSM._stateDefinition.DefaultState[anEvent].prevent_bubble ) || (anEvent == this.opts.startEvent) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> submachine processed and exit due to prevent_bubble directive',3); this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> submachine exit',3); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } } } } /* * Processing of the current event on the current state */ //is the event to be processed? if ( (currentState=='DefaultState') || (currentEventConfiguration == undefined) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> take Event '+anEvent+' configuration from DefaultState (not present in current state)',3); currentEventConfiguration = this._stateDefinition.DefaultState[anEvent]; if (currentEventConfiguration == undefined) { if ( ['start','enterState','exitState','exitMachine'].indexOf(anEvent) < 0) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> take Event '+anEvent+' configuration from catchEvent',3); currentEventConfiguration = this._stateDefinition.DefaultState["catchEvent"]; //we create the dummy event in the default state if it does not exist for the catchall if (!this._stateDefinition['DefaultState'][anEvent]) this._stateDefinition['DefaultState'][anEvent]=jQuery.extend( true,{}, currentEventConfiguration); } } if (currentEventConfiguration == undefined) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event '+anEvent+' does not exist and no catchEvent--> will exit',3); this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } currentStateEvent = 'DefaultState'; } //is this a potential Pop State? if (currentEventConfiguration.pushpop_state && (currentEventConfiguration.pushpop_state =='PopState') && (this.pushStateList.length > 0) ) currentEventConfiguration.next_state = this.pushStateList[this.pushStateList.length-1]; if (currentEventConfiguration.pushpop_state_if_error && (currentEventConfiguration.pushpop_state_if_error =='PopState') && (this.pushStateList.length > 0) ) currentEventConfiguration.next_state_if_error = this.pushStateList[this.pushStateList.length-1]; //look if event is processed when coming from any target if ( !this.myUIObject.is(currentEvent.target) && (this.myUIObject[0] != document) && !$.isWindow(currentEvent.currentTarget) && !$.isWindow(currentEvent.target) && currentEventConfiguration.process_on_UItarget && currentEventConfiguration.process_on_UItarget == true ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event '+anEvent+' does not address the correct UI target (see process_on_UItarget) will exit',3); this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } //look if we let the bubbling of UI event on this event // no bubbling by default if ( /*(this.myUIObject[0] != document) &&*/ (!currentEventConfiguration.UI_event_bubble || currentEventConfiguration.UI_event_bubble==false ) ) { currentEvent.stopPropagation(); this.returnGeneralEventStatus=false; this.rootMachine.returnGeneralEventStatus=false;//the UI events always call the root machine } //is it a delayed event? if ( doForceProcess == false && currentEventConfiguration && currentEventConfiguration.how_process_event && currentEventConfiguration.how_process_event.delay ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event '+anEvent+' delayed will exit',3); this.delayProcess(anEvent, currentEventConfiguration.how_process_event.delay, data); this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } //compute EventIteration if (this._stateDefinition[currentStateEvent][anEvent].EventIteration == undefined) this._stateDefinition[currentStateEvent][anEvent].EventIteration =0; this._stateDefinition[currentStateEvent][anEvent].EventIteration++; this.EventIteration = this._stateDefinition[currentStateEvent][anEvent].EventIteration; //verify if the event can be processed according to 'enter' condition if ( (currentEventConfiguration.process_event_if) && (eval(currentEventConfiguration.process_event_if) == false) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event '+anEvent+' event not allowed to process (process_event_if rule is not OK) will exit',3); if (currentEventConfiguration.propagate_event_on_refused) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Event '+currentEventConfiguration.propagate_event_on_refused+' triggered on exit because of propagate_event_on_refused',3); this.trigger(currentEventConfiguration.propagate_event_on_refused,null,currentEventConfiguration.propagate_event_on_localmachine); } //exit as not accepted... this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); return; } //********************************************* //ok we will really process this event... //********************************************* this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> currently processing',2); var lastprocessEventStatus = this.processEventStatus; this.processEventStatus = 'processing'; var funcReturn = true; var localdata; //clear any preventCancel tag if (data && data[1] && data[1].preventCancelSet) delete(data[1].preventCancelSet); //call to the action (init_function) before transition state if (currentEventConfiguration.init_function) { localdata = [].slice.call(data); localdata.unshift(currentEventConfiguration.properties_init_function); funcReturn= currentEventConfiguration.init_function.apply(this,localdata); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> init_function done',3); } /* * Process the transition of state * */ // do we change state? //is this a Push/Pop State? if (funcReturn != false)//it's ok for next_state { if (currentEventConfiguration.pushpop_state) { switch(currentEventConfiguration.pushpop_state) { case 'PushState': this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Push state to state '+this.currentState,3); this.pushStateList.push(this.currentState); //do not use currentStateEvent! break; case 'PopState': if (this.pushStateList.length > 0) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Pop state to state '+currentEventConfiguration.next_state,3); this.pushStateList.pop(); } break; } } } //do we change of state? if ( (funcReturn != false) && (currentEventConfiguration.next_state) && (currentState != currentEventConfiguration.next_state) && ( ( (currentEventConfiguration.next_state_when == undefined) && (currentEventConfiguration.next_state_on_target == undefined) ) || ( (currentEventConfiguration.next_state_when) && (eval(currentEventConfiguration.next_state_when) == true) ) || ( (currentEventConfiguration.next_state_on_target) && (this.subMachinesRespectTargets(anEvent) == true) ) ) ) { //we reinit the iteration on the events $.each(this._stateDefinition[this.currentState], function(aKey,aValue) { if (!thisFSM._stateDefinition[thisFSM.currentState][aKey]) { thisFSM._log(aKey+" does not exist in state "+thisFSM.currentState,1); } else if (aKey!='delegate_machines') thisFSM._stateDefinition[thisFSM.currentState][aKey].EventIteration =0; }); //we cancel any waiting events on the state this.cancelDelayedProcess(); anEv[0].type='exitState'; //we alert that we're exiting the state (except if we're starting the machine... if (anEvent !=this.opts.startEvent) { this.processEvent('exitState',anEv,true); } /* ********************************************* * we change the current state Here! ********************************************* */ this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Go to (see next_state) '+currentEventConfiguration.next_state,3); this.lastState=this.currentState; this.currentState = currentEventConfiguration.next_state; //********************************************* //and now we're entering the new state //********************************************* anEv[0].type='enterState'; this.processEvent('enterState',anEv,true); //propagate event if asked //propagate event(s) if (currentEventConfiguration.propagate_event != undefined) { if (!(currentEventConfiguration.propagate_event instanceof Array)) currentEventConfiguration.propagate_event=[currentEventConfiguration.propagate_event]; $.each(currentEventConfiguration.propagate_event, function(aKey,aPropagateEvent) { thisFSM._log('processEvent: '+thisFSM.FSMName+':'+currentState+':'+anEvent+'-> trigger event (see propagate_event) ---> '+aPropagateEvent,3); if (aPropagateEvent === true) thisFSM.trigger( anEvent, data[1],currentEventConfiguration.propagate_event_on_localmachine); else thisFSM.trigger( aPropagateEvent, data[1],currentEventConfiguration.propagate_event_on_localmachine); }); } } /* * we stay in the same state? * so process the event propagation */ else if ( (funcReturn != false) && (currentEventConfiguration.propagate_event != undefined) ) { if (!(currentEventConfiguration.propagate_event instanceof Array)) currentEventConfiguration.propagate_event=[currentEventConfiguration.propagate_event]; //propagate event(s) $.each(currentEventConfiguration.propagate_event, function(aKey,aPropagateEvent) { thisFSM._log('processEvent: '+thisFSM.FSMName+':'+currentState+':'+anEvent+'-> trigger event (see propagate_event) ---> '+aPropagateEvent,3); if (aPropagateEvent === true) thisFSM.trigger( anEvent, data[1], currentEventConfiguration.propagate_event_on_localmachine); else thisFSM.trigger( aPropagateEvent, data[1], currentEventConfiguration.propagate_event_on_localmachine); }); } /* * oups there was an error during the processing of the action */ else if ( (funcReturn == false) && (currentEventConfiguration.next_state_if_error) ) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> error in init_function',3); //is this a Push/Pop State? if (currentEventConfiguration.pushpop_state_if_error) { switch(currentEventConfiguration.pushpop_state_if_error) { case 'PushState': this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Push state to state '+this.currentState,3); this.pushStateList.push(this.currentState); //do not use currentStateEvent! break; case 'PopState': if (this.pushStateList.length > 0) { this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Pop state to state '+currentEventConfiguration.next_state_if_error,3); this.pushStateList.pop(); } break; } } /* ********************************************* * we change the current state Here! ********************************************* */ this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> Go to (see next_state_if_error) '+currentEventConfiguration.next_state_if_error,3); this.lastState=this.currentState; this.currentState = currentEventConfiguration.next_state_if_error; //and now that we're entering the new state anEv[0].type='enterState'; this.processEvent('enterState',anEv,true); } else { //nothing to do? this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> nothing to do',3); } // do the exit action if (currentEventConfiguration.out_function) { localdata = [].slice.call(data); localdata.unshift(currentEventConfiguration.properties_out_function); funcReturn= currentEventConfiguration.out_function.apply(this,localdata); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> out_function done',3); } this.processEventStatus = lastprocessEventStatus; //we globally finished the job... this.cleanExitProcess(); this._log('processEvent: '+this.FSMName+':'+currentState+':'+anEvent+'-> EXIT PROCESS',3,-1); };//end of processEvent /* * cleanExitProcess - clean for exit the processing of an event */ fsm_manager.prototype.cleanExitProcess = function(anEvent,data) { // processing lasting events //we don't process the events if we were currently on an immediate event... if ( (this.pushEventList.length) && (this.processEventStatus == 'idle' || this.pushEventList.length>this.opts.maxPushEvent) ) { this.popEvent(); } }; /** * pushEvent - push an event in the flow of the processing of an event * public Method * @param anEvent : an event name * @param data : {event, data} * if data is not an array, we consider that it is an external pushed event...then create one from anEvent and data. */ fsm_manager.prototype.pushEvent = function(anEvent,data) { this._log('pushEvent: ---> '+anEvent); if (this.pushEventList.length>this.opts.maxPushEvent) { this._log('pushEvent: too much events... ---> '+this.pushEventList.length,2); return; } if ( (data == undefined) || (data.length == 0) || data[0].type == undefined ) { var datatmp = new Array(); datatmp[0] = fsm_manager_create_event(this.myUIObject,anEvent); datatmp[1] = data; data = datatmp; } var anEventToPush = {anEvent:anEvent, data:data}; this.pushEventList.push( anEventToPush ); this._log('pushEvent: push nb event ---> '+this.pushEventList.length); }; /* * popEvent - pop an event and process it * private Method * we process event in a FIFO order */ fsm_manager.prototype.popEvent = function() { this._log('popEvent'); if (this.pushEventList.length > 0) { anEventToProcess = this.pushEventList.shift(); this._log('popEvent:'+anEventToProcess.anEvent,3); if (anEventToProcess == undefined || anEventToProcess.anEvent == undefined) return false; this.processEvent(anEventToProcess.anEvent,anEventToProcess.data); return true; } else this._log('popEvent void list'); return false; };// /** * delayProcess - push an event after a delay * private Method * @param anEvent : an event name * @param aDelay : a delay to do the processing * @param data : {event, data} */ fsm_manager.prototype.delayProcess = function(anEvent, aDelay, data) { this._log('delayProcess: ---> '+anEvent); this.preventCancelId++; //let aHashData = this.hashCode(JSON.stringify(data[1])); let currentState = this.currentState; let aDelayedProcessName=this.myUIObject.attr('id')+currentState+anEvent+this.preventCancelId; if (!this._stateDefinition[this.currentState][anEvent])