unpoly-rails 0.34.2 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

@@ -4,6 +4,6 @@ module Unpoly
4
4
  # The current version of the unpoly-rails gem.
5
5
  # This version number is also used for releases of the Unpoly
6
6
  # frontend code.
7
- VERSION = '0.34.2'
7
+ VERSION = '0.35.0'
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- unpoly-rails (0.34.1)
4
+ unpoly-rails (0.34.2)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -1,7 +1,15 @@
1
+ u = up.util
2
+
1
3
  beforeEach ->
2
4
  jasmine.addMatchers
3
5
  toHaveRequestMethod: (util, customEqualityTesters) ->
4
6
  compare: (request, expectedMethod) ->
5
7
  realMethodMatches = (request.method == expectedMethod)
6
- wrappedMethodMatches = util.equals(request.data()['_method'], [expectedMethod], customEqualityTesters)
8
+ formData = request.data()
9
+ if u.isFormData(formData)
10
+ wrappedMethod = formData.get('_method')
11
+ wrappedMethodMatches = (wrappedMethod == expectedMethod)
12
+ else
13
+ wrappedMethod = formData['_method']
14
+ wrappedMethodMatches = util.equals(wrappedMethod, [expectedMethod], customEqualityTesters)
7
15
  pass: realMethodMatches || wrappedMethodMatches
@@ -99,14 +99,12 @@ describe 'up.dom', ->
99
99
  up.replace('.middle', '/bar')
100
100
  expect(jasmine.Ajax.requests.count()).toEqual(2)
101
101
 
102
- describeCapability 'canFormData', ->
103
-
104
- it "does not explode if the original request's { data } is a FormData object", ->
105
- up.replace('.middle', '/foo', method: 'post', data: new FormData()) # POST requests are not cached
106
- expect(jasmine.Ajax.requests.count()).toEqual(1)
107
- @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
108
- secondReplace = -> up.replace('.middle', '/bar')
109
- expect(secondReplace).not.toThrowError()
102
+ it "does not explode if the original request's { data } is a FormData object", ->
103
+ up.replace('.middle', '/foo', method: 'post', data: new FormData()) # POST requests are not cached
104
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
105
+ @respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
106
+ secondReplace = -> up.replace('.middle', '/bar')
107
+ expect(secondReplace).not.toThrowError()
110
108
 
111
109
  describe 'with { data } option', ->
112
110
 
@@ -331,6 +329,26 @@ describe 'up.dom', ->
331
329
  expect($('.container')).toHaveText('new container text')
332
330
  expect(document.title).toBe('Title from HTML')
333
331
 
332
+ it 'does not update the document title if the response has a <title> tag inside an inline SVG image (bugfix)', ->
333
+ affix('.container').text('old container text')
334
+ document.title = 'old document title'
335
+ up.replace('.container', '/path', history: false, title: true)
336
+
337
+ @respondWith """
338
+ <svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
339
+ <g>
340
+ <title>SVG Title Demo example</title>
341
+ <rect x="10" y="10" width="200" height="50" style="fill:none; stroke:blue; stroke-width:1px"/>
342
+ </g>
343
+ </svg>
344
+
345
+ <div class='container'>
346
+ new container text
347
+ </div>
348
+ """
349
+ expect($('.container')).toHaveText('new container text')
350
+ expect(document.title).toBe('old document title')
351
+
334
352
  it "does not extract the title from the response or HTTP header if history isn't updated", ->
335
353
  affix('.container').text('old container text')
336
354
  document.title = 'old document title'
@@ -903,79 +921,80 @@ describe 'up.dom', ->
903
921
 
904
922
  describe 'with { transition } option', ->
905
923
 
906
- describeCapability 'canCssTransition', ->
924
+ it 'morphs between the old and new element', (done) ->
925
+ affix('.element').text('version 1')
926
+ up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
907
927
 
908
- it 'morphs between the old and new element', (done) ->
909
- affix('.element').text('version 1')
910
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
928
+ $ghost1 = $('.element.up-ghost:contains("version 1")')
929
+ expect($ghost1).toHaveLength(1)
930
+ expect(u.opacity($ghost1)).toBeAround(1.0, 0.1)
911
931
 
912
- $ghost1 = $('.element.up-ghost:contains("version 1")')
913
- expect($ghost1).toHaveLength(1)
914
- expect(u.opacity($ghost1)).toBeAround(1.0, 0.1)
932
+ $ghost2 = $('.element.up-ghost:contains("version 2")')
933
+ expect($ghost2).toHaveLength(1)
934
+ expect(u.opacity($ghost2)).toBeAround(0.0, 0.1)
915
935
 
