unpoly-rails 0.27.3 → 0.28.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/dist/unpoly.css +29 -21
  4. data/dist/unpoly.js +291 -134
  5. data/dist/unpoly.min.css +1 -1
  6. data/dist/unpoly.min.js +3 -3
  7. data/lib/assets/javascripts/unpoly.js.coffee +1 -0
  8. data/lib/assets/javascripts/unpoly/browser.js.coffee +22 -5
  9. data/lib/assets/javascripts/unpoly/bus.js.coffee +1 -1
  10. data/lib/assets/javascripts/unpoly/flow.js.coffee +9 -6
  11. data/lib/assets/javascripts/unpoly/form.js.coffee +46 -49
  12. data/lib/assets/javascripts/unpoly/history.js.coffee +2 -2
  13. data/lib/assets/javascripts/unpoly/layout.js.coffee +3 -3
  14. data/lib/assets/javascripts/unpoly/modal.js.coffee +30 -2
  15. data/lib/assets/javascripts/unpoly/motion.js.coffee +6 -6
  16. data/lib/assets/javascripts/unpoly/navigation.js.coffee +1 -1
  17. data/lib/assets/javascripts/unpoly/popup.js.coffee +2 -2
  18. data/lib/assets/javascripts/unpoly/proxy.js.coffee +5 -9
  19. data/lib/assets/javascripts/unpoly/syntax.js.coffee +10 -6
  20. data/lib/assets/javascripts/unpoly/toast.js.coffee +66 -0
  21. data/lib/assets/javascripts/unpoly/tooltip.js.coffee +1 -1
  22. data/lib/assets/javascripts/unpoly/util.js.coffee +47 -21
  23. data/lib/assets/stylesheets/unpoly/toast.css.sass +33 -0
  24. data/lib/unpoly/rails/version.rb +1 -1
  25. data/spec_app/Gemfile.lock +1 -1
  26. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  27. data/spec_app/app/assets/stylesheets/integration_test.sass +14 -0
  28. data/spec_app/app/controllers/application_controller.rb +8 -0
  29. data/spec_app/app/controllers/error_test_controller.rb +5 -0
  30. data/spec_app/app/views/error_test/trigger.erb +72 -0
  31. data/spec_app/app/views/error_test/unexpected_response.erb +3 -0
  32. data/spec_app/app/views/pages/start.erb +4 -0
  33. data/spec_app/config/routes.rb +2 -0
  34. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -1
  35. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +62 -0
  36. data/spec_app/spec/javascripts/up/form_spec.js.coffee +116 -3
  37. data/spec_app/spec/javascripts/up/link_spec.js.coffee +2 -2
  38. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +88 -4
  39. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +27 -1
  40. data/spec_app/spec/javascripts/up/util_spec.js.coffee +12 -0
  41. metadata +8 -3
  42. data/lib/assets/stylesheets/unpoly/error.css.sass +0 -21
@@ -0,0 +1,5 @@
1
+ class ErrorTestController < ApplicationController
2
+
3
+ layout 'integration_test'
4
+
5
+ end
@@ -0,0 +1,72 @@
1
+ <div class="example">
2
+
3
+ <h2>Link to unexpected response</h2>
4
+
5
+ <p>
6
+ <a href="/error_test/unexpected_response" up-target=".link-target">Link</a>
7
+ </p>
8
+
9
+ <div class="area link-target">
10
+ .link-target
11
+ </div>
12
+
13
+ </div>
14
+
15
+ <div class="example">
16
+
17
+ <h2>Form to unexpected response</h2>
18
+
19
+ <%= form_tag "/error_test/unexpected_response", 'up-target' => '.form-target' do %>
20
+
21
+ <p>
22
+ <input type="text" name="param" value="param-value">
23
+ </p>
24
+
25
+ <p>
26
+ <button type="submit">Submit</button>
27
+ </p>
28
+
29
+ <% end %>
30
+
31
+ <div class="area form-target">
32
+ .form-target
33
+ </div>
34
+
35
+ </div>
36
+
37
+ <script>
38
+
39
+ if (false) {
40
+
41
+ var html = '<!DOCTYPE html>\
42
+ <html>\
43
+ <head>\
44
+ <title>Integration test - Unpoly</title>\
45
+ <link rel="stylesheet" media="all" href="/assets/integration_test-6697c9a85c007956bf460adc74ba46bc.css" />\
46
+ <meta name="csrf-param" content="authenticity_token" />\
47
+ <meta name="csrf-token" content="CYCqVYIr0wbjErWnupydWZiO8WMqRn+z4BlHN4g4o3T3UN4hW2sDfQXFT3/ygZt41m0jo/YEl6syohm8b8DsYw==" />\
48
+ </head>\
49
+ <body>\
50
+ <div class="page">\
51
+ <div class="unexpected-response">\
52
+ unexpected response\
53
+ </div>\
54
+ \
55
+ </div>\
56
+ </body>\
57
+ </html>';
58
+
59
+ var selector = '.linx-target';
60
+
61
+ up.fail(['Could not find selector %s in response %s', selector, html], {
62
+ inspect: {
63
+ label: 'Open response',
64
+ callback: function () {
65
+ alert("callback")
66
+ }
67
+ }
68
+ });
69
+
70
+ }
71
+
72
+ </script>
@@ -0,0 +1,3 @@
1
+ <div class="unexpected-response">
2
+ unexpected response
3
+ </div>
@@ -30,4 +30,8 @@
30
30
  <li><%= link_to 'Form (upload)', '/form_test/upload/new' %></li>
