unpoly-rails 0.55.1 → 0.56.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/dist/unpoly-bootstrap3.js +6 -4
  4. data/dist/unpoly-bootstrap3.min.js +1 -1
  5. data/dist/unpoly.js +1323 -805
  6. data/dist/unpoly.min.js +4 -3
  7. data/lib/assets/javascripts/unpoly-bootstrap3/{navigation-ext.coffee → feedback-ext.coffee} +2 -0
  8. data/lib/assets/javascripts/unpoly/browser.coffee.erb +7 -7
  9. data/lib/assets/javascripts/unpoly/bus.coffee.erb +5 -6
  10. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +127 -0
  11. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +1 -1
  12. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +62 -32
  13. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +27 -0
  14. data/lib/assets/javascripts/unpoly/dom.coffee.erb +78 -99
  15. data/lib/assets/javascripts/unpoly/feedback.coffee +147 -96
  16. data/lib/assets/javascripts/unpoly/form.coffee.erb +26 -2
  17. data/lib/assets/javascripts/unpoly/history.coffee +2 -1
  18. data/lib/assets/javascripts/unpoly/layout.coffee.erb +68 -12
  19. data/lib/assets/javascripts/unpoly/link.coffee.erb +10 -4
  20. data/lib/assets/javascripts/unpoly/modal.coffee.erb +11 -9
  21. data/lib/assets/javascripts/unpoly/{motion.coffee → motion.coffee.erb} +184 -322
  22. data/lib/assets/javascripts/unpoly/popup.coffee.erb +13 -12
  23. data/lib/assets/javascripts/unpoly/radio.coffee +1 -1
  24. data/lib/assets/javascripts/unpoly/syntax.coffee +8 -17
  25. data/lib/assets/javascripts/unpoly/tooltip.coffee +11 -11
  26. data/lib/assets/javascripts/unpoly/util.coffee +332 -145
  27. data/lib/unpoly/rails/version.rb +1 -1
  28. data/package.json +1 -1
  29. data/spec_app/Gemfile.lock +1 -1
  30. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  31. data/spec_app/app/assets/stylesheets/integration_test.sass +1 -0
  32. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +4 -0
  33. data/spec_app/app/views/motion_test/transitions.erb +13 -0
  34. data/spec_app/app/views/pages/start.erb +1 -0
  35. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +5 -0
  36. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +5 -0
  37. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +1 -1
  38. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +11 -0
  39. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +5 -0
  40. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +217 -102
  41. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +162 -44
  42. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +97 -10
  43. data/spec_app/spec/javascripts/up/link_spec.js.coffee +3 -3
  44. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +22 -20
  45. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +344 -228
  46. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +1 -1
  47. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +1 -1
  48. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +1 -1
  49. data/spec_app/spec/javascripts/up/util_spec.js.coffee +194 -0
  50. metadata +11 -4
@@ -11,11 +11,11 @@ describe 'up.motion', ->
11
11
  up.animate($element, 'fade-in', duration: 200, easing: 'linear')
12
12
 
13
13
  u.setTimer 5, ->
14
- expect(u.opacity($element)).toBeAround(0.0, 0.25)
14
+ expect($element).toHaveOpacity(0.0, 0.25)
15
15
  u.setTimer 100, ->
16
- expect(u.opacity($element)).toBeAround(0.5, 0.25)
16
+ expect($element).toHaveOpacity(0.5, 0.25)
17
17
  u.setTimer 200, ->
18
- expect(u.opacity($element)).toBeAround(1.0, 0.25)
18
+ expect($element).toHaveOpacity(1.0, 0.25)
19
19
  done()
20
20
 
21
21
  it 'returns a promise that is fulfilled when the animation has completed', (done) ->
@@ -27,15 +27,52 @@ describe 'up.motion', ->
27
27
 
28
28
  u.setTimer 50, ->
29
29
  expect(resolveSpy).not.toHaveBeenCalled()
30
- u.setTimer 100, ->
30
+ u.setTimer 50 + (timingTolerance = 120), ->
31
31
  expect(resolveSpy).toHaveBeenCalled()
32
32
  done()
33
33
 
34
34
  it 'cancels an existing animation on the element by instantly jumping to the last frame', asyncSpec (next) ->
35
35
  $element = affix('.element').text('content')
36
36
  up.animate($element, { 'font-size': '40px' }, duration: 10000, easing: 'linear')
