rasputin 0.5.1

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