31
31
  </ul>
32
32
 
33
+ <ul>
34
+ <li><%= link_to 'Error', '/error_test/trigger' %></li>
35
+ </ul>
36
+
33
37
  </div>
@@ -5,6 +5,8 @@ Rails.application.routes.draw do
5
5
 
6
6
  get 'binding_test/:action', controller: 'binding_test'
7
7
  get 'css_test/:action', controller: 'css_test'
8
+ get 'error_test/:action', controller: 'error_test'
9
+ post 'error_test/:action', controller: 'error_test'
8
10
 
9
11
  namespace :form_test do
10
12
  resource :basic, only: [:new, :create]
@@ -2,7 +2,7 @@ u = up.util
2
2
 
3
3
  beforeEach ->
4
4
  @lastRequest = ->
5
- jasmine.Ajax.requests.mostRecent() or up.util.error('There is no last request')
5
+ jasmine.Ajax.requests.mostRecent() or u.fail('There is no last request')
6
6
 
7
7
  @respondWith = (args...) ->
8
8
  firstArg = args.shift()
@@ -0,0 +1,62 @@
1
+ describe 'up.browser', ->
2
+
3
+ u = up.util
4
+
5
+ describe 'Javascript functions', ->
6
+
7
+ describe 'up.browser.loadPage', ->
8
+
9
+ afterEach ->
10
+ # We're preventing the form to be submitted during tests,
11
+ # so we need to remove it manually after each example.
12
+ $('form.up-page-loader').remove()
13
+
14
+ describe 'for GET requests', ->
15
+
16
+ it 'sets location.href to the given URL', ->
17
+ hrefSetter = up.browser.knife.mock('setLocationHref')
18
+ up.browser.loadPage('/foo')
19
+ expect(hrefSetter).toHaveBeenCalledWith('/foo')
20
+
21
+ it 'encodes { data } params into the URL', ->
22
+ hrefSetter = up.browser.knife.mock('setLocationHref')
23
+ up.browser.loadPage('/foo', data: { param1: 'param1 value', param2: 'param2 value' })
24
+ expect(hrefSetter).toHaveBeenCalledWith('/foo?param1=param1%20value&param2=param2%20value')
25
+
26
+ describe 'for POST requests', ->
27
+
28
+ it 'creates a form, adds all { data } params a hidden fields and submits the form', ->
29
+ submitForm = up.browser.knife.mock('submitForm')
30
+ up.browser.loadPage('/foo', method: 'post', data: { param1: 'param1 value', param2: 'param2 value' })
31
+ expect(submitForm).toHaveBeenCalled()
32
+ $form = $('form.up-page-loader')
33
+ expect($form).toExist()
34
+ expect($form.attr('action')).toEqual('/foo')
35
+ expect($form.attr('method')).toEqual('post')
36
+ expect($form.find('input[name="param1"][value="param1 value"]')).toExist()
37
+ expect($form.find('input[name="param2"][value="param2 value"]')).toExist()
38
+
39
+ it 'submits the Rails CSRF token as another hidden field', ->
40
+ submitForm = up.browser.knife.mock('submitForm')
41
+ spyOn(up.rails, 'csrfField').and.returnValue
42
+ name: 'authenticity-param-name',
43
+ value: 'authenticity-token'
44
+ up.browser.loadPage('/foo', method: 'post')
45
+ expect(submitForm).toHaveBeenCalled()
46
+ $form = $('form.up-page-loader')
47
+ $tokenInput = $form.find('input[name="authenticity-param-name"]')
48
+ expect($tokenInput).toExist()
49
+ expect($tokenInput.val()).toEqual('authenticity-token')
50
+
51
+ u.each ['PUT', 'PATCH', 'DELETE'], (method) ->
52
+
53
+ describe "for #{method} requests", ->
54
+
55
+ it "uses a POST form and sends the actual method as a { _method } param", ->
56
+ submitForm = up.browser.knife.mock('submitForm')
57
+ up.browser.loadPage('/foo', method: method)
58
+ expect(submitForm).toHaveBeenCalled()
59
+ $form = $('form.up-page-loader')
60
+ expect($form).toExist()
61
+ expect($form.attr('method')).toEqual('post')
62
+ expect($form.find('input[name="_method"]').val()).toEqual(method.toLowerCase())
@@ -6,6 +6,9 @@ describe 'up.form', ->
6
6
 