37
- next => up.animate($element, { 'fade-in' }, duration: 100, easing: 'linear')
38
- next => expect($element.css('font-size')).toEqual('40px')
37
+
38
+ next =>
39
+ up.animate($element, { 'fade-in' }, duration: 100, easing: 'linear')
40
+
41
+ next =>
42
+ expect($element.css('font-size')).toEqual('40px')
43
+
44
+ describe 'when up.animate() is called from inside an animation function', ->
45
+
46
+ it 'animates', (done) ->
47
+ $element = affix('.element').text('content')
48
+
49
+ animation = ($element, options) ->
50
+ u.writeInlineStyle($element, opacity: 0)
51
+ up.animate($element, { opacity: 1 }, options)
52
+
53
+ up.animate($element, animation, duration: 300, easing: 'linear')
54
+
55
+ u.setTimer 5, ->
56
+ expect($element).toHaveOpacity(0.0, 0.25)
57
+ u.setTimer 150, ->
58
+ expect($element).toHaveOpacity(0.5, 0.25)
59
+ u.setTimer 300, ->
60
+ expect($element).toHaveOpacity(1.0, 0.25)
61
+ done()
62
+
63
+ it "finishes animations only once", (done) ->
64
+ $element = affix('.element').text('content')
65
+
66
+ animation = ($element, options) ->
67
+ u.writeInlineStyle($element, opacity: 0)
68
+ up.animate($element, { opacity: 1 }, options)
69
+
70
+ up.animate($element, animation, duration: 200, easing: 'linear')
71
+
72
+ u.nextFrame =>
73
+ expect(up.motion.finishCount()).toEqual(1)
74
+ done()
75
+
39
76
 
40
77
  describe 'with animations disabled globally', ->
41
78
 
@@ -77,7 +114,7 @@ describe 'up.motion', ->
77
114
 
78
115
  next =>
79
116
  expect($element.css('font-size')).toEqual('40px')
80
- expect(parseFloat($element.css('opacity'))).toBeAround(0.5, 0.01) # Safari sometimes has rounding errors
117
+ expect(u.opacity($element, 'opacity')).toBeAround(0.5, 0.01) # Safari sometimes has rounding errors
81
118
 
82
119
  it 'cancels animations on children of the given element', asyncSpec (next) ->
83
120
  $parent = affix('.element')
@@ -103,7 +140,7 @@ describe 'up.motion', ->
103
140
  expect(Number($element1.css('opacity'))).toEqual(1)
104
141
  expect(Number($element2.css('opacity'))).toBeAround(0, 0.1)
105
142
 
106
- it 'restores existing transitions on the element', asyncSpec (next) ->
143
+ it 'restores CSS transitions from before the Unpoly animation', asyncSpec (next) ->
107
144
  $element = affix('.element').text('content')
108
145
  $element.css('transition': 'font-size 3s ease')
109
146
  oldTransitionProperty = $element.css('transition-property')
@@ -121,38 +158,80 @@ describe 'up.motion', ->
121
158
  expect(currentTransitionProperty).toContain('font-size')
122
159
  expect(currentTransitionProperty).not.toContain('opacity')
123
160
 
124
- it 'cancels an existing transition on the element by instantly jumping to the last frame', asyncSpec (next) ->
125
- $old = affix('.old').text('old content')
126
- $new = affix('.new').text('new content')
161
+ it 'pauses an existing CSS transitions and restores it once the Unpoly animation is done', asyncSpec (next) ->
162
+ $element = affix('.element').text('content').css
163
+ backgroundColor: 'yellow'
164
+ fontSize: '10px'
165
+ height: '20px'
127
166
 
