upjs-rails 0.10.5 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -155,21 +155,49 @@ up.motion = (->
155
155
 
156
156
  GHOSTING_PROMISE_KEY = 'up-ghosting-promise'
157
157
 
158
- withGhosts = ($old, $new, block) ->
159
- oldCopy = null
160
- newCopy = null
158
+ withGhosts = ($old, $new, options, block) ->
159
+
160
+ oldCopy = undefined
161
+ newCopy = undefined
162
+ oldScrollTop = undefined
163
+ newScrollTop = undefined
164
+
165
+ $viewport = up.layout.viewportOf($old)
161
166
 
162
167
  u.temporaryCss $new, display: 'none', ->
163
- oldCopy = prependGhost($old)
168
+ # Within this block, $new is hidden but $old is visible
169
+ oldCopy = prependCopy($old, $viewport)
164
170
  oldCopy.$ghost.addClass('up-destroying')
165
171
  oldCopy.$bounds.addClass('up-destroying')
172
+ # Remember the previous scroll position in case we will reveal $new below.
173
+ oldScrollTop = $viewport.scrollTop()
174
+ # $viewport.scrollTop(oldScrollTop + 1)
175
+
166
176
  u.temporaryCss $old, display: 'none', ->
167
- newCopy = prependGhost($new)
168
- # $old should take up space in the page flow until the transition ends
169
- $old.css(visibility: 'hidden')
170
-
171
- showNew = u.temporaryCss($new, display: 'none')
177
+ # Within this block, $old is hidden but $new is visible
178
+ up.reveal($new, viewport: $viewport) if options.reveal
179
+ newCopy = prependCopy($new, $viewport)
180
+ newScrollTop = $viewport.scrollTop()
181
+
182
+ # Since we have scrolled the viewport (containing both $old and $new),
183
+ # we must shift the old copy so it looks like it it is still sitting
184
+ # in the same position.
185
+ oldCopy.moveTop(newScrollTop - oldScrollTop)
186
+
187
+ # Hide $old since we no longer need it.
188
+ $old.hide()
189
+
190
+ # We will let $new take up space in the element flow, but hide it.
191
+ # The user will only see the two animated ghosts until the transition
192
+ # is over.
193
+ showNew = u.temporaryCss($new, visibility: 'hidden')
194
+
172
195
  promise = block(oldCopy.$ghost, newCopy.$ghost)
196
+
197
+ # Make a way to look at $old and $new and see if an animation is
198
+ # already in progress. If someone attempted a new animation on the
199
+ # same elements, the stored promises would be resolved by the second
200
+ # animation call, making the transition jump to the last frame instantly.
173
201
  $old.data(GHOSTING_PROMISE_KEY, promise)
174
202
  $new.data(GHOSTING_PROMISE_KEY, promise)
175
203
 
@@ -179,9 +207,6 @@ up.motion = (->
179
207
  oldCopy.$bounds.remove()
180
208
  newCopy.$bounds.remove()
181
209
  # Now that the transition is over we show $new again.
182
- # Since we expect $old to be removed in a heartbeat,
183
- # $new should take up space
184
- $old.css(display: 'none')
185
210
  showNew()
186
211
 
187
212
  promise
@@ -207,11 +232,11 @@ up.motion = (->
207
232
  u.debug('Canceling existing ghosting on %o', $element)
208
233
  existingGhosting.resolve?()
209
234
 
210
- assertIsDeferred = (object, origin) ->
235
+ assertIsDeferred = (object, source) ->
211
236
  if u.isDeferred(object)
212
237
  object
213
238
  else
214
- u.error("Did not return a promise with .then and .resolve methods: %o", origin)
239
+ u.error("Did not return a promise with .then and .resolve methods: %o", source)
215
240
 
216
241
  ###*
217
242
  Performs an animated transition between two elements.
@@ -252,25 +277,31 @@ up.motion = (->
252
277
  The timing function that controls the transition's acceleration.
253
278
  See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
254
279
  for a list of pre-defined timing functions.
280
+ @param {Boolean} [options.reveal=false]
281
+ Whether to reveal the new element by scrolling its parent viewport.
255
282
  @return {Promise}
256
283
  A promise for the transition's end.
257
284
  ###
258
285
  morph = (source, target, transitionOrName, options) ->
259
286
  if up.browser.canCssAnimation()
260
- options = animateOptions(options)
287
+ parsedOptions = u.only(options, 'reveal')
288
+ parsedOptions = u.extend(parsedOptions, animateOptions(options))
261
289
  $old = $(source)
262
290
  $new = $(target)
291
+
263
292
  finish($old)
264
293
  finish($new)
265
- if transitionOrName == 'none' or transitionOrName == false
266
- # don't create ghosts if we aren't really transitioning
267
- none()
268
- else if transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]
269
- withGhosts $old, $new, ($oldGhost, $newGhost) ->
270
- assertIsDeferred(transition($oldGhost, $newGhost, options), transitionOrName)
271
- else if animation = animations[transitionOrName]
294
+
295
+ if transitionOrName == 'none' || transitionOrName == false || animation = animations[transitionOrName]
272
296
  $old.hide()
273
- animate($new, animation, options)
297
+ # Since we can not longer rely on withGhosts to process options.reveal
298
+ # in this branch, we need to do it ourselves.
299
+ up.reveal($new) if options.reveal
300
+ animate($new, animation || 'none', options)
301
+ else if transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]
302
+ withGhosts $old, $new, parsedOptions, ($oldGhost, $newGhost) ->
303
+ transitionPromise = transition($oldGhost, $newGhost, parsedOptions)
304
+ assertIsDeferred(transitionPromise, transitionOrName)
274
305
  else if u.isString(transitionOrName) && transitionOrName.indexOf('/') >= 0
275
306
  parts = transitionOrName.split('/')
276
307
  transition = ($old, $new, options) ->
@@ -278,7 +309,7 @@ up.motion = (->
278
309
  animate($old, parts[0], options),
279
310
  animate($new, parts[1], options)
280
311
  )
281
- morph($old, $new, transition, options)
312
+ morph($old, $new, transition, parsedOptions)
282
313
  else
283
314
  u.error("Unknown transition %o", transitionOrName)
284
315
  else
@@ -289,7 +320,7 @@ up.motion = (->
289
320
  ###*
290
321
  @private
291
322
  ###
292
- prependGhost = ($element) ->
323
+ prependCopy = ($element, $viewport) ->
293
324
  elementDims = u.measure($element, relative: true, inner: true)
294
325
 
295
326
  $ghost = $element.clone()
@@ -313,16 +344,29 @@ up.motion = (->
313
344
  $bounds.css(position: 'absolute')
314
345
  $bounds.css(elementDims)
315
346
 
347
+ top = elementDims.top
348
+
349
+ moveTop = (diff) ->
350
+ if diff != 0
351
+ top += diff
352
+ $bounds.css(top: top)
353
+
316
354
  $ghost.appendTo($bounds)
317
355
  $bounds.insertBefore($element)
318
356
 
319
- # Make sure that we don't shift a child element with margins
320
- # that can no longer collapse against a previous sibling
321
- diff = $ghost.offset().top - $element.offset().top
322
- $bounds.css(top: elementDims.top - diff) if diff != 0
357
+ # In theory, $ghost should now sit over $element perfectly.
358
+ # However, $element might collapse its margin against a previous sibling
359
+ # element, and $ghost does not have the same sibling.
360
+ # So we manually correct $ghost's top position so it aligns with $element.
361
+ moveTop($element.offset().top - $ghost.offset().top)
362
+
363
+ $fixedElements = up.layout.fixedChildren($ghost)
364
+ for fixedElement in $fixedElements
365
+ u.fixedToAbsolute(fixedElement, $viewport)
323
366
 
324
367
  $ghost: $ghost
325
368
  $bounds: $bounds
369
+ moveTop: moveTop
326
370
 
327
371
  ###*
328
372
  Defines a named transition.
@@ -543,7 +587,7 @@ up.motion = (->
543
587
  defaults: config.update
544
588
  none: none
545
589
  when: resolvableWhen
546
- prependGhost: prependGhost
590
+ prependCopy: prependCopy
547
591
 
548
592
  )()
549
593
 
@@ -254,8 +254,12 @@ up.popup = (->
254
254
  @ujs
255
255
  ###
256
256
  up.on('click', '[up-close]', (event, $element) ->
257
- if $element.closest('.up-popup')
257
+ if $element.closest('.up-popup').length
258
258
  close()
259
+ # Only prevent the default when we actually closed a popup.
260
+ # This way we can have buttons that close a popup when within a popup,
261
+ # but link to a destination if not.
262
+ event.preventDefault()
259
263
  )
260
264
 
261
265
  # The framework is reset between tests
@@ -189,6 +189,8 @@ up.proxy = (->
189
189
  else
190
190
  promise = load(request)
191
191
  set(request, promise)
192
+ # Don't cache failed requests
193
+ promise.fail -> remove(request)
192
194
 
193
195
  if pending && !options.preload
194
196
  # This will actually make `pendingCount` higher than the actual
@@ -529,24 +529,38 @@ up.util = (->
529
529
  if existingAnimation = $(this).data(ANIMATION_PROMISE_KEY)
530
530
  existingAnimation.resolve()
531
531
 
532
- measure = ($element, options) ->
533
- coordinates = if options?.relative
534
- $element.position()
532
+ measure = ($element, opts) ->
533
+ opts = options(opts, relative: false, inner: false, full: false)
534
+
535
+ if opts.relative
536
+ if opts.relative == true
537
+ coordinates = $element.position()
538
+ else
539
+ $context = $(opts.relative)
540
+ elementCoords = $element.offset()
541
+ if $context.is(document)
542
+ # The document is always at the origin
543
+ coordinates = elementCoords
544
+ else
545
+ contextCoords = $context.offset()
546
+ coordinates =
547
+ left: elementCoords.left - contextCoords.left
548
+ top: elementCoords.top - contextCoords.top
535
549
  else
536
- $element.offset()
550
+ coordinates = $element.offset()
537
551
 
538
552
  box =
539
553
  left: coordinates.left
540
554
  top: coordinates.top
541
555
 
542
- if options?.inner
556
+ if opts.inner
543
557
  box.width = $element.width()
544
558
  box.height = $element.height()
545
559
  else
546
560
  box.width = $element.outerWidth()
547
561
  box.height = $element.outerHeight()
548
562
 
549
- if options?.full
563
+ if opts.full
550
564
  viewport = clientSize()
551
565
  box.right = viewport.width - (box.left + box.width)
552
566
  box.bottom = viewport.height - (box.top + box.height)
@@ -838,6 +852,33 @@ up.util = (->
838
852
  parent.insertBefore(wrappedNode, wrapper)
839
853
  parent.removeChild(wrapper)
840
854
 
855
+ offsetParent = ($element) ->
856
+ $match = undefined
857
+ while ($element = $element.parent()) && $element.length
858
+ position = $element.css('position')
859
+ console.log("Iteration element is %o with position %o", $element, position)
860
+ if position == 'absolute' || position == 'relative' || $element.is('body')
861
+ $match = $element
862
+ break
863
+ $match
864
+
865
+ fixedToAbsolute = (element, $viewport) ->
866
+ $element = $(element)
867
+ $futureOffsetParent = offsetParent($element)
868
+ # To get a fixed elements distance from the edge of the screen,
869
+ # use position(), not offset(). offset() would include the current
870
+ # scrollTop of the viewport.
871
+ elementCoords = $element.position()
872
+ futureParentCoords = $futureOffsetParent.offset()
873
+ $element.css
874
+ position: 'absolute'
875
+ left: elementCoords.left - futureParentCoords.left
876
+ top: elementCoords.top - futureParentCoords.top + $viewport.scrollTop()
877
+ right: ''
878
+ bottom: ''
879
+
880
+ offsetParent: offsetParent
881
+ fixedToAbsolute: fixedToAbsolute
841
882
  presentAttr: presentAttr
842
883
  createElement: createElement
843
884
  normalizeUrl: normalizeUrl
@@ -3,3 +3,4 @@ defaults = up.layout.defaults()
3
3
  up.layout.defaults
4
4
  fixedTop: defaults.fixedTop.concat(['.navbar-fixed-top'])
5
5
  fixedBottom: defaults.fixedBottom.concat(['.navbar-fixed-bottom'])
6
+ anchoredRight: defaults.anchoredRight.concat(['.navbar-fixed-top', '.navbar-fixed-bottom', '.footer'])
@@ -1,19 +1,19 @@
1
1
  module Upjs
2
2
  module Rails
3
- module CurrentLocation
3
+ module RequestEchoHeaders
4
4
 
5
5
  def self.included(base)
6
- base.before_filter :set_header_for_current_location
6
+ base.before_filter :set_up_request_echo_headers
7
7
  end
8
8
 
9
9
  private
10
10
 
11
- def set_header_for_current_location
11
+ def set_up_request_echo_headers
12
12
  headers['X-Up-Location'] = request.original_url
13
13
  headers['X-Up-Method'] = request.method
14
14
  end
15
15
 
16
- ActionController::Base.include(self)
16
+ ActionController::Base.send(:include, self)
17
17
 
18
18
  end
19
19
  end
@@ -6,7 +6,7 @@ module Upjs
6
6
  headers['X-Up-Selector'].present?
7
7
  end
8
8
 
9
- ActionDispatch::Request.include(self)
9
+ ActionDispatch::Request.send(:include, self)
10
10
 
11
11
  end
12
12
  end
@@ -0,0 +1,28 @@
1
+ # See
2
+ # https://github.com/rails/turbolinks/search?q=request_method&ref=cmdform
3
+ # https://github.com/rails/turbolinks/blob/83d4b3d2c52a681f07900c28adb28bc8da604733/README.md#initialization
4
+ module Upjs
5
+ module Rails
6
+ module RequestMethod
7
+
8
+ COOKIE_NAME = '_up_request_method'
9
+
10
+ def self.included(base)
11
+ base.before_filter :set_up_request_method_cookie
12
+ end
13
+
14
+ private
15
+
16
+ def set_up_request_method_cookie
17
+ if request.get?
18
+ cookies.delete(COOKIE_NAME)
19
+ else
20
+ cookies[COOKIE_NAME] = request.request_method
21
+ end
22
+ end
23
+
24
+ ActionController::Base.send(:include, self)
25
+
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  module Upjs
2
2
  module Rails
3
- VERSION = '0.10.5'
3
+ VERSION = '0.11.0'
4
4
  end
5
5
  end
data/lib/upjs-rails.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "upjs/rails/version"
2
2
  require "upjs/rails/engine"
3
- require "upjs/rails/current_location"
4
- require "upjs/rails/request"
3
+ require "upjs/rails/request_echo_headers"
4
+ require "upjs/rails/request_method_cookie"
5
+ require "upjs/rails/request_ext"
@@ -0,0 +1,5 @@
1
+ # We have a lot of specs that measure the distance of elements
2
+ # from the screen edge, so we can't have the Jasmine runner to have
3
+ # any margins on the body.
4
+ beforeEach ->
5
+ $('body').css('margin-top': 0)
@@ -124,11 +124,12 @@ describe 'up.flow', ->
124
124
  @revealedHTML = $revealedElement.get(0).outerHTML
125
125
  u.resolvedPromise()
126
126
 
127
- it 'reveals an old element before it is being replaced', (done) ->
127
+ it 'reveals a new element before it is being replaced', (done) ->
128
128
  @request = up.replace('.middle', '/path', reveal: true)
129
129
  @respond()
130
130
  @request.then =>
131
- expect(up.reveal).toHaveBeenCalledWith(@oldMiddle)
131
+ expect(up.reveal).not.toHaveBeenCalledWith(@oldMiddle)
132
+ expect(@revealedHTML).toContain('new-middle')
132
133
  done()
133
134
 
134
135
  it 'reveals a new element that is being appended', (done) ->
@@ -16,11 +16,8 @@ describe 'up.history', ->
16
16
 
17
17
  it 'sets an [up-href] attribute to the previous URL and sets the up-restore-scroll attribute to "true"', ->
18
18
  up.history.push('/one')
19
- console.log("url is now %o, previous was %o", up.history.url(), up.history.previousUrl())
20
19
  up.history.push('/two')
21
- console.log("url is now %o, previous was %o", up.history.url(), up.history.previousUrl())
22
20
  $element = up.ready(affix('a[href="/three"][up-back]').text('text'))
23
- console.log("url is now %o, previous was %o", up.history.url(), up.history.previousUrl())
24
21
  expect($element.attr('href')).toEndWith('/three')
25
22
  expect($element.attr('up-href')).toEndWith('/one')
26
23
  expect($element.attr('up-restore-scroll')).toBe('')
@@ -17,8 +17,6 @@ describe 'up.layout', ->
17
17
  beforeEach ->
18
18
  $body = $('body')
19
19
 
20
- @restoreMargin = u.temporaryCss($body, 'margin-top': 0)
21
-
22
20
  @$elements = []
23
21
  @$container = $('<div class="container">').prependTo($body)
24
22
 
@@ -31,7 +29,6 @@ describe 'up.layout', ->
31
29
 
32
30
  afterEach ->
33
31
  @$container.remove()
34
- @restoreMargin()
35
32
 
36
33
  it 'reveals the given element', ->
37
34
  up.reveal(@$elements[0])
@@ -8,6 +8,8 @@ describe 'up.modal', ->
8
8
 
9
9
  describe 'up.modal.open', ->
10
10
 
11
+ assumedScrollbarWidth = 15
12
+
11
13
  it "loads the given link's destination in a dialog window", (done) ->
12
14
  $link = affix('a[href="/path/to"][up-modal=".middle"]').text('link')
13
15
  promise = up.modal.open($link)
@@ -49,7 +51,7 @@ describe 'up.modal', ->
49
51
  expect($modal).toExist()
50
52
  expect($modal.css('overflow-y')).toEqual('scroll')
51
53
  expect($body.css('overflow-y')).toEqual('hidden')
52
- expect(parseInt($body.css('padding-right'))).toBeAround(15, 10)
54
+ expect(parseInt($body.css('padding-right'))).toBeAround(assumedScrollbarWidth, 10)
53
55
 
54
56
  up.modal.close().then ->
55
57
  expect($body.css('overflow-y')).toEqual('scroll')
@@ -57,6 +59,30 @@ describe 'up.modal', ->
57
59
 
58
60
  done()
59
61
 
62
+ it 'pushes right-anchored elements away from the edge of the screen in order to prevent jumping', (done) ->
63
+
64
+ $anchoredElement = affix('div[up-anchored=right]').css
65
+ position: 'absolute'
66
+ top: '0'
67
+ right: '30px'
68
+
69
+ promise = up.modal.open(url: '/foo', target: '.container')
70
+
71
+ @lastRequest().respondWith
72
+ status: 200
73
+ contentType: 'text/html'
74
+ responseText:
75
+ """
76
+ <div class="container">text</div>
77
+ """
78
+
79
+ promise.then ->
80
+ expect(parseInt($anchoredElement.css('right'))).toBeAround(30 + assumedScrollbarWidth, 10)
81
+
82
+ up.modal.close().then ->
83
+ expect(parseInt($anchoredElement.css('right'))).toBeAround(30 , 10)
84
+ done()
85
+
60
86
  describe 'up.modal.close', ->
61
87
 
62
88
  it 'should have tests'
@@ -37,7 +37,7 @@ describe 'up.motion', ->
37
37
 
38
38
  if up.browser.canCssAnimation()
39
39
 
40
- it 'transitions between two element using ghosts', (done) ->
40
+ it 'transitions between two element by animating two copies while keeping the originals in the background', (done) ->
41
41
 
42
42
  $old = affix('.old').text('old content').css(
43
43
  position: 'absolute'
@@ -53,7 +53,7 @@ describe 'up.motion', ->
53
53
  width: '22px',
54
54
  height: '23px'
55
55
  )
56
- up.morph($old, $new, 'cross-fade', duration: 100, easing: 'linear')
56
+ up.morph($old, $new, 'cross-fade', duration: 200, easing: 'linear')
57
57
 
58
58
  # The actual animation will be performed on Ghosts since
59
59
  # two element usually cannot exist in the DOM at the same time
@@ -76,14 +76,12 @@ describe 'up.motion', ->
76
76
 
77
77
  # The actual elements are hidden, but $old will take up its original
78
78
  # space until the animation completes.
79
- expect($old.css(['display', 'visibility'])).toEqual(
79
+ expect($old.css('display')).toEqual('none')
80
+
81
+ expect($new.css(['display', 'visibility'])).toEqual(
80
82
  display: 'block',
81
83
  visibility: 'hidden'
82
84
  )
83
- expect($new.css(['display', 'visibility'])).toEqual(
84
- display: 'none',
85
- visibility: 'visible'
86
- )
87
85
 
88
86
  # Ghosts will hover over $old and $new using absolute positioning,
89
87
  # matching the coordinates of the original elements.
@@ -108,15 +106,15 @@ describe 'up.motion', ->
108
106
  expect(opacity($newGhost)).toBeAround(0.0, 0.25)
109
107
  expect(opacity($oldGhost)).toBeAround(1.0, 0.25)
110
108
 
111
- @setTimer 40, ->
109
+ @setTimer 80, ->
112
110
  expect(opacity($newGhost)).toBeAround(0.4, 0.25)
113
111
  expect(opacity($oldGhost)).toBeAround(0.6, 0.25)
114
112
 
115
- @setTimer 70, ->
113
+ @setTimer 140, ->
116
114
  expect(opacity($newGhost)).toBeAround(0.7, 0.25)
117
115
  expect(opacity($oldGhost)).toBeAround(0.3, 0.25)
118
116
 
119
- @setTimer 125, ->
117
+ @setTimer 250, ->
120
118
  # Once our two ghosts have rendered their visual effect,
121
119
  # we remove them from the DOM.
122
120
  expect($newGhost).not.toBeInDOM()
@@ -124,10 +122,7 @@ describe 'up.motion', ->
124
122
 
125
123
  # The old element is still in the DOM, but hidden.
126
124
  # Morphing does *not* remove the target element.
127
- expect($old.css(['display', 'visibility'])).toEqual(
128
- display: 'none',
129
- visibility: 'hidden'
130
- )
125
+ expect($old.css('display')).toEqual('none')
131
126
  expect($new.css(['display', 'visibility'])).toEqual(
132
127
  display: 'block',
133
128
  visibility: 'visible'
@@ -150,6 +145,39 @@ describe 'up.motion', ->
150
145
  # Check that it's a different ghosts
151
146
  expect($ghost2).not.toEqual($ghost1)
152
147
 
148
+ describe 'with { reveal: true } option', ->
149
+
150
+ it 'reveals the new element while making the old element within the same viewport appear as if it would keep its scroll position', ->
151
+ $container = affix('.container[up-viewport]').css
152
+ 'width': '200px'
153
+ 'height': '200px'
154
+ 'overflow-y': 'scroll'
155
+ 'position': 'fixed'
156
+ 'left': 0,
157
+ 'top': 0
158
+ $old = affix('.old').appendTo($container).css(height: '600px')
159
+ $container.scrollTop(300)
160
+
161
+ $new = affix('.new').insertBefore($old).css(height: '600px')
162
+
163
+ up.morph($old, $new, 'cross-fade', duration: 50, reveal: true)
164
+
165
+ $oldGhost = $('.old.up-ghost')
166
+ $newGhost = $('.new.up-ghost')
167
+
168
+ # Container is scrolled up due to { reveal: true } option.
169
+ # Since $old and $new are sitting in the same viewport with a
170
+ # single shares scrollbar This will make the ghost for $old jump.
171
+ expect($container.scrollTop()).toEqual(0)
172
+
173
+ # See that the ghost for $new is aligned with the top edge
174
+ # of the viewport.
175
+ expect($newGhost.offset().top).toEqual(0)
176
+
177
+ # The ghost for $old is shifted upwards to make it looks like it
178
+ # was at the scroll position before we revealed $new.
179
+ expect($oldGhost.offset().top).toEqual(-300)
180
+
153
181
  else
154
182
 
155
183
  it "doesn't animate and hides the first element instead", ->
@@ -171,14 +199,14 @@ describe 'up.motion', ->
171
199
 
172
200
  it 'should have tests'
173
201
 
174
- describe 'up.motion.prependGhost', ->
202
+ describe 'up.motion.prependCopy', ->
175
203
 
176
204
  afterEach ->
177
205
  $('.up-bounds, .up-ghost, .fixture').remove()
178
206
 
179
207
  it 'clones the given element into a .up-ghost-bounds container and inserts it as a sibling before the element', ->
180
208
  $element = affix('.element').text('element text')
181
- up.motion.prependGhost($element)
209
+ up.motion.prependCopy($element)
182
210
  $bounds = $element.prev()
183
211
  expect($bounds).toExist()
184
212
  expect($bounds).toHaveClass('up-bounds')
@@ -190,13 +218,13 @@ describe 'up.motion', ->
190
218
  it 'removes <script> tags from the cloned element', ->
191
219
  $element = affix('.element')
192
220
  $('<script></script>').appendTo($element)
193
- up.motion.prependGhost($element)
221
+ up.motion.prependCopy($element)
194
222
  $ghost = $('.up-ghost')
195
223
  expect($ghost.find('script')).not.toExist()
196
224
 
197
225
  it 'absolutely positions the ghost over the given element', ->
198
226
  $element = affix('.element')
199
- up.motion.prependGhost($element)
227
+ up.motion.prependCopy($element)
200
228
  $ghost = $('.up-ghost')
201
229
  expect($ghost.offset()).toEqual($element.offset())
202
230
  expect($ghost.width()).toEqual($element.width())
@@ -204,25 +232,43 @@ describe 'up.motion', ->
204
232
 
205
233
  it 'accurately positions the ghost over an element with margins', ->
206
234
  $element = affix('.element').css(margin: '40px')
207
- up.motion.prependGhost($element)
235
+ up.motion.prependCopy($element)
208
236
  $ghost = $('.up-ghost')
209
237
  expect($ghost.offset()).toEqual($element.offset())
210
238
 
211
239
  it "doesn't change the position of a child whose margins no longer collapse", ->
212
240
  $element = affix('.element')
213
241
  $child = $('<div class="child"></div>').css(margin: '40px').appendTo($element)
214
- up.motion.prependGhost($element)
242
+ up.motion.prependCopy($element)
215
243
  $clonedChild = $('.up-ghost .child')
216
244
  expect($clonedChild.offset()).toEqual($child.offset())
217
245
 
218
246
  it 'correctly positions the ghost over an element within a scrolled body', ->
219
- $body = $('body').css(margin: 0)
247
+ $body = $('body')
220
248
  $element1 = $('<div class="fixture"></div>').css(height: '75px').prependTo($body)
221
249
  $element2 = $('<div class="fixture"></div>').css(height: '100px').insertAfter($element1)
222
250
  $body.scrollTop(17)
223
- { $bounds, $ghost } = up.motion.prependGhost($element2)
251
+ { $bounds, $ghost } = up.motion.prependCopy($element2)
224
252
  expect($bounds.css('position')).toBe('absolute')
225
253
  expect($bounds.css('top')).toEqual('75px')
226
254
  expect($ghost.css('position')).toBe('static')
227
255
 
228
256
  it 'correctly positions the ghost over an element within a viewport with overflow-y: scroll'
257
+
258
+ it 'converts fixed elements within the copies to absolutely positioning', ->
259
+ $element = affix('.element').css
260
+ position: 'absolute'
261
+ top: '50px'
262
+ left: '50px'
263
+ $fixedChild = $('<div class="fixed-child" up-fixed></div>').css
264
+ position: 'fixed'
265
+ left: '77px'
266
+ top: '77px'
267
+ $fixedChild.appendTo($element)
268
+ up.motion.prependCopy($element, $('body'))
269
+ $fixedChildGhost = $('.up-ghost .fixed-child')
270
+ expect($fixedChildGhost.css(['position', 'left', 'top'])).toEqual
271
+ position: 'absolute',
272
+ left: '27px',
273
+ top: '27px'
274
+