7
7
  describe 'up.observe', ->
8
8
 
9
+ beforeEach ->
10
+ up.form.config.observeDelay = 0
11
+
9
12
  changeEvents = if up.browser.canInputEvent()
10
13
  # Actually we only need `input`, but we want to notice
11
14
  # if another script manually triggers `change` on the element.
@@ -16,9 +19,9 @@ describe 'up.form', ->
16
19
  # on the element.
17
20
  ['input', 'change', 'keypress', 'paste', 'cut', 'click', 'propertychange']
18
21
 
19
- u.each changeEvents, (eventName) ->
22
+ describe 'when the first argument is a form field', ->
20
23
 
21
- describe 'when the first argument is a form field', ->
24
+ u.each changeEvents, (eventName) ->
22
25
 
23
26
  it "runs the callback when the input receives a '#{eventName}' event and the value changed", (done) ->
24
27
  $input = affix('input[value="old-value"]')
@@ -58,7 +61,107 @@ describe 'up.form', ->
58
61
  $input.trigger(eventName)
59
62
  expect(callback.calls.count()).toEqual(2)
60
63
 
61
- describe 'when the first argument is a form', ->
64
+ describe 'when the first argument is a checkbox', ->
65
+
66
+ it 'runs the callback when the checkbox changes its checked state', (done) ->
67
+ $form = affix('form')
68
+ $checkbox = $form.affix('input[type="checkbox"]')
69
+ callback = jasmine.createSpy('change callback')
70
+ up.observe($checkbox, callback)
71
+ expect($checkbox.is(':checked')).toBe(false)
72
+ $checkbox.get(0).click()
73
+ u.nextFrame ->
74
+ expect($checkbox.is(':checked')).toBe(true)
75
+ expect(callback.calls.count()).toEqual(1)
76
+ $checkbox.get(0).click()
77
+ u.nextFrame ->
78
+ expect($checkbox.is(':checked')).toBe(false)
79
+ expect(callback.calls.count()).toEqual(2)
80
+ done()
81
+
82
+ it 'runs the callback when the checkbox is toggled by clicking its label', (done) ->
83
+ $form = affix('form')
84
+ $checkbox = $form.affix('input#tick[type="checkbox"]')
85
+ $label = $form.affix('label[for="tick"]').text('tick label')
86
+ callback = jasmine.createSpy('change callback')
87
+ up.observe($checkbox, callback)
88
+ expect($checkbox.is(':checked')).toBe(false)
89
+ $label.get(0).click()
90
+ u.nextFrame ->
91
+ expect($checkbox.is(':checked')).toBe(true)
92
+ expect(callback.calls.count()).toEqual(1)
93
+ $label.get(0).click()
94
+ u.nextFrame ->
95
+ expect($checkbox.is(':checked')).toBe(false)
96
+ expect(callback.calls.count()).toEqual(2)
97
+ done()
98
+
99
+ describe 'when the first argument is a radio button group', ->
100
+
101
+ it 'runs the callback when the group changes its selection', (done) ->
102
+ $form = affix('form')
103
+ $radio1 = $form.affix('input[type="radio"][name="group"][value="1"]')
104
+ $radio2 = $form.affix('input[type="radio"][name="group"][value="2"]')
105
+ $group = $radio1.add($radio2)
106
+ callback = jasmine.createSpy('change callback')
107
+ up.observe($group, callback)
108
+ expect($radio1.is(':checked')).toBe(false)
109
+ $radio1.get(0).click()
110
+ u.nextFrame ->
111
+ expect($radio1.is(':checked')).toBe(true)
112
+ expect(callback.calls.count()).toEqual(1)
113
+ $radio2.get(0).click()
114
+ u.nextFrame ->
115
+ expect($radio1.is(':checked')).toBe(false)
116
+ expect(callback.calls.count()).toEqual(2)
117
+ done()
118
+
119
+ it "runs the callbacks when a radio button is selected or deselected by clicking a label in the group", (done) ->
120
+ $form = affix('form')
121
+ $radio1 = $form.affix('input#radio1[type="radio"][name="group"][value="1"]')
122
+ $radio1Label = $form.affix('label[for="radio1"]').text('label 1')
123
+ $radio2 = $form.affix('input#radio2[type="radio"][name="group"][value="2"]')
124
+ $radio2Label = $form.affix('label[for="radio2"]').text('label 2')
125
+ $group = $radio1.add($radio2)
126
+ callback = jasmine.createSpy('change callback')
127
+ up.observe($group, callback)
128
+ expect($radio1.is(':checked')).toBe(false)
129
+ $radio1Label.get(0).click()
130
+ u.nextFrame ->
131
+ expect($radio1.is(':checked')).toBe(true)
132
+ expect(callback.calls.count()).toEqual(1)
133
+ $radio2Label.get(0).click()
134
+ u.nextFrame ->
135
+ expect($radio1.is(':checked')).toBe(false)
136
+ expect(callback.calls.count()).toEqual(2)
137
+ done()
138
+
139
+ it "takes the group's initial selected value into account", (done) ->
140
+ $form = affix('form')
141
+ $radio1 = $form.affix('input[type="radio"][name="group"][value="1"][checked="checked"]')
142
+ $radio2 = $form.affix('input[type="radio"][name="group"][value="2"]')
143
+ $group = $radio1.add($radio2)
144
+ callback = jasmine.createSpy('change callback')
145
+ up.observe($group, callback)
146
+ expect($radio1.is(':checked')).toBe(true)
147
+ expect($radio2.is(':checked')).toBe(false)
148
+ $radio1.get(0).click()
149
+ u.nextFrame ->
150
+ # Since the radio button was already checked, the click doesn't do anything
151
+ expect($radio1.is(':checked')).toBe(true)
152
+ expect($radio2.is(':checked')).toBe(false)
153
+ # Since the radio button was already checked, clicking it again won't trigger the callback
154
+ expect(callback.calls.count()).toEqual(0)
155
+ $radio2.get(0).click()
156
+ u.nextFrame ->
157
+ expect($radio1.is(':checked')).toBe(false)
158
+ expect($radio2.is(':checked')).toBe(true)
159
+ expect(callback.calls.count()).toEqual(1)
160
+ done()
161
+
162
+ describe 'when the first argument is a form', ->
163
+
164
+ u.each changeEvents, (eventName) ->
62
165
 