128
- up.morph($old, $new, 'cross-fade', duration: 2000)
167
+ expect(parseFloat($element.css('fontSize'))).toBeAround(10, 0.1)
168
+ expect(parseFloat($element.css('height'))).toBeAround(20, 0.1)
169
+
170
+ next.after 10, =>
171
+ $element.css
172
+ transition: 'font-size 500ms linear, height 500ms linear'
173
+ fontSize: '100px'
174
+ height: '200px'
175
+
176
+ next.after 250, =>
177
+ # Original CSS transition should now be ~50% done
178
+ @fontSizeBeforeAnimate = parseFloat($element.css('fontSize'))
179
+ @heightBeforeAnimate = parseFloat($element.css('height'))
180
+
181
+ expect(@fontSizeBeforeAnimate).toBeAround(0.5 * (100 - 10), 20)
182
+ expect(@heightBeforeAnimate).toBeAround(0.5 * (200 - 20), 40)
183
+
184
+ up.animate($element, 'fade-in', duration: 500, easing: 'linear')
185
+
186
+ next.after 250, =>
187
+ # Original CSS transition should remain paused at ~50%
188
+ # Unpoly animation should now be ~50% done
189
+ expect(parseFloat($element.css('fontSize'))).toBeAround(@fontSizeBeforeAnimate, 2)
190
+ expect(parseFloat($element.css('height'))).toBeAround(@heightBeforeAnimate, 2)
191
+ expect(parseFloat($element.css('opacity'))).toBeAround(0.5, 0.3)
192
+
193
+ next.after 250, =>
194
+ # Unpoly animation should now be done
195
+ # The original transition resumes. For technical reasons it will take
196
+ # its full duration for the remaining frames of the transition.
197
+ expect(parseFloat($element.css('opacity'))).toBeAround(1.0, 0.3)
198
+
199
+ next.after (500 + (tolerance = 125)), =>
200
+ expect(parseFloat($element.css('fontSize'))).toBeAround(100, 20)
201
+ expect(parseFloat($element.css('height'))).toBeAround(200, 40)
202
+
203
+
204
+ it 'cancels an existing transition on the old element by instantly jumping to the last frame', asyncSpec (next) ->
205
+ $v1 = affix('.element').text('v1')
206
+ $v2 = affix('.element').text('v2')
207
+
208
+ up.morph($v1, $v2, 'cross-fade', duration: 200)
129
209
 
130
210
  next =>
131
- expect($('.up-ghost').length).toBe(2)
211
+ expect($v1).toHaveOpacity(1.0, 0.2)
212
+ expect($v2).toHaveOpacity(0.0, 0.2)
132
213
 
133
- next.await =>
134
- up.motion.finish($old)
214
+ up.motion.finish($v1)
135
215
 
136
- next.after 100, =>
137
- expect($('.up-ghost').length).toBe(0)
138
- expect($old.css('display')).toEqual('none')
139
- expect($new.css('display')).toEqual('block')
216
+ next =>
217
+ expect($v1).toBeDetached()
218
+ expect($v2).toHaveOpacity(1.0, 0.2)
140
219
 
141
- it 'can be called on either element involved in a transition', asyncSpec (next) ->
142
- $old = affix('.old').text('old content')
143
- $new = affix('.new').text('new content')
220
+ it 'cancels an existing transition on the new element by instantly jumping to the last frame', asyncSpec (next) ->
221
+ $v1 = affix('.element').text('v1')
222
+ $v2 = affix('.element').text('v2')
144
223
 
145
- up.morph($old, $new, 'cross-fade', duration: 2000)
224
+ up.morph($v1, $v2, 'cross-fade', duration: 200)
146
225
 
147
226
  next =>
148
- expect($('.up-ghost').length).toBe(2)
227
+ expect($v1).toHaveOpacity(1.0, 0.2)
228
+ expect($v2).toHaveOpacity(0.0, 0.2)
149
229
 
150
- up.motion.finish($new)
230
+ up.motion.finish($v2)
151
231
 
152
232
  next =>
153
- expect($('.up-ghost').length).toBe(0)
154
- expect($old.css('display')).toEqual('none')
155
- expect($new.css('display')).toEqual('block')
233
+ expect($v1).toBeDetached()
234
+ expect($v2).toHaveOpacity(1.0, 0.2)
156
235
 
157
236
 
158
237
  it 'cancels transitions on children of the given element', asyncSpec (next) ->
@@ -163,14 +242,53 @@ describe 'up.motion', ->
163
242
  up.morph($old, $new, 'cross-fade', duration: 2000)
164
243
 
165
244
  next =>
166
- expect($('.up-ghost').length).toBe(2)
245
+ expect($old).toHaveOpacity(1.0, 0.1)
246
+ expect($new).toHaveOpacity(0.0, 0.1)
167
247
 
168
248
  up.motion.finish($parent)
169
249
 
170
250
  next =>
