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
@@ -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.55.1'
7
+ VERSION = '0.56.0'
8
8
  end
9
9
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unpoly",
3
- "version": "0.55.1",
3
+ "version": "0.56.0",
4
4
  "description": "Unobtrusive JavaScript framework",
5
5
  "main": "dist/unpoly.js",
6
6
  "files": [
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- unpoly-rails (0.54.1)
4
+ unpoly-rails (0.55.1)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -1,6 +1,7 @@
1
1
  #= require jquery
2
2
  #= require jquery_ujs
3
3
  #= require es6-promise.auto
4
+ #= require helpers/knife
4
5
  #= require unpoly
5
6
 
6
7
  up.compiler '.compiler', ($element) ->
@@ -31,6 +31,7 @@ a
31
31
 
32
32
  .fixed-top-bar
33
33
  position: fixed
34
+ z-index: 9999999
34
35
  top: 0
35
36
  left: 0
36
37
  right: 0
@@ -14,3 +14,7 @@
14
14
 
15
15
  &:empty
16
16
  display: none
17
+
18
+ // For specs that need to query computed styles
19
+ .red-background
20
+ background-color: #f00
@@ -0,0 +1,13 @@
1
+ <div class="fixed-top-bar">
2
+ <% for animation in %w(cross-fade move-up move-right move-down move-left) do %>
3
+ <span class="button" onclick="up.replace('.morphed-object', 'transitions', { transition: '<%= animation %>', duration: 2000, cache: false })">
4
+ <%= animation %>
5
+ </span>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="example">
10
+ <div class="morphed-object" style="padding: 30px; background-color: #<%= SecureRandom.hex(3) %>; color: white; width: 300px; height: 200px; margin-left: 200px; margin-top: 200px">
11
+ <%= ('a'..'z').to_a.shuffle[0, 20].join %>
12
+ </div>
13
+ </div>
@@ -63,6 +63,7 @@
63
63
  <li><%= link_to 'Hash links (without Unpoly)', '/hash_test/vanilla' %></li>
64
64
  <li><%= link_to 'Revealing long pages', '/reveal_test/long1' %></li>
65
65
  <li><%= link_to 'Animations', '/motion_test/animations' %></li>
66
+ <li><%= link_to 'Transitions', '/motion_test/transitions' %></li>
66
67
  </ul>
67
68
 
68
69
  </div>
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeAttached: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: !up.util.isDetached(actual)
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeDetached: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: up.util.isDetached(actual)
@@ -2,4 +2,4 @@ beforeEach ->
2
2
  jasmine.addMatchers
3
3
  toContain: (util, customEqualityTesters) ->
4
4
  compare: (object, expectedElement) ->
5
- pass: up.util.contains(object, expectedElement)
5
+ pass: up.util.isGiven(object) && up.util.contains(object, expectedElement)
@@ -0,0 +1,11 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toHaveOpacity: (util, customEqualityTesters) ->
4
+ compare: (element, expectedOpacity, tolerance = 0.0) ->
5
+ element = up.util.element(element)
6
+ actualOpacity = up.util.opacity(element)
7
+ result = {}
8
+ result.pass = Math.abs(expectedOpacity - actualOpacity) <= tolerance
9
+ unless result.pass
10
+ result.message = up.browser.sprintf("Expected %o to have opacity %o, but it was %o", element, expectedOpacity, actualOpacity)
11
+ return result
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toHaveOwnProperty: (util, customEqualityTesters) ->
4
+ compare: (object, expectedProperty) ->
5
+ pass: object.hasOwnProperty(expectedProperty)
@@ -48,39 +48,7 @@ describe 'up.dom', ->
48
48
  expect(resolution).toHaveBeenCalled()
49
49
  expect($('.middle')).toHaveText('new-middle')
50
50
 
51
- describe 'cleaning up', ->
52
-
53
- it 'calls destructors on the replaced element', asyncSpec (next) ->
54
- destructor = jasmine.createSpy('destructor')
55
- up.compiler '.container', -> destructor
56
- $container = affix('.container')
57
- up.hello($container)
58
- up.replace('.container', '/path')
59
-
60
- next =>
61
- @respondWith '<div class="container">new text</div>'
62
-
63
- next =>
64
- expect('.container').toHaveText('new text')
65
- expect(destructor).toHaveBeenCalled()
66
-
67
- it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', asyncSpec (next) ->
68
- # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
69
- up.dom.knife.mock('shouldSwapElementsDirectly').and.callFake ($element) -> $element.is('.container')
70
- destructor = jasmine.createSpy('destructor')
71
- up.compiler '.container', -> destructor
72
- $container = affix('.container')
73
- up.hello($container)
74
- up.replace('.container', '/path')
75
-
76
- next =>
77
- @respondWith '<div class="container">new text</div>'
78
-
79
- next =>
80
- expect('.container').toHaveText('new text')
81
- expect(destructor).toHaveBeenCalled()
82
-
83
- describe 'transitions', ->
51
+ describe 'with { transition } option', ->
84
52
 
85
53
  it 'returns a promise that will be fulfilled once the server response was received and the swap transition has completed', asyncSpec (next) ->
86
54
  resolution = jasmine.createSpy()
@@ -99,21 +67,6 @@ describe 'up.dom', ->
99
67
  next.after 80, =>
100
68
  expect(resolution).toHaveBeenCalled()
101
69
 
102
- it 'ignores a { transition } option when replacing the body element', asyncSpec (next) ->
103
- up.dom.knife.mock('swapElementsDirectly') # can't have the example replace the Jasmine test runner UI
104
- up.dom.knife.mock('destroy') # if we don't swap the body, up.dom will destroy it
105
- replaceCallback = jasmine.createSpy()
106
- promise = up.replace('body', '/path', transition: 'cross-fade', duration: 50)
107
- promise.then(replaceCallback)
108
- expect(replaceCallback).not.toHaveBeenCalled()
109
-
110
- next =>
111
- @responseText = '<body>new text</body>'
112
- @respond()
113
-
114
- next =>
115
- expect(replaceCallback).toHaveBeenCalled()
116
-
117
70
  describe 'with { data } option', ->
118
71
 
119
72
  it "uses the given params as a non-GET request's payload", asyncSpec (next) ->
@@ -1438,15 +1391,6 @@ describe 'up.dom', ->
1438
1391
  expect(result.value).toMatch(/Could not find selector/i)
1439
1392
  done()
1440
1393
 
1441
- it "ignores an element that matches the selector but also matches .up-ghost", (done) ->
1442
- html = '<div class="foo-bar">text</div>'
1443
- affix('.foo-bar.up-ghost')
1444
- promise = up.extract('.foo-bar', html)
1445
- promiseState(promise).then (result) =>
1446
- expect(result.state).toEqual('rejected')
1447
- expect(result.value).toMatch(/Could not find selector/i)
1448
- done()
1449
-
1450
1394
  it "ignores an element that matches the selector but also has a parent matching .up-destroying", (done) ->
1451
1395
  html = '<div class="foo-bar">text</div>'
1452
1396
  $parent = affix('.up-destroying')
@@ -1457,16 +1401,6 @@ describe 'up.dom', ->
1457
1401
  expect(result.value).toMatch(/Could not find selector/i)
1458
1402
  done()
1459
1403
 
1460
- it "ignores an element that matches the selector but also has a parent matching .up-ghost", (done) ->
1461
- html = '<div class="foo-bar">text</div>'
1462
- $parent = affix('.up-ghost')
1463
- $child = affix('.foo-bar').appendTo($parent)
1464
- promise = up.extract('.foo-bar', html)
1465
- promiseState(promise).then (result) =>
1466
- expect(result.state).toEqual('rejected')
1467
- expect(result.value).toMatch(/Could not find selector/i)
1468
- done()
1469
-
1470
1404
  it 'only replaces the first element matching the selector', asyncSpec (next) ->
1471
1405
  html = '<div class="foo-bar">text</div>'
1472
1406
  affix('.foo-bar')
@@ -1474,86 +1408,209 @@ describe 'up.dom', ->
1474
1408
  up.extract('.foo-bar', html)
1475
1409
 
1476
1410
  next =>
1477
- elements = $('.foo-bar')
1478
- expect($(elements.get(0)).text()).toEqual('text')
1479
- expect($(elements.get(1)).text()).toEqual('')
1411
+ $elements = $('.foo-bar')
1412
+ expect($($elements.get(0)).text()).toEqual('text')
1413
+ expect($($elements.get(1)).text()).toEqual('')
1414
+
1415
+ it 'focuses an [autofocus] element in the new fragment', asyncSpec (next) ->
1416
+ affix('.foo-bar')
1417
+ up.extract '.foo-bar', """
1418
+ <form class='foo-bar'>
1419
+ <input class="autofocused-input" autofocus>
1420
+ </form>
1421
+ """
1422
+
1423
+ next =>
1424
+ input = $('.autofocused-input').get(0)
1425
+ expect(input).toBeGiven()
1426
+ expect(document.activeElement).toBe(input)
1427
+
1428
+ describe 'cleaning up', ->
1429
+
1430
+ it 'calls destructors on the old element', asyncSpec (next) ->
1431
+ destructor = jasmine.createSpy('destructor')
1432
+ up.compiler '.container', ($element) ->
1433
+ -> destructor($element.text())
1434
+ $container = affix('.container').text('old text')
1435
+ up.hello($container)
1436
+ up.extract('.container', '<div class="container">new text</div>')
1437
+
1438
+ next =>
1439
+ expect('.container').toHaveText('new text')
1440
+ expect(destructor).toHaveBeenCalledWith('old text')
1441
+
1442
+ it 'calls destructors on the old element after a { transition }', (done) ->
1443
+ destructor = jasmine.createSpy('destructor')
1444
+ up.compiler '.container', ($element) ->
1445
+ -> destructor($element.text())
1446
+ $container = affix('.container').text('old text')
1447
+ up.hello($container)
1448
+
1449
+ up.extract('.container', '<div class="container">new text</div>', transition: 'cross-fade', duration: 100)
1450
+
1451
+ u.setTimer 50, =>
1452
+ expect(destructor).not.toHaveBeenCalled()
1453
+
1454
+ u.setTimer 220, =>
1455
+ expect('.container').toHaveText('new text')
1456
+ expect(destructor).toHaveBeenCalledWith('old text')
1457
+ done()
1458
+
1459
+ it 'calls destructors when the replaced element is a singleton element like <body> (bugfix)', asyncSpec (next) ->
1460
+ # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
1461
+ up.motion.knife.mock('isSingletonElement').and.callFake ($element) -> $element.is('.container')
1462
+ destructor = jasmine.createSpy('destructor')
1463
+ up.compiler '.container', -> destructor
1464
+ $container = affix('.container')
1465
+ up.hello($container)
1466
+ up.extract('.container', '<div class="container">new text</div>')
1467
+
1468
+ next =>
1469
+ expect('.container').toHaveText('new text')
1470
+ expect(destructor).toHaveBeenCalled()
1471
+
1472
+ it 'calls destructors while the element is still attached to the DOM, so destructors see ancestry and events bubble up', asyncSpec (next) ->
1473
+ spy = jasmine.createSpy('parent spy')
1474
+ up.compiler '.element', ($element) ->
1475
+ return -> spy($element.text(), $element.parent())
1476
+
1477
+ $parent = affix('.parent')
1478
+ $element = $parent.affix('.element').text('old text')
1479
+ up.hello($element)
1480
+
1481
+ up.extract '.element', '<div class="element">new text</div>'
1482
+
1483
+ next =>
1484
+ expect(spy).toHaveBeenCalledWith('old text', $parent)
1485
+
1486
+ it 'calls destructors while the element is still attached to the DOM when also using a { transition }', (done) ->
1487
+ spy = jasmine.createSpy('parent spy')
1488
+ up.compiler '.element', ($element) ->
1489
+ return ->
1490
+ # We must seek .parent in our ancestry, because our direct parent() is an .up-bounds container
1491
+ spy($element.text(), $element.closest('.parent'))
1492
+
1493
+ $parent = affix('.parent')
1494
+ $element = $parent.affix('.element').text('old text')
1495
+ up.hello($element)
1496
+
1497
+ extractDone = up.extract('.element', '<div class="element">new text</div>', transition: 'cross-fade', duration: 30)
1498
+
1499
+ extractDone.then ->
1500
+ expect(spy).toHaveBeenCalledWith('old text', $parent)
1501
+ done()
1480
1502
 
1481
1503
  describe 'with { transition } option', ->
1482
1504
 
1483
1505
  it 'morphs between the old and new element', asyncSpec (next) ->
1484
- affix('.element').text('version 1')
1485
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1506
+ affix('.element.v1').text('version 1')
1507
+ up.extract('.element', '<div class="element v2">version 2</div>', transition: 'cross-fade', duration: 100, easing: 'linear')
1508
+
1509
+ $old = undefined
1510
+ $new = undefined
1486
1511
 
1487
1512
  next =>
1488
- @$ghost1 = $('.element.up-ghost:contains("version 1")')
1489
- expect(@$ghost1).toHaveLength(1)
1490
- expect(u.opacity(@$ghost1)).toBeAround(1.0, 0.1)
1513
+ $old = $('.element.v1')
1514
+ $new = $('.element.v2')
1491
1515
 
1492
- @$ghost2 = $('.element.up-ghost:contains("version 2")')
1493
- expect(@$ghost2).toHaveLength(1)
1494
- expect(u.opacity(@$ghost2)).toBeAround(0.0, 0.1)
1516
+ expect($old).toHaveLength(1)
1517
+ expect(u.opacity($old)).toBeAround(1.0, 0.2)
1495
1518
 
1496
- next.after 190, =>
1497
- expect(u.opacity(@$ghost1)).toBeAround(0.0, 0.3)
1498
- expect(u.opacity(@$ghost2)).toBeAround(1.0, 0.3)
1519
+ expect($new).toHaveLength(1)
1520
+ expect(u.opacity($new)).toBeAround(0.0, 0.2)
1499
1521
 
1500
- it 'marks the old fragment and its ghost as .up-destroying during the transition', asyncSpec (next) ->
1522
+ next.after 50, =>
1523
+ expect(u.opacity($old)).toBeAround(0.5, 0.2)
1524
+ expect(u.opacity($new)).toBeAround(0.5, 0.2)
1525
+
1526
+ next.after 60, =>
1527
+ expect(u.opacity($new)).toBeAround(1.0, 0.1)
1528
+ expect($old).toBeDetached()
1529
+
1530
+
1531
+ it 'ignores a { transition } option when replacing a singleton element like <body>', asyncSpec (next) ->
1532
+ # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
1533
+ up.motion.knife.mock('isSingletonElement').and.callFake ($element) -> $element.is('.container')
1534
+
1535
+ affix('.container').text('old text')
1536
+
1537
+ extractDone = jasmine.createSpy()
1538
+ promise = up.extract('.container', '<div class="container">new text</div>', transition: 'cross-fade', duration: 200)
1539
+ promise.then(extractDone)
1540
+
1541
+ next =>
1542
+ # See that we've already immediately swapped the element and ignored the duration of 200ms
1543
+ expect(extractDone).toHaveBeenCalled()
1544
+ expect($('.container').length).toEqual(1)
1545
+ expect(u.opacity($('.container'))).toEqual(1.0)
1546
+
1547
+
1548
+ it 'marks the old fragment as .up-destroying during the transition', asyncSpec (next) ->
1501
1549
  affix('.element').text('version 1')
1502
1550
  up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1503
1551
 
1504
1552
  next =>
1505
- $version1 = $('.element:not(.up-ghost):contains("version 1")')
1506
- $version1Ghost = $('.element.up-ghost:contains("version 1")')
1553
+ $version1 = $('.element:contains("version 1")')
1507
1554
  expect($version1).toHaveLength(1)
1508
- expect($version1Ghost).toHaveLength(1)
1509
1555
  expect($version1).toHaveClass('up-destroying')
1510
- expect($version1Ghost).toHaveClass('up-destroying')
1511
1556
 
1512
- $version2 = $('.element:not(.up-ghost):contains("version 2")')
1513
- $version2Ghost = $('.element.up-ghost:contains("version 2")')
1557
+ $version2 = $('.element:contains("version 2")')
1514
1558
  expect($version2).toHaveLength(1)
1515
- expect($version2Ghost).toHaveLength(1)
1516
1559
  expect($version2).not.toHaveClass('up-destroying')
1517
- expect($version2Ghost).not.toHaveClass('up-destroying')
1518
1560
 
1519
1561
  it 'cancels an existing transition by instantly jumping to the last frame', asyncSpec (next) ->
1520
- affix('.element').text('version 1')
1521
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
1562
+ affix('.element.v1').text('version 1')
1563
+
1564
+ up.extract('.element', '<div class="element v2">version 2</div>', transition: 'cross-fade', duration: 200)
1522
1565
 
1523
1566
  next =>
1524
- $ghost1 = $('.element.up-ghost:contains("version 1")')
1567
+ $ghost1 = $('.element:contains("version 1")')
1525
1568
  expect($ghost1).toHaveLength(1)
1526
1569
  expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
1527
1570
 
1528
- $ghost2 = $('.element.up-ghost:contains("version 2")')
1571
+ $ghost2 = $('.element:contains("version 2")')
1529
1572
  expect($ghost2).toHaveLength(1)
1530
1573
  expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
1531
1574
 
1532
1575
  next =>
1533
- up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
1576
+ up.extract('.element', '<div class="element v3">version 3</div>', transition: 'cross-fade', duration: 200)
1534
1577
 
1535
1578
  next =>
1536
- $ghost1 = $('.element.up-ghost:contains("version 1")')
1579
+ $ghost1 = $('.element:contains("version 1")')
1537
1580
  expect($ghost1).toHaveLength(0)
1538
1581
 
1539
- $ghost2 = $('.element.up-ghost:contains("version 2")')
1582
+ $ghost2 = $('.element:contains("version 2")')
1540
1583
  expect($ghost2).toHaveLength(1)
1541
1584
  expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
1542
1585
 
1543
- $ghost3 = $('.element.up-ghost:contains("version 3")')
1586
+ $ghost3 = $('.element:contains("version 3")')
1544
1587
  expect($ghost3).toHaveLength(1)
1545
1588
  expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
1546
1589
 
1547
1590
  it 'delays the resolution of the returned promise until the transition is over', (done) ->
1548
1591
  affix('.element').text('version 1')
1549
1592
  resolution = jasmine.createSpy()
1550
- promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
1593
+ promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 60)
1551
1594
  promise.then(resolution)
