unpoly-rails 0.27.3 → 0.28.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 (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 ->