171
- expect($('.up-ghost').length).toBe(0)
172
- expect($old.css('display')).toEqual('none')
173
- expect($new.css('display')).toEqual('block')
251
+ expect($old).toBeDetached()
252
+ expect($new).toHaveOpacity(1.0)
253
+
254
+
255
+ it 'does not leave .up-bounds elements in the DOM', asyncSpec (next) ->
256
+ $old = affix('.old').text('old content')
257
+ $new = affix('.new').text('new content')
258
+
259
+ up.morph($old, $new, 'cross-fade', duration: 2000)
260
+
261
+ next =>
262
+ up.motion.finish($old)
263
+
264
+ next =>
265
+ expect($old).toBeDetached()
266
+ expect($('.up-bounds').length).toBe(0)
267
+
268
+
269
+ it 'emits an up:motion:finish event on the given animating element, so custom animation functions can react to the finish request', asyncSpec (next) ->
270
+ $element = affix('.element').text('element text')
271
+ listener = jasmine.createSpy('finish event listener')
272
+ $element.on('up:motion:finish', listener)
273
+
274
+ up.animate($element, 'fade-in')
275
+
276
+ next =>
277
+ expect(listener).not.toHaveBeenCalled()
278
+ up.motion.finish()
279
+
280
+ next =>
281
+ expect(listener).toHaveBeenCalled()
282
+
283
+
284
+ it 'does not emit an up:motion:finish event if no element is animating', asyncSpec (next) ->
285
+ listener = jasmine.createSpy('finish event listener')
286
+ up.on('up:motion:finish', listener)
287
+ up.motion.finish()
288
+
289
+ next =>
290
+ expect(listener).not.toHaveBeenCalled()
291
+
174
292
 
175
293
  describe 'when called without arguments', ->
176
294
 
@@ -195,120 +313,196 @@ describe 'up.motion', ->
195
313
 
196
314
  describe 'up.morph', ->
197
315
 
198
- it 'transitions between two element by animating two copies while keeping the originals in the background', asyncSpec (next) ->
316
+ it 'transitions between two element by absolutely positioning one element above the other', asyncSpec (next) ->
317
+ $old = affix('.old').text('old content').css(width: '200px', width: '200px')
318
+ $new = affix('.new').text('new content').css(width: '200px', width: '200px').detach()
319
+
320
+ oldDims = u.measure($old)
199
321
 
200
- $old = affix('.old').text('old content').css(
201
- position: 'absolute'
202
- top: '10px'
203
- left: '11px',
204
- width: '12px',
205
- height: '13px'
206
- )
207
- $new = affix('.new').text('new content').css(
208
- position: 'absolute'
209
- top: '20px'
210
- left: '21px',
211
- width: '22px',
212
- height: '23px'
213
- )
214
322
  up.morph($old, $new, 'cross-fade', duration: 200, easing: 'linear')
215
323
 
216
324
  next =>