916
- $ghost2 = $('.element.up-ghost:contains("version 2")')
917
- expect($ghost2).toHaveLength(1)
918
- expect(u.opacity($ghost2)).toBeAround(0.0, 0.1)
936
+ u.setTimer 190, ->
937
+ expect(u.opacity($ghost1)).toBeAround(0.0, 0.3)
938
+ expect(u.opacity($ghost2)).toBeAround(1.0, 0.3)
939
+ done()
919
940
 
920
- u.setTimer 190, ->
921
- expect(u.opacity($ghost1)).toBeAround(0.0, 0.3)
922
- expect(u.opacity($ghost2)).toBeAround(1.0, 0.3)
923
- done()
941
+ it 'marks the old fragment and its ghost as .up-destroying during the transition', ->
942
+ affix('.element').text('version 1')
943
+ up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
924
944
 
925
- it 'marks the old fragment and its ghost as .up-destroying during the transition', ->
926
- affix('.element').text('version 1')
927
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
945
+ $version1 = $('.element:not(.up-ghost):contains("version 1")')
946
+ $version1Ghost = $('.element.up-ghost:contains("version 1")')
947
+ expect($version1).toHaveLength(1)
948
+ expect($version1Ghost).toHaveLength(1)
949
+ expect($version1).toHaveClass('up-destroying')
950
+ expect($version1Ghost).toHaveClass('up-destroying')
928
951
 
929
- $version1 = $('.element:not(.up-ghost):contains("version 1")')
930
- $version1Ghost = $('.element.up-ghost:contains("version 1")')
931
- expect($version1).toHaveLength(1)
932
- expect($version1Ghost).toHaveLength(1)
933
- expect($version1).toHaveClass('up-destroying')
934
- expect($version1Ghost).toHaveClass('up-destroying')
935
-
936
- $version2 = $('.element:not(.up-ghost):contains("version 2")')
937
- $version2Ghost = $('.element.up-ghost:contains("version 2")')
938
- expect($version2).toHaveLength(1)
939
- expect($version2Ghost).toHaveLength(1)
940
- expect($version2).not.toHaveClass('up-destroying')
941
- expect($version2Ghost).not.toHaveClass('up-destroying')
942
-
943
- it 'cancels an existing transition by instantly jumping to the last frame', ->
944
- affix('.element').text('version 1')
945
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
952
+ $version2 = $('.element:not(.up-ghost):contains("version 2")')
953
+ $version2Ghost = $('.element.up-ghost:contains("version 2")')
954
+ expect($version2).toHaveLength(1)
955
+ expect($version2Ghost).toHaveLength(1)
956
+ expect($version2).not.toHaveClass('up-destroying')
957
+ expect($version2Ghost).not.toHaveClass('up-destroying')
946
958
 
947
- $ghost1 = $('.element.up-ghost:contains("version 1")')
948
- expect($ghost1).toHaveLength(1)
949
- expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
959
+ it 'cancels an existing transition by instantly jumping to the last frame', ->
960
+ affix('.element').text('version 1')
961
+ up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
950
962
 
951
- $ghost2 = $('.element.up-ghost:contains("version 2")')
952
- expect($ghost2).toHaveLength(1)
953
- expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
963
+ $ghost1 = $('.element.up-ghost:contains("version 1")')
964
+ expect($ghost1).toHaveLength(1)
965
+ expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
954
966
 
955
- up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
967
+ $ghost2 = $('.element.up-ghost:contains("version 2")')
968
+ expect($ghost2).toHaveLength(1)
969
+ expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
956
970
 
957
- $ghost1 = $('.element.up-ghost:contains("version 1")')
958
- expect($ghost1).toHaveLength(0)
971
+ up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
959
972
 
960
- $ghost2 = $('.element.up-ghost:contains("version 2")')
961
- expect($ghost2).toHaveLength(1)
962
- expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
973
+ $ghost1 = $('.element.up-ghost:contains("version 1")')
974
+ expect($ghost1).toHaveLength(0)
963
975
 
964
- $ghost3 = $('.element.up-ghost:contains("version 3")')
965
- expect($ghost3).toHaveLength(1)
966
- expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
976
+ $ghost2 = $('.element.up-ghost:contains("version 2")')
977
+ expect($ghost2).toHaveLength(1)
978
+ expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
967
979
 
