unpoly-rails 0.24.1 → 0.25.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README_RAILS.md +8 -1
  4. data/dist/unpoly.css +22 -10
  5. data/dist/unpoly.js +406 -196
  6. data/dist/unpoly.min.css +1 -1
  7. data/dist/unpoly.min.js +3 -3
  8. data/lib/assets/javascripts/unpoly/flow.js.coffee +36 -19
  9. data/lib/assets/javascripts/unpoly/form.js.coffee +1 -2
  10. data/lib/assets/javascripts/unpoly/link.js.coffee +2 -2
  11. data/lib/assets/javascripts/unpoly/modal.js.coffee +174 -81
  12. data/lib/assets/javascripts/unpoly/navigation.js.coffee +3 -1
  13. data/lib/assets/javascripts/unpoly/popup.js.coffee +62 -37
  14. data/lib/assets/javascripts/unpoly/proxy.js.coffee +1 -0
  15. data/lib/assets/javascripts/unpoly/syntax.js.coffee +12 -4
  16. data/lib/assets/javascripts/unpoly/util.js.coffee +55 -13
  17. data/lib/assets/stylesheets/unpoly/modal.css.sass +28 -12
  18. data/lib/unpoly/rails/inspector.rb +26 -0
  19. data/lib/unpoly/rails/version.rb +1 -1
  20. data/spec_app/Gemfile.lock +1 -1
  21. data/spec_app/app/controllers/binding_test_controller.rb +6 -0
  22. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -11
  23. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +21 -7
  24. data/spec_app/spec/javascripts/up/form_spec.js.coffee +15 -0
  25. data/spec_app/spec/javascripts/up/link_spec.js.coffee +11 -10
  26. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +232 -30
  27. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +33 -27
  28. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +72 -0
  29. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +51 -13
  30. metadata +2 -2