217
- # The actual animation will be performed on Ghosts since
218
- # two element usually cannot exist in the DOM at the same time
219
- # without undesired visual effects
220
- @$oldGhost = $('.old.up-ghost')
221
- @$newGhost = $('.new.up-ghost')
222
- expect(@$oldGhost).toExist()
223
- expect(@$newGhost).toExist()
224
-
225
- $oldBounds = @$oldGhost.parent('.up-bounds')
226
- $newBounds = @$newGhost.parent('.up-bounds')
227
- expect($oldBounds).toExist()
228
- expect($newBounds).toExist()
229
-
230
- # Ghosts should be inserted before (not after) the element
231
- # or the browser scroll position will be too low after the
232
- # transition ends.
233
- expect(@$oldGhost.parent().next()).toEqual($old)
234
- expect(@$newGhost.parent().next()).toEqual($new)
235
-
236
- # The old element is removed from the layout flow.
237
- # It will be removed from the DOM after the animation has ended.
238
- expect($old.css('display')).toEqual('none')
239
-
240
- # The new element is invisible due to an opacity of zero,
241
- # but takes up the space in the layout flow.
242
- expect($new.css(['display', 'opacity'])).toEqual(
243
- display: 'block',
244
- opacity: '0'
245
- )
246
-
247
- # We **must not** use `visibility: hidden` to hide the new
248
- # element. This would delay browser painting until the element is
249
- # shown again, causing a flicker while the browser is painting.
250
- expect($new.css('visibility')).not.toEqual('hidden')
251
-
252
- # Ghosts will hover over $old and $new using absolute positioning,
253
- # matching the coordinates of the original elements.
254
- expect($oldBounds.css(['position', 'top', 'left', 'width', 'height'])).toEqual(
255
- position: 'absolute'
256
- top: '10px'
257
- left: '11px',
258
- width: '12px',
259
- height: '13px'
260
- )
261
- expect($newBounds.css(['position', 'top', 'left', 'width', 'height'])).toEqual(
262
- position: 'absolute'
263
- top: '20px'
264
- left: '21px',
265
- width: '22px',
266
- height: '23px'
267
- )
268
-
269
- expect(u.opacity(@$newGhost)).toBeAround(0.0, 0.25)
270
- expect(u.opacity(@$oldGhost)).toBeAround(1.0, 0.25)
271
-
272
- next.after 80, =>
273
- expect(u.opacity(@$newGhost)).toBeAround(0.4, 0.25)
274
- expect(u.opacity(@$oldGhost)).toBeAround(0.6, 0.25)
275
-
276
- next.after 60, =>
277
- expect(u.opacity(@$newGhost)).toBeAround(0.7, 0.25)
278
- expect(u.opacity(@$oldGhost)).toBeAround(0.3, 0.25)
279
-
280
- next.after 110, =>
281
- # Once our two ghosts have rendered their visual effect,
282
- # we remove them from the DOM.
283
- expect(@$newGhost).not.toBeInDOM()
284
- expect(@$oldGhost).not.toBeInDOM()
285
-
286
- # The old element is still in the DOM, but hidden.
287
- # Morphing does *not* remove the target element.
288
- expect($old.css('display')).toEqual('none')
289
- expect($new.css(['display', 'visibility'])).toEqual(
290
- display: 'block',
291
- visibility: 'visible'
292
- )
293
-
294
- it 'cancels an existing transition on the element by instantly jumping to the last frame', asyncSpec (next) ->
295
- $old = affix('.old').text('old content')
296
- $new = affix('.new').text('new content')
297
-
298
- up.morph($old, $new, 'cross-fade', duration: 200)
325
+ expect(u.measure($old)).toEqual(oldDims)
326
+ expect(u.measure($new)).toEqual(oldDims)
327
+
328
+ expect(u.opacity($old)).toBeAround(1.0, 0.25)
329
+ expect(u.opacity($new)).toBeAround(0.0, 0.25)
330
+
331
+ next.after 100, =>
332
+ expect(u.opacity($old)).toBeAround(0.5, 0.25)
333
+ expect(u.opacity($new)).toBeAround(0.5, 0.25)
334
+
335
+ next.after (100 + (tolerance = 110)), =>
336
+ expect(u.opacity($new)).toBeAround(1.0, 0.25)
337
+ expect($old).toBeDetached()
338
+
339
+ it 'does not change the position of sibling elements (as long as the old and new elements are of equal size)', asyncSpec (next) ->
340
+ $container = affix('.container')
341
+
342
+ $before = $container.affix('.before').css(margin: '20px')
343
+ $old = $container.affix('.old').text('old content').css(width: '200px', width: '200px', margin: '20px')
344
+ $new = $container.affix('.new').text('new content').css(width: '200px', width: '200px', margin: '20px').detach()
345
+ $after = $container.affix('.before').css(margin: '20px')
346
+
347
+ beforeDims = u.measure($before)
348
+ afterDims = u.measure($after)
349
+
350
+ up.morph($old, $new, 'cross-fade', duration: 30, easing: 'linear')
351
+
352
+ next =>
353
+ expect(u.measure($before)).toEqual(beforeDims)
354
+ expect(u.measure($after)).toEqual(afterDims)
355
+
356
+ next.after 50, =>
357
+ expect(u.measure($before)).toEqual(beforeDims)
358
+ expect(u.measure($after)).toEqual(afterDims)
359
+
360
+ it 'transitions between two elements that are already positioned absolutely', asyncSpec (next) ->
361
+ elementStyles =
362
+ position: 'absolute'
363
+ left: '30px'
364
+ top: '30px'
365
+ width: '200px'
366
+ width: '200px'
367
+ $old = affix('.old').text('old content').css(elementStyles)
368
+ $new = affix('.new').text('new content').css(elementStyles).detach()
369
+
370
+ oldDims = u.measure($old)
371
+
372
+ up.morph($old, $new, 'cross-fade', duration: 100, easing: 'linear')
373
+
374
+ next =>
375
+ expect(u.measure($old)).toEqual(oldDims)
376
+ expect(u.measure($new)).toEqual(oldDims)
377
+
378
+ next.after (100 + (timingTolerance = 120)), =>
379
+ expect($old).toBeDetached()
380
+ expect(u.measure($new)).toEqual(oldDims)
381
+
382
+ it 'cancels an existing transition on the new element by instantly jumping to the last frame', asyncSpec (next) ->
383
+ $v1 = affix('.element').text('v1')
384
+ $v2 = affix('.element').text('v2')
385
+ $v3 = affix('.element').text('v3')
386
+
387
+ up.morph($v1, $v2, 'cross-fade', duration: 200)
299
388
 