968
- it 'delays the resolution of the returned promise until the transition is over', (done) ->
969
- affix('.element').text('version 1')
970
- resolution = jasmine.createSpy()
971
- promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
972
- promise.then(resolution)
973
- expect(resolution).not.toHaveBeenCalled()
974
- u.setTimer 80, ->
975
- expect(resolution).toHaveBeenCalled()
976
- done()
980
+ $ghost3 = $('.element.up-ghost:contains("version 3")')
981
+ expect($ghost3).toHaveLength(1)
982
+ expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
983
+
984
+ it 'delays the resolution of the returned promise until the transition is over', (done) ->
985
+ affix('.element').text('version 1')
986
+ resolution = jasmine.createSpy()
987
+ promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
988
+ promise.then(resolution)
989
+ expect(resolution).not.toHaveBeenCalled()
990
+ u.setTimer 80, ->
991
+ expect(resolution).toHaveBeenCalled()
992
+ done()
993
+
994
+ describe 'when animation is disabled', ->
977
995
 
978
- describeFallback 'canCssTransition', ->
996
+ beforeEach ->
997
+ up.motion.config.enabled = false
979
998
 
980
999
  it 'immediately swaps the old and new elements', ->
981
1000
  affix('.element').text('version 1')
@@ -1260,31 +1279,29 @@ describe 'up.dom', ->
1260
1279
  expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value1' })
1261
1280
  expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value2' })
1262
1281
 
1263
- describeCapability 'canCssTransition', ->
1264
-
1265
- it "doesn't let the discarded element appear in a transition", (done) ->
1266
- oldTextDuringTransition = undefined
1267
- newTextDuringTransition = undefined
1268
- transition = ($old, $new) ->
1269
- oldTextDuringTransition = squish($old.text())
1270
- newTextDuringTransition = squish($new.text())
1271
- u.resolvedDeferred()
1272
- $container = affix('.container')
1273
- $container.html """
1274
- <div class='foo'>old-foo</div>
1275
- <div class='bar' up-keep>old-bar</div>
1276
- """
1277
- newHtml = """
1278
- <div class='container'>
1279
- <div class='foo'>new-foo</div>
1280
- <div class='bar' up-keep>new-bar</div>
1281
- </div>
1282
- """
1283
- promise = up.extract('.container', newHtml, transition: transition)
1284
- promise.then ->
1285
- expect(oldTextDuringTransition).toEqual('old-foo old-bar')
1286
- expect(newTextDuringTransition).toEqual('new-foo old-bar')
1287
- done()
1282
+ it "doesn't let the discarded element appear in a transition", (done) ->
1283
+ oldTextDuringTransition = undefined
1284
+ newTextDuringTransition = undefined
1285
+ transition = ($old, $new) ->
1286
+ oldTextDuringTransition = squish($old.text())
1287
+ newTextDuringTransition = squish($new.text())
1288
+ u.resolvedDeferred()
1289
+ $container = affix('.container')
1290
+ $container.html """
1291
+ <div class='foo'>old-foo</div>
1292
+ <div class='bar' up-keep>old-bar</div>
1293
+ """
1294
+ newHtml = """
1295
+ <div class='container'>
1296
+ <div class='foo'>new-foo</div>
1297
+ <div class='bar' up-keep>new-bar</div>
1298
+ </div>
1299
+ """
1300
+ promise = up.extract('.container', newHtml, transition: transition)
1301
+ promise.then ->
1302
+ expect(oldTextDuringTransition).toEqual('old-foo old-bar')
1303
+ expect(newTextDuringTransition).toEqual('new-foo old-bar')
1304
+ done()
1288
1305
 
1289
1306
  describe 'up.destroy', ->
1290
1307
 
@@ -9,15 +9,9 @@ describe 'up.form', ->
9
9
  beforeEach ->
10
10
  up.form.config.observeDelay = 0
11
11
 
12
- changeEvents = if up.browser.canInputEvent()
13
- # Actually we only need `input`, but we want to notice
14
- # if another script manually triggers `change` on the element.
15
- ['input', 'change']
16
- else
17
- # Actually we won't ever get `input` from the user in this browser,
18
- # but we want to notice if another script manually triggers `input`
19
- # on the element.
20
- ['input', 'change', 'keypress', 'paste', 'cut', 'click', 'propertychange']
12
+ # Actually we only need `input`, but we want to notice
13
+ # if another script manually triggers `change` on the element.
14
+ changeEvents = ['input', 'change']
21
15
 