@@ -840,6 +840,24 @@ up.util = (($) ->
840
840
  $outer.remove()
841
841
  width
842
842
 
843
+ ###*
844
+ Returns whether the given element is currently showing a vertical scrollbar.
845
+
846
+ @function up.util.documentHasVerticalScrollbar
847
+ @internal
848
+ ###
849
+ documentHasVerticalScrollbar = ->
850
+ body = document.body
851
+ $body = $(body)
852
+ html = document.documentElement
853
+
854
+ bodyOverflow = $body.css('overflow-y')
855
+
856
+ forcedScroll = (bodyOverflow == 'scroll')
857
+ forcedHidden = (bodyOverflow == 'hidden')
858
+
859
+ forcedScroll || (!forcedHidden && html.scrollHeight > html.clientHeight)
860
+
843
861
  ###*
844
862
  Modifies the given function so it only runs once.
845
863
  Subsequent calls will return the previous return value.
@@ -935,6 +953,9 @@ up.util = (($) ->
935
953
  ###
936
954
  cssAnimate = (elementOrSelector, lastFrame, opts) ->
937
955
  $element = $(elementOrSelector)
956
+
957
+ # Don't name the local variable `options` since that would override
958
+ # the `options` function in our scope. We really need `let` :(
938
959
  opts = options(opts,
939
960
  duration: 300,
940
961
  delay: 0,
@@ -944,14 +965,32 @@ up.util = (($) ->
944
965
  # We don't finish an existing animation here, since the public API
945
966
  # we expose as `up.motion.animate` already does this.
946
967
  deferred = $.Deferred()
968
+
969
+ transitionProperties = Object.keys(lastFrame)
947
970
  transition =
948
- 'transition-property': Object.keys(lastFrame).join(', ')
971
+ 'transition-property': transitionProperties.join(', ')
949
972
  'transition-duration': "#{opts.duration}ms"
950
973
  'transition-delay': "#{opts.delay}ms"
951
974
  'transition-timing-function': opts.easing
952
975
  oldTransition = $element.css(Object.keys(transition))
953
976
 
954
977
  $element.addClass('up-animating')
978
+
979
+ transitionFinished = ->
980
+ $element.removeClass('up-animating')
981
+ $element.off('transitionend', onTransitionEnd)
982
+
983
+ onTransitionEnd = (event) ->
984
+ completedProperty = event.originalEvent.propertyName
985
+ if contains(transitionProperties, completedProperty)
986
+ deferred.resolve() # unless isDetached($element)
987
+ transitionFinished()
988
+
989
+ $element.on('transitionend', onTransitionEnd)
990
+
991
+ # Clean up in case we're canceled through some other code that resolves our deferred.
992
+ deferred.then(transitionFinished)
993
+
955
994
  withoutCompositing = forceCompositing($element)
956
995
  $element.css(transition)
957
996
  $element.css(lastFrame)
@@ -963,26 +1002,20 @@ up.util = (($) ->
963
1002
 
964
1003
  # To interrupt the running transition we *must* set it to 'none' exactly.
965
1004
  # We cannot simply restore the old transition properties because browsers
966
- # would simply keep transitioning the old properties.
1005
+ # would simply keep transitioning.
967
1006
  $element.css('transition': 'none')
968
1007
 
969
- # Restoring a previous transition involves some work, so we only do it if
1008
+ # Restoring a previous transition involves forcing a repaint, so we only do it if
970
1009
  # we know the element was transitioning before.
1010
+ # Note that the default transition for elements is actually "all 0s ease 0s"
1011
+ # instead of "none", although that has the same effect as "none".
971
1012
  hadTransitionBefore = !(oldTransition['transition-property'] == 'none' || (oldTransition['transition-property'] == 'all' && oldTransition['transition-duration'][0] == '0'))
972
1013
  if hadTransitionBefore
1014
+ # If there is no repaint between the "none" transition and restoring
1015
+ # the previous transition, the browser will simply keep transitioning.
973
1016
  forceRepaint($element) # :(
974
1017
  $element.css(oldTransition)
975
1018
 
976
- # Since listening to transitionEnd events is painful, we wait for a timeout
977
- # and then resolve our deferred. Maybe revisit that decision some day.
978
- animationEnd = opts.duration + opts.delay
979
- endTimeout = setTimer animationEnd, ->
980
- $element.removeClass('up-animating')
981
- deferred.resolve() unless isDetached($element)
982
- # Clean up in case we're canceled through some other code that
983
- # resolves our deferred.
984
- deferred.then(-> clearTimeout(endTimeout))
985
-
986
1019
  # Return the whole deferred and not just return a thenable.
987
1020
  # Other code will need the possibility to cancel the animation
988
1021
  # by resolving the deferred.
@@ -1652,6 +1685,13 @@ up.util = (($) ->
1652
1685
  else
1653
1686
  {}
1654
1687
 
1688
+ opacity = (element) ->
1689
+ rawOpacity = $(element).css('opacity')
1690
+ if isGiven(rawOpacity)
1691
+ parseFloat(rawOpacity)
1692
+ else
1693
+ undefined
1694
+
1655
1695
  ###*
1656
1696
  Returns whether the given element has been detached from the DOM
1657
1697
  (or whether it was never attached).
@@ -1752,6 +1792,7 @@ up.util = (($) ->
1752
1792
  remove: remove
1753
1793
  memoize: memoize
1754
1794
  scrollbarWidth: scrollbarWidth
1795
+ documentHasVerticalScrollbar: documentHasVerticalScrollbar
1755
1796
  config: config
1756
1797
  cache: cache
1757
1798
  unwrapElement: unwrapElement
@@ -1762,6 +1803,7 @@ up.util = (($) ->
1762
1803
  extractOptions: extractOptions
1763
1804
  isDetached: isDetached
1764
1805
  noop: noop
1806
+ opacity: opacity
1765
1807
 
1766
1808
  )($)
1767
1809
 
@@ -1,48 +1,64 @@
1
- $stratum-backdrop: 10000
2
- $stratum-elements: 11000
1
+ $stratum: 10000
3
2
 
4
- // These could actually be 1000, 2000, 3000 and 4000 since the `fixed` position of some elements defines
3
+ // These could actually be 1000, 2000, 3000 and 4000 since the `fixed` position of an element defines
5
4
  // a stacking context for all contained z-indexes.
6
5
  //
7
6
  // However, let's keep the option open that these elements will one day not have its stacking context.
8
7
  //
9
8
  // Also let's not do 1, 2, 3 and 4 so other elements have a chance to move themselves between the layers.
10
- $substratum-dialog: 12000
11
- $substratum-content: 13000
12
- $substratum-close: 14000
9
+ $substratum-backdrop: 11000
10
+ $substratum-elements: 12000
11
+ $substratum-dialog: 13000
12
+ $substratum-content: 14000
13
+ $substratum-close: 15000
13
14
 
14
15
  $close-height: 36px
15
16
  $close-width: 36px
16
17
  $close-font-size: 34px
17
18
 
18
19
  .up-modal
20
+ position: fixed
21
+ top: 0
22
+ left: 0
23
+ bottom: 0
24
+ right: 0
25
+ z-index: $stratum
26
+ overflow-x: hidden
19
27
 
20
28
  .up-modal-backdrop
21
- z-index: $stratum-backdrop
29
+ z-index: $substratum-backdrop
22
30
  background-color: rgba(90, 90, 90, 0.4)
23
- position: fixed
31
+ position: absolute
24
32
  top: 0
25
33
  right: 0
26
34
  bottom: 0
27
35
  left: 0
28
36
 
29
37
  .up-modal-viewport
30
- z-index: $stratum-elements
31
- position: fixed
38
+ position: absolute
32
39
  top: 0
33
40
  left: 0
34
41
  bottom: 0
35
42
  right: 0
43
+ z-index: $substratum-elements
36
44
  overflow-x: hidden
37
- overflow-y: hidden
45
+ // The viewport always has a scrollbar, except when we're animating (see below)
46
+ overflow-y: scroll
38
47
  // We prefer centering the dialog as an `inline-block`
39
48
  // to giving it a horizontal margin of `auto`. This way
40
49
  // the width of `.up-modal-dialog` is controlled by the
41
50
  // contents of `.up-modal-content`.
42
51
  text-align: center
43
52
 
44
- .up-modal.up-modal-ready &
53
+ .up-modal.up-modal-animating
54
+ // During opening/closing animations we let the .up-modal container take over
55
+ // the scrollbars that would usually be owned by .up-modal-viewport.
56
+ // If .up-modal-viewport had a scrollbar while animating with
57
+ // "zoom-in" it would look strange that the scrollbar is scaled.
58
+ &
45
59
  overflow-y: scroll
60
+ .up-modal-viewport
61
+ overflow-y: hidden
46
62
 
47
63
  .up-modal-dialog
48
64
  z-index: $substratum-dialog
@@ -35,6 +35,32 @@ module Unpoly
35
35
  request.headers['X-Up-Target']
36
36
  end
37
37
 
38
+ ##
39
+ # Tests whether the given CSS selector is targeted by the current fragment update.
40
+ #
41
+ # Note that the matching logic is very simplistic and does not actually know
42
+ # how your page layout is structured. It will return `true` if
43
+ # the tested selector and the requested CSS selector matches exactly, or if the
44
+ # requested selector is `body` or `html`.
45
+ #
46
+ # Always returns `true` if the current request is not an Unpoly fragment update.
47
+ def target?(tested_target)
48
+ if up?
49
+ actual_target = target
50
+ if actual_target == tested_target
51
+ true
52
+ elsif actual_target == 'html'
53
+ true
54
+ elsif actual_target == 'body'
55
+ not ['head', 'title', 'meta'].include?(tested_target)
56
+ else
57
+ false
58
+ end
59
+ else
60
+ true
61
+ end
62
+ end
63
+
38
64
  ##
39
65
  # Returns whether the current form submission should be
40
66
  # [validated](http://unpoly.com/up-validate) (and not be saved to the database).
@@ -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.24.1'
7
+ VERSION = '0.25.0'
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- unpoly-rails (0.23.1)
4
+ unpoly-rails (0.24.1)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -8,6 +8,12 @@ class BindingTestController < ActionController::Base
8
8
  render :text => up.target
9
9
  end
10
10
 
11
+ def up_is_target
12
+ tested_target = params[:tested_target].presence
13
+ tested_target or raise "No target given"
14
+ render :text => up.target?(tested_target).to_s
15
+ end
16
+
11
17
  def is_up_validate
12
18
  render :text => up.validate?.to_s
13
19
  end
@@ -17,7 +17,7 @@ describe BindingTestController do
17
17
 
18
18
  describe '#up' do
19
19
 
20
- describe '#selector' do
20
+ describe '#target' do
21
21
 
22
22
  it 'returns the CSS selector that is requested via Unpoly' do
23
23
  request.headers['X-Up-Target'] = '.foo'
@@ -27,6 +27,87 @@ describe BindingTestController do
27
27
 
28
28
  end
29
29
 
30
+ describe '#target?' do
31
+
32
+ it 'returns true if the tested CSS selector is requested via Unpoly' do
33
+ request.headers['X-Up-Target'] = '.foo'
34
+ get :up_is_target, tested_target: '.foo'
35
+ expect(response.body).to eq('true')
36
+ end
37
+
38
+ it 'returns false if Unpoly is requesting another CSS selector' do
39
+ request.headers['X-Up-Target'] = '.bar'
40
+ get :up_is_target, tested_target: '.foo'
41
+ expect(response.body).to eq('false')
42
+ end
43
+
44
+ it 'returns true if the request is not an Unpoly request' do
45
+ get :up_is_target, tested_target: '.foo'
46
+ expect(response.body).to eq('true')
47
+ end
48
+
49
+ it 'returns true if testing a custom selector, and Unpoly requests "body"' do
50
+ request.headers['X-Up-Target'] = 'body'
51
+ get :up_is_target, tested_target: '.foo'
52
+ expect(response.body).to eq('true')
53
+ end
54
+
55
+ it 'returns true if testing a custom selector, and Unpoly requests "html"' do
56
+ request.headers['X-Up-Target'] = 'html'
57
+ get :up_is_target, tested_target: '.foo'
58
+ expect(response.body).to eq('true')
59
+ end
60
+
61
+ it 'returns true if testing "body", and Unpoly requests "html"' do
62
+ request.headers['X-Up-Target'] = 'html'
63
+ get :up_is_target, tested_target: 'body'
64
+ expect(response.body).to eq('true')
65
+ end
66
+
67
+ it 'returns true if testing "head", and Unpoly requests "html"' do
68
+ request.headers['X-Up-Target'] = 'html'
69
+ get :up_is_target, tested_target: 'head'
70
+ expect(response.body).to eq('true')
71
+ end
72
+
73
+ it 'returns false if the tested CSS selector is "head" but Unpoly requests "body"' do
74
+ request.headers['X-Up-Target'] = 'body'
75
+ get :up_is_target, tested_target: 'head'
76
+ expect(response.body).to eq('false')
77
+ end
78
+
79
+ it 'returns false if the tested CSS selector is "title" but Unpoly requests "body"' do
80
+ request.headers['X-Up-Target'] = 'body'
81
+ get :up_is_target, tested_target: 'title'
82
+ expect(response.body).to eq('false')
83
+ end
84
+
85
+ it 'returns false if the tested CSS selector is "meta" but Unpoly requests "body"' do
86
+ request.headers['X-Up-Target'] = 'body'
87
+ get :up_is_target, tested_target: 'meta'
88
+ expect(response.body).to eq('false')
89
+ end
90
+
91
+ it 'returns true if the tested CSS selector is "head", and Unpoly requests "html"' do
92
+ request.headers['X-Up-Target'] = 'html'
93
+ get :up_is_target, tested_target: 'head'
94
+ expect(response.body).to eq('true')
95
+ end
96
+
97
+ it 'returns true if the tested CSS selector is "title", Unpoly requests "html"' do
98
+ request.headers['X-Up-Target'] = 'html'
99
+ get :up_is_target, tested_target: 'title'
100
+ expect(response.body).to eq('true')
101
+ end
102
+
103
+ it 'returns true if the tested CSS selector is "meta", and Unpoly requests "html"' do
104
+ request.headers['X-Up-Target'] = 'html'
105
+ get :up_is_target, tested_target: 'meta'
106
+ expect(response.body).to eq('true')
107
+ end
108
+
109
+ end
110
+
30
111
  describe '#validate?' do
31
112
 
32
113
  it 'returns true the request is an Unpoly validation call' do
@@ -63,14 +144,4 @@ describe BindingTestController do
63
144
 
64
145
  end
65
146
 
66
-
67
- # describe '#test' do
68
- #
69
- # it 'does stuff' do
70
- # get :test
71
- # expect(response.body).to eq('foo')
72
- # end
73
- #
74
- # end
75
-
76
147
  end
@@ -47,6 +47,20 @@ describe 'up.flow', ->
47
47
  expect(resolution).toHaveBeenCalled()
48
48
  expect($('.middle')).toHaveText('new-middle')
49
49
 
50
+ it 'returns a promise that will be resolved once the server response was received and the swap animations have completed', (done) ->
51
+ resolution = jasmine.createSpy()
52
+ promise = up.replace('.middle', '/path', transition: 'cross-fade', duration: 50)
53
+ promise.then(resolution)
54
+ expect(resolution).not.toHaveBeenCalled()
55
+ expect($('.middle')).toHaveText('old-middle')
56
+ @respond()
57
+ expect(resolution).not.toHaveBeenCalled()
58
+ u.setTimer 20, ->
59
+ expect(resolution).not.toHaveBeenCalled()
60
+ u.setTimer 80, ->
61
+ expect(resolution).toHaveBeenCalled()
62
+ done()
63
+
50
64
  describe 'with { data } option', ->
51
65
 
52
66
  it "uses the given params as a non-GET request's payload", ->
@@ -470,19 +484,19 @@ describe 'up.flow', ->
470
484
 
471
485
  it 'morphs between the old and new element', (done) ->
472
486
  affix('.element').text('version 1')
473
- up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 50)
487
+ up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 200)
474
488
 
475
489
  $ghost1 = $('.element.up-ghost:contains("version 1")')
476
490
  expect($ghost1).toHaveLength(1)
477
- expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
491
+ expect(u.opacity($ghost1)).toBeAround(1.0, 0.1)
478
492
 
479
493
  $ghost2 = $('.element.up-ghost:contains("version 2")')
480
494
  expect($ghost2).toHaveLength(1)
481
- expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
495
+ expect(u.opacity($ghost2)).toBeAround(0.0, 0.1)
482
496
 
483
- u.setTimer 40, ->
484
- expect($ghost1.css('opacity')).toBeAround(0.0, 0.2)
485
- expect($ghost2.css('opacity')).toBeAround(1.0, 0.2)
497
+ u.setTimer 190, ->
498
+ expect(u.opacity($ghost1)).toBeAround(0.0, 0.3)
499
+ expect(u.opacity($ghost2)).toBeAround(1.0, 0.3)
486
500
  done()
487
501
 
488
502
  it 'marks the old fragment and its ghost as .up-destroying during the transition', ->
@@ -534,7 +548,7 @@ describe 'up.flow', ->
534
548
  promise = up.extract('.element', '<div class="element">version 2</div>', transition: 'cross-fade', duration: 30)
535
549
  promise.then(resolution)
536
550
  expect(resolution).not.toHaveBeenCalled()
537
- u.setTimer 50, ->
551
+ u.setTimer 70, ->
538
552
  expect(resolution).toHaveBeenCalled()
539
553
  done()
540
554
 
@@ -215,6 +215,21 @@ describe 'up.form', ->
215
215
  expect(submitSpy).toHaveBeenCalled()
216
216
  done()
217
217
 
218
+ it 'marks the field with an .up-active class while the form is submitting', (done) ->
219
+ $form = affix('form')
220
+ $field = $form.affix('input[up-autosubmit][val="old-value"]')
221
+ up.hello($field)
222
+ submission = $.Deferred()
223
+ submitSpy = up.form.knife.mock('submit').and.returnValue(submission)
224
+ $field.val('new-value')
225
+ $field.trigger('change')
226
+ u.nextFrame ->
227
+ expect(submitSpy).toHaveBeenCalled()
228
+ expect($field).toHaveClass('up-active')
229
+ submission.resolve()
230
+ expect($field).not.toHaveClass('up-active')
231
+ done()
232
+
218
233
  describe 'form[up-autosubmit]', ->
219
234
 
220
235
  it 'submits the form when a change is observed in any of its fields', (done) ->
@@ -44,6 +44,8 @@ describe 'up.link', ->
44
44
 
45
45
  it 'adds history entries and allows the user to use the back- and forward-buttons', (done) ->
46
46
 
47
+ waitForBrowser = 70
48
+
47
49
  # By default, up.history will replace the <body> tag when
48
50
  # the user presses the back-button. We reconfigure this
49
51
  # so we don't lose the Jasmine runner interface.
@@ -83,21 +85,21 @@ describe 'up.link', ->
83
85
  expect(document.title).toEqual('title from three')
84
86
 
85
87
  history.back()
86
- u.setTimer 50, ->
88
+ u.setTimer waitForBrowser, ->
87
89
  respondWith('restored text from two', 'restored title from two')
88
90
  expect($('.target')).toHaveText('restored text from two')
89
91
  expect(location.pathname).toEqual('/two')
90
92
  expect(document.title).toEqual('restored title from two')
91
93
 
92
94
  history.back()
93
- u.setTimer 50, ->
95
+ u.setTimer waitForBrowser, ->
94
96
  respondWith('restored text from one', 'restored title from one')
95
97
  expect($('.target')).toHaveText('restored text from one')
96
98
  expect(location.pathname).toEqual('/one')
97
99
  expect(document.title).toEqual('restored title from one')
98
100
 
99
101
  history.forward()
100
- u.setTimer 50, ->
102
+ u.setTimer waitForBrowser, ->
101
103
  # Since the response is cached, we don't have to respond
102
104
  expect($('.target')).toHaveText('restored text from two', 'restored title from two')
103
105
  expect(location.pathname).toEqual('/two')
@@ -367,7 +369,7 @@ describe 'up.link', ->
367
369
 
368
370
  it 'morphs between the old and new target element', (done) ->
369
371
  affix('.target.old')
370
- $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="200"][up-easing="linear"]')
372
+ $link = affix('a[href="/path"][up-target=".target"][up-transition="cross-fade"][up-duration="300"][up-easing="linear"]')
371
373
  $link.click()
372
374
  @respondWith '<div class="target new">new text</div>'
373
375
 
@@ -375,12 +377,11 @@ describe 'up.link', ->
375
377
  $newGhost = $('.target.new.up-ghost')
376
378
  expect($oldGhost).toExist()
377
379
  expect($newGhost).toExist()
378
- opacity = ($element) -> Number($element.css('opacity'))
379
- expect(opacity($oldGhost)).toBeAround(1, 0.15)
380
- expect(opacity($newGhost)).toBeAround(0, 0.15)
381
- u.setTimer 100, ->
382
- expect(opacity($oldGhost)).toBeAround(0.5, 0.15)
383
- expect(opacity($newGhost)).toBeAround(0.5, 0.15)
380
+ expect(u.opacity($oldGhost)).toBeAround(1, 0.15)
381
+ expect(u.opacity($newGhost)).toBeAround(0, 0.15)
382
+ u.setTimer 150, ->
383
+ expect(u.opacity($oldGhost)).toBeAround(0.5, 0.15)
384
+ expect(u.opacity($newGhost)).toBeAround(0.5, 0.15)
384
385
  done()
385
386
 
386
387
  it 'does not add a history entry when an up-history attribute is set to "false"', ->