unpoly-rails 0.34.2 → 0.35.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -2
- data/lib/assets/javascripts/unpoly/browser.coffee +41 -48
- data/lib/assets/javascripts/unpoly/bus.coffee +2 -1
- data/lib/assets/javascripts/unpoly/dom.coffee +2 -1
- data/lib/assets/javascripts/unpoly/form.coffee +4 -18
- data/lib/assets/javascripts/unpoly/motion.coffee +2 -4
- data/lib/assets/javascripts/unpoly/protocol.coffee +15 -8
- data/lib/assets/javascripts/unpoly/util.coffee +21 -40
- data/lib/unpoly/rails/version.rb +1 -1
- data/spec_app/Gemfile.lock +1 -1
- data/spec_app/spec/javascripts/helpers/to_have_request_method.js.coffee +9 -1
- data/spec_app/spec/javascripts/up/dom_spec.js.coffee +109 -92
- data/spec_app/spec/javascripts/up/form_spec.js.coffee +7 -23
- data/spec_app/spec/javascripts/up/link_spec.js.coffee +16 -18
- data/spec_app/spec/javascripts/up/modal_spec.js.coffee +42 -46
- data/spec_app/spec/javascripts/up/motion_spec.js.coffee +249 -295
- data/spec_app/spec/javascripts/up/popup_spec.js.coffee +27 -29
- data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +3 -5
- data/spec_app/spec/javascripts/up/util_spec.js.coffee +1 -0
- metadata +2 -2
data/lib/unpoly/rails/version.rb
CHANGED
data/spec_app/Gemfile.lock
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
u = up.util
|
2
|
+
|
1
3
|
beforeEach ->
|
2
4
|
jasmine.addMatchers
|
3
5
|
toHaveRequestMethod: (util, customEqualityTesters) ->
|
4
6
|
compare: (request, expectedMethod) ->
|
5
7
|
realMethodMatches = (request.method == expectedMethod)
|
6
|
-
|
8
|
+
formData = request.data()
|
9
|
+
if u.isFormData(formData)
|
10
|
+
wrappedMethod = formData.get('_method')
|
11
|
+
wrappedMethodMatches = (wrappedMethod == expectedMethod)
|
12
|
+
else
|
13
|
+
wrappedMethod = formData['_method']
|
14
|
+
wrappedMethodMatches = util.equals(wrappedMethod, [expectedMethod], customEqualityTesters)
|
7
15
|
pass: realMethodMatches || wrappedMethodMatches
|
@@ -99,14 +99,12 @@ describe 'up.dom', ->
|
|
99
99
|
up.replace('.middle', '/bar')
|
100
100
|
expect(jasmine.Ajax.requests.count()).toEqual(2)
|
101
101
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
secondReplace = -> up.replace('.middle', '/bar')
|
109
|
-
expect(secondReplace).not.toThrowError()
|
102
|
+
it "does not explode if the original request's { data } is a FormData object", ->
|
103
|
+
up.replace('.middle', '/foo', method: 'post', data: new FormData()) # POST requests are not cached
|
104
|
+
expect(jasmine.Ajax.requests.count()).toEqual(1)
|
105
|
+
@respond(responseHeaders: { 'X-Up-Location': '/bar', 'X-Up-Method': 'GET' })
|
106
|
+
secondReplace = -> up.replace('.middle', '/bar')
|
107
|
+
expect(secondReplace).not.toThrowError()
|
110
108
|
|
111
109
|
describe 'with { data } option', ->
|
112
110
|
|
@@ -331,6 +329,26 @@ describe 'up.dom', ->
|
|
331
329
|
expect($('.container')).toHaveText('new container text')
|
332
330
|
expect(document.title).toBe('Title from HTML')
|
333
331
|
|
332
|
+
it 'does not update the document title if the response has a <title> tag inside an inline SVG image (bugfix)', ->
|
333
|
+
affix('.container').text('old container text')
|
334
|
+
document.title = 'old document title'
|
335
|
+
up.replace('.container', '/path', history: false, title: true)
|
336
|
+
|
337
|
+
@respondWith """
|
338
|
+
<svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
|
339
|
+
<g>
|
340
|
+
<title>SVG Title Demo example</title>
|
341
|
+
<rect x="10" y="10" width="200" height="50" style="fill:none; stroke:blue; stroke-width:1px"/>
|
342
|
+
</g>
|
343
|
+
</svg>
|
344
|
+
|
345
|
+
<div class='container'>
|
346
|
+
new container text
|
347
|
+
</div>
|
348
|
+
"""
|
349
|
+
expect($('.container')).toHaveText('new container text')
|
350
|
+
expect(document.title).toBe('old document title')
|
351
|
+
|
334
352
|
it "does not extract the title from the response or HTTP header if history isn't updated", ->
|
335
353
|
affix('.container').text('old container text')
|
336
354
|
document.title = 'old document title'
|
@@ -903,79 +921,80 @@ describe 'up.dom', ->
|
|
903
921
|
|
904
922
|
describe 'with { transition } option', ->
|
905
923
|
|
906
|
-
|
924
|
+
it 'morphs between the old and new element', (done) ->
|
925
|
+
affix('.element').text('version 1')
|
926
|
+
up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
|
907
927
|
|
908
|
-
|
909
|
-
|
910
|
-
|
928
|
+
$ghost1 = $('.element.up-ghost:contains("version 1")')
|
929
|
+
expect($ghost1).toHaveLength(1)
|
930
|
+
expect(u.opacity($ghost1)).toBeAround(1.0, 0.1)
|
911
931
|
|
912
|
-
|
913
|
-
|
914
|
-
|
932
|
+
$ghost2 = $('.element.up-ghost:contains("version 2")')
|
933
|
+
expect($ghost2).toHaveLength(1)
|
934
|
+
expect(u.opacity($ghost2)).toBeAround(0.0, 0.1)
|
915
935
|
|
916
|
-
|
917
|
-
expect($
|
918
|
-
expect(u.opacity($ghost2)).toBeAround(
|
936
|
+
u.setTimer 190, ->
|
937
|
+
expect(u.opacity($ghost1)).toBeAround(0.0, 0.3)
|
938
|
+
expect(u.opacity($ghost2)).toBeAround(1.0, 0.3)
|
939
|
+
done()
|
919
940
|
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
done()
|
941
|
+
it 'marks the old fragment and its ghost as .up-destroying during the transition', ->
|
942
|
+
affix('.element').text('version 1')
|
943
|
+
up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
|
924
944
|
|
925
|
-
|
926
|
-
|
927
|
-
|
945
|
+
$version1 = $('.element:not(.up-ghost):contains("version 1")')
|
946
|
+
$version1Ghost = $('.element.up-ghost:contains("version 1")')
|
947
|
+
expect($version1).toHaveLength(1)
|
948
|
+
expect($version1Ghost).toHaveLength(1)
|
949
|
+
expect($version1).toHaveClass('up-destroying')
|
950
|
+
expect($version1Ghost).toHaveClass('up-destroying')
|
928
951
|
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
$version2 = $('.element:not(.up-ghost):contains("version 2")')
|
937
|
-
$version2Ghost = $('.element.up-ghost:contains("version 2")')
|
938
|
-
expect($version2).toHaveLength(1)
|
939
|
-
expect($version2Ghost).toHaveLength(1)
|
940
|
-
expect($version2).not.toHaveClass('up-destroying')
|
941
|
-
expect($version2Ghost).not.toHaveClass('up-destroying')
|
942
|
-
|
943
|
-
it 'cancels an existing transition by instantly jumping to the last frame', ->
|
944
|
-
affix('.element').text('version 1')
|
945
|
-
up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
|
952
|
+
$version2 = $('.element:not(.up-ghost):contains("version 2")')
|
953
|
+
$version2Ghost = $('.element.up-ghost:contains("version 2")')
|
954
|
+
expect($version2).toHaveLength(1)
|
955
|
+
expect($version2Ghost).toHaveLength(1)
|
956
|
+
expect($version2).not.toHaveClass('up-destroying')
|
957
|
+
expect($version2Ghost).not.toHaveClass('up-destroying')
|
946
958
|
|
947
|
-
|
948
|
-
|
949
|
-
|
959
|
+
it 'cancels an existing transition by instantly jumping to the last frame', ->
|
960
|
+
affix('.element').text('version 1')
|
961
|
+
up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
|
950
962
|
|
951
|
-
|
952
|
-
|
953
|
-
|
963
|
+
$ghost1 = $('.element.up-ghost:contains("version 1")')
|
964
|
+
expect($ghost1).toHaveLength(1)
|
965
|
+
expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
|
954
966
|
|
955
|
-
|
967
|
+
$ghost2 = $('.element.up-ghost:contains("version 2")')
|
968
|
+
expect($ghost2).toHaveLength(1)
|
969
|
+
expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
|
956
970
|
|
957
|
-
|
958
|
-
expect($ghost1).toHaveLength(0)
|
971
|
+
up.extract('.element', '<div class="element">version 3</div>', transition: 'cross-fade', duration: 200)
|
959
972
|
|
960
|
-
|
961
|
-
|
962
|
-
expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
|
973
|
+
$ghost1 = $('.element.up-ghost:contains("version 1")')
|
974
|
+
expect($ghost1).toHaveLength(0)
|
963
975
|
|
964
|
-
|
965
|
-
|
966
|
-
|
976
|
+
$ghost2 = $('.element.up-ghost:contains("version 2")')
|
977
|
+
expect($ghost2).toHaveLength(1)
|
978
|
+
expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
|
967
979
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
980
|
+
$ghost3 = $('.element.up-ghost:contains("version 3")')
|
981
|
+
expect($ghost3).toHaveLength(1)
|
982
|
+
expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
|
983
|
+
|
984
|
+
it 'delays the resolution of the returned promise until the transition is over', (done) ->
|
985
|
+
affix('.element').text('version 1')
|
986
|
+
resolution = jasmine.createSpy()
|
987
|
+
promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
|
988
|
+
promise.then(resolution)
|
989
|
+
expect(resolution).not.toHaveBeenCalled()
|
990
|
+
u.setTimer 80, ->
|
991
|
+
expect(resolution).toHaveBeenCalled()
|
992
|
+
done()
|
993
|
+
|
994
|
+
describe 'when animation is disabled', ->
|
977
995
|
|
978
|
-
|
996
|
+
beforeEach ->
|
997
|
+
up.motion.config.enabled = false
|
979
998
|
|
980
999
|
it 'immediately swaps the old and new elements', ->
|
981
1000
|
affix('.element').text('version 1')
|
@@ -1260,31 +1279,29 @@ describe 'up.dom', ->
|
|
1260
1279
|
expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value1' })
|
1261
1280
|
expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value2' })
|
1262
1281
|
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
<div class='
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
expect(newTextDuringTransition).toEqual('new-foo old-bar')
|
1287
|
-
done()
|
1282
|
+
it "doesn't let the discarded element appear in a transition", (done) ->
|
1283
|
+
oldTextDuringTransition = undefined
|
1284
|
+
newTextDuringTransition = undefined
|
1285
|
+
transition = ($old, $new) ->
|
1286
|
+
oldTextDuringTransition = squish($old.text())
|
1287
|
+
newTextDuringTransition = squish($new.text())
|
1288
|
+
u.resolvedDeferred()
|
1289
|
+
$container = affix('.container')
|
1290
|
+
$container.html """
|
1291
|
+
<div class='foo'>old-foo</div>
|
1292
|
+
<div class='bar' up-keep>old-bar</div>
|
1293
|
+
"""
|
1294
|
+
newHtml = """
|
1295
|
+
<div class='container'>
|
1296
|
+
<div class='foo'>new-foo</div>
|
1297
|
+
<div class='bar' up-keep>new-bar</div>
|
1298
|
+
</div>
|
1299
|
+
"""
|
1300
|
+
promise = up.extract('.container', newHtml, transition: transition)
|
1301
|
+
promise.then ->
|
1302
|
+
expect(oldTextDuringTransition).toEqual('old-foo old-bar')
|
1303
|
+
expect(newTextDuringTransition).toEqual('new-foo old-bar')
|
1304
|
+
done()
|
1288
1305
|
|
1289
1306
|
describe 'up.destroy', ->
|
1290
1307
|
|
@@ -9,15 +9,9 @@ describe 'up.form', ->
|
|
9
9
|
beforeEach ->
|
10
10
|
up.form.config.observeDelay = 0
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
['input', 'change']
|
16
|
-
else
|
17
|
-
# Actually we won't ever get `input` from the user in this browser,
|
18
|
-
# but we want to notice if another script manually triggers `input`
|
19
|
-
# on the element.
|
20
|
-
['input', 'change', 'keypress', 'paste', 'cut', 'click', 'propertychange']
|
12
|
+
# Actually we only need `input`, but we want to notice
|
13
|
+
# if another script manually triggers `change` on the element.
|
14
|
+
changeEvents = ['input', 'change']
|
21
15
|
|
22
16
|
describe 'when the first argument is a form field', ->
|
23
17
|
|
@@ -342,20 +336,10 @@ describe 'up.form', ->
|
|
342
336
|
beforeEach ->
|
343
337
|
@$form.affix('input[name="file-field"][type="file"]')
|
344
338
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
data = @lastRequest().data()
|
350
|
-
expect(u.isFormData(data)).toBe(true)
|
351
|
-
|
352
|
-
describeFallback 'canFormData', ->
|
353
|
-
|
354
|
-
it 'falls back to a vanilla form submission', ->
|
355
|
-
form = @$form.get(0)
|
356
|
-
spyOn(form, 'submit')
|
357
|
-
up.submit(@$form)
|
358
|
-
expect(form.submit).toHaveBeenCalled()
|
339
|
+
it 'transfers the form fields via FormData', ->
|
340
|
+
up.submit(@$form)
|
341
|
+
data = @lastRequest().data()
|
342
|
+
expect(u.isFormData(data)).toBe(true)
|
359
343
|
|
360
344
|
describeFallback 'canPushState', ->
|
361
345
|
|
@@ -421,24 +421,22 @@ describe 'up.link', ->
|
|
421
421
|
|
422
422
|
describe 'with [up-transition] modifier', ->
|
423
423
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
expect(u.opacity($
|
438
|
-
u.
|
439
|
-
|
440
|
-
expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
|
441
|
-
done()
|
424
|
+
it 'morphs between the old and new target element', (done) ->
|
425
|
+
affix('.target.old')
|
426
|
+
$link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="500"][up-easing="linear"]')
|
427
|
+
Trigger.clickSequence($link)
|
428
|
+
@respondWith '<div class="target new">new text</div>'
|
429
|
+
|
430
|
+
$oldGhost = $('.target.old.up-ghost')
|
431
|
+
$newGhost = $('.target.new.up-ghost')
|
432
|
+
expect($oldGhost).toExist()
|
433
|
+
expect($newGhost).toExist()
|
434
|
+
expect(u.opacity($oldGhost)).toBeAround(1, 0.15)
|
435
|
+
expect(u.opacity($newGhost)).toBeAround(0, 0.15)
|
436
|
+
u.setTimer 250, ->
|
437
|
+
expect(u.opacity($oldGhost)).toBeAround(0.5, 0.15)
|
438
|
+
expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
|
439
|
+
done()
|
442
440
|
|
443
441
|
describe 'wih a CSS selector in the [up-fallback] attribute', ->
|
444
442
|
|
@@ -101,22 +101,20 @@ describe 'up.modal', ->
|
|
101
101
|
expect(parseInt($body.css('padding-right'))).toBe(0)
|
102
102
|
done()
|
103
103
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
expect($
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
expect($viewport.css('overflow-y')).toEqual('hidden')
|
119
|
-
done()
|
104
|
+
it "gives the scrollbar to .up-modal instead of .up-modal-viewport while animating, so we don't see scaled scrollbars in a zoom-in animation", (done) ->
|
105
|
+
openPromise = up.modal.extract('.container', '<div class="container">text</div>', animation: 'fade-in', duration: 100)
|
106
|
+
$modal = $('.up-modal')
|
107
|
+
$viewport = $modal.find('.up-modal-viewport')
|
108
|
+
expect($modal.css('overflow-y')).toEqual('scroll')
|
109
|
+
expect($viewport.css('overflow-y')).toEqual('hidden')
|
110
|
+
openPromise.then ->
|
111
|
+
expect($modal.css('overflow-y')).not.toEqual('scroll')
|
112
|
+
expect($viewport.css('overflow-y')).toEqual('scroll')
|
113
|
+
closePromise = up.modal.close(animation: 'fade-out', duration: 200)
|
114
|
+
u.nextFrame ->
|
115
|
+
expect($modal.css('overflow-y')).toEqual('scroll')
|
116
|
+
expect($viewport.css('overflow-y')).toEqual('hidden')
|
117
|
+
done()
|
120
118
|
|
121
119
|
it 'does not add right padding to the body if the body has overflow-y: hidden', (done) ->
|
122
120
|
restoreBody = u.temporaryCss($('body'), 'overflow-y': 'hidden')
|
@@ -185,48 +183,46 @@ describe 'up.modal', ->
|
|
185
183
|
expect(bodyPadding).not.toBeAround(2 * assumedScrollbarWidth, 2 * 5)
|
186
184
|
done()
|
187
185
|
|
188
|
-
|
186
|
+
it 'closes the current modal and wait for its close animation to finish before starting the open animation of a second modal', (done) ->
|
187
|
+
up.modal.config.openAnimation = 'fade-in'
|
188
|
+
up.modal.config.openDuration = 5
|
189
|
+
up.modal.config.closeAnimation = 'fade-out'
|
190
|
+
up.modal.config.closeDuration = 60
|
189
191
|
|
190
|
-
|
191
|
-
|
192
|
-
up.
|
193
|
-
|
194
|
-
up.modal.config.closeDuration = 60
|
192
|
+
events = []
|
193
|
+
u.each ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed'], (event) ->
|
194
|
+
up.on event, ->
|
195
|
+
events.push(event)
|
195
196
|
|
196
|
-
|
197
|
-
u.each ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed'], (event) ->
|
198
|
-
up.on event, ->
|
199
|
-
events.push(event)
|
197
|
+
up.modal.extract('.target', '<div class="target">response1</div>')
|
200
198
|
|
201
|
-
|
199
|
+
# First modal is starting opening animation
|
200
|
+
expect(events).toEqual ['up:modal:open']
|
201
|
+
expect($('.target')).toHaveText('response1')
|
202
202
|
|
203
|
-
|
204
|
-
|
203
|
+
u.setTimer 80, ->
|
204
|
+
# First modal has completed opening animation
|
205
|
+
expect(events).toEqual ['up:modal:open', 'up:modal:opened']
|
205
206
|
expect($('.target')).toHaveText('response1')
|
206
207
|
|
207
|
-
|
208
|
-
|
209
|
-
expect(events).toEqual ['up:modal:open', 'up:modal:opened']
|
210
|
-
expect($('.target')).toHaveText('response1')
|
211
|
-
|
212
|
-
# We open another modal, which will cause the first modal to start closing
|
213
|
-
up.modal.extract('.target', '<div class="target">response2</div>')
|
208
|
+
# We open another modal, which will cause the first modal to start closing
|
209
|
+
up.modal.extract('.target', '<div class="target">response2</div>')
|
214
210
|
|
215
|
-
|
211
|
+
expect($('.target')).toHaveText('response1')
|
216
212
|
|
217
|
-
|
213
|
+
u.setTimer 20, ->
|
218
214
|
|
219
|
-
|
220
|
-
|
221
|
-
|
215
|
+
# Second modal is still waiting for first modal's closing animaton to finish.
|
216
|
+
expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close']
|
217
|
+
expect($('.target')).toHaveText('response1')
|
222
218
|
|
223
|
-
|
219
|
+
u.setTimer 200, ->
|
224
220
|
|
225
|
-
|
226
|
-
|
227
|
-
|
221
|
+
# First modal has finished closing, second modal has finished opening.
|
222
|
+
expect(events).toEqual ['up:modal:open', 'up:modal:opened', 'up:modal:close', 'up:modal:closed', 'up:modal:open', 'up:modal:opened']
|
223
|
+
expect($('.target')).toHaveText('response2')
|
228
224
|
|
229
|
-
|
225
|
+
done()
|
230
226
|
|
231
227
|
it 'closes an opening modal if a second modal starts opening before the first modal has finished its open animation', (done) ->
|
232
228
|
up.modal.config.openAnimation = 'fade-in'
|