22
16
  describe 'when the first argument is a form field', ->
23
17
 
@@ -342,20 +336,10 @@ describe 'up.form', ->
342
336
  beforeEach ->
343
337
  @$form.affix('input[name="file-field"][type="file"]')
344
338
 
345
- describeCapability 'canFormData', ->
346
-
347
- it 'transfers the form fields via FormData', ->
348
- up.submit(@$form)
349
- data = @lastRequest().data()
350
- expect(u.isFormData(data)).toBe(true)
351
-
352
- describeFallback 'canFormData', ->
353
-
354
- it 'falls back to a vanilla form submission', ->
355
- form = @$form.get(0)
356
- spyOn(form, 'submit')
357
- up.submit(@$form)
358
- expect(form.submit).toHaveBeenCalled()
339
+ it 'transfers the form fields via FormData', ->
340
+ up.submit(@$form)
341
+ data = @lastRequest().data()
342
+ expect(u.isFormData(data)).toBe(true)
359
343
 
360
344
  describeFallback 'canPushState', ->
361
345
 
@@ -421,24 +421,22 @@ describe 'up.link', ->
421
421
 
422
422
  describe 'with [up-transition] modifier', ->
423
423
 
424
- describeCapability 'canCssTransition', ->
425
-
426
- it 'morphs between the old and new target element', (done) ->
427
- affix('.target.old')
428
- $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="500"][up-easing="linear"]')
429
- Trigger.clickSequence($link)
430
- @respondWith '<div class="target new">new text</div>'
431
-
432
- $oldGhost = $('.target.old.up-ghost')
433
- $newGhost = $('.target.new.up-ghost')
434
- expect($oldGhost).toExist()
435
- expect($newGhost).toExist()
436
- expect(u.opacity($oldGhost)).toBeAround(1, 0.15)
437
- expect(u.opacity($newGhost)).toBeAround(0, 0.15)
438
- u.setTimer 250, ->
439
- expect(u.opacity($oldGhost)).toBeAround(0.5, 0.15)
440
- expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
441
- done()
424
+ it 'morphs between the old and new target element', (done) ->
425
+ affix('.target.old')
426
+ $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="500"][up-easing="linear"]')
427
+ Trigger.clickSequence($link)
428
+ @respondWith '<div class="target new">new text</div>'
429
+
430
+ $oldGhost = $('.target.old.up-ghost')
431
+ $newGhost = $('.target.new.up-ghost')
432
+ expect($oldGhost).toExist()
433
+ expect($newGhost).toExist()
434
+ expect(u.opacity($oldGhost)).toBeAround(1, 0.15)
435
+ expect(u.opacity($newGhost)).toBeAround(0, 0.15)
436
+ u.setTimer 250, ->
437
+ expect(u.opacity($oldGhost)).toBeAround(0.5, 0.15)
438
+ expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
439
+ done()
442
440
 
443
441
  describe 'wih a CSS selector in the [up-fallback] attribute', ->
444
442
 
@@ -101,22 +101,20 @@ describe 'up.modal', ->
101
101
  expect(parseInt($body.css('padding-right'))).toBe(0)
102
102
  done()
103
103
 
104
- describeCapability 'canCssTransition', ->
105
-
106
- it "gives the scrollbar to .up-modal instead of .up-modal-viewport while animating, so we don't see scaled scrollbars in a zoom-in animation", (done) ->
107
- openPromise = up.modal.extract('.container', '<div class="container">text</div>', animation: 'fade-in', duration: 100)
108
- $modal = $('.up-modal')
109
- $viewport = $modal.find('.up-modal-viewport')
110
- expect($modal.css('overflow-y')).toEqual('scroll')
111
- expect($viewport.css('overflow-y')).toEqual('hidden')
112
- openPromise.then ->
113
- expect($modal.css('overflow-y')).not.toEqual('scroll')
114
- expect($viewport.css('overflow-y')).toEqual('scroll')
115
- closePromise = up.modal.close(animation: 'fade-out', duration: 200)
116
- u.nextFrame ->
117
- expect($modal.css('overflow-y')).toEqual('scroll')
118
- expect($viewport.css('overflow-y')).toEqual('hidden')
119
- done()
104
+ it "gives the scrollbar to .up-modal instead of .up-modal-viewport while animating, so we don't see scaled scrollbars in a zoom-in animation", (done) ->
105
+ openPromise = up.modal.extract('.container', '<div class="container">text</div>', animation: 'fade-in', duration: 100)
106
+ $modal = $('.up-modal')
107
+ $viewport = $modal.find('.up-modal-viewport')
108
+ expect($modal.css('overflow-y')).toEqual('scroll')
109
+ expect($viewport.css('overflow-y')).toEqual('hidden')
110
+ openPromise.then ->
111
+ expect($modal.css('overflow-y')).not.toEqual('scroll')
112
+ expect($viewport.css('overflow-y')).toEqual('scroll')
113
+ closePromise = up.modal.close(animation: 'fade-out', duration: 200)
114
+ u.nextFrame ->
115
+ expect($modal.css('overflow-y')).toEqual('scroll')
116
+ expect($viewport.css('overflow-y')).toEqual('hidden')
117
+ done()
120
118
 