300
389
  next =>
301
- @$ghost1 = $('.old.up-ghost')
302
- expect(@$ghost1).toHaveLength(1)
390
+ expect($v1).toHaveOpacity(1.0, 0.2)
391
+ expect($v2).toHaveOpacity(0.0, 0.2)
303
392
 
304
- up.morph($old, $new, 'cross-fade', duration: 200)
393
+ up.morph($v2, $v3, 'cross-fade', duration: 200)
305
394
 
306
395
  next =>
307
- @$ghost2 = $('.old.up-ghost')
308
- # Check that we didn't create additional ghosts
309
- expect(@$ghost2).toHaveLength(1)
310
- # Check that it's a different ghosts
311
- expect(@$ghost2).not.toEqual(@$ghost1)
396
+ expect($v1).toBeDetached()
397
+ expect($v2).toHaveOpacity(1.0, 0.2)
398
+ expect($v3).toHaveOpacity(0.0, 0.2)
399
+
400
+ it 'detaches the old element in the DOM', (done) ->
401
+ $v1 = affix('.element').text('v1')
402
+ $v2 = affix('.element').text('v2')
403
+
404
+ morphDone = up.morph($v1, $v2, 'cross-fade', duration: 5)
405
+
406
+ morphDone.then ->
407
+ expect($v1).toBeDetached()
408
+ expect($v2).toBeAttached()
409
+ done()
410
+
411
+ it 'does not leave .up-bounds elements in the DOM', (done) ->
412
+ $v1 = affix('.element').text('v1')
413
+ $v2 = affix('.element').text('v2')
414
+
415
+ morphDone = up.morph($v1, $v2, 'cross-fade', duration: 5)
416
+
417
+ morphDone.then ->
418
+ expect('.up-bounds').not.toExist()
419
+ done()
420
+
421
+
422
+ describe 'when up.animate() is called from inside a transition function', ->
423
+
424
+ it 'animates', asyncSpec (next) ->
425
+ $old = affix('.old').text('old content')
426
+ $new = affix('.new').text('new content').detach()
427
+
428
+ oldDims = u.measure($old)
429
+
430
+ transition = ($old, $new, options) ->
431
+ up.animate($old, 'fade-out', options)
432
+ up.animate($new, 'fade-in', options)
433
+
434
+ up.morph($old, $new, transition, duration: 200, easing: 'linear')
435
+
436
+ next =>
437
+ expect(u.measure($old)).toEqual(oldDims)
438
+ expect(u.measure($new)).toEqual(oldDims)
439
+
440
+ expect(u.opacity($old)).toBeAround(1.0, 0.25)
441
+ expect(u.opacity($new)).toBeAround(0.0, 0.25)
442
+
443
+ next.after 100, =>
444
+ expect(u.opacity($old)).toBeAround(0.5, 0.25)
445
+ expect(u.opacity($new)).toBeAround(0.5, 0.25)
446
+
447
+ next.after (100 + (tolerance = 110)), =>
448
+ expect(u.opacity($new)).toBeAround(1.0, 0.1)
449
+ expect($old).toBeDetached()
450
+ expect($new).toBeAttached()
451
+
452
+ it 'finishes animations only once', asyncSpec (next) ->
453
+ $old = affix('.old').text('old content')
454
+ $new = affix('.new').text('new content').detach()
455
+
456
+ transition = ($old, $new, options) ->
457
+ up.animate($old, 'fade-out', options)
458
+ up.animate($new, 'fade-in', options)
459
+
460
+ up.morph($old, $new, transition, duration: 200, easing: 'linear')
461
+
462
+ next ->
463
+ expect(up.motion.finishCount()).toEqual(1)
464
+
465
+ describe 'when up.morph() is called from inside a transition function', ->
466
+
467
+ it 'morphs', asyncSpec (next) ->
468
+ $old = affix('.old').text('old content')
469
+ $new = affix('.new').text('new content').detach()
470
+
471
+ oldDims = u.measure($old)
472
+
473
+ transition = ($old, $new, options) ->
474
+ up.morph($old, $new, 'cross-fade', options)
475
+
476
+ up.morph($old, $new, transition, duration: 200, easing: 'linear')
477
+
478
+ next =>
479
+ expect(u.measure($old)).toEqual(oldDims)
480
+ expect(u.measure($new)).toEqual(oldDims)
481
+
482
+ expect(u.opacity($old)).toBeAround(1.0, 0.25)
483
+ expect(u.opacity($new)).toBeAround(0.0, 0.25)
484
+
485
+ next.after 100, =>
486
+ expect(u.opacity($old)).toBeAround(0.5, 0.25)
487
+ expect(u.opacity($new)).toBeAround(0.5, 0.25)
488
+
489
+ next.after (100 + (tolerance = 110)), =>
490
+ expect(u.opacity($new)).toBeAround(1.0, 0.25)
491
+ expect($old).toBeDetached()
492
+ expect($new).toBeAttached()
493
+
494
+ it "finishes animations only once", asyncSpec (next) ->
495
+ $old = affix('.old').text('old content')
496
+ $new = affix('.new').text('new content').detach()
497
+
498
+ transition = ($old, $new, options) ->
499
+ up.morph($old, $new, 'cross-fade', options)
500
+
501
+ up.morph($old, $new, transition, duration: 200, easing: 'linear')
502
+
503
+ next ->
504
+ expect(up.motion.finishCount()).toEqual(1)
505
+
312
506
 
