/
FxGraph.js
206 lines (178 loc) · 6.04 KB
/
FxGraph.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// ==Builder==
// @required
// @package FxGraph
// ==/Builder==
/*
Class: Fx.Graph
Class for simplifying multistep animations that are driven by
user generated events - especially via the keybard/mouse. You define
a graph of states and a controller object. The controller object
will generate events that will drive the animation based on the
graph option passed in via the Fx.Graph initializer.
*/
Fx.Graph = new Class({
Implements: [Options, Events],
/*
Function: initialize
Initialize the animation graph
Parameters:
el - a dom element
*/
initialize: function(el, options) {
this.element = ($type(el) == 'string') ? $(el) : el;
this.controller = options.controller;
this.graph = options.graph;
this.direction = 'next';
this.directions = {};
this.delays = [];
this.flags = {};
this.processStates(options.graph);
this.element.set('morph', {
duration: options.duration,
transition: options.transition,
onComplete: this.onStateArrive.bind(this)
});
},
/*
Function: processStates
Process the graph description. Adds events to the controller
so that the graph can be controlled via events.
Parameters:
graph - the graph object passed in via the initialize options.
*/
processStates: function(graph){
$H(graph).each(function(state, name) {
if(state.first) this.currentState = name;
this.getDirections(name, graph);
if(state.events) state.events.each(function(stateEvent) {
this.controller.addEvent(stateEvent.type, function(evt) {
if(this.currentState != name) return;
if(stateEvent.flag) this.flags[stateEvent.flag] = true
if(stateEvent.unflag) delete this.flags[stateEvent.unflag];
if(!stateEvent.direction && !stateEvent.state) return;
var not = $get(stateEvent, 'condition', 'not');
if(not && (new Set($H(this.flags).getKeys())).aintersection(not).length != 0) return;
var nextState;
if(stateEvent.direction) {
this.direction = stateEvent.direction;
nextState = state[this.direction];
}
if(stateEvent.state) nextState = stateEvent.state;
this.clearTimers();
this.setState(nextState);
}.bind(this));
}, this);
}, this);
},
/*
Function: handleEventForState
*/
handleEventForState: function(state, eventType, event)
{
},
/*
Function: getDirections
Determine the direction relationships between a state and other states on the graph.
Parameters:
state - a state name.
graph - the graph to examine.
*/
getDirections: function(state, graph)
{
var result = {};
function collect(dir, r) {
var curState = state;
while(graph[curState][dir]) {
var ns = graph[curState][dir];
r[ns] = dir;
curState = ns;
}
return r;
};
this.directions[state] = collect('previous', collect('next', {}));
},
/*
Function: clearTimers
Clear any hold timers.
*/
clearTimers: function() {
if(this.delays.length > 0) {
this.delays.each($clear);
this.delays = [];
}
},
/*
Function: state
Return the current state of the animation graph.
*/
state: function() { return this.currentState; },
/*
Function: setState
Set the current state of the graph. Has the side effect
of triggering the morph to that state.
Parameters:
name - the name of the state.
options -
animate: flag for animating to the state. defaults to true.
direction: the direction the animation is going.
*/
setState: function(name, options) {
var direction = $get(options, 'direction'),
animate = $get(options, 'animate'),
hold = $get(options, 'hold') || this.graph[name].hold,
exitFn = $get(this.graph, this.transitionState, 'onExit'),
state = this.graph[name];
if (direction){
this.direction = direction;
} else {
this.direction = this.directions[$pick(this.transitionState, this.currentState)][name];
}
if(exitFn) exitFn(this.element, this);
this.transitionState = name;
if(state.onStart) state.onStart(this.element, this);
this.cancel();
if(animate !== false) {
var morph = state.selector;
if(!morph) morph = ($callable(state.styles)) ? state.styles() : state.styles;
this.element.get('morph').removeEvents('onComplete');
this.element.get('morph').addEvent('onComplete', this.onStateArrive.bind(this, [hold]));
this.element.morph(morph);
} else {
this.onStateArrive(hold);
}
},
/*
Function: onStateArrive
Call on each time the graph arrives at a state. If there's
a hold, delayed execution to the next state. The next state
is based on the current direction of the graph.
*/
onStateArrive: function(hold) {
function fix(selector) {
return selector.substr(1, selector.length-1);
};
if(this.graph[this.currentState].selector) this.element.removeClass(fix(this.graph[this.currentState].selector));
if(this.graph[this.transitionState].selector) this.element.addClass(fix(this.graph[this.transitionState].selector));
Fx.Graph.clear.each(function(style) {
this.element.setStyle(style, '');
}, this);
this.currentState = this.transitionState;
var state = this.graph[this.currentState];
if(state.onComplete) state.onComplete(this.element, this);
if(state.last) return;
if(hold) {
this.delays.push(this.setState.delay(hold.duration, this, [state[this.direction]]));
} else if(state[this.direction]) {
this.setState(state[this.direction]);
}
},
/*
Function: cancel
Cancel the current animation.
*/
cancel: function(clearTimers) {
if(clearTimers === true) this.clearTimers();
this.element.get('morph').cancel();
}
});
Fx.Graph.clear = ['width', 'height', 'left', 'right', 'top', 'bottom', 'background-color', 'opacity', 'visibility'];