63
166
  it "runs the callback when any of the form's inputs receives a '#{eventName}' event and the value changed", (done) ->
64
167
  $form = affix('form')
@@ -82,6 +185,16 @@ describe 'up.form', ->
82
185
  expect(callback).not.toHaveBeenCalled()
83
186
  done()
84
187
 
188
+ # it 'runs the callback only once when a radio button group changes its selection', ->
189
+ # $form = affix('form')
190
+ # $radio1 = $form.affix('input[type="radio"][name="group"][value="1"][checked="checked"]')
191
+ # $radio2 = $form.affix('input[type="radio"][name="group"][value="2"]')
192
+ # callback = jasmine.createSpy('change callback')
193
+ # up.observe($form, callback)
194
+ # $radio2.get(0).click()
195
+ # u.nextFrame ->
196
+ # expect(callback.calls.count()).toEqual(1)
197
+
85
198
  describe 'up.submit', ->
86
199
 
87
200
  describeCapability 'canPushState', ->
@@ -395,7 +395,7 @@ describe 'up.link', ->
395
395
 
396
396
  it 'morphs between the old and new target element', (done) ->
397
397
  affix('.target.old')
398
- $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="300"][up-easing="linear"]')
398
+ $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="500"][up-easing="linear"]')
399
399
  $link.click()