1552
1595
  expect(resolution).not.toHaveBeenCalled()
1553
- u.setTimer 80, ->
1596
+
1597
+ u.setTimer 40, ->
1598
+ expect(resolution).not.toHaveBeenCalled()
1599
+
1600
+ u.setTimer 200, ->
1554
1601
  expect(resolution).toHaveBeenCalled()
1555
1602
  done()
1556
1603
 
1604
+ it 'attaches the new element to the DOM before compilers are called, so they can see their parents and trigger bubbling events', asyncSpec (next)->
1605
+ $parent = affix('.parent')
1606
+ $element = $parent.affix('.element').text('old text')
1607
+ spy = jasmine.createSpy('parent spy')
1608
+ up.compiler '.element', ($element) -> spy($element.text(), $element.parent())
1609
+ up.extract '.element', '<div class="element">new text</div>', transition: 'cross-fade', duration: 50
1610
+
1611
+ next =>
1612
+ expect(spy).toHaveBeenCalledWith('new text', $parent)
1613
+
1557
1614
  describe 'when animation is disabled', ->
1558
1615
 
1559
1616
  beforeEach ->
@@ -1567,7 +1624,7 @@ describe 'up.dom', ->
1567
1624
  expect($('.up-ghost')).toHaveLength(0)
1568
1625
 