121
119
  it 'does not add right padding to the body if the body has overflow-y: hidden', (done) ->
122
120
  restoreBody = u.temporaryCss($('body'), 'overflow-y': 'hidden')
@@ -185,48 +183,46 @@ describe 'up.modal', ->
185
183
  expect(bodyPadding).not.toBeAround(2 * assumedScrollbarWidth, 2 * 5)
186
184
  done()
187
185
 
188
- describeCapability 'canCssTransition', ->
186
+ it 'closes the current modal and wait for its close animation to finish before starting the open animation of a second modal', (done) ->
187
+ up.modal.config.openAnimation = 'fade-in'
188
+ up.modal.config.openDuration = 5
189
+ up.modal.config.closeAnimation = 'fade-out'
190
+ up.modal.config.closeDuration = 60
189
191
 
190
- it 'closes the current modal and wait for its close animation to finish before starting the open animation of a second modal', (done) ->
191
- up.modal.config.openAnimation = 'fade-in'
192
- up.modal.config.openDuration = 5
193
- up.modal.config.closeAnimation = 'fade-out'
194
- up.modal.config.closeDuration = 60
192
+ events = []
193
+ u.each ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed'], (event) ->
194
+ up.on event, ->
195
+ events.push(event)
195
196
 
196
- events = []
197
- u.each ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed'], (event) ->
198
- up.on event, ->
199
- events.push(event)
197
+ up.modal.extract('.target', '<div class="target">response1</div>')
200
198
 
201
- up.modal.extract('.target', '<div class="target">response1</div>')
199
+ # First modal is starting opening animation
200
+ expect(events).toEqual ['up:modal:open']
201
+ expect($('.target')).toHaveText('response1')
202
202
 
203
- # First modal is starting opening animation
204
- expect(events).toEqual ['up:modal:open']
203
+ u.setTimer 80, ->
204
+ # First modal has completed opening animation
205
+ expect(events).toEqual ['up:modal:open', 'up:modal:opened']
205
206
  expect($('.target')).toHaveText('response1')
206
207
 
207
- u.setTimer 80, ->
208
- # First modal has completed opening animation
209
- expect(events).toEqual ['up:modal:open', 'up:modal:opened']
210
- expect($('.target')).toHaveText('response1')
211
-
212
- # We open another modal, which will cause the first modal to start closing
213
- up.modal.extract('.target', '<div class="target">response2</div>')
208
+ # We open another modal, which will cause the first modal to start closing
209
+ up.modal.extract('.target', '<div class="target">response2</div>')
214
210
 
215
- expect($('.target')).toHaveText('response1')
211
+ expect($('.target')).toHaveText('response1')
216
212
 
217
- u.setTimer 20, ->
213
+ u.setTimer 20, ->
218
214
 
219
- # Second modal is still waiting for first modal's closing animaton to finish.
220
- expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close']
221
- expect($('.target')).toHaveText('response1')
215
+ # Second modal is still waiting for first modal's closing animaton to finish.
216
+ expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close']
217
+ expect($('.target')).toHaveText('response1')
222
218
 
223
- u.setTimer 200, ->
219
+ u.setTimer 200, ->
224
220
 
225
- # First modal has finished closing, second modal has finished opening.
226
- expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed', 'up:modal:open', 'up:modal:opened']
227
- expect($('.target')).toHaveText('response2')
221
+ # First modal has finished closing, second modal has finished opening.
222
+ expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed', 'up:modal:open', 'up:modal:opened']
223
+ expect($('.target')).toHaveText('response2')
228
224
 
229
- done()
225
+ done()
230
226
 
231
227
  it 'closes an opening modal if a second modal starts opening before the first modal has finished its open animation', (done) ->
232
228
  up.modal.config.openAnimation = 'fade-in'