1 // ========================================================================== 2 // Project: SC.Statechart - A Statechart Framework for SproutCore 3 // Copyright: ©2010, 2011 Michael Cohen, and contributors. 4 // Portions @2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /*globals SC */ 9 10 //@if(debug) 11 SC.TRACE_STATECHART_STYLE = { 12 init: 'font-style: italic; font-weight: bold;', // Initialization 13 action: 'color: #5922ab; font-style: italic; font-weight: bold;', // Actions and events 14 actionInfo: 'color: #5922ab; font-style: italic;', // Actions and events 15 route: 'color: #a67000; font-style: italic;', // Routing 16 gotoState: 'color: #479a48; font-style: italic; font-weight: bold;', // Goto state 17 gotoStateInfo: 'color: #479a48; font-style: italic;', // Goto state 18 enter: 'color: #479a48; font-style: italic; font-weight: bold;', // Entering 19 exit: 'color: #479a48; font-style: italic; font-weight: bold;' // Exiting 20 }; 21 //@endif 22 23 /** 24 @class 25 26 Represents a state within a statechart. 27 28 The statechart actively manages all states belonging to it. When a state is created, 29 it immediately registers itself with it parent states. 30 31 You do not create an instance of a state itself. The statechart manager will go through its 32 state heirarchy and create the states itself. 33 34 For more information on using statecharts, see SC.StatechartManager. 35 36 @author Michael Cohen 37 @extends SC.Object 38 */ 39 SC.State = SC.Object.extend( 40 /** @lends SC.State.prototype */ { 41 42 //@if(debug) 43 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 44 45 /** 46 Indicates if this state should trace actions. Useful for debugging 47 purposes. Managed by the statechart. 48 49 @see SC.StatechartManager#trace 50 51 @type Boolean 52 */ 53 trace: function() { 54 var key = this.getPath('statechart.statechartTraceKey'); 55 return this.getPath('statechart.%@'.fmt(key)); 56 }.property().cacheable(), 57 58 /** @private */ 59 _statechartTraceDidChange: function() { 60 this.notifyPropertyChange('trace'); 61 }, 62 63 /** 64 Used to log a state trace message 65 */ 66 stateLogTrace: function(msg, style) { 67 var sc = this.get('statechart'); 68 sc.statechartLogTrace(" %@: %@".fmt(this, msg), style); 69 }, 70 71 /* END DEBUG ONLY PROPERTIES AND METHODS */ 72 //@endif 73 74 /** 75 The name of the state 76 77 @type String 78 */ 79 name: null, 80 81 /** 82 This state's parent state. Managed by the statechart 83 84 @type State 85 */ 86 parentState: null, 87 88 /** 89 This state's history state. Can be null. Managed by the statechart. 90 91 @type State 92 */ 93 historyState: null, 94 95 /** 96 Used to indicate the initial substate of this state to enter into. 97 98 You assign the value with the name of the state. Upon creation of 99 the state, the statechart will automatically change the property 100 to be a corresponding state object 101 102 The substate is only to be this state's immediate substates. If 103 no initial substate is assigned then this states initial substate 104 will be an instance of an empty state (SC.EmptyState). 105 106 Note that a statechart's root state must always have an explicity 107 initial substate value assigned else an error will be thrown. 108 109 @property {String|State} 110 */ 111 initialSubstate: null, 112 113 /** 114 Used to indicates if this state's immediate substates are to be 115 concurrent (orthogonal) to each other. 116 117 @type Boolean 118 */ 119 substatesAreConcurrent: NO, 120 121 /** 122 The immediate substates of this state. Managed by the statechart. 123 124 @type Array 125 */ 126 substates: null, 127 128 /** 129 The statechart that this state belongs to. Assigned by the owning 130 statechart. 131 132 @type Statechart 133 */ 134 statechart: null, 135 136 /** 137 Indicates if this state has been initialized by the statechart 138 139 @propety {Boolean} 140 */ 141 stateIsInitialized: NO, 142 143 /** 144 An array of this state's current substates. Managed by the statechart 145 146 @propety {Array} 147 */ 148 currentSubstates: null, 149 150 /** 151 An array of this state's substates that are currently entered. Managed by 152 the statechart. 153 154 @type Array 155 */ 156 enteredSubstates: null, 157 158 /** 159 Can optionally assign what route this state is to represent. 160 161 If assigned then this state will be notified to handle the route when triggered 162 any time the app's location changes and matches this state's assigned route. 163 The handler invoked is this state's {@link #routeTriggered} method. 164 165 The value assigned to this property is dependent on the underlying routing 166 mechanism used by the application. The default routing mechanism is to use 167 SC.routes. 168 169 @property {String|Hash} 170 171 @see #routeTriggered 172 @see #location 173 @see SC.StatechartDelegate 174 */ 175 representRoute: null, 176 177 /** 178 Indicates who the owner is of this state. If not set on the statechart 179 then the owner is the statechart, otherwise it is the assigned 180 object. Managed by the statechart. 181 182 @see SC.StatechartManager#owner 183 184 @type SC.Object 185 */ 186 owner: function() { 187 var sc = this.get('statechart'), 188 key = sc ? sc.get('statechartOwnerKey') : null, 189 owner = sc ? sc.get(key) : null; 190 return owner ? owner : sc; 191 }.property().cacheable(), 192 193 /** 194 Returns the statechart's assigned delegate. A statechart delegate is one 195 that adheres to the {@link SC.StatechartDelegate} mixin. 196 197 @type SC.Object 198 199 @see SC.StatechartDelegate 200 */ 201 statechartDelegate: function() { 202 return this.getPath('statechart.statechartDelegate'); 203 }.property().cacheable(), 204 205 /** 206 A volatile property used to get and set the app's current location. 207 208 This computed property defers to the the statechart's delegate to 209 actually update and acquire the app's location. 210 211 Note: Binding for this particular case is discouraged since in most 212 cases we need the location value immediately. If we were to use 213 bindings then the location value wouldn't be updated until at least 214 the end of one run loop. It is also advised that the delegate not 215 have its `statechartUpdateLocationForState` and 216 `statechartAcquireLocationForState` methods implemented where bindings 217 are used since they will inadvertenly stall the location value from 218 propogating immediately. 219 220 @type String 221 222 @see SC.StatechartDelegate#statechartUpdateLocationForState 223 @see SC.StatechartDelegate#statechartAcquireLocationForState 224 */ 225 location: function(key, value) { 226 var sc = this.get('statechart'), 227 del = this.get('statechartDelegate'); 228 229 if (value !== undefined) { 230 del.statechartUpdateLocationForState(sc, value, this); 231 } 232 233 return del.statechartAcquireLocationForState(sc, this); 234 }.property().idempotent(), 235 236 init: function() { 237 sc_super(); 238 239 this._registeredEventHandlers = {}; 240 this._registeredStringEventHandlers = {}; 241 this._registeredRegExpEventHandlers = []; 242 this._registeredStateObserveHandlers = {}; 243 this._registeredSubstatePaths = {}; 244 this._registeredSubstates = []; 245 this._isEnteringState = NO; 246 this._isExitingState = NO; 247 248 // Setting up observes this way is faster then using .observes, 249 // which adds a noticable increase in initialization time. 250 251 var sc = this.get('statechart'), 252 ownerKey = sc ? sc.get('statechartOwnerKey') : null; 253 254 if (sc) { 255 sc.addObserver(ownerKey, this, '_statechartOwnerDidChange'); 256 257 //@if(debug) 258 var traceKey = sc ? sc.get('statechartTraceKey') : null; 259 sc.addObserver(traceKey, this, '_statechartTraceDidChange'); 260 //@endif 261 } 262 }, 263 264 destroy: function() { 265 var sc = this.get('statechart'), 266 ownerKey = sc ? sc.get('statechartOwnerKey') : null; 267 268 if (sc) { 269 sc.removeObserver(ownerKey, this, '_statechartOwnerDidChange'); 270 271 //@if(debug) 272 var traceKey = sc ? sc.get('statechartTraceKey') : null; 273 sc.removeObserver(traceKey, this, '_statechartTraceDidChange'); 274 //@endif 275 } 276 277 var substates = this.get('substates'); 278 if (substates) { 279 substates.forEach(function(state) { 280 state.destroy(); 281 }); 282 } 283 284 this._teardownAllStateObserveHandlers(); 285 286 this.set('substates', null); 287 this.set('currentSubstates', null); 288 this.set('enteredSubstates', null); 289 this.set('parentState', null); 290 this.set('historyState', null); 291 this.set('initialSubstate', null); 292 this.set('statechart', null); 293 294 //@if(debug) 295 this.notifyPropertyChange('trace'); 296 //@endif 297 298 this.notifyPropertyChange('owner'); 299 300 this._registeredEventHandlers = null; 301 this._registeredStringEventHandlers = null; 302 this._registeredRegExpEventHandlers = null; 303 this._registeredStateObserveHandlers = null; 304 this._registeredSubstatePaths = null; 305 this._registeredSubstates = null; 306 307 sc_super(); 308 }, 309 310 /** 311 Used to initialize this state. To only be called by the owning statechart. 312 */ 313 initState: function() { 314 if (this.get('stateIsInitialized')) return; 315 316 this._registerWithParentStates(); 317 this._setupRouteHandling(); 318 319 var key = null, 320 value = null, 321 state = null, 322 substates = [], 323 matchedInitialSubstate = NO, 324 initialSubstate = this.get('initialSubstate'), 325 substatesAreConcurrent = this.get('substatesAreConcurrent'), 326 valueIsFunc = NO, 327 historyState = null; 328 329 this.set('substates', substates); 330 331 if (SC.kindOf(initialSubstate, SC.HistoryState) && initialSubstate.isClass) { 332 historyState = this.createSubstate(initialSubstate); 333 this.set('initialSubstate', historyState); 334 335 if (SC.none(historyState.get('defaultState'))) { 336 this.stateLogError("Initial substate is invalid. History state requires the name of a default state to be set"); 337 this.set('initialSubstate', null); 338 historyState = null; 339 } 340 } 341 342 // Iterate through all this state's substates, if any, create them, and then initialize 343 // them. This causes a recursive process. 344 for (key in this) { 345 value = this[key]; 346 valueIsFunc = SC.typeOf(value) === SC.T_FUNCTION; 347 348 if (valueIsFunc && value.isEventHandler) { 349 this._registerEventHandler(key, value); 350 continue; 351 } 352 353 if (valueIsFunc && value.isStateObserveHandler) { 354 this._registerStateObserveHandler(key, value); 355 continue; 356 } 357 358 if (valueIsFunc && value.statePlugin) { 359 value = value.apply(this); 360 } 361 362 if (SC.kindOf(value, SC.State) && value.isClass && this[key] !== this.constructor) { 363 state = this._addSubstate(key, value); 364 if (key === initialSubstate) { 365 this.set('initialSubstate', state); 366 matchedInitialSubstate = YES; 367 } else if (historyState && historyState.get('defaultState') === key) { 368 historyState.set('defaultState', state); 369 matchedInitialSubstate = YES; 370 } 371 } 372 } 373 374 if (!SC.none(initialSubstate) && !matchedInitialSubstate) { 375 this.stateLogError("Unable to set initial substate %@ since it did not match any of state's %@ substates".fmt(initialSubstate, this)); 376 } 377 378 if (substates.length === 0) { 379 if (!SC.none(initialSubstate)) { 380 this.stateLogWarning("Unable to make %@ an initial substate since state %@ has no substates".fmt(initialSubstate, this)); 381 } 382 } 383 else if (substates.length > 0) { 384 state = this._addEmptyInitialSubstateIfNeeded(); 385 if (!state && initialSubstate && substatesAreConcurrent) { 386 this.set('initialSubstate', null); 387 this.stateLogWarning("Can not use %@ as initial substate since substates are all concurrent for state %@".fmt(initialSubstate, this)); 388 } 389 } 390 391 this.notifyPropertyChange('substates'); 392 this.set('currentSubstates', []); 393 this.set('enteredSubstates', []); 394 this.set('stateIsInitialized', YES); 395 }, 396 397 /** @private 398 399 Used to bind this state with a route this state is to represent if a route has been assigned. 400 401 When invoked, the method will delegate the actual binding strategy to the statechart delegate 402 via the delegate's {@link SC.StatechartDelegate#statechartBindStateToRoute} method. 403 404 Note that a state cannot be bound to a route if this state is a concurrent state. 405 406 @see #representRoute 407 @see SC.StatechartDelegate#statechartBindStateToRoute 408 */ 409 _setupRouteHandling: function() { 410 var route = this.get('representRoute'), 411 sc = this.get('statechart'), 412 del = this.get('statechartDelegate'); 413 414 if (SC.none(route)) return; 415 416 if (this.get('isConcurrentState')) { 417 this.stateLogError("State %@ cannot handle route '%@' since state is concurrent".fmt(this, route)); 418 return; 419 } 420 421 del.statechartBindStateToRoute(sc, this, route, this.routeTriggered); 422 }, 423 424 /** 425 Main handler that gets triggered whenever the app's location matches this state's assigned 426 route. 427 428 When invoked the handler will first refer to the statechart delegate to determine if it 429 should actually handle the route via the delegate's 430 {@see SC.StatechartDelegate#statechartShouldStateHandleTriggeredRoute} method. If the 431 delegate allows the handling of the route then the state will continue on with handling 432 the triggered route by calling the state's {@link #handleTriggeredRoute} method, otherwise 433 the state will cancel the handling and inform the delegate through the delegate's 434 {@see SC.StatechartDelegate#statechartStateCancelledHandlingRoute} method. 435 436 The handler will create a state route context ({@link SC.StateRouteContext}) object 437 that packages information about what is being currently handled. This context object gets 438 passed along to the delegate's invoked methods as well as the state transition process. 439 440 Note that this method is not intended to be directly called or overridden. 441 442 @see #representRoute 443 @see SC.StatechartDelegate#statechartShouldStateHandleRoute 444 @see SC.StatechartDelegate#statechartStateCancelledHandlingRoute 445 @see #createStateRouteHandlerContext 446 @see #handleTriggeredRoute 447 */ 448 routeTriggered: function(params) { 449 if (this._isEnteringState) return; 450 451 var sc = this.get('statechart'), 452 del = this.get('statechartDelegate'), 453 loc = this.get('location'); 454 455 var attr = { 456 state: this, 457 location: loc, 458 params: params, 459 handler: this.routeTriggered 460 }; 461 462 var context = this.createStateRouteHandlerContext(attr); 463 464 if (del.statechartShouldStateHandleTriggeredRoute(sc, this, context)) { 465 //@if(debug) 466 if (this.get('trace') && loc) { 467 this.stateLogTrace("will handle route '%@'".fmt(loc), SC.TRACE_STATECHART_STYLE.route); 468 } 469 //@endif 470 this.handleTriggeredRoute(context); 471 } else { 472 del.statechartStateCancelledHandlingTriggeredRoute(sc, this, context); 473 } 474 }, 475 476 /** 477 Constructs a new instance of a state routing context object. 478 479 @param {Hash} attr attributes to apply to the constructed object 480 @return {SC.StateRouteContext} 481 482 @see #handleRoute 483 */ 484 createStateRouteHandlerContext: function(attr) { 485 return SC.StateRouteHandlerContext.create(attr); 486 }, 487 488 /** 489 Invoked by this state's {@link #routeTriggered} method if the state is 490 actually allowed to handle the triggered route. 491 492 By default the method invokes a state transition to this state. 493 */ 494 handleTriggeredRoute: function(context) { 495 this.gotoState(this, context); 496 }, 497 498 /** @private */ 499 _addEmptyInitialSubstateIfNeeded: function() { 500 var initialSubstate = this.get('initialSubstate'), 501 substatesAreConcurrent = this.get('substatesAreConcurrent'); 502 503 if (initialSubstate || substatesAreConcurrent) return null; 504 505 var state = this.createSubstate(SC.EmptyState); 506 this.set('initialSubstate', state); 507 this.get('substates').push(state); 508 this[state.get('name')] = state; 509 state.initState(); 510 this.stateLogWarning("state %@ has no initial substate defined. Will default to using an empty state as initial substate".fmt(this)); 511 return state; 512 }, 513 514 /** @private */ 515 _addSubstate: function(name, state, attr) { 516 var substates = this.get('substates'); 517 518 attr = SC.clone(attr) || {}; 519 attr.name = name; 520 state = this.createSubstate(state, attr); 521 substates.push(state); 522 this[name] = state; 523 state.initState(); 524 return state; 525 }, 526 527 /** 528 Used to dynamically add a substate to this state. Once added successfully you 529 are then able to go to it from any other state within the owning statechart. 530 531 A couple of notes when adding a substate: 532 533 - If this state does not have any substates, then in addition to the 534 substate being added, an empty state will also be added and set as the 535 initial substate. To make the added substate the initial substate, set 536 this object's initialSubstate property. 537 538 - If this state is a current state, the added substate will not be entered. 539 540 - If this state is entered and its substates are concurrent, the added 541 substate will not be entered. 542 543 If this state is either entered or current and you'd like the added substate 544 to take affect, you will need to explicitly reenter this state by calling 545 its `reenter` method. 546 547 Be aware that the name of the state you are adding must not conflict with 548 the name of a property on this state or else you will get an error. 549 In addition, this state must be initialized to add substates. 550 551 @param {String} name a unique name for the given substate. 552 @param {SC.State} state a class that derives from `SC.State` 553 @param {Object} [attr] literal to be applied to the substate 554 @returns {SC.State} an instance of the given state class 555 */ 556 addSubstate: function(name, state, attr) { 557 if (SC.empty(name)) { 558 this.stateLogError("Can not add substate. name required"); 559 return null; 560 } 561 562 if (this[name] !== undefined) { 563 this.stateLogError("Can not add substate '%@'. Already a defined property".fmt(name)); 564 return null; 565 } 566 567 if (!this.get('stateIsInitialized')) { 568 this.stateLogError("Can not add substate '%@'. this state is not yet initialized".fmt(name)); 569 return null; 570 } 571 572 var len = arguments.length; 573 574 if (len === 1) { 575 state = SC.State; 576 } else if (len === 2 && SC.typeOf(state) === SC.T_HASH) { 577 attr = state; 578 state = SC.State; 579 } 580 581 var stateIsValid = SC.kindOf(state, SC.State) && state.isClass; 582 583 if (!stateIsValid) { 584 this.stateLogError("Can not add substate '%@'. must provide a state class".fmt(name)); 585 return null; 586 } 587 588 state = this._addSubstate(name, state, attr); 589 this._addEmptyInitialSubstateIfNeeded(); 590 this.notifyPropertyChange('substates'); 591 592 return state; 593 }, 594 595 /** 596 creates a substate for this state 597 */ 598 createSubstate: function(state, attr) { 599 attr = attr || {}; 600 return state.create({ 601 parentState: this, 602 statechart: this.get('statechart') 603 }, attr); 604 }, 605 606 /** @private 607 608 Registers event handlers with this state. Event handlers are special 609 functions on the state that are intended to handle more than one event. This 610 compared to basic functions that only respond to a single event that reflects 611 the name of the method. 612 */ 613 _registerEventHandler: function(name, handler) { 614 var events = handler.events, 615 event = null, 616 len = events.length, 617 i = 0; 618 619 this._registeredEventHandlers[name] = handler; 620 621 for (; i < len; i += 1) { 622 event = events[i]; 623 624 if (SC.typeOf(event) === SC.T_STRING) { 625 this._registeredStringEventHandlers[event] = { 626 name: name, 627 handler: handler 628 }; 629 continue; 630 } 631 632 if (event instanceof RegExp) { 633 this._registeredRegExpEventHandlers.push({ 634 name: name, 635 handler: handler, 636 regexp: event 637 }); 638 continue; 639 } 640 641 this.stateLogError("Invalid event %@ for event handler %@ in state %@".fmt(event, name, this)); 642 } 643 }, 644 645 /** @private 646 647 Registers state observe handlers with this state. State observe handlers behave just like 648 when you apply observes() on a method but will only be active when the state is currently 649 entered, otherwise the handlers are inactive until the next time the state is entered 650 */ 651 _registerStateObserveHandler: function(name, handler) { 652 var i = 0, 653 args = handler.args, 654 len = args.length, 655 arg, validHandlers = YES; 656 657 for (; i < len; i += 1) { 658 arg = args[i]; 659 if (SC.typeOf(arg) !== SC.T_STRING || SC.empty(arg)) { 660 this.stateLogError("Invalid argument %@ for state observe handler %@ in state %@".fmt(arg, name, this)); 661 validHandlers = NO; 662 } 663 } 664 665 if (!validHandlers) return; 666 667 this._registeredStateObserveHandlers[name] = handler.args; 668 }, 669 670 /** @private 671 Will traverse up through this state's parent states to register 672 this state with them. 673 */ 674 _registerWithParentStates: function() { 675 var parent = this.get('parentState'); 676 while (!SC.none(parent)) { 677 parent._registerSubstate(this); 678 parent = parent.get('parentState'); 679 } 680 }, 681 682 /** @private 683 Will register a given state as a substate of this state 684 */ 685 _registerSubstate: function(state) { 686 var path = state.pathRelativeTo(this); 687 if (SC.none(path)) return; 688 689 this._registeredSubstates.push(state); 690 691 // Keep track of states based on their relative path 692 // to this state. 693 var regPaths = this._registeredSubstatePaths; 694 if (regPaths[state.get('name')] === undefined) { 695 regPaths[state.get('name')] = { }; 696 } 697 698 var paths = regPaths[state.get('name')]; 699 paths[path] = state; 700 }, 701 702 /** 703 Will generate path for a given state that is relative to this state. It is 704 required that the given state is a substate of this state. 705 706 If the heirarchy of the given state to this state is the following: 707 A > B > C, where A is this state and C is the given state, then the 708 relative path generated will be "B.C" 709 */ 710 pathRelativeTo: function(state) { 711 var path = this.get('name'), 712 parent = this.get('parentState'); 713 714 while (!SC.none(parent) && parent !== state) { 715 path = "%@.%@".fmt(parent.get('name'), path); 716 parent = parent.get('parentState'); 717 } 718 719 if (parent !== state && state !== this) { 720 this.stateLogError('Can not generate relative path from %@ since it not a parent state of %@'.fmt(state, this)); 721 return null; 722 } 723 724 return path; 725 }, 726 727 /** 728 Used to get a substate of this state that matches a given value. 729 730 If the value is a state object, then the value will be returned if it is indeed 731 a substate of this state, otherwise null is returned. 732 733 If the given value is a string, then the string is assumed to be a path expression 734 to a substate. The value is then parsed to find the closest match. For path expression 735 syntax, refer to the {@link SC.StatePathMatcher} class. 736 737 If there is no match then null is returned. If there is more than one match then null 738 is return and an error is generated indicating ambiguity of the given value. 739 740 An optional callback can be provided to handle the scenario when either no 741 substate is found or there is more than one match. The callback is then given 742 the opportunity to further handle the outcome and return a result which the 743 getSubstate method will then return. The callback should have the following 744 signature: 745 746 function(state, value, paths) 747 748 - state: The state getState was invoked on 749 - value: The value supplied to getState 750 - paths: An array of substate paths that matched the given value 751 752 If there were no matches then `paths` is not provided to the callback. 753 754 You can also optionally provide a target that the callback is invoked on. If no 755 target is provided then this state is used as the target. 756 757 @param value {State|String} used to identify a substate of this state 758 @param [callback] {Function} the callback 759 @param [target] {Object} the target 760 */ 761 getSubstate: function(value, callback, target) { 762 if (!value) return null; 763 764 var valueType = SC.typeOf(value); 765 766 // If the value is an object then just check if the value is 767 // a registered substate of this state, and if so return it. 768 if (valueType === SC.T_OBJECT) { 769 return this._registeredSubstates.indexOf(value) > -1 ? value : null; 770 } 771 772 if (valueType !== SC.T_STRING) { 773 this.stateLogError("Can not find matching subtype. value must be an object or string: %@".fmt(value)); 774 return null; 775 } 776 777 var matcher = SC.StatePathMatcher.create({ state: this, expression: value }), 778 matches = [], key; 779 780 if (matcher.get('tokens').length === 0) return null; 781 782 var paths = this._registeredSubstatePaths[matcher.get('lastPart')]; 783 if (!paths) return this._notifySubstateNotFound(callback, target, value); 784 785 for (key in paths) { 786 if (matcher.match(key)) { 787 matches.push(paths[key]); 788 } 789 } 790 791 if (matches.length === 1) return matches[0]; 792 793 if (matches.length > 1) { 794 var keys = []; 795 for (key in paths) { keys.push(key); } 796 797 if (callback) return this._notifySubstateNotFound(callback, target, value, keys); 798 799 var msg = "Can not find substate matching '%@' in state %@. Ambiguous with the following: %@"; 800 this.stateLogError(msg.fmt(value, this.get('fullPath'), keys.join(', '))); 801 } 802 803 return this._notifySubstateNotFound(callback, target, value); 804 }, 805 806 /** @private */ 807 _notifySubstateNotFound: function(callback, target, value, keys) { 808 return callback ? callback.call(target || this, this, value, keys) : null; 809 }, 810 811 /** 812 Will attempt to get a state relative to this state. 813 814 A state is returned based on the following: 815 816 1. First check this state's substates for a match; and 817 2. If no matching substate then attempt to get the state from 818 this state's parent state. 819 820 Therefore states are recursively traversed up to the root state 821 to identify a match, and if found is ultimately returned, otherwise 822 null is returned. In the case that the value supplied is ambiguous 823 an error message is returned. 824 825 The value provided can either be a state object or a state path expression. 826 For path expression syntax, refer to the {@link SC.StatePathMatcher} class. 827 */ 828 getState: function(value) { 829 if (value === this.get('name')) return this; 830 if (SC.kindOf(value, SC.State)) return value; 831 return this.getSubstate(value, this._handleSubstateNotFound); 832 }, 833 834 /** @private */ 835 _handleSubstateNotFound: function(state, value, keys) { 836 var parentState = this.get('parentState'); 837 838 if (parentState) return parentState.getState(value); 839 840 if (keys) { 841 var msg = "Can not find state matching '%@'. Ambiguous with the following: %@"; 842 this.stateLogError(msg.fmt(value, keys.join(', '))); 843 } 844 845 return null; 846 }, 847 848 /** 849 Used to go to a state in the statechart either directly from this state if it is a current state, 850 or from the first relative current state from this state. 851 852 If the value given is a string then it is considered a state path expression. The path is then 853 used to find a state relative to this state based on rules of the {@link #getState} method. 854 855 @param value {SC.State|String} the state to go to 856 @param [context] {Hash|Object} context object that will be supplied to all states that are 857 exited and entered during the state transition process. Context can not be an instance of 858 SC.State. 859 */ 860 gotoState: function(value, context) { 861 var state = this.getState(value); 862 863 if (!state) { 864 var msg = "can not go to state %@ from state %@. Invalid value."; 865 this.stateLogError(msg.fmt(value, this)); 866 return; 867 } 868 869 var from = this.findFirstRelativeCurrentState(state); 870 this.get('statechart').gotoState(state, from, false, context); 871 }, 872 873 /** 874 Used to go to a given state's history state in the statechart either directly from this state if it 875 is a current state or from one of this state's current substates. 876 877 If the value given is a string then it is considered a state path expression. The path is then 878 used to find a state relative to this state based on rules of the {@link #getState} method. 879 880 Method can be called in the following ways: 881 882 // With one argument 883 gotoHistoryState(<value>) 884 885 // With two arguments 886 gotoHistoryState(<value>, <boolean | hash>) 887 888 // With three arguments 889 gotoHistoryState(<value>, <boolean>, <hash>) 890 891 Where <value> is either a string or a SC.State object and <hash> is a regular JS hash object. 892 893 @param value {SC.State|String} the state whose history state to go to 894 @param [recusive] {Boolean} indicates whether to follow history states recusively starting 895 from the given state 896 @param [context] {Hash|Object} context object that will be supplied to all states that are exited 897 entered during the state transition process. Context can not be an instance of SC.State. 898 */ 899 gotoHistoryState: function(value, recursive, context) { 900 var state = this.getState(value); 901 902 if (!state) { 903 var msg = "can not go to history state %@ from state %@. Invalid value."; 904 this.stateLogError(msg.fmt(value, this)); 905 return; 906 } 907 908 var from = this.findFirstRelativeCurrentState(state); 909 this.get('statechart').gotoHistoryState(state, from, recursive, context); 910 }, 911 912 /** 913 Resumes an active goto state transition process that has been suspended. 914 */ 915 resumeGotoState: function() { 916 this.get('statechart').resumeGotoState(); 917 }, 918 919 /** 920 Used to check if a given state is a current substate of this state. Mainly used in cases 921 when this state is a concurrent state. 922 923 @param state {State|String} either a state object or the name of a state 924 @returns {Boolean} true is the given state is a current substate, otherwise false is returned 925 */ 926 stateIsCurrentSubstate: function(state) { 927 if (SC.typeOf(state) === SC.T_STRING) state = this.get('statechart').getState(state); 928 var current = this.get('currentSubstates'); 929 return !!current && current.indexOf(state) >= 0; 930 }, 931 932 /** 933 Used to check if a given state is a substate of this state that is currently entered. 934 935 @param state {State|String} either a state object of the name of a state 936 @returns {Boolean} true if the given state is a entered substate, otherwise false is returned 937 */ 938 stateIsEnteredSubstate: function(state) { 939 if (SC.typeOf(state) === SC.T_STRING) state = this.get('statechart').getState(state); 940 var entered = this.get('enteredSubstates'); 941 return !!entered && entered.indexOf(state) >= 0; 942 }, 943 944 /** 945 Indicates if this state is the root state of the statechart. 946 947 @type Boolean 948 */ 949 isRootState: function() { 950 return this.getPath('statechart.rootState') === this; 951 }.property(), 952 953 /** 954 Indicates if this state is a current state of the statechart. 955 956 @type Boolean 957 */ 958 isCurrentState: function() { 959 return this.stateIsCurrentSubstate(this); 960 }.property('currentSubstates').cacheable(), 961 962 /** 963 Indicates if this state is a concurrent state 964 965 @type Boolean 966 */ 967 isConcurrentState: function() { 968 return this.getPath('parentState.substatesAreConcurrent'); 969 }.property(), 970 971 /** 972 Indicates if this state is a currently entered state. 973 974 A state is currently entered if during a state transition process the 975 state's enterState method was invoked, but only after its exitState method 976 was called, if at all. 977 */ 978 isEnteredState: function() { 979 return this.stateIsEnteredSubstate(this); 980 }.property('enteredSubstates').cacheable(), 981 982 /** 983 Indicate if this state has any substates 984 985 @propety {Boolean} 986 */ 987 hasSubstates: function() { 988 return this.getPath('substates.length') > 0; 989 }.property('substates'), 990 991 /** 992 Indicates if this state has any current substates 993 */ 994 hasCurrentSubstates: function() { 995 var current = this.get('currentSubstates'); 996 return !!current && current.get('length') > 0; 997 }.property('currentSubstates').cacheable(), 998 999 /** 1000 Indicates if this state has any currently entered substates 1001 */ 1002 hasEnteredSubstates: function() { 1003 var entered = this.get('enteredSubstates'); 1004 return !!entered && entered.get('length') > 0; 1005 }.property('enteredSubstates').cacheable(), 1006 1007 /** 1008 Will attempt to find a current state in the statechart that is relative to 1009 this state. 1010 1011 Ordered set of rules to find a relative current state: 1012 1013 1. If this state is a current state then it will be returned 1014 1015 2. If this state has no current states and this state has a parent state then 1016 return parent state's first relative current state, otherwise return null 1017 1018 3. If this state has more than one current state then use the given anchor state 1019 to get a corresponding substate that can be used to find a current state relative 1020 to the substate, if a substate was found. 1021 1022 4. If (3) did not find a relative current state then default to returning 1023 this state's first current substate. 1024 1025 @param anchor {State|String} Optional. a substate of this state used to help direct 1026 finding a current state 1027 @return {SC.State} a current state 1028 */ 1029 findFirstRelativeCurrentState: function(anchor) { 1030 if (this.get('isCurrentState')) return this; 1031 1032 var currentSubstates = this.get('currentSubstates') || [], 1033 numCurrent = currentSubstates.get('length'), 1034 parent = this.get('parentState'); 1035 1036 if (numCurrent === 0) { 1037 return parent ? parent.findFirstRelativeCurrentState() : null; 1038 } 1039 1040 if (numCurrent > 1) { 1041 anchor = this.getSubstate(anchor); 1042 if (anchor) return anchor.findFirstRelativeCurrentState(); 1043 } 1044 1045 return currentSubstates[0]; 1046 }, 1047 1048 /** 1049 Used to re-enter this state. Call this only when the state a current state of 1050 the statechart. 1051 */ 1052 reenter: function() { 1053 if (this.get('isEnteredState')) { 1054 this.gotoState(this); 1055 } else { 1056 SC.Logger.error('Can not re-enter state %@ since it is not an entered state in the statechart'.fmt(this)); 1057 } 1058 }, 1059 1060 /** 1061 Called by the statechart to allow a state to try and handle the given event. If the 1062 event is handled by the state then YES is returned, otherwise NO. 1063 1064 There is a particular order in how an event is handled by a state: 1065 1066 1. Basic function whose name matches the event 1067 2. Registered event handler that is associated with an event represented as a string 1068 3. Registered event handler that is associated with events matching a regular expression 1069 4. The unknownEvent function 1070 1071 Use of event handlers that are associated with events matching a regular expression may 1072 incur a performance hit, so they should be used sparingly. 1073 1074 The unknownEvent function is only invoked if the state has it, otherwise it is skipped. Note that 1075 you should be careful when using unknownEvent since it can be either abused or cause unexpected 1076 behavior. 1077 1078 Example of a state using all four event handling techniques: 1079 1080 SC.State.extend({ 1081 1082 // Basic function handling event 'foo' 1083 foo: function(arg1, arg2) { ... }, 1084 1085 // event handler that handles 'frozen' and 'canuck' 1086 eventHandlerA: function(event, arg1, arg2) { 1087 ... 1088 }.handleEvent('frozen', 'canuck'), 1089 1090 // event handler that handles events matching the regular expression /num\d/ 1091 // ex. num1, num2 1092 eventHandlerB: function(event, arg1, arg2) { 1093 ... 1094 }.handleEvent(/num\d/), 1095 1096 // Handle any event that was not handled by some other 1097 // method on the state 1098 unknownEvent: function(event, arg1, arg2) { 1099 1100 } 1101 1102 }); 1103 */ 1104 tryToHandleEvent: function(event, arg1, arg2) { 1105 //@if(debug) 1106 var trace = this.get('trace'); 1107 //@endif 1108 1109 var sc = this.get('statechart'), 1110 ret; 1111 1112 // First check if the name of the event is the same as a registered event handler. If so, 1113 // then do not handle the event. 1114 if (this._registeredEventHandlers[event]) { 1115 this.stateLogWarning("state %@ can not handle event '%@' since it is a registered event handler".fmt(this, event)); 1116 return NO; 1117 } 1118 1119 if (this._registeredStateObserveHandlers[event]) { 1120 this.stateLogWarning("state %@ can not handle event '%@' since it is a registered state observe handler".fmt(this, event)); 1121 return NO; 1122 } 1123 1124 // Now begin by trying a basic method on the state to respond to the event 1125 if (SC.typeOf(this[event]) === SC.T_FUNCTION) { 1126 //@if(debug) 1127 if (trace) this.stateLogTrace("will handle event '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.actionInfo); 1128 //@endif 1129 sc.stateWillTryToHandleEvent(this, event, event); 1130 ret = (this[event](arg1, arg2) !== NO); 1131 sc.stateDidTryToHandleEvent(this, event, event, ret); 1132 return ret; 1133 } 1134 1135 // Try an event handler that is associated with an event represented as a string 1136 var handler = this._registeredStringEventHandlers[event]; 1137 if (handler) { 1138 //@if(debug) 1139 if (trace) this.stateLogTrace("%@ will handle event '%@'".fmt(handler.name, event), SC.TRACE_STATECHART_STYLE.actionInfo); 1140 //@endif 1141 sc.stateWillTryToHandleEvent(this, event, handler.name); 1142 ret = (handler.handler.call(this, event, arg1, arg2) !== NO); 1143 sc.stateDidTryToHandleEvent(this, event, handler.name, ret); 1144 return ret; 1145 } 1146 1147 // Try an event handler that is associated with events matching a regular expression 1148 1149 var len = this._registeredRegExpEventHandlers.length, 1150 i = 0; 1151 1152 for (; i < len; i += 1) { 1153 handler = this._registeredRegExpEventHandlers[i]; 1154 if (event.match(handler.regexp)) { 1155 //@if(debug) 1156 if (trace) this.stateLogTrace("%@ will handle event '%@'".fmt(handler.name, event), SC.TRACE_STATECHART_STYLE.actionInfo); 1157 //@endif 1158 sc.stateWillTryToHandleEvent(this, event, handler.name); 1159 ret = (handler.handler.call(this, event, arg1, arg2) !== NO); 1160 sc.stateDidTryToHandleEvent(this, event, handler.name, ret); 1161 return ret; 1162 } 1163 } 1164 1165 // Final attempt. If the state has an unknownEvent function then invoke it to 1166 // handle the event 1167 if (SC.typeOf(this['unknownEvent']) === SC.T_FUNCTION) { 1168 //@if(debug) 1169 if (trace) this.stateLogTrace("unknownEvent will handle event '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.actionInfo); 1170 //@endif 1171 sc.stateWillTryToHandleEvent(this, event, 'unknownEvent'); 1172 ret = (this.unknownEvent(event, arg1, arg2) !== NO); 1173 sc.stateDidTryToHandleEvent(this, event, 'unknownEvent', ret); 1174 return ret; 1175 } 1176 1177 // Nothing was able to handle the given event for this state 1178 return NO; 1179 }, 1180 1181 /** 1182 Called whenever this state is to be entered during a state transition process. This 1183 is useful when you want the state to perform some initial set up procedures. 1184 1185 If when entering the state you want to perform some kind of asynchronous action, such 1186 as an animation or fetching remote data, then you need to return an asynchronous 1187 action, which is done like so: 1188 1189 enterState: function() { 1190 return this.performAsync('foo'); 1191 } 1192 1193 After returning an action to be performed asynchronously, the statechart will suspend 1194 the active state transition process. In order to resume the process, you must call 1195 this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous 1196 action is to be perform, then nothing needs to be returned. 1197 1198 When the enterState method is called, an optional context value may be supplied if 1199 one was provided to the gotoState method. 1200 1201 In the case that the context being supplied is a state context object 1202 ({@link SC.StateRouteHandlerContext}), an optional `enterStateByRoute` method can be invoked 1203 on this state if the state has implemented the method. If `enterStateByRoute` is 1204 not part of this state then the `enterState` method will be invoked by default. The 1205 `enterStateByRoute` is simply a convenience method that helps removes checks to 1206 determine if the context provide is a state route context object. 1207 1208 @param {Hash} [context] value if one was supplied to gotoState when invoked 1209 1210 @see #representRoute 1211 */ 1212 enterState: function(context) { }, 1213 1214 /** 1215 Notification called just before enterState is invoked. 1216 1217 Note: This is intended to be used by the owning statechart but it can be overridden if 1218 you need to do something special. 1219 1220 @param {Hash} [context] value if one was supplied to gotoState when invoked 1221 @see #enterState 1222 */ 1223 stateWillBecomeEntered: function(context) { 1224 this._isEnteringState = YES; 1225 }, 1226 1227 /** 1228 Notification called just after enterState is invoked. 1229 1230 Note: This is intended to be used by the owning statechart but it can be overridden if 1231 you need to do something special. 1232 1233 @param context {Hash} Optional value if one was supplied to gotoState when invoked 1234 @see #enterState 1235 */ 1236 stateDidBecomeEntered: function(context) { 1237 this._setupAllStateObserveHandlers(); 1238 this._isEnteringState = NO; 1239 }, 1240 1241 /** 1242 Called whenever this state is to be exited during a state transition process. This is 1243 useful when you want the state to peform some clean up procedures. 1244 1245 If when exiting the state you want to perform some kind of asynchronous action, such 1246 as an animation or fetching remote data, then you need to return an asynchronous 1247 action, which is done like so: 1248 1249 exitState: function() { 1250 return this.performAsync('foo'); 1251 } 1252 1253 After returning an action to be performed asynchronously, the statechart will suspend 1254 the active state transition process. In order to resume the process, you must call 1255 this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous 1256 action is to be perform, then nothing needs to be returned. 1257 1258 When the exitState method is called, an optional context value may be supplied if 1259 one was provided to the gotoState method. 1260 1261 @param context {Hash} Optional value if one was supplied to gotoState when invoked 1262 */ 1263 exitState: function(context) { }, 1264 1265 /** 1266 Notification called just before exitState is invoked. 1267 1268 Note: This is intended to be used by the owning statechart but it can be overridden 1269 if you need to do something special. 1270 1271 @param context {Hash} Optional value if one was supplied to gotoState when invoked 1272 @see #exitState 1273 */ 1274 stateWillBecomeExited: function(context) { 1275 this._isExitingState = YES; 1276 this._teardownAllStateObserveHandlers(); 1277 }, 1278 1279 /** 1280 Notification called just after exitState is invoked. 1281 1282 Note: This is intended to be used by the owning statechart but it can be overridden 1283 if you need to do something special. 1284 1285 @param context {Hash} Optional value if one was supplied to gotoState when invoked 1286 @see #exitState 1287 */ 1288 stateDidBecomeExited: function(context) { 1289 this._isExitingState = NO; 1290 }, 1291 1292 /** @private 1293 1294 Used to setup all the state observer handlers. Should be done when 1295 the state has been entered. 1296 */ 1297 _setupAllStateObserveHandlers: function() { 1298 this._configureAllStateObserveHandlers('addObserver'); 1299 }, 1300 1301 /** @private 1302 1303 Used to teardown all the state observer handlers. Should be done when 1304 the state is being exited. 1305 */ 1306 _teardownAllStateObserveHandlers: function() { 1307 this._configureAllStateObserveHandlers('removeObserver'); 1308 }, 1309 1310 /** @private 1311 1312 Primary method used to either add or remove this state as an observer 1313 based on all the state observe handlers that have been registered with 1314 this state. 1315 1316 Note: The code to add and remove the state as an observer has been 1317 taken from the observerable mixin and made slightly more generic. However, 1318 having this code in two different places is not ideal, but for now this 1319 will have to do. In the future the code should be refactored so that 1320 there is one common function that both the observerable mixin and the 1321 statechart framework use. 1322 */ 1323 _configureAllStateObserveHandlers: function(action) { 1324 var key, values, dotIndex, path, observer, i, root; 1325 1326 for (key in this._registeredStateObserveHandlers) { 1327 values = this._registeredStateObserveHandlers[key]; 1328 for (i = 0; i < values.length; i += 1) { 1329 path = values[i]; observer = key; 1330 1331 // Use the dot index in the path to determine how the state 1332 // should add itself as an observer. 1333 1334 dotIndex = path.indexOf('.'); 1335 1336 if (dotIndex < 0) { 1337 this[action](path, this, observer); 1338 } else if (path.indexOf('*') === 0) { 1339 this[action](path.slice(1), this, observer); 1340 } else { 1341 root = null; 1342 1343 if (dotIndex === 0) { 1344 root = this; path = path.slice(1); 1345 } else if (dotIndex === 4 && path.slice(0, 5) === 'this.') { 1346 root = this; path = path.slice(5); 1347 } else if (dotIndex < 0 && path.length === 4 && path === 'this') { 1348 root = this; path = ''; 1349 } 1350 1351 SC.Observers[action](path, this, observer, root); 1352 } 1353 } 1354 } 1355 }, 1356 1357 /** 1358 Call when an asynchronous action need to be performed when either entering or exiting 1359 a state. 1360 1361 @see enterState 1362 @see exitState 1363 */ 1364 performAsync: function(func, arg1, arg2) { 1365 return SC.Async.perform(func, arg1, arg2); 1366 }, 1367 1368 /** @override 1369 1370 Returns YES if this state can respond to the given event, otherwise 1371 NO is returned 1372 1373 @param event {String} the value to check 1374 @returns {Boolean} 1375 */ 1376 respondsToEvent: function(event) { 1377 if (this._registeredEventHandlers[event]) return false; 1378 if (SC.typeOf(this[event]) === SC.T_FUNCTION) return true; 1379 if (this._registeredStringEventHandlers[event]) return true; 1380 if (this._registeredStateObserveHandlers[event]) return false; 1381 1382 var len = this._registeredRegExpEventHandlers.length, 1383 i = 0, 1384 handler; 1385 1386 for (; i < len; i += 1) { 1387 handler = this._registeredRegExpEventHandlers[i]; 1388 if (event.match(handler.regexp)) return true; 1389 } 1390 1391 return SC.typeOf(this['unknownEvent']) === SC.T_FUNCTION; 1392 }, 1393 1394 /** 1395 Returns the path for this state relative to the statechart's 1396 root state. 1397 1398 The path is a dot-notation string representing the path from 1399 this state to the statechart's root state, but without including 1400 the root state in the path. For instance, if the name of this 1401 state if "foo" and the parent state's name is "bar" where bar's 1402 parent state is the root state, then the full path is "bar.foo" 1403 1404 @type String 1405 */ 1406 fullPath: function() { 1407 var root = this.getPath('statechart.rootState'); 1408 if (!root) return this.get('name'); 1409 return this.pathRelativeTo(root); 1410 }.property('name', 'parentState').cacheable(), 1411 1412 toString: function() { 1413 return this.get('fullPath'); 1414 }, 1415 1416 /** @private */ 1417 _statechartOwnerDidChange: function() { 1418 this.notifyPropertyChange('owner'); 1419 }, 1420 1421 /** 1422 Used to log a state warning message 1423 */ 1424 stateLogWarning: function(msg) { 1425 var sc = this.get('statechart'); 1426 sc.statechartLogWarning(msg); 1427 }, 1428 1429 /** 1430 Used to log a state error message 1431 */ 1432 stateLogError: function(msg) { 1433 var sc = this.get('statechart'); 1434 sc.statechartLogError(msg); 1435 } 1436 1437 }); 1438 1439 /** 1440 Use this when you want to plug-in a state into a statechart. This is beneficial 1441 in cases where you split your statechart's states up into multiple files and 1442 don't want to fuss with the sc_require construct. 1443 1444 Example: 1445 1446 MyApp.statechart = SC.Statechart.create({ 1447 rootState: SC.State.design({ 1448 initialSubstate: 'a', 1449 a: SC.State.plugin('path.to.a.state.class'), 1450 b: SC.State.plugin('path.to.another.state.class') 1451 }) 1452 }); 1453 1454 You can also supply hashes the plugin feature in order to enhance a state or 1455 implement required functionality: 1456 1457 SomeMixin = { ... }; 1458 1459 stateA: SC.State.plugin('path.to.state', SomeMixin, { ... }) 1460 1461 @param value {String} property path to a state class 1462 @param args {Hash,...} Optional. Hash objects to be added to the created state 1463 */ 1464 SC.State.plugin = function(value) { 1465 var args; 1466 1467 // Fast arguments access. 1468 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 1469 args = new Array(arguments.length - 1); // SC.A(arguments).shift() 1470 for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 1]; } 1471 1472 var func = function() { 1473 var klass = SC.objectForPropertyPath(value); 1474 if (!klass) { 1475 console.error('SC.State.plugin: Unable to determine path %@'.fmt(value)); 1476 return undefined; 1477 } 1478 if (!klass.isClass || !klass.kindOf(SC.State)) { 1479 console.error('SC.State.plugin: Unable to extend. %@ must be a class extending from SC.State'.fmt(value)); 1480 return undefined; 1481 } 1482 return klass.extend.apply(klass, args); 1483 }; 1484 func.statePlugin = YES; 1485 return func; 1486 }; 1487 1488 SC.State.design = SC.State.extend; 1489