1
+ use crate :: utils:: { add_event_listener, EventListenerHandle } ;
1
2
use leptos:: { html:: ElementDescriptor , * } ;
2
- use std:: ops:: Deref ;
3
+ use std:: { ops:: Deref , time :: Duration } ;
3
4
4
5
/// # CSS Transition
5
- ///
6
+ ///
6
7
/// Reference to https://vuejs.org/guide/built-ins/transition.html
7
8
#[ component]
8
9
pub fn CSSTransition < T , CF , IV > (
@@ -20,91 +21,234 @@ where
20
21
IV : IntoView ,
21
22
{
22
23
let display = create_rw_signal ( ( !show. get_untracked ( ) ) . then_some ( "display: none;" ) ) ;
23
- let remove_class_name = store_value ( None :: < RemoveClassName > ) ;
24
24
25
25
node_ref. on_load ( move |node_el| {
26
- let el = node_el. clone ( ) . into_any ( ) ;
27
- let el = el . deref ( ) ;
26
+ let any_el = node_el. clone ( ) . into_any ( ) ;
27
+ let el = any_el . deref ( ) . clone ( ) ;
28
28
let class_list = el. class_list ( ) ;
29
- let remove_class = Callback :: new ( move |_| {
30
- remove_class_name. update_value ( |class| {
31
- if let Some ( class) = class. take ( ) {
32
- match class {
33
- RemoveClassName :: Enter ( active, to) => {
34
- let _ = class_list. remove_2 ( & active, & to) ;
35
- if let Some ( on_after_enter) = on_after_enter {
36
- on_after_enter. call ( ( ) ) ;
37
- }
38
- }
39
- RemoveClassName :: Leave ( active, to) => {
40
- let _ = class_list. remove_2 ( & active, & to) ;
41
- display. set ( Some ( "display: none;" ) ) ;
42
- if let Some ( on_after_leave) = on_after_leave {
43
- on_after_leave. call ( ( ) ) ;
44
- }
45
- }
46
- }
29
+ let end_handle = StoredValue :: new ( None :: < EventListenerHandle > ) ;
30
+ let end_count = StoredValue :: new ( None :: < usize > ) ;
31
+ let finish = StoredValue :: new ( None :: < Callback < ( ) > > ) ;
32
+
33
+ let on_end = Callback :: new ( move |remove : Callback < ( ) > | {
34
+ let Some ( CSSTransitionInfo {
35
+ types,
36
+ prop_count,
37
+ timeout,
38
+ } ) = get_transition_info ( & el)
39
+ else {
40
+ remove. call ( ( ) ) ;
41
+ return ;
42
+ } ;
43
+
44
+ finish. set_value ( Some ( Callback :: new ( move |_| {
45
+ end_count. set_value ( None ) ;
46
+ remove. call ( ( ) ) ;
47
+ end_handle. update_value ( |h| {
48
+ h. take ( ) . map ( |h| {
49
+ h. remove ( ) ;
50
+ } ) ;
51
+ } ) ;
52
+ } ) ) ) ;
53
+
54
+ set_timeout (
55
+ move || {
56
+ finish. update_value ( |v| {
57
+ v. take ( ) . map ( |f| f. call ( ( ) ) ) ;
58
+ } ) ;
59
+ } ,
60
+ Duration :: from_millis ( timeout + 1 ) ,
61
+ ) ;
62
+
63
+ end_count. set_value ( Some ( 0 ) ) ;
64
+ let event_listener = move || {
65
+ end_count. update_value ( |v| {
66
+ let Some ( v) = v else {
67
+ return ;
68
+ } ;
69
+ * v += 1 ;
70
+ } ) ;
71
+ if end_count. with_value ( |v| {
72
+ let Some ( v) = v else {
73
+ return false ;
74
+ } ;
75
+ * v >= prop_count
76
+ } ) {
77
+ finish. update_value ( |v| {
78
+ v. take ( ) . map ( |f| f. call ( ( ) ) ) ;
79
+ } ) ;
47
80
}
48
- } ) ;
81
+ } ;
82
+ let handle = match types {
83
+ AnimationTypes :: Transition => {
84
+ add_event_listener ( any_el. clone ( ) , ev:: transitionend, move |_| event_listener ( ) )
85
+ }
86
+ AnimationTypes :: Animation => {
87
+ add_event_listener ( any_el. clone ( ) , ev:: animationend, move |_| event_listener ( ) )
88
+ }
89
+ } ;
90
+ end_handle. set_value ( Some ( handle) ) ;
49
91
} ) ;
50
92
51
- let _ = node_el
52
- . on ( ev:: transitionend, move |_| {
53
- remove_class. call ( ( ) ) ;
54
- } )
55
- . on ( ev:: animationend, move |_| {
56
- remove_class. call ( ( ) ) ;
93
+ let on_finish = move || {
94
+ finish. update_value ( |v| {
95
+ v. take ( ) . map ( |f| f. call ( ( ) ) ) ;
57
96
} ) ;
58
- } ) ;
97
+ } ;
98
+
99
+ let on_enter_fn = {
100
+ let class_list = class_list. clone ( ) ;
101
+ Callback :: new ( move |name : String | {
102
+ let enter_from = format ! ( "{name}-enter-from" ) ;
103
+ let enter_active = format ! ( "{name}-enter-active" ) ;
104
+ let enter_to = format ! ( "{name}-enter-to" ) ;
105
+
106
+ let _ = class_list. add_2 ( & enter_from, & enter_active) ;
107
+ display. set ( None ) ;
108
+
109
+ let class_list = class_list. clone ( ) ;
110
+ next_frame ( move || {
111
+ let _ = class_list. remove_1 ( & enter_from) ;
112
+ let _ = class_list. add_1 ( & enter_to) ;
59
113
60
- create_render_effect ( move |prev : Option < bool > | {
61
- let show = show. get ( ) ;
62
- if let Some ( node_el) = node_ref. get_untracked ( ) {
63
- if let Some ( prev) = prev {
64
- let name = name. get_untracked ( ) ;
65
-
66
- let el = node_el. into_any ( ) ;
67
- let el = el. deref ( ) ;
68
- let class_list = el. class_list ( ) ;
69
-
70
- if show && !prev {
71
- let enter_from = format ! ( "{name}-enter-from" ) ;
72
- let enter_active = format ! ( "{name}-enter-active" ) ;
73
- let enter_to = format ! ( "{name}-enter-to" ) ;
74
-
75
- let _ = class_list. add_2 ( & enter_from, & enter_active) ;
76
- display. set ( None ) ;
77
- request_animation_frame ( move || {
78
- let _ = class_list. remove_1 ( & enter_from) ;
79
- let _ = class_list. add_1 ( & enter_to) ;
80
- remove_class_name
81
- . set_value ( Some ( RemoveClassName :: Enter ( enter_active, enter_to) ) ) ;
82
- if let Some ( on_enter) = on_enter {
83
- on_enter. call ( ( ) ) ;
114
+ let remove = Callback :: new ( move |_| {
115
+ let _ = class_list. remove_2 ( & enter_active, & enter_to) ;
116
+ if let Some ( on_after_enter) = on_after_enter {
117
+ on_after_enter. call ( ( ) ) ;
84
118
}
85
119
} ) ;
86
- } else if !show && prev {
87
- let leave_from = format ! ( "{name}-leave-from" ) ;
88
- let leave_active = format ! ( "{name}-leave-active" ) ;
89
- let leave_to = format ! ( "{name}-leave-to" ) ;
90
-
91
- let _ = class_list. add_2 ( & leave_from, & leave_active) ;
92
- request_animation_frame ( move || {
93
- let _ = class_list. remove_1 ( & leave_from) ;
94
- let _ = class_list. add_1 ( & leave_to) ;
95
- remove_class_name
96
- . set_value ( Some ( RemoveClassName :: Leave ( leave_active, leave_to) ) ) ;
120
+ on_end. call ( remove) ;
121
+
122
+ if let Some ( on_enter) = on_enter {
123
+ on_enter. call ( ( ) ) ;
124
+ }
125
+ } ) ;
126
+ } )
127
+ } ;
128
+
129
+ let on_leave_fn = {
130
+ let class_list = class_list. clone ( ) ;
131
+ Callback :: new ( move |name : String | {
132
+ let leave_from = format ! ( "{name}-leave-from" ) ;
133
+ let leave_active = format ! ( "{name}-leave-active" ) ;
134
+ let leave_to = format ! ( "{name}-leave-to" ) ;
135
+
136
+ let _ = class_list. add_2 ( & leave_from, & leave_active) ;
137
+
138
+ let class_list = class_list. clone ( ) ;
139
+ next_frame ( move || {
140
+ let _ = class_list. remove_1 ( & leave_from) ;
141
+ let _ = class_list. add_1 ( & leave_to) ;
142
+
143
+ let remove = Callback :: new ( move |_| {
144
+ let _ = class_list. remove_2 ( & leave_active, & leave_to) ;
145
+ display. set ( Some ( "display: none;" ) ) ;
146
+ if let Some ( on_after_leave) = on_after_leave {
147
+ on_after_leave. call ( ( ) ) ;
148
+ }
97
149
} ) ;
98
- }
150
+ on_end. call ( remove) ;
151
+ } ) ;
152
+ } )
153
+ } ;
154
+
155
+ create_render_effect ( move |prev : Option < bool > | {
156
+ let show = show. get ( ) ;
157
+ let Some ( prev) = prev else {
158
+ return show;
159
+ } ;
160
+
161
+ let name = name. get_untracked ( ) ;
162
+
163
+ if show && !prev {
164
+ on_finish ( ) ;
165
+ on_enter_fn. call ( name) ;
166
+ } else if !show && prev {
167
+ on_finish ( ) ;
168
+ on_leave_fn. call ( name) ;
99
169
}
100
- }
101
- show
170
+
171
+ show
172
+ } ) ;
102
173
} ) ;
103
174
104
175
children ( display. read_only ( ) )
105
176
}
106
177
107
- enum RemoveClassName {
108
- Enter ( String , String ) ,
109
- Leave ( String , String ) ,
178
+ fn next_frame ( cb : impl FnOnce ( ) + ' static ) {
179
+ request_animation_frame ( move || {
180
+ request_animation_frame ( cb) ;
181
+ } ) ;
182
+ }
183
+
184
+ #[ derive( PartialEq ) ]
185
+ enum AnimationTypes {
186
+ Transition ,
187
+ Animation ,
188
+ }
189
+
190
+ struct CSSTransitionInfo {
191
+ types : AnimationTypes ,
192
+ prop_count : usize ,
193
+ timeout : u64 ,
194
+ }
195
+
196
+ fn get_transition_info ( el : & web_sys:: HtmlElement ) -> Option < CSSTransitionInfo > {
197
+ let styles = window ( ) . get_computed_style ( el) . ok ( ) . flatten ( ) ?;
198
+
199
+ let get_style_properties = |property : & str | {
200
+ styles
201
+ . get_property_value ( property)
202
+ . unwrap_or_default ( )
203
+ . split ( ", " )
204
+ . map ( |s| s. to_string ( ) )
205
+ . collect :: < Vec < _ > > ( )
206
+ } ;
207
+
208
+ let transition_delays = get_style_properties ( "transition-delay" ) ;
209
+ let transition_durations = get_style_properties ( "transition-duration" ) ;
210
+ let transition_timeout = get_timeout ( transition_delays, & transition_durations) ;
211
+ let animation_delays = get_style_properties ( "animation-delay" ) ;
212
+ let animation_durations = get_style_properties ( "animation-duration" ) ;
213
+ let animation_timeout = get_timeout ( animation_delays, & animation_durations) ;
214
+
215
+ let timeout = u64:: max ( transition_timeout, animation_timeout) ;
216
+ let ( types, prop_count) = if timeout > 0 {
217
+ if transition_timeout > animation_timeout {
218
+ ( AnimationTypes :: Transition , transition_durations. len ( ) )
219
+ } else {
220
+ ( AnimationTypes :: Animation , animation_durations. len ( ) )
221
+ }
222
+ } else {
223
+ return None ;
224
+ } ;
225
+
226
+ Some ( CSSTransitionInfo {
227
+ types,
228
+ prop_count,
229
+ timeout,
230
+ } )
231
+ }
232
+
233
+ fn get_timeout ( mut delays : Vec < String > , durations : & Vec < String > ) -> u64 {
234
+ while delays. len ( ) < durations. len ( ) {
235
+ delays. append ( & mut delays. clone ( ) )
236
+ }
237
+
238
+ fn to_ms ( s : & String ) -> u64 {
239
+ if s == "auto" || s. is_empty ( ) {
240
+ return 0 ;
241
+ }
242
+
243
+ let s = s. split_at ( s. len ( ) - 1 ) . 0 ;
244
+
245
+ ( s. parse :: < f32 > ( ) . unwrap_or_default ( ) * 1000.0 ) . floor ( ) as u64
246
+ }
247
+
248
+ durations
249
+ . iter ( )
250
+ . enumerate ( )
251
+ . map ( |( i, d) | to_ms ( d) + to_ms ( & delays[ i] ) )
252
+ . max ( )
253
+ . unwrap_or_default ( )
110
254
}
0 commit comments