313
507
  describe 'with { reveal: true } option', ->
314
508
 
@@ -325,14 +519,11 @@ describe 'up.motion', ->
325
519
 
326
520
  expect($container.scrollTop()).toEqual(300)
327
521
 
328
- $new = affix('.new').insertBefore($old).css(height: '600px')
522
+ $new = affix('.new').css(height: '600px').detach()
329
523
 
330
524
  up.morph($old, $new, 'cross-fade', duration: 50, reveal: true)
331
525
 
332
526
  next =>
333
- $oldGhost = $('.old.up-ghost')
334
- $newGhost = $('.new.up-ghost')
335
-
336
527
  # Container is scrolled up due to { reveal: true } option.
337
528
  # Since $old and $new are sitting in the same viewport with a
338
529
  # single shared scrollbar, this will make the ghost for $old jump.
@@ -340,41 +531,41 @@ describe 'up.motion', ->
340
531
 
341
532
  # See that the ghost for $new is aligned with the top edge
342
533
  # of the viewport.
343
- expect($newGhost.offset().top).toEqual(0)
534
+ expect($new.offset().top).toEqual(0)
344
535
 
345
- # The ghost for $old is shifted upwards to make it looks like it
536
+ # The absolitized $old is shifted upwards to make it looks like it
346
537
  # was at the scroll position before we revealed $new.
347
- expect($oldGhost.offset().top).toEqual(-300)
538
+ expect($old.offset().top).toEqual(-300)
348
539
 
349
540
  describe 'with animations disabled globally', ->
350
541
 
351
542
  beforeEach ->
352
543
  up.motion.config.enabled = false
353
544
 
354
- it "doesn't animate and hides the old element instead", asyncSpec (next) ->
545
+ it "doesn't animate and detaches the old element instead", asyncSpec (next) ->
355
546
  $old = affix('.old').text('old content')
356
547
  $new = affix('.new').text('new content')
357
548
  up.morph($old, $new, 'cross-fade', duration: 1000)
358
549
 
359
550
  next =>
360
- expect($old).toBeHidden()
361
- expect($new).toBeVisible()
362
- expect($new.css('opacity')).toEqual('1')
551
+ expect($old).toBeDetached()
552
+ expect($new).toBeAttached()
553
+ expect($new).toHaveOpacity(1.0)
363
554
 
364
555
 
365
556
  [false, null, undefined, 'none', 'none/none', '', [], [undefined, null], ['none', 'none'], ['none', {}]].forEach (noneTransition) ->
366
557
 
367
- describe "when called with a `#{noneTransition}` transition", ->
558
+ describe "when called with a `#{JSON.stringify(noneTransition)}` transition", ->
368
559
 
