unpoly-rails 0.55.1 → 0.56.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.

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
-