1569
1626
  it "replaces the elements directly, since first inserting and then removing would shift scroll positions", asyncSpec (next) ->
1570
- swapDirectlySpy = up.dom.knife.mock('swapElementsDirectly')
1627
+ swapDirectlySpy = up.motion.knife.mock('swapElementsDirectly')
1571
1628
  affix('.element').text('version 1')
1572
1629
  up.extract('.element', '<div class="element">version 2</div>', transition: false)
1573
1630
 
@@ -1896,7 +1953,7 @@ describe 'up.dom', ->
1896
1953
  expect(insertedListener).toHaveBeenCalledWith(jasmine.anything(), $('.container'), jasmine.anything())
1897
1954
  expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.container .keeper'), jasmine.anything())
1898
1955
 
1899
- it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', (next) ->
1956
+ it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', asyncSpec (next) ->
1900
1957
  keptListener = jasmine.createSpy()
1901
1958
  up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
1902
1959
  $container = affix('.container')
@@ -2009,6 +2066,64 @@ describe 'up.dom', ->
2009
2066
  expect(document.title).toEqual('Title from options')
2010
2067
  done()
2011
2068
 
2069
+ it 'runs an animation before removal with { animate } option', asyncSpec (next) ->
2070
+ $element = affix('.element')
2071
+ up.destroy($element, animation: 'fade-out', duration: 150, easing: 'linear')
2072
+
2073
+ next ->
2074
+ expect($element).toHaveOpacity(1.0, 0.2)
2075
+
2076
+ next.after 75, ->
2077
+ expect($element).toHaveOpacity(0.5, 0.2)
2078
+
2079
+ next.after (75 + 20), ->
2080
+ expect($element).toBeDetached()
2081
+
2082
+ it 'marks the element as .up-destroying while it is animating', asyncSpec (next) ->
2083
+ $element = affix('.element')
2084
+ up.destroy($element, animation: 'fade-out', duration: 80, easing: 'linear')
2085
+
2086
+ next ->
2087
+ expect($element).toHaveClass('up-destroying')
2088
+
2089
+ it 'emits an up:fragment:destroy event while the element is still in the DOM', asyncSpec (next) ->
2090
+ $element = affix('.element')
2091
+ expect($element).toBeAttached()
2092
+
2093
+ listener = jasmine.createSpy('event listener')
2094
+ $element.on('up:fragment:destroy', listener)
2095
+
2096
+ destroyDone = up.destroy($element, animation: 'fade-out', duration: 30)
2097
+
2098
+ next ->
2099
+ expect(listener).toHaveBeenCalledWith(jasmine.objectContaining($element: $element))
2100
+ expect($element).toBeAttached()
2101
+
2102
+ next.await(destroyDone)
2103
+
2104
+ next ->
2105
+ expect($element).toBeDetached()
2106
+
2107
+ it 'emits an up:fragment:destroyed event on the former parent element after the element has been removed from the DOM', asyncSpec (next) ->
2108
+ $parent = affix('.parent')
2109
+ $element = $parent.affix('.element')
2110
+ expect($element).toBeAttached()
2111
+
2112
+ listener = jasmine.createSpy('event listener')
2113
+
2114
+ $parent.on('up:fragment:destroyed', listener)
2115
+
2116
+ destroyDone = up.destroy($element, animation: 'fade-out', duration: 30)
2117
+
2118
+ next ->
2119
+ expect(listener).not.toHaveBeenCalled()
2120
+ expect($element).toBeAttached()
2121
+
2122
+ next.await(destroyDone)
2123
+
2124
+ next ->
2125
+ expect(listener).toHaveBeenCalledWith(jasmine.objectContaining($element: $element, $parent: $parent))
2126
+ expect($element).toBeDetached()
2012
2127
 
2013
2128
  describe 'up.reload', ->
2014
2129