369
- it "doesn't animate and hides the old element instead", asyncSpec (next) ->
560
+ it "doesn't animate and detaches the old element instead", asyncSpec (next) ->
370
561
  $old = affix('.old').text('old content')
371
562
  $new = affix('.new').text('new content')
372
563
  up.morph($old, $new, noneTransition, duration: 1000)
373
564
 
374
565
  next =>
375
- expect($old).toBeHidden()
376
- expect($new).toBeVisible()
377
- expect($new.css('opacity')).toEqual('1')
566
+ expect($old).toBeDetached()
567
+ expect($new).toBeAttached()
568
+ expect($new).toHaveOpacity(1.0)
378
569
 
379
570
 
380
571
  describe 'up.transition', ->
@@ -384,78 +575,3 @@ describe 'up.motion', ->
384
575
  describe 'up.animation', ->
385
576
 
386
577
  it 'should have tests'
387
-
388
- describe 'up.motion.prependCopy', ->
389
-
390
- afterEach ->
391
- $('.up-bounds, .up-ghost, .fixture').remove()
392
-
393
- it 'clones the given element into a .up-ghost-bounds container and inserts it as a sibling before the element', ->
394
- $element = affix('.element').text('element text')
395
- up.motion.prependCopy($element)
396
- $bounds = $element.prev()
397
- expect($bounds).toExist()
398
- expect($bounds).toHaveClass('up-bounds')
399
- $ghost = $bounds.children(':first')# $ghost.find('.element')
400
- expect($ghost).toExist()
401
- expect($ghost).toHaveClass('element')
402
- expect($ghost).toHaveText('element text')
403
-
404
- it 'removes <script> tags from the cloned element', ->
405
- $element = affix('.element')
406
- $('<script></script>').appendTo($element)
407
- up.motion.prependCopy($element)
408
- $ghost = $('.up-ghost')
409
- expect($ghost.find('script')).not.toExist()
410
-
411
- it 'absolutely positions the ghost over the given element', ->
412
- $element = affix('.element')
413
- up.motion.prependCopy($element)
414
- $ghost = $('.up-ghost')
415
- expect($ghost.offset()).toEqual($element.offset())
416
- expect($ghost.width()).toEqual($element.width())
417
- expect($ghost.height()).toEqual($element.height())
418
-
419
- it 'accurately positions the ghost over an element with margins', ->
420
- $element = affix('.element').css(margin: '40px')
421
- up.motion.prependCopy($element)
422
- $ghost = $('.up-ghost')
423
- expect($ghost.offset()).toEqual($element.offset())
424
-
425
- it "doesn't change the position of a child whose margins no longer collapse", ->
426
- $element = affix('.element')
427
- $child = $('<div class="child"></div>').css(margin: '40px').appendTo($element)
428
- up.motion.prependCopy($element)
429
- $clonedChild = $('.up-ghost .child')
430
- expect($clonedChild.offset().top).toBeAround($child.offset().top, 0.5)
431
- expect($clonedChild.offset().left).toBeAround($child.offset().left, 0.5)
432
-
433
- it 'correctly positions the ghost over an element within a scrolled body', ->
434
- $body = $('body')
435
- $element1 = $('<div class="fixture"></div>').css(height: '75px').prependTo($body)
436
- $element2 = $('<div class="fixture"></div>').css(height: '100px').insertAfter($element1)
437
- $body.scrollTop(17)
438
- { $bounds, $ghost } = up.motion.prependCopy($element2)
439
- expect($bounds.css('position')).toBe('absolute')
440
- expect($bounds.css('top')).toEqual('75px')
441
- expect($ghost.css('position')).toBe('static')
442
-
443
- it 'correctly positions the ghost over an element within a viewport with overflow-y: scroll'
444
-
445
- it 'converts fixed elements within the copies to absolutely positioning', ->
446
- $element = affix('.element').css
447
- position: 'absolute'
448
- top: '50px'
449
- left: '50px'
450
- $fixedChild = $('<div class="fixed-child" up-fixed></div>').css
451
- position: 'fixed'
452
- left: '77px'
453
- top: '77px'
454
- $fixedChild.appendTo($element)
455
- up.motion.prependCopy($element, $('body'))
456
- $fixedChildGhost = $('.up-ghost .fixed-child')
457
- expect($fixedChildGhost.css(['position', 'left', 'top'])).toEqual
458
- position: 'absolute',
459
- left: '27px',
460
- top: '27px'
461
-