embient 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3790 @@
1
+
2
+ (function(exports) {
3
+ // ==========================================================================
4
+ // Project: SproutCore Statechart
5
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
6
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
7
+ // License: Licensed under MIT license (see license.js)
8
+ // ==========================================================================
9
+
10
+ SC.handleActions = function(func) {
11
+ var args = Array.prototype.slice.call(arguments);
12
+ // remove func
13
+ args.shift();
14
+
15
+ func.isActionHandler = YES;
16
+ func.actions = args;
17
+ return func;
18
+ };
19
+
20
+ SC.stateObserves = function(func) {
21
+ var args = Array.prototype.slice.call(arguments);
22
+ // remove func
23
+ args.shift();
24
+
25
+ func.isStateObserveHandler = YES;
26
+ func.args = args;
27
+ return func;
28
+ };
29
+
30
+ })({});
31
+
32
+
33
+ (function(exports) {
34
+ // ==========================================================================
35
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
36
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
37
+ // Portions @2011 Apple Inc. All rights reserved.
38
+ // License: Licensed under MIT license (see license.js)
39
+ // ==========================================================================
40
+
41
+ /*globals SC */
42
+
43
+ SC.StatechartSequenceMatcher = SC.Object.extend({
44
+
45
+ statechartMonitor: null,
46
+
47
+ match: null,
48
+
49
+ MISMATCH: {},
50
+
51
+ begin: function() {
52
+ this._stack = [];
53
+ this.beginSequence();
54
+ this._start = this._stack[0];
55
+ return this;
56
+ },
57
+
58
+ end: function() {
59
+ this.endSequence();
60
+
61
+ if (this._stack.length > 0) {
62
+ throw "can not match sequence. sequence matcher has been left in an invalid state";
63
+ }
64
+
65
+ var monitor = this.statechartMonitor,
66
+ result = this._matchSequence(this._start, 0) === monitor.sequence.length;
67
+
68
+ this.set('match', result);
69
+
70
+ return result;
71
+ },
72
+
73
+ entered: function() {
74
+ this._addStatesToCurrentGroup('entered', arguments);
75
+ return this;
76
+ },
77
+
78
+ exited: function() {
79
+ this._addStatesToCurrentGroup('exited', arguments);
80
+ return this;
81
+ },
82
+
83
+ beginConcurrent: function() {
84
+ var group = {
85
+ type: 'concurrent',
86
+ values: []
87
+ };
88
+ if (this._peek()) this._peek().values.push(group);
89
+ this._stack.push(group);
90
+ return this;
91
+ },
92
+
93
+ endConcurrent: function() {
94
+ this._stack.pop();
95
+ return this;
96
+ },
97
+
98
+ beginSequence: function() {
99
+ var group = {
100
+ type: 'sequence',
101
+ values: []
102
+ };
103
+ if (this._peek()) this._peek().values.push(group);
104
+ this._stack.push(group);
105
+ return this;
106
+ },
107
+
108
+ endSequence: function() {
109
+ this._stack.pop();
110
+ return this;
111
+ },
112
+
113
+ _peek: function() {
114
+ var len = this._stack.length;
115
+ return len === 0 ? null : this._stack[len - 1];
116
+ },
117
+
118
+ _addStatesToCurrentGroup: function(action, states) {
119
+ var group = this._peek(), len = states.length, i = 0;
120
+ for (; i < len; i += 1) {
121
+ group.values.push({ action: action, state: states[i] });
122
+ }
123
+ },
124
+
125
+ _matchSequence: function(sequence, marker) {
126
+ var values = sequence.values,
127
+ len = values.length,
128
+ i = 0, val,
129
+ monitor = this.statechartMonitor;
130
+
131
+ if (len === 0) return marker;
132
+ if (marker > monitor.sequence.length) return this.MISMATCH;
133
+
134
+ for (; i < len; i += 1) {
135
+ val = values[i];
136
+
137
+ if (val.type === 'sequence') {
138
+ marker = this._matchSequence(val, marker);
139
+ } else if (val.type === 'concurrent') {
140
+ marker = this._matchConcurrent(val, marker);
141
+ } else if (!this._matchItems(val, monitor.sequence[marker])){
142
+ return this.MISMATCH;
143
+ } else {
144
+ marker += 1;
145
+ }
146
+
147
+ if (marker === this.MISMATCH) return this.MISMATCH;
148
+ }
149
+
150
+ return marker;
151
+ },
152
+
153
+ // A
154
+ // B (concurrent [X, Y])
155
+ // X
156
+ // M
157
+ // N
158
+ // Y
159
+ // O
160
+ // P
161
+ // C
162
+ //
163
+ // 0 1 2 3 4 5 6 7 8
164
+ // ^ ^
165
+ // A B (X M N) (Y O P) C
166
+ // ^ ^
167
+ // A B (Y O P) (X M N) C
168
+
169
+ _matchConcurrent: function(concurrent, marker) {
170
+ var values = concurrent.values.slice(0),
171
+ len = values.length,
172
+ i = 0, val, tempMarker = marker, match = false,
173
+ monitor = this.statechartMonitor;
174
+
175
+ if (len === 0) return marker;
176
+ if (marker > monitor.sequence.length) return this.MISMATCH;
177
+
178
+ while (values.length > 0) {
179
+ for (i = 0; i < len; i += 1) {
180
+ val = values[i];
181
+
182
+ if (val.type === 'sequence') {
183
+ tempMarker = this._matchSequence(val, marker);
184
+ } else if (val.type === 'concurrent') {
185
+ tempMarker = this._matchConcurrent(val, marker);
186
+ } else if (!this._matchItems(val, monitor.sequence[marker])){
187
+ tempMarker = this.MISMATCH;
188
+ } else {
189
+ tempMarker = marker + 1;
190
+ }
191
+
192
+ if (tempMarker !== this.MISMATCH) break;
193
+ }
194
+
195
+ if (tempMarker === this.MISMATCH) return this.MISMATCH;
196
+ values.removeAt(i);
197
+ len = values.length;
198
+ marker = tempMarker;
199
+ }
200
+
201
+ return marker;
202
+ },
203
+
204
+ _matchItems: function(matcherItem, monitorItem) {
205
+ if (!matcherItem || !monitorItem) return false;
206
+
207
+ if (matcherItem.action !== monitorItem.action) {
208
+ return false;
209
+ }
210
+
211
+ if (SC.typeOf(matcherItem.state) === "instance" && matcherItem.state === monitorItem.state) {
212
+ return true;
213
+ }
214
+
215
+ if (matcherItem.state === monitorItem.state.get('stateName')) {
216
+ return true;
217
+ }
218
+
219
+ return false;
220
+ }
221
+
222
+ });
223
+
224
+ })({});
225
+
226
+
227
+ (function(exports) {
228
+ // ==========================================================================
229
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
230
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
231
+ // Portions @2011 Apple Inc. All rights reserved.
232
+ // License: Licensed under MIT license (see license.js)
233
+ // ==========================================================================
234
+ /*globals SC */
235
+ SC.StatechartMonitor = SC.Object.extend({
236
+
237
+ statechart: null,
238
+
239
+ sequence: null,
240
+
241
+ init: function() {
242
+ this.reset();
243
+ },
244
+
245
+ reset: function() {
246
+ this.propertyWillChange('length');
247
+ this.sequence = [];
248
+ this.propertyDidChange('length');
249
+ },
250
+
251
+ length: function() {
252
+ return this.sequence.length;
253
+ }.property(),
254
+
255
+ pushEnteredState: function(state) {
256
+ this.propertyWillChange('length');
257
+ this.sequence.push({ action: 'entered', state: state });
258
+ this.propertyDidChange('length');
259
+ },
260
+
261
+ pushExitedState: function(state) {
262
+ this.propertyWillChange('length');
263
+ this.sequence.push({ action: 'exited', state: state });
264
+ this.propertyDidChange('length');
265
+ },
266
+
267
+ matchSequence: function() {
268
+ return SC.StatechartSequenceMatcher.create({
269
+ statechartMonitor: this
270
+ });
271
+ },
272
+
273
+ matchEnteredStates: function() {
274
+ var expected = Array.prototype.slice.call(arguments.length === 1 ? arguments[0] : arguments),
275
+ actual = this.getPath('statechart.enteredStates'),
276
+ matched = 0,
277
+ statechart = this.get('statechart');
278
+
279
+ if (expected.length !== actual.length) return NO;
280
+
281
+ expected.forEach(function(item) {
282
+ if (SC.typeOf(item) === "string") item = statechart.getState(item);
283
+ if (!item) return;
284
+ if (statechart.stateIsEntered(item) && item.get('isEnteredState')) matched += 1;
285
+ });
286
+
287
+ return matched === actual.length;
288
+ },
289
+
290
+ toString: function() {
291
+ var seq = "",
292
+ i = 0,
293
+ len = 0,
294
+ item = null;
295
+
296
+ seq += "[";
297
+
298
+ len = this.sequence.length;
299
+ for (i = 0; i < len; i += 1) {
300
+ item = this.sequence[i];
301
+ seq += "%@ %@".fmt(item.action, item.state.get('fullPath'));
302
+ if (i < len - 1) seq += ", ";
303
+ }
304
+
305
+ seq += "]";
306
+
307
+ return seq;
308
+ }
309
+
310
+ });
311
+
312
+ })({});
313
+
314
+
315
+ (function(exports) {
316
+ // ==========================================================================
317
+ // Project: SproutCore Statechart
318
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
319
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
320
+ // License: Licensed under MIT license (see license.js)
321
+ // ==========================================================================
322
+ })({});
323
+
324
+
325
+ (function(exports) {
326
+ // ==========================================================================
327
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
328
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
329
+ // Portions @2011 Apple Inc. All rights reserved.
330
+ // License: Licensed under MIT license (see license.js)
331
+ // ==========================================================================
332
+
333
+ /*globals SC */
334
+
335
+ if (SC.EXTEND_PROTOTYPES) {
336
+
337
+ /**
338
+ Extends the JS Function object with the handleActions method that
339
+ will provide more advanced action handling capabilities when constructing
340
+ your statechart's states.
341
+
342
+ By default, when you add a method to a state, the state will react to
343
+ actions that matches a method's name, like so:
344
+
345
+ {{{
346
+
347
+ state = SC.State.extend({
348
+
349
+ // Will be invoked when a action named "foo" is sent to this state
350
+ foo: function(action, sender, context) { ... }
351
+
352
+ })
353
+
354
+ }}}
355
+
356
+ In some situations, it may be advantageous to use one method that can react to
357
+ multiple actions instead of having multiple methods that essentially all do the
358
+ same thing. In order to set a method to handle more than one action you use
359
+ the handleActions method which can be supplied a list of string and/or regular
360
+ expressions. The following example demonstrates the use of handleActions:
361
+
362
+ {{{
363
+
364
+ state = SC.State.extend({
365
+
366
+ actionHandlerA: function(action, sender, context) {
367
+
368
+ }.handleActions('foo', 'bar'),
369
+
370
+ actionHandlerB: function(action, sender, context) {
371
+
372
+ }.handleActions(/num\d/, 'decimal')
373
+
374
+ })
375
+
376
+ }}}
377
+
378
+ Whenever actions 'foo' and 'bar' are sent to the state, the method actionHandlerA
379
+ will be invoked. When there is an action that matches the regular expression
380
+ /num\d/ or the action is 'decimal' then actionHandlerB is invoked. In both
381
+ cases, the name of the action will be supplied to the action handler.
382
+
383
+ It should be noted that the use of regular expressions may impact performance
384
+ since that statechart will not be able to fully optimize the action handling logic based
385
+ on its use. Therefore the use of regular expression should be used sparingly.
386
+
387
+ @param {(String|RegExp)...} args
388
+ */
389
+ Function.prototype.handleActions = function() {
390
+ var args = Array.prototype.slice.call(arguments);
391
+ args.unshift(this);
392
+ return SC.handleActions.apply(SC, args);
393
+ };
394
+
395
+ /**
396
+ Extends the JS Function object with the stateObserves method that will
397
+ create a state observe handler on a given state object.
398
+
399
+ Use a stateObserves() instead of the common observes() method when you want a
400
+ state to observer changes to some property on the state itself or some other
401
+ object.
402
+
403
+ Any method on the state that has stateObserves is considered a state observe
404
+ handler and behaves just like when you use observes() on a method, but with an
405
+ important difference. When you apply stateObserves to a method on a state, those
406
+ methods will be active *only* when the state is entered, otherwise those methods
407
+ will be inactive. This removes the need for you having to explicitly call
408
+ addObserver and removeObserver. As an example:
409
+
410
+ {{{
411
+
412
+ state = SC.State.extend({
413
+
414
+ foo: null,
415
+
416
+ user: null,
417
+
418
+ observeHandlerA: function(target, key) {
419
+
420
+ }.stateObserves('MyApp.someController.status'),
421
+
422
+ observeHandlerB: function(target, key) {
423
+
424
+ }.stateObserves('foo'),
425
+
426
+ observeHandlerC: function(target, key) {
427
+
428
+ }.stateObserves('.user.name', '.user.salary')
429
+
430
+ })
431
+
432
+ }}}
433
+
434
+ Above, state has three state observe handlers: observeHandlerA, observeHandlerB, and
435
+ observeHandlerC. When state is entered, the state will automatically add itself as
436
+ an observer for all of its registered state observe handlers. Therefore when
437
+ foo changes, observeHandlerB will be invoked, and when MyApp.someController's status
438
+ changes then observeHandlerA will be invoked. The moment that state is exited then
439
+ the state will automatically remove itself as an observer for all of its registered
440
+ state observe handlers. Therefore none of the state observe handlers will be
441
+ invoked until the next time the state is entered.
442
+
443
+ @param {String...} args
444
+ */
445
+ Function.prototype.stateObserves = function() {
446
+ var args = Array.prototype.slice.call(arguments);
447
+ args.unshift(this);
448
+ return SC.stateObserves.apply(SC, args);
449
+ };
450
+ }
451
+
452
+ })({});
453
+
454
+
455
+ (function(exports) {
456
+ // ==========================================================================
457
+ // Project: SproutCore Statechart
458
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
459
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
460
+ // License: Licensed under MIT license (see license.js)
461
+ // ==========================================================================
462
+ })({});
463
+
464
+
465
+ (function(exports) {
466
+ // ==========================================================================
467
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
468
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
469
+ // Portions @2011 Apple Inc. All rights reserved.
470
+ // License: Licensed under MIT license (see license.js)
471
+ // ==========================================================================
472
+ /*globals SC */
473
+ /**
474
+ @class
475
+
476
+ Represents a call that is intended to be asynchronous. This is
477
+ used during a state transition process when either entering or
478
+ exiting a state.
479
+
480
+ @extends SC.Object
481
+ @author Michael Cohen
482
+ */
483
+ SC.Async = SC.Object.extend(
484
+ /** @scope SC.Async.prototype */{
485
+
486
+ func: null,
487
+
488
+ arg1: null,
489
+
490
+ arg2: null,
491
+
492
+ /** @private
493
+ Called by the statechart
494
+ */
495
+ tryToPerform: function(state) {
496
+ var func = this.get('func'),
497
+ arg1 = this.get('arg1'),
498
+ arg2 = this.get('arg2'),
499
+ funcType = SC.typeOf(func);
500
+
501
+ if (funcType === "string") {
502
+ SC.tryToPerform(state, func, arg1, arg2);
503
+ } else if (funcType === "function") {
504
+ func.apply(state, [arg1, arg2]);
505
+ }
506
+ }
507
+
508
+ });
509
+
510
+ /**
511
+ Singleton
512
+ */
513
+ SC.Async.reopenClass(/** @scope SC.Async */{
514
+
515
+ /**
516
+ Call in either a state's enterState or exitState method when you
517
+ want a state to perform an asynchronous action, such as an animation.
518
+
519
+ Examples:
520
+
521
+ {{
522
+
523
+ SC.State.extend({
524
+
525
+ enterState: function() {
526
+ return SC.Async.perform('foo');
527
+ },
528
+
529
+ exitState: function() {
530
+ return SC.Async.perform('bar', 100);
531
+ }
532
+
533
+ foo: function() { ... },
534
+
535
+ bar: function(arg) { ... }
536
+
537
+ });
538
+
539
+ }}
540
+
541
+ @param func {String|Function} the functio to be invoked on a state
542
+ @param arg1 Optional. An argument to pass to the given function
543
+ @param arg2 Optional. An argument to pass to the given function
544
+ @return {SC.Async} a new instance of a SC.Async
545
+ */
546
+ perform: function(func, arg1, arg2) {
547
+ return SC.Async.create({ func: func, arg1: arg1, arg2: arg2 });
548
+ }
549
+
550
+ });
551
+
552
+ })({});
553
+
554
+
555
+ (function(exports) {
556
+ // ==========================================================================
557
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
558
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
559
+ // Portions @2011 Apple Inc. All rights reserved.
560
+ // License: Licensed under MIT license (see license.js)
561
+ // ==========================================================================
562
+ /*globals SC */
563
+
564
+ var get = SC.get, set = SC.set, getPath = SC.getPath, slice = Array.prototype.slice;
565
+
566
+ /**
567
+ @class
568
+
569
+ Represents a state within a statechart.
570
+
571
+ The statechart actively manages all states belonging to it. When a state is created,
572
+ it immediately registers itself with it parent states.
573
+
574
+ You do not create an instance of a state itself. The statechart manager will go through its
575
+ state heirarchy and create the states itself.
576
+
577
+ For more information on using statecharts, see SC.StatechartManager.
578
+
579
+ @author Michael Cohen
580
+ @extends SC.Object
581
+ */
582
+ SC.State = SC.Object.extend(
583
+ /** @lends SC.State.prototype */ {
584
+
585
+ // walk like a duck!
586
+ isState: true,
587
+
588
+ /**
589
+ The name of the state
590
+
591
+ @property {String}
592
+ */
593
+ stateName: null,
594
+
595
+ /**
596
+ This state's parent state. Managed by the statechart
597
+
598
+ @property {State}
599
+ */
600
+ parentState: null,
601
+
602
+ /**
603
+ This state's history state. Can be null. Managed by the statechart.
604
+
605
+ @property {State}
606
+ */
607
+ historyState: null,
608
+
609
+ /**
610
+ Used to indicate the initial substate of this state to enter into.
611
+
612
+ You assign the value with the name of the state. Upon creation of
613
+ the state, the statechart will automatically change the property
614
+ to be a corresponding state object
615
+
616
+ The substate is only to be this state's immediate substates. If
617
+ no initial substate is assigned then this states initial substate
618
+ will be an instance of an empty state (SC.EmptyState).
619
+
620
+ Note that a statechart's root state must always have an explicity
621
+ initial substate value assigned else an error will be thrown.
622
+
623
+ @property {String|State}
624
+ */
625
+ initialSubstate: null,
626
+
627
+ /**
628
+ Used to indicates if this state's immediate substates are to be
629
+ concurrent (orthogonal) to each other.
630
+
631
+ @property {Boolean}
632
+ */
633
+ substatesAreConcurrent: false,
634
+
635
+ /**
636
+ The immediate substates of this state. Managed by the statechart.
637
+
638
+ @property {Array}
639
+ */
640
+ substates: null,
641
+
642
+ /**
643
+ The statechart that this state belongs to. Assigned by the owning
644
+ statechart.
645
+
646
+ @property {Statechart}
647
+ */
648
+ statechart: null,
649
+
650
+ /**
651
+ Indicates if this state has been initialized by the statechart
652
+
653
+ @propety {Boolean}
654
+ */
655
+ stateIsInitialized: false,
656
+
657
+ /**
658
+ An array of this state's current substates. Managed by the statechart
659
+
660
+ @propety {Array}
661
+ */
662
+ currentSubstates: null,
663
+
664
+ /**
665
+ An array of this state's substates that are currently entered. Managed by
666
+ the statechart.
667
+
668
+ @property {Array}
669
+ */
670
+ enteredSubstates: null,
671
+
672
+ /**
673
+ Can optionally assign what route this state is to represent.
674
+
675
+ If assigned then this state will be notified to handle the route when triggered
676
+ any time the app's location changes and matches this state's assigned route.
677
+ The handler invoked is this state's {@link #routeTriggered} method.
678
+
679
+ The value assigned to this property is dependent on the underlying routing
680
+ mechanism used by the application. The default routing mechanism is to use
681
+ SC.routes.
682
+
683
+ @property {String|Hash}
684
+
685
+ @see #routeTriggered
686
+ @see #location
687
+ @see SC.StatechartDelegate
688
+ */
689
+ representRoute: null,
690
+
691
+ /**
692
+ Indicates if this state should trace actions. Useful for debugging
693
+ purposes. Managed by the statechart.
694
+
695
+ @see SC.StatechartManager#trace
696
+
697
+ @property {Boolean}
698
+ */
699
+ trace: function() {
700
+ var key = getPath(this, 'statechart.statechartTraceKey');
701
+ return getPath(this, 'statechart.%@'.fmt(key));
702
+ }.property().cacheable(),
703
+
704
+ /**
705
+ Indicates who the owner is of this state. If not set on the statechart
706
+ then the owner is the statechart, otherwise it is the assigned
707
+ object. Managed by the statechart.
708
+
709
+ @see SC.StatechartManager#owner
710
+
711
+ @property {SC.Object}
712
+ */
713
+ owner: function() {
714
+ var sc = get(this, 'statechart'),
715
+ key = sc ? get(sc, 'statechartOwnerKey') : null,
716
+ owner = sc ? get(sc, key) : null;
717
+ return owner ? owner : sc;
718
+ }.property().cacheable(),
719
+
720
+ /**
721
+ Returns the statechart's assigned delegate. A statechart delegate is one
722
+ that adheres to the {@link SC.StatechartDelegate} mixin.
723
+
724
+ @property {SC.Object}
725
+
726
+ @see SC.StatechartDelegate
727
+ */
728
+ statechartDelegate: function() {
729
+ return this.getPath('statechart.statechartDelegate');
730
+ }.property().cacheable(),
731
+
732
+ /**
733
+ A volatile property used to get and set the app's current location.
734
+
735
+ This computed property defers to the the statechart's delegate to
736
+ actually update and acquire the app's location.
737
+
738
+ Note: Binding for this pariticular case is discouraged since in most
739
+ cases we need the location value immediately. If we were to use
740
+ bindings then the location value wouldn't be updated until at least
741
+ the end of one run loop. It is also advised that the delegate not
742
+ have its `statechartUpdateLocationForState` and
743
+ `statechartAcquireLocationForState` methods implemented where bindings
744
+ are used since they will inadvertenly stall the location value from
745
+ propogating immediately.
746
+
747
+ @property {String}
748
+
749
+ @see SC.StatechartDelegate#statechartUpdateLocationForState
750
+ @see SC.StatechartDelegate#statechartAcquireLocationForState
751
+ */
752
+ location: function(key, value) {
753
+ var sc = this.get('statechart'),
754
+ del = this.get('statechartDelegate');
755
+
756
+ if (value !== undefined) {
757
+ del.statechartUpdateLocationForState(sc, value, this);
758
+ }
759
+
760
+ return del.statechartAcquireLocationForState(sc, this);
761
+ }.property(),
762
+
763
+ init: function() {
764
+ this._registeredActionHandlers = {};
765
+ this._registeredStringActionHandlers = {};
766
+ this._registeredRegExpActionHandlers = [];
767
+ this._registeredStateObserveHandlers = {};
768
+ this._isEnteringState = NO;
769
+ this._isExitingState = NO;
770
+
771
+ // Setting up observes this way is faster then using .observes,
772
+ // which adds a noticable increase in initialization time.
773
+ var statechart = get(this, 'statechart'),
774
+ ownerKey = statechart ? get(statechart, 'statechartOwnerKey') : null,
775
+ traceKey = statechart ? get(statechart, 'statechartTraceKey') : null;
776
+
777
+ if (statechart) {
778
+ statechart.addObserver(ownerKey, this, '_statechartOwnerDidChange');
779
+ statechart.addObserver(traceKey, this, '_statechartTraceDidChange');
780
+ }
781
+ },
782
+
783
+ destroy: function() {
784
+ var sc = get(this, 'statechart'),
785
+ ownerKey = sc ? get(sc, 'statechartOwnerKey') : null,
786
+ traceKey = sc ? get(sc, 'statechartTraceKey') : null;
787
+
788
+ if (sc) {
789
+ SC.removeObserver(sc, ownerKey, this, '_statechartOwnerDidChange');
790
+ SC.removeObserver(sc, traceKey, this, '_statechartTraceDidChange');
791
+ }
792
+
793
+ var substates = get(this, 'substates');
794
+ if (substates) {
795
+ substates.forEach(function(state) {
796
+ state.destroy();
797
+ });
798
+ }
799
+
800
+ this._teardownAllStateObserveHandlers();
801
+
802
+ set(this, 'substates', null);
803
+ set(this, 'currentSubstates', null);
804
+ set(this, 'enteredSubstates', null);
805
+ set(this, 'parentState', null);
806
+ set(this, 'historyState', null);
807
+ set(this, 'initialSubstate', null);
808
+ set(this, 'statechart', null);
809
+
810
+ this.notifyPropertyChange('trace');
811
+ this.notifyPropertyChange('owner');
812
+
813
+ this._registeredActionHandlers = null;
814
+ this._registeredStringActionHandlers = null;
815
+ this._registeredRegExpActionHandlers = null;
816
+ this._registeredStateObserveHandlers = null;
817
+
818
+ this._super();
819
+ },
820
+
821
+ /**
822
+ Used to initialize this state. To only be called by the owning statechart.
823
+ */
824
+ initState: function() {
825
+ if (get(this, 'stateIsInitialized')) return;
826
+
827
+ this._registerWithParentStates();
828
+ this._setupRouteHandling();
829
+
830
+ var key = null,
831
+ value = null,
832
+ state = null,
833
+ substates = [],
834
+ matchedInitialSubstate = NO,
835
+ initialSubstate = get(this, 'initialSubstate'),
836
+ substatesAreConcurrent = get(this, 'substatesAreConcurrent'),
837
+ statechart = get(this, 'statechart'),
838
+ i = 0,
839
+ len = 0,
840
+ valueIsFunc = NO,
841
+ historyState = null;
842
+
843
+ if (SC.HistoryState.detect(initialSubstate) && initialSubstate.isClass) {
844
+ historyState = this.createHistoryState(initialSubstate, { parentState: this, statechart: statechart });
845
+ set(this, 'initialSubstate', historyState);
846
+
847
+ if (SC.none(get(historyState, 'defaultState'))) {
848
+ this.stateLogError("Initial substate is invalid. History state requires the name of a default state to be set");
849
+ set(this, 'initialSubstate', null);
850
+ historyState = null;
851
+ }
852
+ }
853
+
854
+ // Iterate through all this state's substates, if any, create them, and then initialize
855
+ // them. This causes a recursive process.
856
+ for (key in this) {
857
+ value = this[key];
858
+ valueIsFunc = SC.typeOf(value) === "function";
859
+
860
+ if (valueIsFunc && value.isActionHandler) {
861
+ this._registerActionHandler(key, value);
862
+ continue;
863
+ }
864
+
865
+ if (valueIsFunc && value.isStateObserveHandler) {
866
+ this._registerStateObserveHandler(key, value);
867
+ continue;
868
+ }
869
+
870
+ if (valueIsFunc && value.statePlugin) {
871
+ value = value.apply(this);
872
+ }
873
+
874
+ if (SC.State.detect(value) && value.isClass && this[key] !== this.constructor) {
875
+ state = this.createSubstate(value, { stateName: key, parentState: this, statechart: statechart });
876
+ substates.push(state);
877
+ this[key] = state;
878
+ state.initState();
879
+ if (key === initialSubstate) {
880
+ set(this, 'initialSubstate', state);
881
+ matchedInitialSubstate = YES;
882
+ } else if (historyState && get(historyState, 'defaultState') === key) {
883
+ set(historyState, 'defaultState', state);
884
+ matchedInitialSubstate = YES;
885
+ }
886
+ }
887
+ }
888
+
889
+ if (!SC.none(initialSubstate) && !matchedInitialSubstate) {
890
+ this.stateLogError("Unable to set initial substate %@ since it did not match any of state's %@ substates".fmt(initialSubstate, this));
891
+ }
892
+
893
+ if (substates.length === 0) {
894
+ if (!SC.none(initialSubstate)) {
895
+ this.stateLogWarning("Unable to make %@ an initial substate since state %@ has no substates".fmt(initialSubstate, this));
896
+ }
897
+ }
898
+ else if (substates.length > 0) {
899
+ if (SC.none(initialSubstate) && !substatesAreConcurrent) {
900
+ state = this.createEmptyState({ parentState: this, statechart: statechart });
901
+ set(this, 'initialSubstate', state);
902
+ substates.push(state);
903
+ this[get(state, 'stateName')] = state;
904
+ state.initState();
905
+ this.stateLogWarning("state %@ has no initial substate defined. Will default to using an empty state as initial substate".fmt(this));
906
+ }
907
+ else if (!SC.none(initialSubstate) && substatesAreConcurrent) {
908
+ set(this, 'initialSubstate', null);
909
+ this.stateLogWarning("Can not use %@ as initial substate since substates are all concurrent for state %@".fmt(initialSubstate, this));
910
+ }
911
+ }
912
+
913
+ set(this, 'substates', substates);
914
+ set(this, 'currentSubstates', []);
915
+ set(this, 'enteredSubstates', []);
916
+ set(this, 'stateIsInitialized', YES);
917
+ },
918
+
919
+ /** @private
920
+
921
+ Used to bind this state with a route this state is to represent if a route has been assigned.
922
+
923
+ When invoked, the method will delegate the actual binding strategy to the statechart delegate
924
+ via the delegate's {@link SC.StatechartDelegate#statechartBindStateToRoute} method.
925
+
926
+ Note that a state cannot be bound to a route if this state is a concurrent state.
927
+
928
+ @see #representRoute
929
+ @see SC.StatechartDelegate#statechartBindStateToRoute
930
+ */
931
+ _setupRouteHandling: function() {
932
+ var route = this.get('representRoute'),
933
+ sc = this.get('statechart'),
934
+ del = this.get('statechartDelegate');
935
+
936
+ if (!route) return;
937
+
938
+ if (this.get('isConcurrentState')) {
939
+ this.stateLogError("State %@ cannot handle route '%@' since state is concurrent".fmt(this, route));
940
+ return;
941
+ }
942
+
943
+ del.statechartBindStateToRoute(sc, this, route, this.routeTriggered);
944
+ },
945
+
946
+ /**
947
+ Main handler that gets triggered whenever the app's location matches this state's assigned
948
+ route.
949
+
950
+ When invoked the handler will first refer to the statechart delegate to determine if it
951
+ should actually handle the route via the delegate's
952
+ {@see SC.StatechartDelegate#statechartShouldStateHandleTriggeredRoute} method. If the
953
+ delegate allows the handling of the route then the state will continue on with handling
954
+ the triggered route by calling the state's {@link #handleTriggeredRoute} method, otherwise
955
+ the state will cancel the handling and inform the delegate through the delegate's
956
+ {@see SC.StatechartDelegate#statechartStateCancelledHandlingRoute} method.
957
+
958
+ The handler will create a state route context ({@link SC.StateRouteContext}) object
959
+ that packages information about what is being currently handled. This context object gets
960
+ passed along to the delegate's invoked methods as well as the state transition process.
961
+
962
+ Note that this method is not intended to be directly called or overridden.
963
+
964
+ @see #representRoute
965
+ @see SC.StatechartDelegate#statechartShouldStateHandleRoute
966
+ @see SC.StatechartDelegate#statechartStateCancelledHandlingRoute
967
+ @see #createStateRouteHandlerContext
968
+ @see #handleTriggeredRoute
969
+ */
970
+ routeTriggered: function(params) {
971
+ if (this._isEnteringState) return;
972
+
973
+ var sc = this.get('statechart'),
974
+ del = this.get('statechartDelegate'),
975
+ loc = this.get('location');
976
+
977
+ var attr = {
978
+ state: this,
979
+ location: loc,
980
+ params: params,
981
+ handler: this.routeTriggered
982
+ };
983
+
984
+ var context = this.createStateRouteHandlerContext(attr);
985
+
986
+ if (del.statechartShouldStateHandleTriggeredRoute(sc, this, context)) {
987
+ if (this.get('trace') && loc) {
988
+ this.stateLogTrace("will handle route '%@'".fmt(loc));
989
+ }
990
+ this.handleTriggeredRoute(context);
991
+ } else {
992
+ del.statechartStateCancelledHandlingTriggeredRoute(sc, this, context);
993
+ }
994
+ },
995
+
996
+ /**
997
+ Constructs a new instance of a state routing context object.
998
+
999
+ @param {Hash} attr attributes to apply to the constructed object
1000
+ @return {SC.StateRouteContext}
1001
+
1002
+ @see #handleRoute
1003
+ */
1004
+ createStateRouteHandlerContext: function(attr) {
1005
+ return SC.StateRouteHandlerContext.create(attr);
1006
+ },
1007
+
1008
+ /**
1009
+ Invoked by this state's {@link #routeTriggered} method if the state is
1010
+ actually allowed to handle the triggered route.
1011
+
1012
+ By default the method invokes a state transition to this state.
1013
+ */
1014
+ handleTriggeredRoute: function(context) {
1015
+ this.gotoState(this, context);
1016
+ },
1017
+
1018
+ /**
1019
+ creates a substate for this state
1020
+ */
1021
+ createSubstate: function(state, attrs) {
1022
+ return state.create(attrs);
1023
+ },
1024
+
1025
+ /**
1026
+ Create a history state for this state
1027
+ */
1028
+ createHistoryState: function(state, attrs) {
1029
+ return state.create(attrs);
1030
+ },
1031
+
1032
+ /**
1033
+ Create an empty state for this state's initial substate
1034
+ */
1035
+ createEmptyState: function(attrs) {
1036
+ return SC.EmptyState.create(attrs);
1037
+ },
1038
+
1039
+ /** @private
1040
+
1041
+ Registers action handlers with this state. Action handlers are special
1042
+ functions on the state that are intended to handle more than one action. This
1043
+ compared to basic functions that only respond to a single action that reflects
1044
+ the name of the method.
1045
+ */
1046
+ _registerActionHandler: function(name, handler) {
1047
+ var actions = handler.actions,
1048
+ action = null,
1049
+ len = actions.length,
1050
+ i = 0;
1051
+
1052
+ this._registeredActionHandlers[name] = handler;
1053
+
1054
+ for (; i < len; i += 1) {
1055
+ action = actions[i];
1056
+
1057
+ if (SC.typeOf(action) === "string") {
1058
+ this._registeredStringActionHandlers[action] = {
1059
+ name: name,
1060
+ handler: handler
1061
+ };
1062
+ continue;
1063
+ }
1064
+
1065
+ if (action instanceof RegExp) {
1066
+ this._registeredRegExpActionHandlers.push({
1067
+ name: name,
1068
+ handler: handler,
1069
+ regexp: action
1070
+ });
1071
+ continue;
1072
+ }
1073
+
1074
+ this.stateLogError("Invalid action %@ for action handler %@ in state %@".fmt(action, name, this));
1075
+ }
1076
+ },
1077
+
1078
+ /** @private
1079
+
1080
+ Registers state observe handlers with this state. State observe handlers behave just like
1081
+ when you apply observes() on a method but will only be active when the state is currently
1082
+ entered, otherwise the handlers are inactive until the next time the state is entered
1083
+ */
1084
+ _registerStateObserveHandler: function(name, handler) {
1085
+ var i = 0,
1086
+ args = handler.args,
1087
+ len = args.length,
1088
+ arg, validHandlers = YES;
1089
+
1090
+ for (; i < len; i += 1) {
1091
+ arg = args[i];
1092
+ if (SC.typeOf(arg) !== "string" || SC.empty(arg)) {
1093
+ this.stateLogError("Invalid argument %@ for state observe handler %@ in state %@".fmt(arg, name, this));
1094
+ validHandlers = NO;
1095
+ }
1096
+ }
1097
+
1098
+ if (!validHandlers) return;
1099
+
1100
+ this._registeredStateObserveHandlers[name] = handler.args;
1101
+ },
1102
+
1103
+ /** @private
1104
+ Will traverse up through this state's parent states to register
1105
+ this state with them.
1106
+ */
1107
+ _registerWithParentStates: function() {
1108
+ this._registerSubstate(this);
1109
+ var parent = get(this, 'parentState');
1110
+ while (!SC.none(parent)) {
1111
+ parent._registerSubstate(this);
1112
+ parent = get(parent, 'parentState');
1113
+ }
1114
+ },
1115
+
1116
+ /** @private
1117
+ Will register a given state as a substate of this state
1118
+ */
1119
+ _registerSubstate: function(state) {
1120
+ var path = state.pathRelativeTo(this);
1121
+ if (SC.none(path)) return;
1122
+
1123
+ // Create special private member variables to help
1124
+ // keep track of substates and access them.
1125
+ if (SC.none(this._registeredSubstatePaths)) {
1126
+ this._registeredSubstatePaths = {};
1127
+ this._registeredSubstates = [];
1128
+ }
1129
+
1130
+ this._registeredSubstates.push(state);
1131
+
1132
+ // Keep track of states based on their relative path
1133
+ // to this state.
1134
+ var regPaths = this._registeredSubstatePaths;
1135
+ if (regPaths[get(state, 'stateName')] === undefined) {
1136
+ regPaths[get(state, 'stateName')] = { __ki_paths__: [] };
1137
+ }
1138
+
1139
+ var paths = regPaths[get(state, 'stateName')];
1140
+ paths[path] = state;
1141
+ paths.__ki_paths__.push(path);
1142
+ },
1143
+
1144
+ /**
1145
+ Will generate path for a given state that is relative to this state. It is
1146
+ required that the given state is a substate of this state.
1147
+
1148
+ If the heirarchy of the given state to this state is the following:
1149
+ A > B > C, where A is this state and C is the given state, then the
1150
+ relative path generated will be "B.C"
1151
+ */
1152
+ pathRelativeTo: function(state) {
1153
+ var path = get(this, 'stateName'),
1154
+ parent = get(this, 'parentState');
1155
+
1156
+ while (!SC.none(parent) && parent !== state) {
1157
+ path = "%@.%@".fmt(get(parent, 'stateName'), path);
1158
+ parent = get(parent, 'parentState');
1159
+ }
1160
+
1161
+ if (parent !== state && state !== this) {
1162
+ this.stateLogError('Can not generate relative path from %@ since it not a parent state of %@'.fmt(state, this));
1163
+ return null;
1164
+ }
1165
+
1166
+ return path;
1167
+ },
1168
+
1169
+ /**
1170
+ Used to get a substate of this state that matches a given value.
1171
+
1172
+ If the value is a state object, then the value will be returned if it is indeed
1173
+ a substate of this state, otherwise null is returned.
1174
+
1175
+ If the given value is a string, then the string is assumed to be a path to a substate.
1176
+ The value is then parsed to find the closes match. If there is no match then null
1177
+ is returned. If there is more than one match then null is return and an error
1178
+ is generated indicating ambiguity of the given value.
1179
+
1180
+ Note that when the value is a string, it is assumed to be a path relative to this
1181
+ state; not the root state of the statechart.
1182
+ */
1183
+ getSubstate: function(value) {
1184
+ var valueType = SC.typeOf(value);
1185
+
1186
+ // If the value is an object then just check if the value is
1187
+ // a registered substate of this state, and if so return it.
1188
+ if (value instanceof SC.State) {
1189
+ return this._registeredSubstates.indexOf(value) > -1 ? value : null;
1190
+ }
1191
+
1192
+ if (valueType !== "string") {
1193
+ this.stateLogError("Can not find matching subtype. value must be an object or string: %@".fmt(value));
1194
+ return null;
1195
+ }
1196
+
1197
+ // The value is a string. Therefore treat the value as a relative path to
1198
+ // a substate of this state.
1199
+
1200
+ // Extract that last part of the string. Ex. 'foo' => 'foo', 'foo.bar' => 'bar'
1201
+ var matches = value.match(/(^|\.)(\w+)$/);
1202
+ if (!matches) return null;
1203
+
1204
+ // Get all the paths related to the matched value. If no paths then return null.
1205
+ var paths = this._registeredSubstatePaths[matches[2]];
1206
+ if (SC.none(paths)) return null;
1207
+
1208
+ // Do a quick check to see if there is a path that exactly matches the given
1209
+ // value, and if so return the corresponding state
1210
+ var state = paths[value];
1211
+ if (!SC.none(state)) return state;
1212
+
1213
+ // No exact match found. If the value given is a basic string with no ".", then check
1214
+ // if there is only one path containing that string. If so, return it. If there is
1215
+ // more than one path then it is ambiguous as to what state is trying to be reached.
1216
+ if (matches[1] === "") {
1217
+ if (paths.__ki_paths__.length === 1) {
1218
+ state = paths[paths.__ki_paths__[0]];
1219
+ } else if (paths.__ki_paths__.length > 1) {
1220
+ var msg = 'Can not find substate matching %@ in state %@. Ambiguous with the following: %@';
1221
+ this.stateLogError(msg.fmt(value, this, paths.__ki_paths__));
1222
+ }
1223
+ }
1224
+
1225
+ return state;
1226
+ },
1227
+
1228
+ /**
1229
+ Used to go to a state in the statechart either directly from this state if it is a current state,
1230
+ or from one of the current states within the same tree of states. To find the current state in the
1231
+ same tree, we walk up the hierarchy and look on each parentState for current substates.
1232
+ When the first current substate is found, it is chosen to transition from. This approach is much
1233
+ better than just choosing the first current state of the statechart, since this can lead to problems
1234
+ with transitions between concurrent states.
1235
+
1236
+ Note that if the value given is a string, it will be assumed to be a path to a state. The path
1237
+ will be relative to the statechart's root state; not relative to this state.
1238
+
1239
+ Method can be called in the following ways:
1240
+
1241
+ // With one argument
1242
+ gotoState(<state>)
1243
+
1244
+ // With two arguments
1245
+ gotoState(<state>, <hash>)
1246
+
1247
+ Where <state> is either a string or a SC.State object and <hash> is a regular JS hash object.
1248
+
1249
+ @param state {SC.State|String} the state to go to
1250
+ @param context {Hash} Optional. context object that will be supplied to all states that are
1251
+ exited and entered during the state transition process
1252
+ */
1253
+ gotoState: function(state, context) {
1254
+ var fromState = null;
1255
+
1256
+ if (get(this, 'isCurrentState')) {
1257
+ fromState = this;
1258
+ } else {
1259
+ nextParentState = this;
1260
+
1261
+ while(true) {
1262
+ if(nextParentState.get('hasCurrentSubstates')) {
1263
+ break;
1264
+ }
1265
+ nextParentState = nextParentState.get('parentState');
1266
+ }
1267
+
1268
+ fromState = nextParentState.get('currentSubstates')[0];
1269
+ }
1270
+
1271
+ get(this, 'statechart').gotoState(state, fromState, context);
1272
+ },
1273
+
1274
+ /**
1275
+ Used to go to a given state's history state in the statechart either directly from this state if it
1276
+ is a current state or from one of this state's current substates.
1277
+
1278
+ Note that if the value given is a string, it will be assumed to be a path to a state. The path
1279
+ will be relative to the statechart's root state; not relative to this state.
1280
+
1281
+ Method can be called in the following ways:
1282
+
1283
+ // With one argument
1284
+ gotoHistoryState(<state>)
1285
+
1286
+ // With two arguments
1287
+ gotoHistoryState(<state>, <boolean | hash>)
1288
+
1289
+ // With three arguments
1290
+ gotoHistoryState(<state>, <boolean>, <hash>)
1291
+
1292
+ Where <state> is either a string or a SC.State object and <hash> is a regular JS hash object.
1293
+
1294
+ @param state {SC.State|String} the state whose history state to go to
1295
+ @param recusive {Boolean} Optional. Indicates whether to follow history states recusively starting
1296
+ from the given state
1297
+ @param context {Hash} Optional. context object that will be supplied to all states that are exited
1298
+ entered during the state transition process
1299
+ */
1300
+ gotoHistoryState: function(state, recursive, context) {
1301
+ var fromState = null;
1302
+
1303
+ if (get(this, 'isCurrentState')) {
1304
+ fromState = this;
1305
+ } else if (get(this, 'hasCurrentSubstates')) {
1306
+ fromState = get(this, 'currentSubstates')[0];
1307
+ }
1308
+
1309
+ get(this, 'statechart').gotoHistoryState(state, fromState, recursive, context);
1310
+ },
1311
+
1312
+ /**
1313
+ Resumes an active goto state transition process that has been suspended.
1314
+ */
1315
+ resumeGotoState: function() {
1316
+ get(this, 'statechart').resumeGotoState();
1317
+ },
1318
+
1319
+ /**
1320
+ Used to check if a given state is a current substate of this state. Mainly used in cases
1321
+ when this state is a concurrent state.
1322
+
1323
+ @param state {State|String} either a state object or the name of a state
1324
+ @returns {Boolean} true is the given state is a current substate, otherwise false is returned
1325
+ */
1326
+ stateIsCurrentSubstate: function(state) {
1327
+ if (SC.typeOf(state) === "string") state = get(this, 'statechart').getState(state);
1328
+ var current = get(this, 'currentSubstates');
1329
+ return !!current && current.indexOf(state) >= 0;
1330
+ },
1331
+
1332
+ /**
1333
+ Used to check if a given state is a substate of this state that is currently entered.
1334
+
1335
+ @param state {State|String} either a state object of the name of a state
1336
+ @returns {Boolean} true if the given state is a entered substate, otherwise false is returned
1337
+ */
1338
+ stateIsEnteredSubstate: function(state) {
1339
+ if (SC.typeOf(state) === "string") state = get(this, 'statechart').getState(state);
1340
+ var entered = get(this, 'enteredSubstates');
1341
+ return !!entered && entered.indexOf(state) >= 0;
1342
+ },
1343
+
1344
+ /**
1345
+ Indicates if this state is the root state of the statechart.
1346
+
1347
+ @property {Boolean}
1348
+ */
1349
+ isRootState: function() {
1350
+ return getPath(this, 'statechart.rootState') === this;
1351
+ }.property(),
1352
+
1353
+ /**
1354
+ Indicates if this state is a current state of the statechart.
1355
+
1356
+ @property {Boolean}
1357
+ */
1358
+ isCurrentState: function() {
1359
+ return this.stateIsCurrentSubstate(this);
1360
+ }.property('currentSubstates').cacheable(),
1361
+
1362
+ /**
1363
+ Indicates if this state is a concurrent state
1364
+
1365
+ @property {Boolean}
1366
+ */
1367
+ isConcurrentState: function() {
1368
+ return getPath(this, 'parentState.substatesAreConcurrent');
1369
+ }.property(),
1370
+
1371
+ /**
1372
+ Indicates if this state is a currently entered state.
1373
+
1374
+ A state is currently entered if during a state transition process the
1375
+ state's enterState method was invoked, but only after its exitState method
1376
+ was called, if at all.
1377
+ */
1378
+ isEnteredState: function() {
1379
+ return this.stateIsEnteredSubstate(this);
1380
+ }.property('enteredSubstates').cacheable(),
1381
+
1382
+ /**
1383
+ Indicate if this state has any substates
1384
+
1385
+ @propety {Boolean}
1386
+ */
1387
+ hasSubstates: function() {
1388
+ return getPath(this, 'substates.length') > 0;
1389
+ }.property('substates'),
1390
+
1391
+ /**
1392
+ Indicates if this state has any current substates
1393
+ */
1394
+ hasCurrentSubstates: function() {
1395
+ var current = get(this, 'currentSubstates');
1396
+ return !!current && get(current, 'length') > 0;
1397
+ }.property('currentSubstates').cacheable(),
1398
+
1399
+ /**
1400
+ Indicates if this state has any currently entered substates
1401
+ */
1402
+ hasEnteredSubstates: function() {
1403
+ var entered = get(this, 'enteredSubstates');
1404
+ return !!entered && get(entered, 'length') > 0;
1405
+ }.property('enteredSubstates').cacheable(),
1406
+
1407
+ /**
1408
+ Used to re-enter this state. Call this only when the state a current state of
1409
+ the statechart.
1410
+ */
1411
+ reenter: function() {
1412
+ var statechart = get(this, 'statechart');
1413
+ if (get(this, 'isCurrentState')) {
1414
+ statechart.gotoState(this);
1415
+ } else {
1416
+ SC.Logger.error('Can not re-enter state %@ since it is not a current state in the statechart'.fmt(this));
1417
+ }
1418
+ },
1419
+
1420
+ /**
1421
+ Called by the statechart to allow a state to try and handle the given action. If the
1422
+ action is handled by the state then YES is returned, otherwise NO.
1423
+
1424
+ There is a particular order in how an action is handled by a state:
1425
+
1426
+ 1. Basic function whose name matches the action
1427
+ 2. Registered action handler that is associated with an action represented as a string
1428
+ 3. Registered action handler that is associated with actions matching a regular expression
1429
+ 4. The unknownAction function
1430
+
1431
+ Use of action handlers that are associated with actions matching a regular expression may
1432
+ incur a performance hit, so they should be used sparingly.
1433
+
1434
+ The unknownAction function is only invoked if the state has it, otherwise it is skipped. Note that
1435
+ you should be careful when using unknownAction since it can be either abused or cause unexpected
1436
+ behavior.
1437
+
1438
+ Example of a state using all four action handling techniques:
1439
+
1440
+ SC.State.extend({
1441
+
1442
+ // Basic function handling action 'foo'
1443
+ foo: function(arg1, arg2) { ... },
1444
+
1445
+ // action handler that handles 'frozen' and 'canuck'
1446
+ actionHandlerA: function(action, arg1, arg2) {
1447
+ ...
1448
+ }.handleActions('frozen', 'canuck'),
1449
+
1450
+ // action handler that handles actions matching the regular expression /num\d/
1451
+ // ex. num1, num2
1452
+ actionHandlerB: function(action, arg1, arg2) {
1453
+ ...
1454
+ }.handleActions(/num\d/),
1455
+
1456
+ // Handle any action that was not handled by some other
1457
+ // method on the state
1458
+ unknownAction: function(action, arg1, arg2) {
1459
+
1460
+ }
1461
+
1462
+ });
1463
+ */
1464
+ tryToHandleAction: function(action, arg1, arg2) {
1465
+
1466
+ var trace = get(this, 'trace');
1467
+
1468
+ // First check if the name of the action is the same as a registered action handler. If so,
1469
+ // then do not handle the action.
1470
+ if (this._registeredActionHandlers[action]) {
1471
+ this.stateLogWarning("state %@ can not handle action %@ since it is a registered action handler".fmt(this, action));
1472
+ return NO;
1473
+ }
1474
+
1475
+ if (this._registeredStateObserveHandlers[action]) {
1476
+ this.stateLogWarning("state %@ can not handle action %@ since it is a registered state observe handler".fmt(this, action));
1477
+ return NO;
1478
+ }
1479
+
1480
+ // Now begin by trying a basic method on the state to respond to the action
1481
+ if (SC.typeOf(this[action]) === "function") {
1482
+ if (trace) this.stateLogTrace("will handle action %@".fmt(action));
1483
+ return (this[action](arg1, arg2) !== NO);
1484
+ }
1485
+
1486
+ // Try an action handler that is associated with an action represented as a string
1487
+ var handler = this._registeredStringActionHandlers[action];
1488
+ if (handler) {
1489
+ if (trace) this.stateLogTrace("%@ will handle action %@".fmt(handler.name, action));
1490
+ return (handler.handler.call(this, action, arg1, arg2) !== NO);
1491
+ }
1492
+
1493
+ // Try an action handler that is associated with actions matching a regular expression
1494
+
1495
+ var len = this._registeredRegExpActionHandlers.length,
1496
+ i = 0;
1497
+
1498
+ for (; i < len; i += 1) {
1499
+ handler = this._registeredRegExpActionHandlers[i];
1500
+ if (action.match(handler.regexp)) {
1501
+ if (trace) this.stateLogTrace("%@ will handle action %@".fmt(handler.name, action));
1502
+ return (handler.handler.call(this, action, arg1, arg2) !== NO);
1503
+ }
1504
+ }
1505
+
1506
+ // Final attempt. If the state has an unknownAction function then invoke it to
1507
+ // handle the action
1508
+ if (SC.typeOf(this['unknownAction']) === "function") {
1509
+ if (trace) this.stateLogTrace("unknownAction will handle action %@".fmt(action));
1510
+ return (this.unknownAction(action, arg1, arg2) !== NO);
1511
+ }
1512
+
1513
+ // Nothing was able to handle the given action for this state
1514
+ return NO;
1515
+ },
1516
+
1517
+ /**
1518
+ Called whenever this state is to be entered during a state transition process. This
1519
+ is useful when you want the state to perform some initial set up procedures.
1520
+
1521
+ If when entering the state you want to perform some kind of asynchronous action, such
1522
+ as an animation or fetching remote data, then you need to return an asynchronous
1523
+ action, which is done like so:
1524
+
1525
+ enterState: function() {
1526
+ return this.performAsync('foo');
1527
+ }
1528
+
1529
+ After returning an action to be performed asynchronously, the statechart will suspend
1530
+ the active state transition process. In order to resume the process, you must call
1531
+ this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous
1532
+ action is to be perform, then nothing needs to be returned.
1533
+
1534
+ When the enterState method is called, an optional context value may be supplied if
1535
+ one was provided to the gotoState method.
1536
+
1537
+ @param context {Hash} Optional value if one was supplied to gotoState when invoked
1538
+ */
1539
+ enterState: function(context) { },
1540
+
1541
+ /**
1542
+ Notification called just before enterState is invoked.
1543
+
1544
+ Note: This is intended to be used by the owning statechart but it can be overridden if
1545
+ you need to do something special.
1546
+
1547
+ @see #enterState
1548
+ */
1549
+ stateWillBecomeEntered: function(context) {
1550
+ this._isEnteringState = YES;
1551
+ },
1552
+
1553
+ /**
1554
+ Notification called just after enterState is invoked.
1555
+
1556
+ Note: This is intended to be used by the owning statechart but it can be overridden if
1557
+ you need to do something special.
1558
+
1559
+ @see #enterState
1560
+ */
1561
+ stateDidBecomeEntered: function(context) {
1562
+ this._setupAllStateObserveHandlers();
1563
+ this._isEnteringState = NO;
1564
+ },
1565
+
1566
+ /**
1567
+ Called whenever this state is to be exited during a state transition process. This is
1568
+ useful when you want the state to peform some clean up procedures.
1569
+
1570
+ If when exiting the state you want to perform some kind of asynchronous action, such
1571
+ as an animation or fetching remote data, then you need to return an asynchronous
1572
+ action, which is done like so:
1573
+
1574
+ exitState: function() {
1575
+ return this.performAsync('foo');
1576
+ }
1577
+
1578
+ After returning an action to be performed asynchronously, the statechart will suspend
1579
+ the active state transition process. In order to resume the process, you must call
1580
+ this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous
1581
+ action is to be perform, then nothing needs to be returned.
1582
+
1583
+ When the exitState method is called, an optional context value may be supplied if
1584
+ one was provided to the gotoState method.
1585
+
1586
+ @param context {Hash} Optional value if one was supplied to gotoState when invoked
1587
+ */
1588
+ exitState: function(context) { },
1589
+
1590
+ /**
1591
+ Notification called just before exitState is invoked.
1592
+
1593
+ Note: This is intended to be used by the owning statechart but it can be overridden
1594
+ if you need to do something special.
1595
+
1596
+ @see #exitState
1597
+ */
1598
+ stateWillBecomeExited: function(context) {
1599
+ this._isExitingState = YES;
1600
+ this._teardownAllStateObserveHandlers();
1601
+ },
1602
+
1603
+ /**
1604
+ Notification called just after exitState is invoked.
1605
+
1606
+ Note: This is intended to be used by the owning statechart but it can be overridden
1607
+ if you need to do something special.
1608
+
1609
+ @see #exitState
1610
+ */
1611
+ stateDidBecomeExited: function(context) {
1612
+ this._isExitingState = NO;
1613
+ },
1614
+
1615
+ /** @private
1616
+
1617
+ Used to setup all the state observer handlers. Should be done when
1618
+ the state has been entered.
1619
+ */
1620
+ _setupAllStateObserveHandlers: function() {
1621
+ this._configureAllStateObserveHandlers('addObserver');
1622
+ },
1623
+
1624
+ /** @private
1625
+
1626
+ Used to teardown all the state observer handlers. Should be done when
1627
+ the state is being exited.
1628
+ */
1629
+ _teardownAllStateObserveHandlers: function() {
1630
+ this._configureAllStateObserveHandlers('removeObserver');
1631
+ },
1632
+
1633
+ /** @private
1634
+
1635
+ Primary method used to either add or remove this state as an observer
1636
+ based on all the state observe handlers that have been registered with
1637
+ this state.
1638
+
1639
+ Note: The code to add and remove the state as an observer has been
1640
+ taken from the observerable mixin and made slightly more generic. However,
1641
+ having this code in two different places is not ideal, but for now this
1642
+ will have to do. In the future the code should be refactored so that
1643
+ there is one common function that both the observerable mixin and the
1644
+ statechart framework use.
1645
+ */
1646
+ _configureAllStateObserveHandlers: function(action) {
1647
+ var key, values, path, observer, i, tuple;
1648
+
1649
+ for (key in this._registeredStateObserveHandlers) {
1650
+ values = this._registeredStateObserveHandlers[key];
1651
+ for (i = 0; i < values.length; i += 1) {
1652
+ path = values[i]; observer = key;
1653
+ tuple = SC.normalizeTuple(this, path);
1654
+ SC[action](tuple[0], tuple[1], this, observer);
1655
+ }
1656
+ }
1657
+ },
1658
+
1659
+ /**
1660
+ Call when an asynchronous action need to be performed when either entering or exiting
1661
+ a state.
1662
+
1663
+ @see enterState
1664
+ @see exitState
1665
+ */
1666
+ performAsync: function(func, arg1, arg2) {
1667
+ return SC.Async.perform(func, arg1, arg2);
1668
+ },
1669
+
1670
+ /** @override
1671
+
1672
+ Returns YES if this state can respond to the given action, otherwise
1673
+ NO is returned
1674
+
1675
+ @param action {String} the value to check
1676
+ @returns {Boolean}
1677
+ */
1678
+ respondsToAction: function(action) {
1679
+ if (this._registeredActionHandlers[action]) return false;
1680
+ if (SC.typeOf(this[action]) === "function") return true;
1681
+ if (this._registeredStringActionHandlers[action]) return true;
1682
+ if (this._registeredStateObserveHandlers[action]) return false;
1683
+
1684
+ var len = this._registeredRegExpActionHandlers.length,
1685
+ i = 0,
1686
+ handler;
1687
+
1688
+ for (; i < len; i += 1) {
1689
+ handler = this._registeredRegExpActionHandlers[i];
1690
+ if (action.match(handler.regexp)) return true;
1691
+ }
1692
+
1693
+ return SC.typeOf(this['unknownAction']) === "function";
1694
+ },
1695
+
1696
+ /**
1697
+ Returns the path for this state relative to the statechart's
1698
+ root state.
1699
+
1700
+ The path is a dot-notation string representing the path from
1701
+ this state to the statechart's root state, but without including
1702
+ the root state in the path. For instance, if the name of this
1703
+ state if "foo" and the parent state's name is "bar" where bar's
1704
+ parent state is the root state, then the full path is "bar.foo"
1705
+
1706
+ @property {String}
1707
+ */
1708
+ fullPath: function() {
1709
+ var root = getPath(this, 'statechart.rootState');
1710
+ if (!root) return get(this, 'stateName');
1711
+ return this.pathRelativeTo(root);
1712
+ }.property('stateName', 'parentState').cacheable(),
1713
+
1714
+ // toString: function() {
1715
+ // var className = this._super();
1716
+ // return "%@<%@, %@>".fmt(className, get(this, 'fullPath'), SC.guidFor(this));
1717
+ // },
1718
+
1719
+ /** @private */
1720
+ _statechartTraceDidChange: function() {
1721
+ this.notifyPropertyChange('trace');
1722
+ },
1723
+
1724
+ /** @private */
1725
+ _statechartOwnerDidChange: function() {
1726
+ this.notifyPropertyChange('owner');
1727
+ },
1728
+
1729
+ /**
1730
+ Used to log a state trace message
1731
+ */
1732
+ stateLogTrace: function(msg) {
1733
+ var sc = get(this, 'statechart');
1734
+ sc.statechartLogTrace("%@: %@".fmt(this, msg));
1735
+ },
1736
+
1737
+ /**
1738
+ Used to log a state warning message
1739
+ */
1740
+ stateLogWarning: function(msg) {
1741
+ var sc = get(this, 'statechart');
1742
+ sc.statechartLogWarning(msg);
1743
+ },
1744
+
1745
+ /**
1746
+ Used to log a state error message
1747
+ */
1748
+ stateLogError: function(msg) {
1749
+ var sc = get(this, 'statechart');
1750
+ sc.statechartLogError(msg);
1751
+ }
1752
+
1753
+ });
1754
+
1755
+ /**
1756
+ Use this when you want to plug-in a state into a statechart. This is beneficial
1757
+ in cases where you split your statechart's states up into multiple files and
1758
+ don't want to fuss with the sc_require construct.
1759
+
1760
+ Example:
1761
+
1762
+ MyApp.statechart = SC.Statechart.create({
1763
+ rootState: SC.State.extend({
1764
+ initialSubstate: 'a',
1765
+ a: SC.State.plugin('path.to.a.state.class'),
1766
+ b: SC.State.plugin('path.to.another.state.class')
1767
+ })
1768
+ });
1769
+
1770
+ You can also supply hashes the plugin feature in order to enhance a state or
1771
+ implement required functionality:
1772
+
1773
+ SomeMixin = { ... };
1774
+
1775
+ stateA: SC.State.plugin('path.to.state', SomeMixin, { ... })
1776
+
1777
+ @param value {String} property path to a state class
1778
+ @param args {Hash,...} Optional. Hash objects to be added to the created state
1779
+ */
1780
+ SC.State.plugin = function(value) {
1781
+ var args = slice.call(arguments); args.shift();
1782
+ var func = function() {
1783
+ var klass = SC.getPath(window, value);
1784
+ if (!klass) {
1785
+ console.error('SC.State.plugin: Unable to determine path %@'.fmt(value));
1786
+ return undefined;
1787
+ }
1788
+ if (!klass.isClass || (klass.isInstance && !(klass instanceof SC.State))) {
1789
+ console.error('SC.State.plugin: Unable to extend. %@ must be a class extending from SC.State'.fmt(value));
1790
+ return undefined;
1791
+ }
1792
+ return klass.extend.apply(klass, args);
1793
+ };
1794
+ func.statePlugin = YES;
1795
+ return func;
1796
+ };
1797
+
1798
+ })({});
1799
+
1800
+
1801
+ (function(exports) {
1802
+ // ==========================================================================
1803
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
1804
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
1805
+ // Portions @2011 Apple Inc. All rights reserved.
1806
+ // License: Licensed under MIT license (see license.js)
1807
+ // ==========================================================================
1808
+ /*globals SC */
1809
+ /**
1810
+ The default name given to an empty state
1811
+ */
1812
+ SC.EMPTY_STATE_NAME = "__EMPTY_STATE__";
1813
+
1814
+ /**
1815
+ @class
1816
+
1817
+ Represents an empty state that gets assigned as a state's initial substate
1818
+ if the state does not have an initial substate defined.
1819
+
1820
+ @extends SC.State
1821
+ */
1822
+ SC.EmptyState = SC.State.extend(/** @scope SC.EmptyState.prototype */{
1823
+
1824
+ name: SC.EMPTY_STATE_NAME,
1825
+
1826
+ enterState: function() {
1827
+ var msg = "No initial substate was defined for state %@. Entering default empty state";
1828
+ this.stateLogWarning(msg.fmt(this.get('parentState')));
1829
+ }
1830
+
1831
+ });
1832
+
1833
+ })({});
1834
+
1835
+
1836
+ (function(exports) {
1837
+ // ==========================================================================
1838
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
1839
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
1840
+ // Portions @2011 Apple Inc. All rights reserved.
1841
+ // License: Licensed under MIT license (see license.js)
1842
+ // ==========================================================================
1843
+
1844
+ /*globals SC */
1845
+ /**
1846
+ @class
1847
+
1848
+ Represents a history state that can be assigned to a SC.State object's
1849
+ initialSubstate property.
1850
+
1851
+ If a SC.HistoryState object is assigned to a state's initial substate,
1852
+ then after a state is entered the statechart will refer to the history
1853
+ state object to determine the next course of action. If the state has
1854
+ its historyState property assigned then the that state will be entered,
1855
+ otherwise the default state assigned to history state object will be entered.
1856
+
1857
+ An example of how to use:
1858
+
1859
+ stateA: SC.State.extend({
1860
+
1861
+ initialSubstate: SC.HistoryState({
1862
+ defaultState: 'stateB'
1863
+ }),
1864
+
1865
+ stateB: SC.State.extend({ ... }),
1866
+
1867
+ stateC: SC.State.extend({ ... })
1868
+
1869
+ })
1870
+
1871
+ @author Michael Cohen
1872
+ @extends SC.Object
1873
+ */
1874
+ SC.HistoryState = SC.Object.extend(
1875
+ /** @scope SC.HistoryState.prototype */{
1876
+
1877
+ /**
1878
+ Used to indicate if the statechart should recurse the
1879
+ history states after entering the this object's parent state
1880
+
1881
+ @property {Boolean}
1882
+ */
1883
+ isRecursive: NO,
1884
+
1885
+ /**
1886
+ The default state to enter if the parent state does not
1887
+ yet have its historyState property assigned to something
1888
+ other than null.
1889
+
1890
+ The value assigned to this property must be the name of an
1891
+ immediate substate that belongs to the parent state. The
1892
+ statechart will manage the property upon initialization.
1893
+
1894
+ @property {String}
1895
+ */
1896
+ defaultState: null,
1897
+
1898
+ /** @private
1899
+ Managed by the statechart
1900
+
1901
+ The statechart that owns this object.
1902
+ */
1903
+ statechart: null,
1904
+
1905
+ /** @private
1906
+ Managed by the statechart
1907
+
1908
+ The state that owns this object
1909
+ */
1910
+ parentState: null,
1911
+
1912
+ /**
1913
+ Used by the statechart during a state transition process.
1914
+
1915
+ Returns a state to enter based on whether the parent state has
1916
+ its historyState property assigned. If not then this object's
1917
+ assigned default state is returned.
1918
+ */
1919
+ state: function() {
1920
+ var defaultState = this.get('defaultState'),
1921
+ historyState = this.getPath('parentState.historyState');
1922
+ return !!historyState ? historyState : defaultState;
1923
+ }.property().cacheable(),
1924
+
1925
+ /** @private */
1926
+ parentHistoryStateDidChange: function() {
1927
+ this.notifyPropertyChange('state');
1928
+ }.observes('*parentState.historyState')
1929
+
1930
+ });
1931
+
1932
+ })({});
1933
+
1934
+
1935
+ (function(exports) {
1936
+ // ==========================================================================
1937
+ // Project: SproutCore - JavaScript Application Framework
1938
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
1939
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
1940
+ // License: Licensed under MIT license (see license.js)
1941
+ // ==========================================================================
1942
+
1943
+ /**
1944
+ @namespace
1945
+
1946
+ Support methods for the Delegate design pattern.
1947
+
1948
+ The Delegate design pattern makes it easy to delegate a portion of your
1949
+ application logic to another object. This is most often used in views to
1950
+ delegate various application-logic decisions to controllers in order to
1951
+ avoid having to bake application-logic directly into the view itself.
1952
+
1953
+ The methods provided by this mixin make it easier to implement this pattern
1954
+ but they are not required to support delegates.
1955
+
1956
+ ## The Pattern
1957
+
1958
+ The delegate design pattern typically means that you provide a property,
1959
+ usually ending in "delegate", that can be set to another object in the
1960
+ system.
1961
+
1962
+ When events occur or logic decisions need to be made that you would prefer
1963
+ to delegate, you can call methods on the delegate if it is set. If the
1964
+ delegate is not set, you should provide some default functionality instead.
1965
+
1966
+ Note that typically delegates are not observable, hence it is not necessary
1967
+ to use get() to retrieve the value of the delegate.
1968
+
1969
+ @since SproutCore 1.0
1970
+
1971
+ */
1972
+ SC.DelegateSupport = {
1973
+
1974
+ /**
1975
+ Selects the delegate that implements the specified method name. Pass one
1976
+ or more delegates. The receiver is automatically included as a default.
1977
+
1978
+ This can be more efficient than using invokeDelegateMethod() which has
1979
+ to marshall arguments into a delegate call.
1980
+
1981
+ @param {String} methodName
1982
+ @param {Object...} delegate one or more delegate arguments
1983
+ @returns {Object} delegate or null
1984
+ */
1985
+ delegateFor: function(methodName) {
1986
+ var idx = 1,
1987
+ len = arguments.length,
1988
+ ret ;
1989
+
1990
+ while(idx<len) {
1991
+ ret = arguments[idx];
1992
+ if (ret && ret[methodName] !== undefined) return ret ;
1993
+ idx++;
1994
+ }
1995
+
1996
+ return (this[methodName] !== undefined) ? this : null;
1997
+ },
1998
+
1999
+ /**
2000
+ Invokes the named method on the delegate that you pass. If no delegate
2001
+ is defined or if the delegate does not implement the method, then a
2002
+ method of the same name on the receiver will be called instead.
2003
+
2004
+ You can pass any arguments you want to pass onto the delegate after the
2005
+ delegate and methodName.
2006
+
2007
+ @param {Object} delegate a delegate object. May be null.
2008
+ @param {String} methodName a method name
2009
+ @param {Object...} args (OPTIONAL) any additional arguments
2010
+
2011
+ @returns {Object} value returned by delegate
2012
+ */
2013
+ invokeDelegateMethod: function(delegate, methodName, args) {
2014
+ args = SC.A(arguments); args = args.slice(2, args.length) ;
2015
+ if (!delegate || !delegate[methodName]) delegate = this ;
2016
+
2017
+ var method = delegate[methodName];
2018
+ return method ? method.apply(delegate, args) : null;
2019
+ },
2020
+
2021
+ /**
2022
+ Search the named delegates for the passed property. If one is found,
2023
+ gets the property value and returns it. If none of the passed delegates
2024
+ implement the property, search the receiver for the property as well.
2025
+
2026
+ @param {String} key the property to get.
2027
+ @param {Object} delegate one or more delegate
2028
+ @returns {Object} property value or undefined
2029
+ */
2030
+ getDelegateProperty: function(key, delegate) {
2031
+ var idx = 1,
2032
+ len = arguments.length,
2033
+ ret ;
2034
+
2035
+ while(idx<len) {
2036
+ ret = arguments[idx++];
2037
+ if (ret && ret[key] != undefined) {
2038
+ return ret.get ? ret.get(key) : ret[key] ;
2039
+ }
2040
+ }
2041
+
2042
+ return (this[key] != undefined) ? this.get(key) : undefined ;
2043
+ }
2044
+
2045
+ };
2046
+
2047
+ })({});
2048
+
2049
+
2050
+ (function(exports) {
2051
+ // ==========================================================================
2052
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
2053
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
2054
+ // Portions @2011 Apple Inc. All rights reserved.
2055
+ // License: Licensed under MIT license (see license.js)
2056
+ // ==========================================================================
2057
+
2058
+ /*globals SC */
2059
+
2060
+ /**
2061
+ @class
2062
+
2063
+ Apply to objects that are to represent a delegate for a SC.Statechart object.
2064
+ When assigned to a statechart, the statechart and its associated states will
2065
+ use the delegate in order to make various decisions.
2066
+
2067
+ @see SC.Statechart#delegate
2068
+
2069
+ @author Michael Cohen
2070
+ */
2071
+
2072
+ SC.StatechartDelegate = /** @scope SC.StatechartDelegate.prototype */ {
2073
+
2074
+ // Walk like a duck
2075
+ isStatechartDelegate: YES,
2076
+
2077
+ // Route Handling Management
2078
+
2079
+ /**
2080
+ Called to update the application's current location.
2081
+
2082
+ The location provided is dependent upon the application's underlying
2083
+ routing mechanism.
2084
+
2085
+ @param {SC.StatechartManager} statechart the statechart
2086
+ @param {String|Hash} location the new location
2087
+ @param {SC.State} state the state requesting the location update
2088
+ */
2089
+ statechartUpdateLocationForState: function(statechart, location, state) {
2090
+ SC.routes.set('location', location);
2091
+ },
2092
+
2093
+ /**
2094
+ Called to acquire the application's current location.
2095
+
2096
+ @param {SC.StatechartManager} statechart the statechart
2097
+ @param {SC.State} state the state requesting the location
2098
+ @returns {String} the location
2099
+ */
2100
+ statechartAcquireLocationForState: function(statechart, state) {
2101
+ return SC.routes.get('location');
2102
+ },
2103
+
2104
+ /**
2105
+ Used to bind a state's handler to a route. When the application's location
2106
+ matches the given route, the state's handler is to be invoked.
2107
+
2108
+ The statechart and states remain completely independent of how the underlying
2109
+ routing mechanism works thereby providing a looser coupling and more flexibility
2110
+ in how routing is to work. Given this flexiblity, it is important that a route
2111
+ assigned (using the {@link SC.State#representRoute} property) to a state strictly
2112
+ conforms to the underlying routing mechanism's criteria in order for the given
2113
+ handler to be properly invoked.
2114
+
2115
+ By default the {@link SC.routes} mechanism is used to bind the state's handler with
2116
+ the given route.
2117
+
2118
+ @param {SC.StatechartManager} statechart the statechart
2119
+ @param {SC.State} state the state to bind the route to
2120
+ @param {String|Hash} route the route that is to be bound to the state
2121
+ @param {Function|String} handler the method on the state to be invoked when the route
2122
+ gets triggered.
2123
+
2124
+ @see SC.State#representRoute
2125
+ */
2126
+ statechartBindStateToRoute: function(statechart, state, route, handler) {
2127
+ SC.routes.add(route, state, handler);
2128
+ },
2129
+
2130
+ /**
2131
+ Invoked by a state that has been notified to handle a triggered route. The state
2132
+ asks if it should go ahead an actually handle the triggered route. If no then
2133
+ the state's handler will no longer continue and finish by calling this delegate's
2134
+ `statechartStateCancelledHandlingTriggeredRoute` method. If yes then the state will
2135
+ continue with handling the triggered route.
2136
+
2137
+ By default `YES` is returned.
2138
+
2139
+ @param {SC.StatechartManager} statechart the statechart
2140
+ @param {SC.State} state the state making the request
2141
+ @param {SC.StateRouteHandlerContext} routeContext contextual information about the handling
2142
+ of a route
2143
+
2144
+ @see #statechartStateCancelledHandlingTriggeredRoute
2145
+ */
2146
+ statechartShouldStateHandleTriggeredRoute: function(statechart, state, context) {
2147
+ return YES;
2148
+ },
2149
+
2150
+ /**
2151
+ Invoked by a state that has been informed by the delegate to not handle a triggered route.
2152
+ Used this for any additional clean up or processing that you may wish to perform.
2153
+
2154
+ @param {SC.StatechartManager} statechart the statechart
2155
+ @param {SC.State} state the state making the request
2156
+ @param {SC.StateRouteHandlerContext} routeContext contextual information about the handling
2157
+ of a route
2158
+
2159
+ @see #statechartShouldStateHandleTriggeredRoute
2160
+ */
2161
+ statechartStateCancelledHandlingTriggeredRoute: function(statechart, state, context) { }
2162
+
2163
+ };
2164
+
2165
+ })({});
2166
+
2167
+
2168
+ (function(exports) {
2169
+ // ==========================================================================
2170
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
2171
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
2172
+ // Portions @2011 Apple Inc. All rights reserved.
2173
+ // License: Licensed under MIT license (see license.js)
2174
+ // ==========================================================================
2175
+
2176
+ /*globals SC */
2177
+
2178
+ /**
2179
+ @class
2180
+
2181
+ Represents contextual information for whenever a state handles a triggered
2182
+ route. In additional to retaining contextual information, you can also
2183
+ use the object to retry trigging the state's route handler. Useful in cases
2184
+ where you need to defer the handling of the route for a later time.
2185
+
2186
+ @see SC.State
2187
+
2188
+ @extends SC.Object
2189
+ @author Michael Cohen
2190
+ */
2191
+ SC.StateRouteHandlerContext = SC.Object.extend(
2192
+ /** @scope SC.StateRouteContext.prototype */{
2193
+
2194
+ // walk like a duck
2195
+ isStateRouteHandlerContext: true,
2196
+
2197
+ /**
2198
+ The state that constructed this context object.
2199
+
2200
+ @property {SC.State}
2201
+ */
2202
+ state: null,
2203
+
2204
+ /**
2205
+ The location that caused the state's route to be
2206
+ triggered.
2207
+
2208
+ @property {String}
2209
+ */
2210
+ location: null,
2211
+
2212
+ /**
2213
+ The parameters that were supplied to the state's
2214
+ handler when the state's route was triggered.
2215
+
2216
+ @property {Hash}
2217
+ */
2218
+ params: null,
2219
+
2220
+ /**
2221
+ The handler that got invoked when the state's
2222
+ route was triggered. This can either be a reference
2223
+ to the actual method or a name of the method.
2224
+
2225
+ @property {Function|String}
2226
+ */
2227
+ handler: null,
2228
+
2229
+ /**
2230
+ Used to retry invoking the state's handler for when
2231
+ the state's route gets triggered. When called this will
2232
+ essentially perform the same call as when the handler
2233
+ was originally triggered on state.
2234
+ */
2235
+ retry: function() {
2236
+ var state = this.get('state'),
2237
+ params = this.get('params'),
2238
+ handler = this.get('handler');
2239
+
2240
+ if (SC.typeOf(handler) === 'string') {
2241
+ handler = state[handler];
2242
+ }
2243
+
2244
+ if (SC.typeOf(handler) === 'function') {
2245
+ handler.apply(state, [params]);
2246
+ }
2247
+ }
2248
+
2249
+ });
2250
+
2251
+ })({});
2252
+
2253
+
2254
+ (function(exports) {
2255
+ // ==========================================================================
2256
+ // Project: SC.Statechart - A Statechart Framework for SproutCore
2257
+ // Copyright: ©2010, 2011 Michael Cohen, and contributors.
2258
+ // Portions @2011 Apple Inc. All rights reserved.
2259
+ // License: Licensed under MIT license (see license.js)
2260
+ // ==========================================================================
2261
+ /*globals SC */
2262
+ var get = SC.get, set = SC.set, getPath = SC.getPath;
2263
+
2264
+ /**
2265
+ @class
2266
+
2267
+ The startchart manager mixin allows an object to be a statechart. By becoming a statechart, the
2268
+ object can then be manage a set of its own states.
2269
+
2270
+ This implemention of the statechart manager closely follows the concepts stated in D. Harel's
2271
+ original paper "Statecharts: A Visual Formalism For Complex Systems"
2272
+ (www.wisdom.weizmann.ac.il/~harel/papers/Statecharts.pdf).
2273
+
2274
+ The statechart allows for complex state heircharies by nesting states within states, and
2275
+ allows for state orthogonality based on the use of concurrent states.
2276
+
2277
+ At minimum, a statechart must have one state: The root state. All other states in the statechart
2278
+ are a decendents (substates) of the root state.
2279
+
2280
+ The following example shows how states are nested within a statechart:
2281
+
2282
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2283
+ rootState: SC.State.extend({
2284
+ initialSubstate: 'stateA',
2285
+
2286
+ stateA: SC.State.extend({
2287
+ // ... can continue to nest further states
2288
+ }),
2289
+
2290
+ stateB: SC.State.extend({
2291
+ // ... can continue to nest further states
2292
+ })
2293
+ })
2294
+ });
2295
+
2296
+ Note how in the example above, the root state as an explicit initial substate to enter into. If no
2297
+ initial substate is provided, then the statechart will default to the the state's first substate.
2298
+
2299
+ You can also defined states without explicitly defining the root state. To do so, simply create properties
2300
+ on your object that represents states. Upon initialization, a root state will be constructed automatically
2301
+ by the mixin and make the states on the object substates of the root state. As an example:
2302
+
2303
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2304
+ initialState: 'stateA',
2305
+
2306
+ stateA: SC.State.extend({
2307
+ // ... can continue to nest further states
2308
+ }),
2309
+
2310
+ stateB: SC.State.extend({
2311
+ // ... can continue to nest further states
2312
+ })
2313
+ });
2314
+
2315
+ If you liked to specify a class that should be used as the root state but using the above method to defined
2316
+ states, you can set the rootStateExample property with a class that extends from SC.State. If the
2317
+ rootStateExample property is not explicitly assigned the then default class used will be SC.State.
2318
+
2319
+ To provide your statechart with orthogonality, you use concurrent states. If you use concurrent states,
2320
+ then your statechart will have multiple current states. That is because each concurrent state represents an
2321
+ independent state structure from other concurrent states. The following example shows how to provide your
2322
+ statechart with concurrent states:
2323
+
2324
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2325
+ rootState: SC.State.extend({
2326
+ substatesAreConcurrent: true,
2327
+
2328
+ stateA: SC.State.extend({
2329
+ // ... can continue to nest further states
2330
+ }),
2331
+
2332
+ stateB: SC.State.extend({
2333
+ // ... can continue to nest further states
2334
+ })
2335
+ })
2336
+ });
2337
+
2338
+ Above, to indicate that a state's substates are concurrent, you just have to set the substatesAreConcurrent to
2339
+ true. Once done, then stateA and stateB will be independent of each other and each will manage their
2340
+ own current substates. The root state will then have more then one current substate.
2341
+
2342
+ To define concurrent states directly on the object without explicitly defining a root, you can do the
2343
+ following:
2344
+
2345
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2346
+ statesAreConcurrent: true,
2347
+
2348
+ stateA: SC.State.extend({
2349
+ // ... can continue to nest further states
2350
+ }),
2351
+
2352
+ stateB: SC.State.extend({
2353
+ // ... can continue to nest further states
2354
+ })
2355
+ });
2356
+
2357
+ Remember that a startchart can have a mixture of nested and concurrent states in order for you to
2358
+ create as complex of statecharts that suite your needs. Here is an example of a mixed state structure:
2359
+
2360
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2361
+ rootState: SC.State.extend({
2362
+ initialSubstate: 'stateA',
2363
+
2364
+ stateA: SC.State.extend({
2365
+ substatesAreConcurrent: true,
2366
+
2367
+ stateM: SC.State.extend({ ... })
2368
+ stateN: SC.State.extend({ ... })
2369
+ stateO: SC.State.extend({ ... })
2370
+ }),
2371
+
2372
+ stateB: SC.State.extend({
2373
+ initialSubstate: 'stateX',
2374
+
2375
+ stateX: SC.State.extend({ ... })
2376
+ stateY: SC.State.desgin({ ... })
2377
+ })
2378
+ })
2379
+ });
2380
+
2381
+ Depending on your needs, a statechart can have lots of states, which can become hard to manage all within
2382
+ one file. To modularize your states and make them easier to manage and maintain, you can plug-in states
2383
+ into other states. Let's say we are using the statechart in the last example above, and all the code is
2384
+ within one file. We could update the code and split the logic across two or more files like so:
2385
+
2386
+ // state_a.js
2387
+
2388
+ MyApp.StateA = SC.State.extend({
2389
+ substatesAreConcurrent: true,
2390
+
2391
+ stateM: SC.State.extend({ ... })
2392
+ stateN: SC.State.extend({ ... })
2393
+ stateO: SC.State.extend({ ... })
2394
+ });
2395
+
2396
+ // state_b.js
2397
+
2398
+ MyApp.StateB = SC.State.extend({
2399
+ substatesAreConcurrent: true,
2400
+
2401
+ stateM: SC.State.extend({ ... })
2402
+ stateN: SC.State.extend({ ... })
2403
+ stateO: SC.State.extend({ ... })
2404
+ });
2405
+
2406
+ // statechart.js
2407
+
2408
+ MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
2409
+ rootState: SC.State.extend({
2410
+ initialSubstate: 'stateA',
2411
+ stateA: SC.State.plugin('MyApp.StateA'),
2412
+ stateB: SC.State.plugin('MyApp.StateB')
2413
+ })
2414
+ });
2415
+
2416
+ Using state plug-in functionality is optional. If you use the plug-in feature you can break up your statechart
2417
+ into as many files as you see fit.
2418
+
2419
+ @author Michael Cohen
2420
+ */
2421
+
2422
+ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
2423
+
2424
+ // Walk like a duck
2425
+ isStatechart: true,
2426
+
2427
+ /**
2428
+ Indicates if this statechart has been initialized
2429
+
2430
+ @property {Boolean}
2431
+ */
2432
+ statechartIsInitialized: false,
2433
+
2434
+ /**
2435
+ The root state of this statechart. All statecharts must have a root state.
2436
+
2437
+ If this property is left unassigned then when the statechart is initialized
2438
+ it will used the rootStateExample, initialState, and statesAreConcurrent
2439
+ properties to construct a root state.
2440
+
2441
+ @see #rootStateExample
2442
+ @see #initialState
2443
+ @see #statesAreConcurrent
2444
+
2445
+ @property {SC.State}
2446
+ */
2447
+ rootState: null,
2448
+
2449
+ /**
2450
+ Represents the class used to construct a class that will be the root state for
2451
+ this statechart. The class assigned must derive from SC.State.
2452
+
2453
+ This property will only be used if the rootState property is not assigned.
2454
+
2455
+ @see #rootState
2456
+
2457
+ @property {SC.State}
2458
+ */
2459
+ rootStateExample: SC.State,
2460
+
2461
+ /**
2462
+ Indicates what state should be the intiail state of this statechart. The value
2463
+ assigned must be the name of a property on this object that represents a state.
2464
+ As well, the statesAreConcurrent must be set to false.
2465
+
2466
+ This property will only be used if the rootState property is not assigned.
2467
+
2468
+ @see #rootState
2469
+
2470
+ @property {String}
2471
+ */
2472
+ initialState: null,
2473
+
2474
+ /**
2475
+ Indicates if properties on this object representing states are concurrent to each other.
2476
+ If true then they are concurrent, otherwise they are not. If the true, then the
2477
+ initialState property must not be assigned.
2478
+
2479
+ This property will only be used if the rootState property is not assigned.
2480
+
2481
+ @see #rootState
2482
+
2483
+ @property {Boolean}
2484
+ */
2485
+ statesAreConcurrent: false,
2486
+
2487
+ /**
2488
+ Indicates whether to use a monitor to monitor that statechart's activities. If true then
2489
+ the monitor will be active, otherwise the monitor will not be used. Useful for debugging
2490
+ purposes.
2491
+
2492
+ @property {Boolean}
2493
+ */
2494
+ monitorIsActive: false,
2495
+
2496
+ /**
2497
+ A statechart monitor that can be used to monitor this statechart. Useful for debugging purposes.
2498
+ A monitor will only be used if monitorIsActive is true.
2499
+
2500
+ @property {SC.StatechartMonitor}
2501
+ */
2502
+ monitor: null,
2503
+
2504
+ /**
2505
+ Used to specify what property (key) on the statechart should be used as the trace property. By
2506
+ default the property is 'trace'.
2507
+
2508
+ @property {String}
2509
+ */
2510
+ statechartTraceKey: 'trace',
2511
+
2512
+ /**
2513
+ Indicates whether to trace the statecharts activities. If true then the statechart will output
2514
+ its activites to the browser's JS console. Useful for debugging purposes.
2515
+
2516
+ @see #statechartTraceKey
2517
+
2518
+ @property {Boolean}
2519
+ */
2520
+ trace: false,
2521
+
2522
+ /**
2523
+ Used to specify what property (key) on the statechart should be used as the owner property. By
2524
+ default the property is 'owner'.
2525
+
2526
+ @property {String}
2527
+ */
2528
+ statechartOwnerKey: 'owner',
2529
+
2530
+ /**
2531
+ Sets who the owner is of this statechart. If null then the owner is this object otherwise
2532
+ the owner is the assigned object.
2533
+
2534
+ @see #statechartOwnerKey
2535
+
2536
+ @property {SC.Object}
2537
+ */
2538
+ owner: null,
2539
+
2540
+ /**
2541
+ Indicates if the statechart should be automatically initialized by this
2542
+ object after it has been created. If true then initStatechart will be
2543
+ called automatically, otherwise it will not.
2544
+
2545
+ @property {Boolean}
2546
+ */
2547
+ autoInitStatechart: true,
2548
+
2549
+ /**
2550
+ If yes, any warning messages produced by the statechart or any of its states will
2551
+ not be logged, otherwise all warning messages will be logged.
2552
+
2553
+ While designing and debugging your statechart, it's best to keep this value false.
2554
+ In production you can then suppress the warning messages.
2555
+
2556
+ @property {Boolean}
2557
+ */
2558
+ suppressStatechartWarnings: false,
2559
+
2560
+ /**
2561
+ A statechart delegate used by the statechart and the states that the statechart
2562
+ manages. The value assigned must adhere to the {@link SC.StatechartDelegate} mixin.
2563
+
2564
+ @property {SC.Object}
2565
+
2566
+ @see SC.StatechartDelegate
2567
+ */
2568
+ delegate: null,
2569
+
2570
+ /**
2571
+ Computed property that returns an objects that adheres to the
2572
+ {@link SC.StatechartDelegate} mixin. If the {@link #delegate} is not
2573
+ assigned then this object is the default value returned.
2574
+
2575
+ @see SC.StatechartDelegate
2576
+ @see #delegate
2577
+ */
2578
+ statechartDelegate: function() {
2579
+ var del = this.get('delegate');
2580
+ return this.delegateFor('isStatechartDelegate', del);
2581
+ }.property('delegate'),
2582
+
2583
+ init: function() {
2584
+ if (get(this, 'autoInitStatechart')) {
2585
+ this.initStatechart();
2586
+ }
2587
+ },
2588
+
2589
+ destroy: function() {
2590
+ var root = get(this, 'rootState'),
2591
+ traceKey = get(this, 'statechartTraceKey');
2592
+
2593
+ SC.removeObserver(this, traceKey, this, '_statechartTraceDidChange');
2594
+
2595
+ root.destroy();
2596
+ set(this, 'rootState', null);
2597
+ this._super();
2598
+ },
2599
+
2600
+ /**
2601
+ Initializes the statechart. By initializing the statechart, it will create all the states and register
2602
+ them with the statechart. Once complete, the statechart can be used to go to states and send actions to.
2603
+ */
2604
+ initStatechart: function() {
2605
+ if (get(this, 'statechartIsInitialized')) return;
2606
+
2607
+ this._gotoStateLocked = false;
2608
+ this._sendActionLocked = false;
2609
+ this._pendingStateTransitions = [];
2610
+ this._pendingSentActions = [];
2611
+
2612
+ if (get(this, 'monitorIsActive')) {
2613
+ set(this, 'monitor', SC.StatechartMonitor.create({ statechart: this }));
2614
+ }
2615
+
2616
+ var traceKey = get(this, 'statechartTraceKey');
2617
+
2618
+ this.addObserver(traceKey, this, '_statechartTraceDidChange');
2619
+ this._statechartTraceDidChange();
2620
+
2621
+ var trace = get(this, 'allowStatechartTracing'),
2622
+ rootState = get(this, 'rootState'),
2623
+ msg;
2624
+
2625
+ if (trace) this.statechartLogTrace("BEGIN initialize statechart");
2626
+
2627
+ // If no root state was explicitly defined then try to construct
2628
+ // a root state class
2629
+ if (!rootState) {
2630
+ rootState = this._constructRootStateClass();
2631
+ }
2632
+ else if (SC.typeOf(rootState) === "function" && rootState.statePlugin) {
2633
+ rootState = rootState.apply(this);
2634
+ }
2635
+
2636
+ if (!(SC.State.detect(rootState) && rootState.isClass)) {
2637
+ msg = "Unable to initialize statechart. Root state must be a state class";
2638
+ this.statechartLogError(msg);
2639
+ throw msg;
2640
+ }
2641
+
2642
+ rootState = this.createRootState(rootState, {
2643
+ statechart: this,
2644
+ stateName: SC.ROOT_STATE_NAME
2645
+ });
2646
+
2647
+ set(this, 'rootState', rootState);
2648
+ rootState.initState();
2649
+
2650
+ if (SC.EmptyState.detect(get(rootState, 'initialSubstate'))) {
2651
+ msg = "Unable to initialize statechart. Root state must have an initial substate explicilty defined";
2652
+ this.statechartLogError(msg);
2653
+ throw msg;
2654
+ }
2655
+
2656
+ if (!SC.empty(get(this, 'initialState'))) {
2657
+ var key = 'initialState';
2658
+ set(this, key, get(rootState, get(this, key)));
2659
+ }
2660
+
2661
+ set(this, 'statechartIsInitialized', true);
2662
+ this.gotoState(rootState);
2663
+
2664
+ if (trace) this.statechartLogTrace("END initialize statechart");
2665
+ },
2666
+
2667
+ /**
2668
+ Will create a root state for the statechart
2669
+ */
2670
+ createRootState: function(state, attrs) {
2671
+ if (!attrs) attrs = {};
2672
+ state = state.create(attrs);
2673
+ return state;
2674
+ },
2675
+
2676
+ /**
2677
+ Returns an array of all the current states for this statechart
2678
+
2679
+ @returns {Array} the current states
2680
+ */
2681
+ currentStates: function() {
2682
+ return getPath(this, 'rootState.currentSubstates');
2683
+ }.property().cacheable(),
2684
+
2685
+ /**
2686
+ Returns the first current state for this statechart.
2687
+
2688
+ @return {SC.State}
2689
+ */
2690
+ firstCurrentState: function() {
2691
+ var cs = get(this, 'currentStates');
2692
+ return cs ? cs.objectAt(0) : null;
2693
+ }.property('currentStates').cacheable(),
2694
+
2695
+ /**
2696
+ Returns the count of the current states for this statechart
2697
+
2698
+ @returns {Number} the count
2699
+ */
2700
+ currentStateCount: function() {
2701
+ return getPath(this, 'currentStates.length');
2702
+ }.property('currentStates').cacheable(),
2703
+
2704
+ /**
2705
+ Checks if a given state is a current state of this statechart.
2706
+
2707
+ @param state {State|String} the state to check
2708
+ @returns {Boolean} true if the state is a current state, otherwise fals is returned
2709
+ */
2710
+ stateIsCurrentState: function(state) {
2711
+ return get(this, 'rootState').stateIsCurrentSubstate(state);
2712
+ },
2713
+
2714
+ /**
2715
+ Returns an array of all the states that are currently entered for
2716
+ this statechart.
2717
+
2718
+ @returns {Array} the currently entered states
2719
+ */
2720
+ enteredStates: function() {
2721
+ return getPath(this, 'rootState.enteredSubstates');
2722
+ }.property().cacheable(),
2723
+
2724
+ /**
2725
+ Checks if a given state is a currently entered state of this statechart.
2726
+
2727
+ @param state {State|String} the state to check
2728
+ @returns {Boolean} true if the state is a currently entered state, otherwise false is returned
2729
+ */
2730
+ stateIsEntered: function(state) {
2731
+ return get(this, 'rootState').stateIsEnteredSubstate(state);
2732
+ },
2733
+
2734
+ /**
2735
+ Checks if the given value represents a state is this statechart
2736
+
2737
+ @param value {State|String} either a state object or the name of a state
2738
+ @returns {Boolean} true if the state does belong ot the statechart, otherwise false is returned
2739
+ */
2740
+ doesContainState: function(value) {
2741
+ return !SC.none(this.getState(value));
2742
+ },
2743
+
2744
+ /**
2745
+ Gets a state from the statechart that matches the given value
2746
+
2747
+ @param value {State|String} either a state object of the name of a state
2748
+ @returns {State} if a match then the matching state is returned, otherwise null is returned
2749
+ */
2750
+ getState: function(value) {
2751
+ return get(this, 'rootState').getSubstate(value);
2752
+ },
2753
+
2754
+ /**
2755
+ When called, the statechart will proceed with making state transitions in the statechart starting from
2756
+ a current state that meet the statechart conditions. When complete, some or all of the statechart's
2757
+ current states will be changed, and all states that were part of the transition process will either
2758
+ be exited or entered in a specific order.
2759
+
2760
+ The state that is given to go to will not necessarily be a current state when the state transition process
2761
+ is complete. The final state or states are dependent on factors such an initial substates, concurrent
2762
+ states, and history states.
2763
+
2764
+ Because the statechart can have one or more current states, it may be necessary to indicate what current state
2765
+ to start from. If no current state to start from is provided, then the statechart will default to using
2766
+ the first current state that it has; depending of the make up of the statechart (no concurrent state vs.
2767
+ with concurrent states), the outcome may be unexpected. For a statechart with concurrent states, it is best
2768
+ to provide a current state in which to start from.
2769
+
2770
+ When using history states, the statechart will first make transitions to the given state and then use that
2771
+ state's history state and recursively follow each history state's history state until there are no
2772
+ more history states to follow. If the given state does not have a history state, then the statechart
2773
+ will continue following state transition procedures.
2774
+
2775
+ Method can be called in the following ways:
2776
+
2777
+ // With one argument.
2778
+ gotoState(<state>)
2779
+
2780
+ // With two argument.
2781
+ gotoState(<state>, <state | boolean | hash>)
2782
+
2783
+ // With three argument.
2784
+ gotoState(<state>, <state>, <boolean | hash>)
2785
+ gotoState(<state>, <boolean>, <hash>)
2786
+
2787
+ // With four argument.
2788
+ gotoState(<state>, <state>, <boolean>, <hash>)
2789
+
2790
+ where <state> is either a SC.State object or a string and <hash> is a regular JS hash object.
2791
+
2792
+ @param state {SC.State|String} the state to go to (may not be the final state in the transition process)
2793
+ @param fromCurrentState {SC.State|String} Optional. The current state to start the transition process from.
2794
+ @param useHistory {Boolean} Optional. Indicates whether to include using history states in the transition process
2795
+ @param context {Hash} Optional. A context object that will be passed to all exited and entered states
2796
+ */
2797
+ gotoState: function(state, fromCurrentState, useHistory, context) {
2798
+
2799
+ if (!get(this, 'statechartIsInitialized')) {
2800
+ this.statechartLogError("can not go to state %@. statechart has not yet been initialized".fmt(state));
2801
+ return;
2802
+ }
2803
+
2804
+ if (get(this, 'isDestroyed')) {
2805
+ this.statechartLogError("can not go to state %@. statechart is destroyed".fmt(this));
2806
+ return;
2807
+ }
2808
+
2809
+ var args = this._processGotoStateArgs(arguments);
2810
+
2811
+ state = args.state;
2812
+ fromCurrentState = args.fromCurrentState;
2813
+ useHistory = args.useHistory;
2814
+ context = args.context;
2815
+
2816
+ var pivotState = null,
2817
+ exitStates = [],
2818
+ enterStates = [],
2819
+ trace = get(this, 'allowStatechartTracing'),
2820
+ rootState = get(this, 'rootState'),
2821
+ paramState = state,
2822
+ paramFromCurrentState = fromCurrentState,
2823
+ msg;
2824
+
2825
+ var stateObject = rootState.getSubstate(state);
2826
+
2827
+ if (SC.none(stateObject)) {
2828
+ this.statechartLogError("Can not to goto state %@. Not a recognized state in statechart".fmt(paramState));
2829
+ return;
2830
+ }
2831
+
2832
+ if (this._gotoStateLocked) {
2833
+ // There is a state transition currently happening. Add this requested state
2834
+ // transition to the queue of pending state transitions. The request will
2835
+ // be invoked after the current state transition is finished.
2836
+ this._pendingStateTransitions.push({
2837
+ state: stateObject,
2838
+ fromCurrentState: fromCurrentState,
2839
+ useHistory: useHistory,
2840
+ context: context
2841
+ });
2842
+
2843
+ return;
2844
+ }
2845
+
2846
+ // Lock the current state transition so that no other requested state transition
2847
+ // interferes.
2848
+ this._gotoStateLocked = true;
2849
+
2850
+ if (!SC.none(fromCurrentState)) {
2851
+ // Check to make sure the current state given is actually a current state of this statechart
2852
+ fromCurrentState = rootState.getSubstate(fromCurrentState);
2853
+ if (SC.none(fromCurrentState) || !get(fromCurrentState, 'isCurrentState')) {
2854
+ msg = "Can not to goto state %@. %@ is not a recognized current state in statechart";
2855
+ this.statechartLogError(msg.fmt(paramState, paramFromCurrentState));
2856
+ this._gotoStateLocked = false;
2857
+ return;
2858
+ }
2859
+ }
2860
+ else if (getPath(this, 'currentStates.length') > 0) {
2861
+ // No explicit current state to start from; therefore, just use the first current state as
2862
+ // a default, if there is a current state.
2863
+ fromCurrentState = get(this, 'currentStates')[0];
2864
+ msg = "gotoState: fromCurrentState not explicitly provided. Using a default current state to transition from: %@";
2865
+ this.statechartLogWarning(msg.fmt(fromCurrentState));
2866
+ }
2867
+
2868
+ if (trace) {
2869
+ this.statechartLogTrace("BEGIN gotoState: %@".fmt(stateObject));
2870
+ msg = "starting from current state: %@";
2871
+ msg = msg.fmt(fromCurrentState ? fromCurrentState : '---');
2872
+ this.statechartLogTrace(msg);
2873
+ msg = "current states before: %@";
2874
+ msg = msg.fmt(getPath(this, 'currentStates.length') > 0 ? get(this, 'currentStates') : '---');
2875
+ this.statechartLogTrace(msg);
2876
+ }
2877
+
2878
+ // If there is a current state to start the transition process from, then determine what
2879
+ // states are to be exited
2880
+ if (!SC.none(fromCurrentState)) {
2881
+ exitStates = this._createStateChain(fromCurrentState);
2882
+ }
2883
+
2884
+ // Now determine the initial states to be entered
2885
+ enterStates = this._createStateChain(stateObject);
2886
+
2887
+ // Get the pivot state to indicate when to go from exiting states to entering states
2888
+ pivotState = this._findPivotState(exitStates, enterStates);
2889
+
2890
+ if (pivotState) {
2891
+ if (trace) this.statechartLogTrace("pivot state = %@".fmt(pivotState));
2892
+ if (get(pivotState, 'substatesAreConcurrent')) {
2893
+ this.statechartLogError("Can not go to state %@ from %@. Pivot state %@ has concurrent substates.".fmt(stateObject, fromCurrentState, pivotState));
2894
+ this._gotoStateLocked = false;
2895
+ return;
2896
+ }
2897
+ }
2898
+
2899
+ // Collect what actions to perform for the state transition process
2900
+ var gotoStateActions = [];
2901
+
2902
+ // Go ahead and find states that are to be exited
2903
+ this._traverseStatesToExit(exitStates.shift(), exitStates, pivotState, gotoStateActions);
2904
+
2905
+ // Now go find states that are to entered
2906
+ if (pivotState !== stateObject) {
2907
+ this._traverseStatesToEnter(enterStates.pop(), enterStates, pivotState, useHistory, gotoStateActions);
2908
+ } else {
2909
+ this._traverseStatesToExit(pivotState, [], null, gotoStateActions);
2910
+ this._traverseStatesToEnter(pivotState, null, null, useHistory, gotoStateActions);
2911
+ }
2912
+
2913
+ // Collected all the state transition actions to be performed. Now execute them.
2914
+ this._executeGotoStateActions(stateObject, gotoStateActions, null, context);
2915
+ },
2916
+
2917
+ /**
2918
+ Indicates if the statechart is in an active goto state process
2919
+ */
2920
+ gotoStateActive: function() {
2921
+ return this._gotoStateLocked;
2922
+ }.property(),
2923
+
2924
+ /**
2925
+ Indicates if the statechart is in an active goto state process
2926
+ that has been suspended
2927
+ */
2928
+ gotoStateSuspended: function() {
2929
+ return this._gotoStateLocked && !!this._gotoStateSuspendedPoint;
2930
+ }.property(),
2931
+
2932
+ /**
2933
+ Resumes an active goto state transition process that has been suspended.
2934
+ */
2935
+ resumeGotoState: function() {
2936
+ if (!get(this, 'gotoStateSuspended')) {
2937
+ this.statechartLogError("Can not resume goto state since it has not been suspended");
2938
+ return;
2939
+ }
2940
+
2941
+ var point = this._gotoStateSuspendedPoint;
2942
+ this._executeGotoStateActions(point.gotoState, point.actions, point.marker, point.context);
2943
+ },
2944
+
2945
+ /** @private */
2946
+ _executeGotoStateActions: function(gotoState, actions, marker, context) {
2947
+ var action = null,
2948
+ len = actions.length,
2949
+ actionResult = null;
2950
+
2951
+ marker = SC.none(marker) ? 0 : marker;
2952
+
2953
+ for (; marker < len; marker += 1) {
2954
+ action = actions[marker];
2955
+ switch (action.action) {
2956
+ case SC.EXIT_STATE:
2957
+ actionResult = this._exitState(action.state, context);
2958
+ break;
2959
+
2960
+ case SC.ENTER_STATE:
2961
+ actionResult = this._enterState(action.state, action.currentState, context);
2962
+ break;
2963
+ }
2964
+
2965
+ //
2966
+ // Check if the state wants to perform an asynchronous action during
2967
+ // the state transition process. If so, then we need to first
2968
+ // suspend the state transition process and then invoke the
2969
+ // asynchronous action. Once called, it is then up to the state or something
2970
+ // else to resume this statechart's state transition process by calling the
2971
+ // statechart's resumeGotoState method.
2972
+ //
2973
+ if (actionResult instanceof SC.Async) {
2974
+ this._gotoStateSuspendedPoint = {
2975
+ gotoState: gotoState,
2976
+ actions: actions,
2977
+ marker: marker + 1,
2978
+ context: context
2979
+ };
2980
+
2981
+ actionResult.tryToPerform(action.state);
2982
+ return;
2983
+ }
2984
+ }
2985
+
2986
+ this.beginPropertyChanges();
2987
+ this.notifyPropertyChange('currentStates');
2988
+ this.notifyPropertyChange('enteredStates');
2989
+ this.endPropertyChanges();
2990
+
2991
+ if (get(this, 'allowStatechartTracing')) {
2992
+ this.statechartLogTrace("current states after: %@".fmt(get(this, 'currentStates')));
2993
+ this.statechartLogTrace("END gotoState: %@".fmt(gotoState));
2994
+ }
2995
+
2996
+ this._cleanupStateTransition();
2997
+ },
2998
+ /** @private */
2999
+ _cleanupStateTransition: function() {
3000
+ this._currentGotoStateAction = null;
3001
+ this._gotoStateSuspendedPoint = null;
3002
+ this._gotoStateActions = null;
3003
+ this._gotoStateLocked = NO;
3004
+ this._flushPendingStateTransition();
3005
+ },
3006
+
3007
+ /** @private */
3008
+ _exitState: function(state, context) {
3009
+ var parentState;
3010
+
3011
+ if (get(state, 'currentSubstates').indexOf(state) >= 0) {
3012
+ parentState = get(state, 'parentState');
3013
+ while (parentState) {
3014
+ get(parentState, 'currentSubstates').removeObject(state);
3015
+ parentState = get(parentState, 'parentState');
3016
+ }
3017
+ }
3018
+
3019
+ parentState = state;
3020
+ while (parentState) {
3021
+ get(parentState, 'enteredSubstates').removeObject(state);
3022
+ parentState = get(parentState, 'parentState');
3023
+ }
3024
+
3025
+ if (get(this, 'allowStatechartTracing')) this.statechartLogTrace("<-- exiting state: %@".fmt(state));
3026
+
3027
+ set(state, 'currentSubstates', []);
3028
+ state.notifyPropertyChange('isCurrentState');
3029
+
3030
+ state.stateWillBecomeExited(context);
3031
+ var result = this.exitState(state, context);
3032
+ state.stateDidBecomeExited(context);
3033
+
3034
+ if (get(this, 'monitorIsActive')) get(this, 'monitor').pushExitedState(state);
3035
+
3036
+ state._traverseStatesToExit_skipState = false;
3037
+
3038
+ return result;
3039
+ },
3040
+
3041
+ /**
3042
+ What will actually invoke a state's exitState method.
3043
+
3044
+ Called during the state transition process whenever the gotoState method is
3045
+ invoked.
3046
+
3047
+ @param state {SC.State} the state whose enterState method is to be invoked
3048
+ @param context {Hash} a context hash object to provide the enterState method
3049
+ */
3050
+ exitState: function(state, context) {
3051
+ return state.exitState(context);
3052
+ },
3053
+
3054
+ /** @private */
3055
+ _enterState: function(state, current, context) {
3056
+ var parentState = get(state, 'parentState');
3057
+ if (parentState && !get(state, 'isConcurrentState')) set(parentState, 'historyState', state);
3058
+
3059
+ if (current) {
3060
+ parentState = state;
3061
+ while (parentState) {
3062
+ get(parentState, 'currentSubstates').pushObject(state);
3063
+ parentState = get(parentState, 'parentState');
3064
+ }
3065
+ }
3066
+
3067
+ parentState = state;
3068
+ while (parentState) {
3069
+ get(parentState, 'enteredSubstates').pushObject(state);
3070
+ parentState = get(parentState, 'parentState');
3071
+ }
3072
+
3073
+ if (get(this, 'allowStatechartTracing')) this.statechartLogTrace("--> entering state: %@".fmt(state));
3074
+
3075
+ state.notifyPropertyChange('isCurrentState');
3076
+
3077
+ state.stateWillBecomeEntered(context);
3078
+ var result = this.enterState(state, context);
3079
+ state.stateDidBecomeEntered(context);
3080
+
3081
+ if (get(this, 'monitorIsActive')) get(this, 'monitor').pushEnteredState(state);
3082
+
3083
+ return result;
3084
+ },
3085
+
3086
+ /**
3087
+ What will actually invoke a state's enterState method.
3088
+
3089
+ Called during the state transition process whenever the gotoState method is
3090
+ invoked.
3091
+
3092
+ @param state {SC.State} the state whose enterState method is to be invoked
3093
+ @param context {Hash} a context hash object to provide the enterState method
3094
+ */
3095
+ enterState: function(state, context) {
3096
+ if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) {
3097
+ return state.enterStateByRoute(context);
3098
+ } else {
3099
+ return state.enterState(context);
3100
+ }
3101
+ },
3102
+
3103
+ /**
3104
+ When called, the statechart will proceed to make transitions to the given state then follow that
3105
+ state's history state.
3106
+
3107
+ You can either go to a given state's history recursively or non-recursively. To go to a state's history
3108
+ recursively means to following each history state's history state until no more history states can be
3109
+ followed. Non-recursively means to just to the given state's history state but do not recusively follow
3110
+ history states. If the given state does not have a history state, then the statechart will just follow
3111
+ normal procedures when making state transitions.
3112
+
3113
+ Because a statechart can have one or more current states, depending on if the statechart has any concurrent
3114
+ states, it is optional to provided current state in which to start the state transition process from. If no
3115
+ current state is provided, then the statechart will default to the first current state that it has; which,
3116
+ depending on the make up of that statechart, can lead to unexpected outcomes. For a statechart with concurrent
3117
+ states, it is best to explicitly supply a current state.
3118
+
3119
+ Method can be called in the following ways:
3120
+
3121
+ // With one arguments.
3122
+ gotoHistoryState(<state>)
3123
+
3124
+ // With two arguments.
3125
+ gotoHistoryState(<state>, <state | boolean | hash>)
3126
+
3127
+ // With three arguments.
3128
+ gotoHistoryState(<state>, <state>, <boolean | hash>)
3129
+ gotoHistoryState(<state>, <boolean>, <hash>)
3130
+
3131
+ // With four argumetns
3132
+ gotoHistoryState(<state>, <state>, <boolean>, <hash>)
3133
+
3134
+ where <state> is either a SC.State object or a string and <hash> is a regular JS hash object.
3135
+
3136
+ @param state {SC.State|String} the state to go to and follow it's history state
3137
+ @param fromCurrentState {SC.State|String} Optional. the current state to start the state transition process from
3138
+ @param recursive {Boolean} Optional. whether to follow history states recursively.
3139
+ */
3140
+ gotoHistoryState: function(state, fromCurrentState, recursive, context) {
3141
+ if (!get(this, 'statechartIsInitialized')) {
3142
+ this.statechartLogError("can not go to state %@'s history state. Statechart has not yet been initialized".fmt(state));
3143
+ return;
3144
+ }
3145
+
3146
+ var args = this._processGotoStateArgs(arguments);
3147
+
3148
+ state = args.state;
3149
+ fromCurrentState = args.fromCurrentState;
3150
+ recursive = args.useHistory;
3151
+ context = args.context;
3152
+
3153
+ state = this.getState(state);
3154
+
3155
+ if (!state) {
3156
+ this.statechartLogError("Can not to goto state %@'s history state. Not a recognized state in statechart".fmt(state));
3157
+ return;
3158
+ }
3159
+
3160
+ var historyState = get(state, 'historyState');
3161
+
3162
+ if (!recursive) {
3163
+ if (historyState) {
3164
+ this.gotoState(historyState, fromCurrentState, context);
3165
+ } else {
3166
+ this.gotoState(state, fromCurrentState, context);
3167
+ }
3168
+ } else {
3169
+ this.gotoState(state, fromCurrentState, true, context);
3170
+ }
3171
+ },
3172
+
3173
+ /**
3174
+ Sends a given action to all the statechart's current states.
3175
+
3176
+ If a current state does can not respond to the sent action, then the current state's parent state
3177
+ will be tried. This process is recursively done until no more parent state can be tried.
3178
+
3179
+ @param action {String} name of the action
3180
+ @param arg1 {Object} optional argument
3181
+ @param arg2 {Object} optional argument
3182
+ @returns {SC.Responder} the responder that handled it or null
3183
+ */
3184
+ sendAction: function(action, arg1, arg2) {
3185
+
3186
+ if (get(this, 'isDestroyed')) {
3187
+ this.statechartLogError("can send action %@. statechart is destroyed".fmt(action));
3188
+ return;
3189
+ }
3190
+
3191
+ var statechartHandledAction = false,
3192
+ actionHandled = false,
3193
+ currentStates = get(this, 'currentStates').slice(),
3194
+ checkedStates = {},
3195
+ len = 0,
3196
+ i = 0,
3197
+ state = null,
3198
+ trace = get(this, 'allowStatechartTracing');
3199
+
3200
+ if (this._sendActionLocked || this._gotoStateLocked) {
3201
+ // Want to praction any actions from being processed by the states until
3202
+ // they have had a chance to handle the most immediate action or completed
3203
+ // a state transition
3204
+ this._pendingSentActions.push({
3205
+ action: action,
3206
+ arg1: arg1,
3207
+ arg2: arg2
3208
+ });
3209
+
3210
+ return;
3211
+ }
3212
+
3213
+ this._sendActionLocked = true;
3214
+
3215
+ if (trace) {
3216
+ this.statechartLogTrace("BEGIN sendAction: action<%@>".fmt(action));
3217
+ }
3218
+
3219
+ len = get(currentStates, 'length');
3220
+ for (; i < len; i += 1) {
3221
+ actionHandled = false;
3222
+ state = currentStates[i];
3223
+ if (!get(state, 'isCurrentState')) continue;
3224
+ while (!actionHandled && state) {
3225
+ if (!checkedStates[get(state, 'fullPath')]) {
3226
+ actionHandled = state.tryToHandleAction(action, arg1, arg2);
3227
+ checkedStates[get(state, 'fullPath')] = true;
3228
+ }
3229
+ if (!actionHandled) state = get(state, 'parentState');
3230
+ else statechartHandledAction = true;
3231
+ }
3232
+ }
3233
+
3234
+ // Now that all the states have had a chance to process the
3235
+ // first action, we can go ahead and flush any pending sent actions.
3236
+ this._sendActionLocked = false;
3237
+
3238
+ if (trace) {
3239
+ if (!statechartHandledAction) this.statechartLogTrace("No state was able handle action %@".fmt(action));
3240
+ this.statechartLogTrace("END sendAction: action<%@>".fmt(action));
3241
+ }
3242
+
3243
+ var result = this._flushPendingSentActions();
3244
+
3245
+ return statechartHandledAction ? this : (result ? this : null);
3246
+ },
3247
+
3248
+ /** @private
3249
+
3250
+ Creates a chain of states from the given state to the greatest ancestor state (the root state). Used
3251
+ when perform state transitions.
3252
+ */
3253
+ _createStateChain: function(state) {
3254
+ var chain = [];
3255
+
3256
+ while (state) {
3257
+ chain.push(state);
3258
+ state = get(state, 'parentState');
3259
+ }
3260
+
3261
+ return chain;
3262
+ },
3263
+
3264
+ /** @private
3265
+
3266
+ Finds a pivot state from two given state chains. The pivot state is the state indicating when states
3267
+ go from being exited to states being entered during the state transition process. The value
3268
+ returned is the fist matching state between the two given state chains.
3269
+ */
3270
+ _findPivotState: function(stateChain1, stateChain2) {
3271
+ if (stateChain1.length === 0 || stateChain2.length === 0) return null;
3272
+
3273
+ var pivot = stateChain1.find(function(state, index) {
3274
+ if (stateChain2.indexOf(state) >= 0) return true;
3275
+ });
3276
+
3277
+ return pivot;
3278
+ },
3279
+
3280
+ /** @private
3281
+
3282
+ Recursively follow states that are to be exited during a state transition process. The exit
3283
+ process is to start from the given state and work its way up to when either all exit
3284
+ states have been reached based on a given exit path or when a stop state has been reached.
3285
+
3286
+ @param state {State} the state to be exited
3287
+ @param exitStatePath {Array} an array representing a path of states that are to be exited
3288
+ @param stopState {State} an explicit state in which to stop the exiting process
3289
+ */
3290
+ _traverseStatesToExit: function(state, exitStatePath, stopState, gotoStateActions) {
3291
+ if (!state || state === stopState) return;
3292
+
3293
+ var trace = get(this, 'allowStatechartTracing');
3294
+
3295
+ // This state has concurrent substates. Therefore we have to make sure we
3296
+ // exit them up to this state before we can go any further up the exit chain.
3297
+ if (get(state, 'substatesAreConcurrent')) {
3298
+ var i = 0,
3299
+ currentSubstates = get(state, 'currentSubstates'),
3300
+ len = currentSubstates.length,
3301
+ currentState = null;
3302
+
3303
+ for (; i < len; i += 1) {
3304
+ currentState = currentSubstates[i];
3305
+ if (currentState._traverseStatesToExit_skipState === true) continue;
3306
+ var chain = this._createStateChain(currentState);
3307
+ this._traverseStatesToExit(chain.shift(), chain, state, gotoStateActions);
3308
+ }
3309
+ }
3310
+
3311
+ gotoStateActions.push({ action: SC.EXIT_STATE, state: state });
3312
+ if (get(state, 'isCurrentState')) state._traverseStatesToExit_skipState = true;
3313
+ this._traverseStatesToExit(exitStatePath.shift(), exitStatePath, stopState, gotoStateActions);
3314
+ },
3315
+
3316
+ /** @private
3317
+
3318
+ Recursively follow states that are to be entred during the state transition process. The
3319
+ enter process is to start from the given state and work its way down a given enter path. When
3320
+ the end of enter path has been reached, then continue entering states based on whether
3321
+ an initial substate is defined, there are concurrent substates or history states are to be
3322
+ followed; when none of those condition are met then the enter process is done.
3323
+
3324
+ @param state {State} the sate to be entered
3325
+ @param enterStatePath {Array} an array representing an initial path of states that are to be entered
3326
+ @param pivotState {State} The state pivoting when to go from exiting states to entering states
3327
+ @param useHistory {Boolean} indicates whether to recursively follow history states
3328
+ */
3329
+ _traverseStatesToEnter: function(state, enterStatePath, pivotState, useHistory, gotoStateActions) {
3330
+ if (!state) return;
3331
+
3332
+ var trace = get(this, 'allowStatechartTracing');
3333
+
3334
+ // We do not want to enter states in the enter path until the pivot state has been reached. After
3335
+ // the pivot state has been reached, then we can go ahead and actually enter states.
3336
+ if (pivotState) {
3337
+ if (state !== pivotState) {
3338
+ this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, pivotState, useHistory, gotoStateActions);
3339
+ } else {
3340
+ this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, null, useHistory, gotoStateActions);
3341
+ }
3342
+ }
3343
+
3344
+ // If no more explicit enter path instructions, then default to enter states based on
3345
+ // other criteria
3346
+ else if (!enterStatePath || enterStatePath.length === 0) {
3347
+ var gotoStateAction = { action: SC.ENTER_STATE, state: state, currentState: false };
3348
+ gotoStateActions.push(gotoStateAction);
3349
+
3350
+ var initialSubstate = get(state, 'initialSubstate'),
3351
+ historyState = get(state, 'historyState');
3352
+
3353
+ // State has concurrent substates. Need to enter all of the substates
3354
+ if (get(state, 'substatesAreConcurrent')) {
3355
+ this._traverseConcurrentStatesToEnter(get(state, 'substates'), null, useHistory, gotoStateActions);
3356
+ }
3357
+
3358
+ // State has substates and we are instructed to recursively follow the state's
3359
+ // history state if it has one.
3360
+ else if (get(state, 'hasSubstates') && historyState && useHistory) {
3361
+ this._traverseStatesToEnter(historyState, null, null, useHistory, gotoStateActions);
3362
+ }
3363
+
3364
+ // State has an initial substate to enter
3365
+ else if (initialSubstate) {
3366
+ if (initialSubstate instanceof SC.HistoryState) {
3367
+ if (!useHistory) useHistory = get(initialSubstate, 'isRecursive');
3368
+ initialSubstate = get(initialSubstate, 'state');
3369
+ }
3370
+ this._traverseStatesToEnter(initialSubstate, null, null, useHistory, gotoStateActions);
3371
+ }
3372
+
3373
+ // Looks like we hit the end of the road. Therefore the state has now become
3374
+ // a current state of the statechart.
3375
+ else {
3376
+ gotoStateAction.currentState = true;
3377
+ }
3378
+ }
3379
+
3380
+ // Still have an explicit enter path to follow, so keep moving through the path.
3381
+ else if (enterStatePath.length > 0) {
3382
+ gotoStateActions.push({ action: SC.ENTER_STATE, state: state });
3383
+ var nextState = enterStatePath.pop();
3384
+ this._traverseStatesToEnter(nextState, enterStatePath, null, useHistory, gotoStateActions);
3385
+
3386
+ // We hit a state that has concurrent substates. Must go through each of the substates
3387
+ // and enter them
3388
+ if (get(state, 'substatesAreConcurrent')) {
3389
+ this._traverseConcurrentStatesToEnter(get(state, 'substates'), nextState, useHistory, gotoStateActions);
3390
+ }
3391
+ }
3392
+ },
3393
+
3394
+ /** @override
3395
+
3396
+ Returns true if the named value translates into an executable function on
3397
+ any of the statechart's current states or the statechart itself.
3398
+
3399
+ @param action {String} the property name to check
3400
+ @returns {Boolean}
3401
+ */
3402
+ respondsTo: function(action) {
3403
+ var currentStates = get(this, 'currentStates'),
3404
+ len = get(currentStates, 'length'),
3405
+ i = 0, state = null;
3406
+
3407
+ for (; i < len; i += 1) {
3408
+ state = currentStates.objectAt(i);
3409
+ while (state) {
3410
+ if (state.respondsToAction(action)) return true;
3411
+ state = get(state, 'parentState');
3412
+ }
3413
+ }
3414
+
3415
+ // None of the current states can respond. Now check the statechart itself
3416
+ return SC.typeOf(this[action]) === "function";
3417
+ },
3418
+
3419
+ /** @override
3420
+
3421
+ Attemps to handle a given action against any of the statechart's current states and the
3422
+ statechart itself. If any current state can handle the action or the statechart itself can
3423
+ handle the action then true is returned, otherwise false is returned.
3424
+
3425
+ @param action {String} what to perform
3426
+ @param arg1 {Object} Optional
3427
+ @param arg2 {Object} Optional
3428
+ @returns {Boolean} true if handled, false if not handled
3429
+ */
3430
+ tryToPerform: function(action, arg1, arg2) {
3431
+ if (this.respondsTo(action)) {
3432
+ if (SC.typeOf(this[action]) === "function") return (this[action](arg1, arg2) !== false);
3433
+ else return !!this.sendAction(action, arg1, arg2);
3434
+ } return false;
3435
+ },
3436
+
3437
+ /**
3438
+ Used to invoke a method on current states. If the method can not be executed
3439
+ on a current state, then the state's parent states will be tried in order
3440
+ of closest ancestry.
3441
+
3442
+ A few notes:
3443
+
3444
+ 1. Calling this is not the same as calling sendAction or sendAction.
3445
+ Rather, this should be seen as calling normal methods on a state that
3446
+ will *not* call gotoState or gotoHistoryState.
3447
+ 2. A state will only ever be invoked once per call. So if there are two
3448
+ or more current states that have the same parent state, then that parent
3449
+ state will only be invoked once if none of the current states are able
3450
+ to invoke the given method.
3451
+
3452
+ When calling this method, you are able to supply zero ore more arguments
3453
+ that can be pass onto the method called on the states. As an example
3454
+
3455
+ invokeStateMethod('render', context, firstTime);
3456
+
3457
+ The above call will invoke the render method on the current states
3458
+ and supply the context and firstTime arguments to the method.
3459
+
3460
+ Because a statechart can have more than one current state and the method
3461
+ invoked may return a value, the addition of a callback function may be provided
3462
+ in order to handle the returned value for each state. As an example, let's say
3463
+ we want to call a calculate method on the current states where the method
3464
+ will return a value when invoked. We can handle the returned values like so:
3465
+
3466
+ invokeStateMethod('calculate', value, function(state, result) {
3467
+ // .. handle the result returned from calculate that was invoked
3468
+ // on the given state
3469
+ })
3470
+
3471
+ If the method invoked does not return a value and a callback function is
3472
+ supplied, then result value will simply be undefined. In all cases, if
3473
+ a callback function is given, it must be the last value supplied to this
3474
+ method.
3475
+
3476
+ invokeStateMethod will return a value if only one state was able to have
3477
+ the given method invoked on it, otherwise no value is returned.
3478
+
3479
+ @param methodName {String} methodName a method name
3480
+ @param args {Object...} Optional. any additional arguments
3481
+ @param func {Function} Optional. a callback function. Must be the last
3482
+ value supplied if provided.
3483
+
3484
+ @returns a value if the number of current states is one, otherwise undefined
3485
+ is returned. The value is the result of the method that got invoked
3486
+ on a state.
3487
+ */
3488
+ invokeStateMethod: function(methodName, args, func) {
3489
+ if (methodName === 'unknownAction') {
3490
+ this.statechartLogError("can not invoke method unkownAction");
3491
+ return;
3492
+ }
3493
+
3494
+ args = Array.prototype.slice.call(arguments); args.shift();
3495
+
3496
+ var len = args.length,
3497
+ arg = len > 0 ? args[len - 1] : null,
3498
+ callback = SC.typeOf(arg) === "function" ? args.pop() : null,
3499
+ currentStates = get(this, 'currentStates'),
3500
+ i = 0, state = null, checkedStates = {},
3501
+ method, result = undefined, calledStates = 0;
3502
+
3503
+ len = get(currentStates, 'length');
3504
+
3505
+ for (; i < len; i += 1) {
3506
+ state = currentStates.objectAt(i);
3507
+ while (state) {
3508
+ if (checkedStates[get(state, 'fullPath')]) break;
3509
+ checkedStates[get(state, 'fullPath')] = true;
3510
+ method = state[methodName];
3511
+ if (SC.typeOf(method) === "function" && !method.isActionHandler) {
3512
+ result = method.apply(state, args);
3513
+ if (callback) callback.call(this, state, result);
3514
+ calledStates += 1;
3515
+ break;
3516
+ }
3517
+ state = get(state, 'parentState');
3518
+ }
3519
+ }
3520
+
3521
+ return calledStates === 1 ? result : undefined;
3522
+ },
3523
+
3524
+ /** @private
3525
+
3526
+ Iterate over all the given concurrent states and enter them
3527
+ */
3528
+ _traverseConcurrentStatesToEnter: function(states, exclude, useHistory, gotoStateActions) {
3529
+ var i = 0,
3530
+ len = states.length,
3531
+ state = null;
3532
+
3533
+ for (; i < len; i += 1) {
3534
+ state = states[i];
3535
+ if (state !== exclude) this._traverseStatesToEnter(state, null, null, useHistory, gotoStateActions);
3536
+ }
3537
+ },
3538
+
3539
+ /** @private
3540
+
3541
+ Called by gotoState to flush a pending state transition at the front of the
3542
+ pending queue.
3543
+ */
3544
+ _flushPendingStateTransition: function() {
3545
+ if (!this._pendingStateTransitions) {
3546
+ this.statechartLogError("Unable to flush pending state transition. _pendingStateTransitions is invalid");
3547
+ return;
3548
+ }
3549
+ var pending = this._pendingStateTransitions.shift();
3550
+ if (!pending) return;
3551
+ this.gotoState(pending.state, pending.fromCurrentState, pending.useHistory, pending.context);
3552
+ },
3553
+
3554
+ /** @private
3555
+
3556
+ Called by sendAction to flush a pending actions at the front of the pending
3557
+ queue
3558
+ */
3559
+ _flushPendingSentActions: function() {
3560
+ var pending = this._pendingSentActions.shift();
3561
+ if (!pending) return null;
3562
+ return this.sendAction(pending.action, pending.arg1, pending.arg2);
3563
+ },
3564
+
3565
+ /** @private */
3566
+ _monitorIsActiveDidChange: function() {
3567
+ if (get(this, 'monitorIsActive') && SC.none(get(this, 'monitor'))) {
3568
+ set(this, 'monitor', SC.StatechartMonitor.create());
3569
+ }
3570
+ }.observes('monitorIsActive'),
3571
+
3572
+ /** @private
3573
+ Will process the arguments supplied to the gotoState method.
3574
+
3575
+ TODO: Come back to this and refactor the code. It works, but it
3576
+ could certainly be improved
3577
+ */
3578
+ _processGotoStateArgs: function(args) {
3579
+ var processedArgs = {
3580
+ state: null,
3581
+ fromCurrentState: null,
3582
+ useHistory: false,
3583
+ context: null
3584
+ },
3585
+ len = null,
3586
+ value = null;
3587
+
3588
+ args = Array.prototype.slice.call(args);
3589
+ args = args.filter(function(item) {
3590
+ return !(item === undefined);
3591
+ });
3592
+ len = args.length;
3593
+
3594
+ if (len < 1) return processedArgs;
3595
+
3596
+ processedArgs.state = args[0];
3597
+
3598
+ if (len === 2) {
3599
+ value = args[1];
3600
+ switch (SC.typeOf(value)) {
3601
+ case "boolean":
3602
+ processedArgs.useHistory = value;
3603
+ break;
3604
+ case "object":
3605
+ if (!value.get || !value.get('isState')) {
3606
+ processedArgs.context = value;
3607
+ }
3608
+ break;
3609
+ default:
3610
+ processedArgs.fromCurrentState = value;
3611
+ }
3612
+ }
3613
+ else if (len === 3) {
3614
+ value = args[1];
3615
+ if (SC.typeOf(value) === "boolean") {
3616
+ processedArgs.useHistory = value;
3617
+ processedArgs.context = args[2];
3618
+ } else {
3619
+ processedArgs.fromCurrentState = value;
3620
+ value = args[2];
3621
+ if (SC.typeOf(value) === "boolean") {
3622
+ processedArgs.useHistory = value;
3623
+ } else {
3624
+ processedArgs.context = value;
3625
+ }
3626
+ }
3627
+ }
3628
+ else {
3629
+ processedArgs.fromCurrentState = args[1];
3630
+ processedArgs.useHistory = args[2];
3631
+ processedArgs.context = args[3];
3632
+ }
3633
+
3634
+ return processedArgs;
3635
+ },
3636
+
3637
+ /** @private
3638
+
3639
+ Will return a newly constructed root state class. The root state will have substates added to
3640
+ it based on properties found on this state that derive from a SC.State class. For the
3641
+ root state to be successfully built, the following much be met:
3642
+
3643
+ - The rootStateExample property must be defined with a class that derives from SC.State
3644
+ - Either the initialState or statesAreConcurrent property must be set, but not both
3645
+ - There must be one or more states that can be added to the root state
3646
+
3647
+ */
3648
+ _constructRootStateClass: function() {
3649
+ var rsExampleKey = 'rootStateExample',
3650
+ rsExample = get(this, rsExampleKey),
3651
+ initialState = get(this, 'initialState'),
3652
+ statesAreConcurrent = get(this, 'statesAreConcurrent'),
3653
+ stateCount = 0,
3654
+ key, value, valueIsFunc, attrs = {};
3655
+
3656
+ if (SC.typeOf(rsExample) === "function" && rsExample.statePlugin) {
3657
+ rsExample = rsExample.apply(this);
3658
+ }
3659
+
3660
+ if (!(SC.State.detect(rsExample) && rsExample.isClass)) {
3661
+ this._logStatechartCreationError("Invalid root state example");
3662
+ return null;
3663
+ }
3664
+
3665
+ if (statesAreConcurrent && !SC.empty(initialState)) {
3666
+ this._logStatechartCreationError("Can not assign an initial state when states are concurrent");
3667
+ } else if (statesAreConcurrent) {
3668
+ attrs.substatesAreConcurrent = true;
3669
+ } else if (SC.typeOf(initialState) === "string") {
3670
+ attrs.initialSubstate = initialState;
3671
+ } else {
3672
+ this._logStatechartCreationError("Must either define initial state or assign states as concurrent");
3673
+ return null;
3674
+ }
3675
+
3676
+ for (key in this) {
3677
+ if (key === rsExampleKey) continue;
3678
+
3679
+ value = this[key];
3680
+ valueIsFunc = SC.typeOf(value) === "function";
3681
+
3682
+ if (valueIsFunc && value.statePlugin) {
3683
+ value = value.apply(this);
3684
+ }
3685
+
3686
+ if (SC.State.detect(value) && value.isClass && this[key] !== this.constructor) {
3687
+ attrs[key] = value;
3688
+ stateCount += 1;
3689
+ }
3690
+ }
3691
+
3692
+ if (stateCount === 0) {
3693
+ this._logStatechartCreationError("Must define one or more states");
3694
+ return null;
3695
+ }
3696
+
3697
+ return rsExample.extend(attrs);
3698
+ },
3699
+
3700
+ /** @private */
3701
+ _logStatechartCreationError: function(msg) {
3702
+ SC.Logger.error("Unable to create statechart for %@: %@.".fmt(this, msg));
3703
+ },
3704
+
3705
+ /**
3706
+ Used to log a statechart trace message
3707
+ */
3708
+ statechartLogTrace: function(msg) {
3709
+ SC.Logger.info("%@: %@".fmt(get(this, 'statechartLogPrefix'), msg));
3710
+ },
3711
+
3712
+ /**
3713
+ Used to log a statechart error message
3714
+ */
3715
+ statechartLogError: function(msg) {
3716
+ SC.Logger.error("ERROR %@: %@".fmt(get(this, 'statechartLogPrefix'), msg));
3717
+ },
3718
+
3719
+ /**
3720
+ Used to log a statechart warning message
3721
+ */
3722
+ statechartLogWarning: function(msg) {
3723
+ if (get(this, 'suppressStatechartWarnings')) return;
3724
+ SC.Logger.warn("WARN %@: %@".fmt(get(this, 'statechartLogPrefix'), msg));
3725
+ },
3726
+
3727
+ /** @property */
3728
+ statechartLogPrefix: function() {
3729
+ var className = this.constructor.toString(),
3730
+ name = get(this, 'name'), prefix;
3731
+
3732
+ if (SC.empty(name)) prefix = "%@<%@>".fmt(className, SC.guidFor(this));
3733
+ else prefix = "%@<%@, %@>".fmt(className, name, SC.guidFor(this));
3734
+
3735
+ return prefix;
3736
+ }.property().cacheable(),
3737
+
3738
+ /** @private @property */
3739
+ allowStatechartTracing: function() {
3740
+ var key = get(this, 'statechartTraceKey');
3741
+ return get(this, key);
3742
+ }.property().cacheable(),
3743
+
3744
+ /** @private */
3745
+ _statechartTraceDidChange: function() {
3746
+ this.notifyPropertyChange('allowStatechartTracing');
3747
+ }
3748
+
3749
+ };
3750
+
3751
+ SC.mixin(SC.StatechartManager, SC.DelegateSupport, SC.StatechartDelegate);
3752
+
3753
+ /**
3754
+ The default name given to a statechart's root state
3755
+ */
3756
+ SC.ROOT_STATE_NAME = "__ROOT_STATE__";
3757
+
3758
+ /**
3759
+ Constants used during the state transition process
3760
+ */
3761
+ SC.EXIT_STATE = 0;
3762
+ SC.ENTER_STATE = 1;
3763
+
3764
+ /**
3765
+ A Startchart class.
3766
+ */
3767
+ SC.Statechart = SC.Object.extend(SC.StatechartManager, {
3768
+ autoInitStatechart: false
3769
+ });
3770
+
3771
+ })({});
3772
+
3773
+
3774
+ (function(exports) {
3775
+ // ==========================================================================
3776
+ // Project: SproutCore Statechart
3777
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
3778
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
3779
+ // License: Licensed under MIT license (see license.js)
3780
+ // ==========================================================================
3781
+ })({});
3782
+
3783
+
3784
+ (function(exports) {
3785
+ // ==========================================================================
3786
+ // Project: SproutCore Runtime
3787
+ // Copyright: ©2011 Strobe Inc. and contributors.
3788
+ // License: Licensed under MIT license (see license.js)
3789
+ // ==========================================================================
3790
+ })({});