400
400
  @respondWith '<div class="target new">new text</div>'
401
401
 
@@ -405,7 +405,7 @@ describe 'up.link', ->
405
405
  expect($newGhost).toExist()
406
406
  expect(u.opacity($oldGhost)).toBeAround(1, 0.15)
407
407
  expect(u.opacity($newGhost)).toBeAround(0, 0.15)
408
- u.setTimer 150, ->
408
+ u.setTimer 250, ->
409
409
  expect(u.opacity($oldGhost)).toBeAround(0.5, 0.15)
410
410
  expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
411
411
  done()
@@ -318,13 +318,24 @@ describe 'up.modal', ->
318
318
 
319
319
  describe 'up.modal.close', ->
320
320
 
321
- it 'closes a currently open modal'
321
+ it 'closes a currently open modal', (done) ->
322
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>')
322
323
 
323
- it 'does nothing if no modal is open'
324
+ modalContent = $('.modal')
325
+ expect(modalContent).toBeInDOM()
324
326
 
325
- describe 'up.modal.source', ->
327
+ up.modal.close().then ->
328
+ expect(modalContent).not.toBeInDOM()
329
+ done()
326
330
 
327
- it 'should have tests'
331
+ it 'does nothing if no modal is open', (done) ->
332
+ wasClosed = false
333
+ up.on 'up:modal:close', ->
334
+ wasClosed = true
335
+
336
+ up.modal.close().then ->
337
+ expect(wasClosed).toBe(false)
338
+ done()
328
339
 
329
340
  describe 'unobtrusive behavior', ->
330
341
 
@@ -443,6 +454,79 @@ describe 'up.modal', ->
443
454
  expect(wasClosed).toBe(false)
444
455
  expect(wasDefaultPrevented).toBe(false)
445
456
 
457
+ describe 'template behavior', ->
458
+
459
+ it 'closes the modal on close icon click', (done) ->
460
+ wasClosed = false
461
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false)
462
+
463
+ closeIcon = $('.up-modal-close')
464
+ up.on 'up:modal:close', ->
465
+ wasClosed = true
466
+
467
+ closeIcon.click()
468
+ u.nextFrame ->
469
+ expect(wasClosed).toBe(true)
470
+ done()
471
+
472
+ it 'closes the modal on backdrop click', (done) ->
473
+ wasClosed = false
474
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false)
475
+
476
+ backdrop = $('.up-modal-backdrop')
477
+ up.on 'up:modal:close', ->
478
+ wasClosed = true
479
+
480
+ backdrop.click()
481
+ u.nextFrame ->
482
+ expect(wasClosed).toBe(true)
483
+ done()
484
+
485
+ it 'closes the modal when the user presses the escape key', (done) ->
486
+ wasClosed = false
487
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false)
488
+ up.on 'up:modal:close', ->
489
+ wasClosed = true
490
+
491
+ escapeEvent = $.Event('keydown', keyCode: 27)
492
+ $('body').trigger(escapeEvent)
493
+ u.nextFrame ->
494
+ expect(wasClosed).toBe(true)
495
+ done()
496
+
497
+ describe 'when opened with { closable: false }', ->
498
+
499
+ it 'does not render a close icon', ->
500
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false, closable: false)
501
+
502
+ modal = $('.up-modal')
503
+ expect(modal).not.toContainElement('.up-modal-close')
504
+
505
+ it 'does not close the modal on backdrop click', (done) ->
506
+ wasClosed = false
507
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false, closable: false)
508
+
509
+ backdrop = $('.up-modal-backdrop')
510
+ up.on 'up:modal:close', ->
511
+ wasClosed = true
512
+
513
+ backdrop.click()
514
+ u.nextFrame ->
515
+ expect(wasClosed).toBe(false)
516
+ done()
517
+
518
+ it 'does not close the modal when the user presses the escape key', (done) ->
519
+ wasClosed = false
520
+ up.modal.extract('.modal', '<div class="modal">Modal content</div>', animation: false, closable: false)
521
+ up.on 'up:modal:close', ->
522
+ wasClosed = true
523
+
524
+ escapeEvent = $.Event('keydown', keyCode: 27)
525
+ $('body').trigger(escapeEvent)
526
+ u.nextFrame ->
527
+ expect(wasClosed).toBe(false)
528
+ done()
529
+
446
530
  describe 'when replacing content', ->
447
531
 
448
532
  beforeEach ->