unpoly-rails 0.55.1 → 0.56.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/dist/unpoly-bootstrap3.js +6 -4
  4. data/dist/unpoly-bootstrap3.min.js +1 -1
  5. data/dist/unpoly.js +1323 -805
  6. data/dist/unpoly.min.js +4 -3
  7. data/lib/assets/javascripts/unpoly-bootstrap3/{navigation-ext.coffee → feedback-ext.coffee} +2 -0
  8. data/lib/assets/javascripts/unpoly/browser.coffee.erb +7 -7
  9. data/lib/assets/javascripts/unpoly/bus.coffee.erb +5 -6
  10. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +127 -0
  11. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +1 -1
  12. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +62 -32
  13. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +27 -0
  14. data/lib/assets/javascripts/unpoly/dom.coffee.erb +78 -99
  15. data/lib/assets/javascripts/unpoly/feedback.coffee +147 -96
  16. data/lib/assets/javascripts/unpoly/form.coffee.erb +26 -2
  17. data/lib/assets/javascripts/unpoly/history.coffee +2 -1
  18. data/lib/assets/javascripts/unpoly/layout.coffee.erb +68 -12
  19. data/lib/assets/javascripts/unpoly/link.coffee.erb +10 -4
  20. data/lib/assets/javascripts/unpoly/modal.coffee.erb +11 -9
  21. data/lib/assets/javascripts/unpoly/{motion.coffee → motion.coffee.erb} +184 -322
  22. data/lib/assets/javascripts/unpoly/popup.coffee.erb +13 -12
  23. data/lib/assets/javascripts/unpoly/radio.coffee +1 -1
  24. data/lib/assets/javascripts/unpoly/syntax.coffee +8 -17
  25. data/lib/assets/javascripts/unpoly/tooltip.coffee +11 -11
  26. data/lib/assets/javascripts/unpoly/util.coffee +332 -145
  27. data/lib/unpoly/rails/version.rb +1 -1
  28. data/package.json +1 -1
  29. data/spec_app/Gemfile.lock +1 -1
  30. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  31. data/spec_app/app/assets/stylesheets/integration_test.sass +1 -0
  32. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +4 -0
  33. data/spec_app/app/views/motion_test/transitions.erb +13 -0
  34. data/spec_app/app/views/pages/start.erb +1 -0
  35. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +5 -0
  36. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +5 -0
  37. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +1 -1
  38. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +11 -0
  39. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +5 -0
  40. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +217 -102
  41. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +162 -44
  42. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +97 -10
  43. data/spec_app/spec/javascripts/up/link_spec.js.coffee +3 -3
  44. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +22 -20
  45. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +344 -228
  46. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +1 -1
  47. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +1 -1
  48. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +1 -1
  49. data/spec_app/spec/javascripts/up/util_spec.js.coffee +194 -0
  50. metadata +11 -4
@@ -2,12 +2,13 @@
2
2
  Navigation feedback
3
3
  ===================
4
4
 
5
- Unpoly automatically adds CSS classes to links while they are
6
- currently loading ([`.up-active`](/a.up-active)) or
7
- pointing to the current location ([`.up-current`](/a.up-current)).
5
+ Unpoly automatically adds the class [`.up-active`](/a.up-active) to links or forms while they are loading.
8
6
 
9
- By styling these classes with CSS you can provide instant feedback to user interactions.
10
- This improves the perceived speed of your interface.
7
+ By marking navigation elements as [`[up-nav]`](/up-nav), contained links that point to the current location
8
+ automatically get the [`.up-current`](/up-nav-a.up-current) class.
9
+
10
+ You should style [`.up-active`](/a.up-active) and [`.up-current`](/up-nav a.up-current) with CSS to
11
+ provide instant feedback to user interactions. This improves the perceived speed of your interface.
11
12
 
12
13
  \#\#\# Example
13
14
 
@@ -45,92 +46,122 @@ up.feedback = (($) ->
45
46
  @property up.feedback.config
46
47
  @param {Array<string>} [config.currentClasses]
47
48
  An array of classes to set on [links that point the current location](/a.up-current).
49
+ @param {Array<string>} [config.navs]
50
+ An array of CSS selectors that match [navigation components](/up-nav).
48
51
  @stable
49
52
  ###
50
53
  config = u.config
51
54
  currentClasses: ['up-current']
55
+ navs: ['[up-nav]']
56
+
57
+ previousUrlSet = undefined
58
+ currentUrlSet = undefined
52
59
 
53
60
  reset = ->
54
61
  config.reset()
55
-
56
- currentClass = ->
57
- classes = config.currentClasses
58
- classes = classes.concat(['up-current'])
59
- classes = u.uniq(classes)
60
- classes.join(' ')
62
+ previousUrlSet = undefined
63
+ currentUrlSet = undefined
61
64
 
62
65
  CLASS_ACTIVE = 'up-active'
63
- SELECTOR_SECTION = 'a, [up-href]'
66
+ SELECTOR_LINK = 'a, [up-href]'
67
+
68
+ navSelector = ->
69
+ config.navs.join(',')
64
70
 
65
71
  normalizeUrl = (url) ->
66
72
  if u.isPresent(url)
67
73
  u.normalizeUrl(url, stripTrailingSlash: true)
68
74
 
75
+ NORMALIZED_SECTION_URLS_KEY = 'up-normalized-urls'
76
+
69
77
  sectionUrls = ($section) ->
70
- urls = []
71
- for attr in ['href', 'up-href', 'up-alias']
72
- if value = u.presentAttr($section, attr)
73
- values = if attr == 'up-alias' then value.split(' ') else [value]
74
- for url in values
75
- unless url == '#'
76
- url = normalizeUrl(url)
77
- urls.push(url)
78
+ # Check if we have computed the URLs before.
79
+ # Computation is sort of expensive (multiplied by number of links),
80
+ # so we cache the results in a data attribute.
81
+ unless urls = $section.data(NORMALIZED_SECTION_URLS_KEY)
82
+ urls = buildSectionUrls($section)
83
+ $section.data(NORMALIZED_SECTION_URLS_KEY, urls)
78
84
  urls
79
85
 
80
- urlSet = (urls) ->
81
- urls = u.map(urls, normalizeUrl)
82
- urls = u.compact(urls)
83
-
84
- matches = (testUrl) ->
85
- if testUrl.substr(-1) == '*'
86
- doesMatchPrefix(testUrl.slice(0, -1))
87
- else
88
- doesMatchFully(testUrl)
89
-
90
- doesMatchFully = (testUrl) ->
91
- u.contains(urls, testUrl)
92
-
93
- doesMatchPrefix = (prefix) ->
94
- u.detect urls, (url) ->
95
- url.indexOf(prefix) == 0
96
-
97
- matchesAny = (testUrls) ->
98
- u.detect(testUrls, matches)
99
-
100
- matchesAny: matchesAny
101
-
102
- locationChanged = ->
103
- currentUrls = urlSet([
104
- up.browser.url(),
105
- up.modal.url(),
106
- up.modal.coveredUrl(),
107
- up.popup.url(),
108
- up.popup.coveredUrl()
109
- ])
110
-
111
- klass = currentClass()
86
+ buildSectionUrls = ($section) ->
87
+ urls = []
112
88
 
113
- u.each $(SELECTOR_SECTION), (section) ->
114
- $section = $(section)
115
- # if $section is marked up with up-follow,
116
- # the actual link might be a child element.
117
- urls = sectionUrls($section)
89
+ # A link with an unsafe method will never be higlighted with .up-current,
90
+ # so we cache an empty array.
91
+ if up.link.isSafe($section)
92
+ for attr in ['href', 'up-href', 'up-alias']
93
+ if value = u.presentAttr($section, attr)
94
+ # Allow to include multiple space-separated URLs in [up-alias]
95
+ for url in value.split(/\s+/)
96
+ unless url == '#'
97
+ url = normalizeUrl(url)
98
+ urls.push(url)
99
+ urls
118
100
 
119
- if up.link.isSafe($section) && currentUrls.matchesAny(urls)
120
- $section.addClass(klass)
121
- else if $section.hasClass(klass) && $section.closest('.up-destroying').length == 0
122
- $section.removeClass(klass)
101
+ buildCurrentUrlSet = ->
102
+ urls = [
103
+ up.browser.url(), # The URL displayed in the address bar
104
+ up.modal.url(), # Even when a modal does not change the address bar, we consider the URL of its content
105
+ up.modal.coveredUrl(), # The URL of the page behind the modal
106
+ up.popup.url(), # Even when a popup does not change the address bar, we consider the URL of its content
107
+ up.popup.coveredUrl() # The URL of the page behind the popup
108
+ ]
109
+ new up.UrlSet(urls, { normalizeUrl })
110
+
111
+ updateAllNavigationSectionsIfLocationChanged = ->
112
+ previousUrlSet = currentUrlSet
113
+ currentUrlSet = buildCurrentUrlSet()
114
+ unless currentUrlSet.isEqual(previousUrlSet)
115
+ updateAllNavigationSections($('body'))
116
+
117
+ updateAllNavigationSections = ($root) ->
118
+ $navs = u.selectInSubtree($root, navSelector())
119
+ $sections = u.selectInSubtree($navs, SELECTOR_LINK)
120
+ updateCurrentClassForLinks($sections)
121
+
122
+ updateNavigationSectionsInNewFragment = ($fragment) ->
123
+ if $fragment.closest(navSelector()).length
124
+ # If the new fragment is an [up-nav], or if the new fragment is a child of an [up-nav],
125
+ # all links in the new fragment are considered sections that we need to update.
126
+ # Note that:
127
+ # - The [up-nav] element might not be part of this update.
128
+ # It might already be in the DOM, and only a child was updated.
129
+ # - The $fragment might be a link itself
130
+ # - We do not need to update sibling links of $fragment that have been processed before.
131
+ $sections = u.selectInSubtree($fragment, SELECTOR_LINK)
132
+ updateCurrentClassForLinks($sections)
133
+ else
134
+ updateAllNavigationSections($fragment)
135
+
136
+ updateCurrentClassForLinks = ($links) ->
137
+ currentUrlSet ||= buildCurrentUrlSet()
138
+ u.each $links, (link) ->
139
+ $link = $(link)
140
+ urls = sectionUrls($link)
141
+
142
+ # We use Element#classList to manipulate classes instead of jQuery's
143
+ # addClass and removeClass. Since we are in an inner loop, we want to
144
+ # be as fast as we can.
145
+ classList = link.classList
146
+ if currentUrlSet.matchesAny(urls)
147
+ for klass in config.currentClasses
148
+ # Once we drop IE11 support in 2020 we can call add() with multiple arguments
149
+ classList.add(klass)
150
+ else
151
+ for klass in config.currentClasses
152
+ # Once we drop IE11 support in 2020 we can call remove() with multiple arguments
153
+ classList.remove(klass)
123
154
 
124
155
  ###**
125
- @function findActionableArea
156
+ @function findActivatableArea
126
157
  @param {string|Element|jQuery} elementOrSelector
127
158
  @internal
128
159
  ###
129
- findActionableArea = (elementOrSelector) ->
160
+ findActivatableArea = (elementOrSelector) ->
130
161
  $area = $(elementOrSelector)
131
- if $area.is(SELECTOR_SECTION)
162
+ if $area.is(SELECTOR_LINK)
132
163
  # Try to enlarge links that are expanded with [up-expand] on a surrounding container.
133
- $area = u.presence($area.parent(SELECTOR_SECTION)) || $area
164
+ $area = u.presence($area.parent(SELECTOR_LINK)) || $area
134
165
  $area
135
166
 
136
167
  ###**
@@ -167,7 +198,7 @@ up.feedback = (($) ->
167
198
  elementOrSelector = args.shift()
168
199
  action = args.pop()
169
200
  options = u.options(args[0])
170
- $element = findActionableArea(elementOrSelector)
201
+ $element = findActivatableArea(elementOrSelector)
171
202
  unless options.preload
172
203
  $element.addClass(CLASS_ACTIVE)
173
204
  if action
@@ -249,39 +280,57 @@ up.feedback = (($) ->
249
280
  @internal
250
281
  ###
251
282
  stop = (elementOrSelector) ->
252
- $element = findActionableArea(elementOrSelector)
283
+ $element = findActivatableArea(elementOrSelector)
253
284
  $element.removeClass(CLASS_ACTIVE)
254
285
 
255
286
  ###**
256
- Links that point to the current location are assigned
257
- the `up-current` class automatically.
287
+ Marks this element as a navigation component, such as a menu or navigation bar.
288
+
289
+ When a link within an `[up-nav]` element points to the current location, it is assigned the `.up-current` class. When the browser navigates to another location, the class is removed automatically.
258
290
 
259
- The use case for this is navigation bars:
291
+ You may also assign `[up-nav]` to an individual link instead of an navigational container.
260
292
 
261
- <nav>
293
+ If you don't want to manually add this attribute to every navigational element, you can configure selectors to automatically match your navigation components in [`up.feedback.config.navs`](/up.feedback.config#config.navs).
294
+
295
+
296
+ \#\#\# Example
297
+
298
+ Let's take a simple menu with two links. The menu has been marked with the `[up-nav]` attribute:
299
+
300
+ <div up-nav>
262
301
  <a href="/foo">Foo</a>
263
302
  <a href="/bar">Bar</a>
264
- </nav>
303
+ </div>
265
304
 
266
- If the browser location changes to `/foo`, the markup changes to this:
305
+ If the browser location changes to `/foo`, the first link is marked as `.up-current`:
267
306
 
268
- <nav>
307
+ <div up-nav>
269
308
  <a href="/foo" class="up-current">Foo</a>
270
309
  <a href="/bar">Bar</a>
271
- </nav>
310
+ </div>
311
+
312
+ If the browser location changes to `/bar`, the first link automatically loses its `.up-current` class. Now the second link is marked as `.up-current`:
313
+
314
+ <div up-nav>
315
+ <a href="/foo">Foo</a>
316
+ <a href="/bar" class="up-current">Bar</a>
317
+ </div>
318
+
272
319
 
273
- \#\#\# What's considered to be "current"?
320
+ \#\#\# What is considered to be "current"?
274
321
 
275
322
  The current location is considered to be either:
276
323
 
277
324
  - the URL displayed in the browser window's location bar
278
- - the source URL of a currently opened [modal dialog](/up.modal)
279
- - the source URL of a currently opened [popup overlay](/up.popup)
325
+ - the source URL of a [modal dialog](/up.modal)
326
+ - the URL of the page behind a [modal dialog](/up.modal)
327
+ - the source URL of a [popup overlay](/up.popup)
328
+ - the URL of the content behind a [popup overlay](/up.popup)
280
329
 
281
330
  A link matches the current location (and is marked as `.up-current`) if it matches either:
282
331
 
283
332
  - the link's `href` attribute
284
- - the link's [`up-href`](#turn-any-element-into-a-link) attribute
333
+ - the link's `up-href` attribute
285
334
  - a space-separated list of URLs in the link's `up-alias` attribute
286
335
 
287
336
  \#\#\# Matching URL by prefix
@@ -289,27 +338,29 @@ up.feedback = (($) ->
289
338
  You can mark a link as `.up-current` whenever the current URL matches a prefix.
290
339
  To do so, end the `up-alias` attribute in an asterisk (`*`).
291
340
 
292
- For instance, the following link is highlighted for both `/reports` and `/reports/123`:
341
+ For instance, the following `[up-nav]` link is highlighted for both `/reports` and `/reports/123`:
293
342
 
294
- <a href="/reports" up-alias="/reports/*">Reports</a>
343
+ <a up-nav href="/reports" up-alias="/reports/*">Reports</a>
295
344
 
296
- @selector a.up-current
345
+ @selector [up-nav]
297
346
  @stable
298
347
  ###
299
- up.on 'up:fragment:inserted', ->
300
- # When a fragment is inserted it might either have brought a location change
301
- # with it, or it might have opened a modal / popup which we consider
302
- # to be secondary location sources (the primary being the browser's
303
- # location bar).
304
- locationChanged()
305
-
306
- up.on 'up:fragment:destroyed', (event, $fragment) ->
307
- # If the destroyed fragment is a modal or popup container
308
- # this changes which URLs we consider currents.
309
- # Also modals and popups restore their previous history
310
- # once they close.
311
- if $fragment.is('.up-modal, .up-popup')
312
- locationChanged()
348
+
349
+ ###**
350
+ When a link within an `[up-nav]` element points to the current location, it is assigned the `.up-current` class.
351
+
352
+ See [`[up-nav]`](/up-nav) for more documentation and examples.
353
+
354
+ @selector [up-nav] a.up-current
355
+ @stable
356
+ ###
357
+
358
+ # Even when the modal or popup does not change history, we consider the URLs of the content it displays.
359
+ up.on 'up:history:pushed up:history:replaced up:history:restored up:modal:opened up:modal:closed up:popup:opened up:popup:closed', (event) ->
360
+ updateAllNavigationSectionsIfLocationChanged()
361
+
362
+ up.on 'up:fragment:inserted', (event, $newFragment) ->
363
+ updateNavigationSectionsInNewFragment($newFragment)
313
364
 
314
365
  # The framework is reset between tests
315
366
  up.on 'up:framework:reset', reset
@@ -45,7 +45,7 @@ up.form = (($) ->
45
45
  @internal
46
46
  ###
47
47
  fieldSelector = ->
48
- u.multiSelector(config.fields)
48
+ config.fields.join(',')
49
49
 
50
50
  ###**
51
51
  Submits a form via AJAX and updates a page fragment with the response.
@@ -283,7 +283,7 @@ up.form = (($) ->
283
283
  delay = u.option(u.presentAttr($element, 'up-delay'), options.delay, config.observeDelay)
284
284
  delay = parseInt(delay)
285
285
 
286
- $fields = fieldSelector().selectInSubtree($element)
286
+ $fields = u.selectInSubtree($element, fieldSelector())
287
287
 
288
288
  destructors = u.map $fields, (field) ->
289
289
  observeField($(field), delay, callback)
@@ -816,6 +816,28 @@ up.form = (($) ->
816
816
  A CSS selector for elements whose visibility depends on this field's value.
817
817
  @stable
818
818
  ###
819
+
820
+ ###**
821
+ Only shows this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values.
822
+
823
+ See [`input[up-switch]`](/input-up-switch) for more documentation and examples.
824
+
825
+ @selector [up-show-for]
826
+ @param {string} [up-show-for]
827
+ A space-separated list of input values for which this element should be shown.
828
+ @stable
829
+ ###
830
+
831
+ ###**
832
+ Hides this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values.
833
+
834
+ See [`input[up-switch]`](/input-up-switch) for more documentation and examples.
835
+
836
+ @selector [up-hide-for]
837
+ @param {string} [up-hide-for]
838
+ A space-separated list of input values for which this element should be hidden.
839
+ @stable
840
+ ###
819
841
  up.compiler '[up-switch]', ($field) ->
820
842
  switchTargets($field)
821
843
 
@@ -944,6 +966,8 @@ up.form = (($) ->
944
966
  ###
945
967
  up.compiler '[up-autosubmit]', ($formOrField) -> autosubmit($formOrField)
946
968
 
969
+ up.compiler '[autofocus]', { batch: true }, ($input) -> $input.last().focus()
970
+
947
971
  up.on 'up:framework:reset', reset
948
972
 
949
973
  <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %>
@@ -94,7 +94,8 @@ up.history = (($) ->
94
94
  @internal
95
95
  ###
96
96
  replace = (url) ->
97
- manipulate('replaceState', url)
97
+ if manipulate('replaceState', url)
98
+ up.emit('up:history:replaced', url: url)
98
99
 
99
100
  ###**
100
101
  Adds a new history entry and updates the browser's
@@ -57,7 +57,7 @@ up.layout = (($) ->
57
57
  ###
58
58
  config = u.config
59
59
  duration: 0
60
- viewports: [document, '.up-modal-viewport', '[up-viewport]']
60
+ viewports: ['.up-modal-viewport', '[up-viewport]']
61
61
  fixedTop: ['[up-fixed~=top]']
62
62
  fixedBottom: ['[up-fixed~=bottom]']
63
63
  anchoredRight: ['[up-anchored~=right]', '[up-fixed~=top]', '[up-fixed~=bottom]', '[up-fixed~=right]']
@@ -74,7 +74,7 @@ up.layout = (($) ->
74
74
  reset = ->
75
75
  config.reset()
76
76
  lastScrollTops.clear()
77
- scrollingTracker.finish()
77
+ scrollingTracker.reset()
78
78
 
79
79
  ###**
80
80
  Scrolls the given viewport to the given Y-position.
@@ -161,6 +161,9 @@ up.layout = (($) ->
161
161
  @internal
162
162
  ###
163
163
  finishScrolling = (element) ->
164
+ # Don't emit expensive events if no animation can be running anyway
165
+ return Promise.resolve() unless up.motion.isEnabled()
166
+
164
167
  $scrollable = scrollableElementForViewport(element)
165
168
  scrollingTracker.finish($scrollable)
166
169
 
@@ -169,7 +172,8 @@ up.layout = (($) ->
169
172
  @internal
170
173
  ###
171
174
  anchoredRight = ->
172
- u.multiSelector(config.anchoredRight).select()
175
+ selector = config.anchoredRight.join(',')
176
+ $(selector)
173
177
 
174
178
  ###**
175
179
  @function measureObstruction
@@ -179,10 +183,10 @@ up.layout = (($) ->
179
183
  measureObstruction = ->
180
184
  measurePosition = (obstructor, cssAttr) ->
181
185
  $obstructor = $(obstructor)
182
- anchorPosition = $obstructor.css(cssAttr)
186
+ anchorPosition = u.readComputedStyleNumber($obstructor, cssAttr)
183
187
  unless u.isPresent(anchorPosition)
184
188
  up.fail("Fixed element %o must have a CSS attribute %s", $obstructor.get(0), cssAttr)
185
- parseFloat(anchorPosition) + $obstructor.height()
189
+ anchorPosition + $obstructor.height()
186
190
 
187
191
  fixedTopBottoms = for obstructor in $(config.fixedTop.join(', '))
188
192
  measurePosition(obstructor, 'top')
@@ -313,22 +317,23 @@ up.layout = (($) ->
313
317
  Promise.resolve()
314
318
 
315
319
  viewportSelector = ->
316
- u.multiSelector(config.viewports)
320
+ config.viewports.join(',')
317
321
 
318
322
  ###**
319
323
  Returns the viewport for the given element.
320
324
 
321
- Throws an error if no viewport could be found.
325
+ Returns `$(document)` if no better viewpoint could be found.
322
326
 
323
327
  @function up.layout.viewportOf
324
328
  @param {string|Element|jQuery} selectorOrElement
329
+ @return {jQuery}
325
330
  @internal
326
331
  ###
327
332
  viewportOf = (selectorOrElement, options = {}) ->
328
333
  $element = $(selectorOrElement)
329
- $viewport = viewportSelector().seekUp($element)
330
- if $viewport.length == 0 && options.strict isnt false
331
- up.fail("Could not find viewport for %o", $element)
334
+ $viewport = $element.closest(viewportSelector())
335
+ if $viewport.length == 0
336
+ $viewport = $(document)
332
337
  $viewport
333
338
 
334
339
  ###**
@@ -342,7 +347,7 @@ up.layout = (($) ->
342
347
  ###
343
348
  viewportsWithin = (selectorOrElement) ->
344
349
  $element = $(selectorOrElement)
345
- viewportSelector().selectInSubtree($element)
350
+ u.selectInSubtree($element, viewportSelector())
346
351
 
347
352
  ###**
348
353
  Returns a jQuery collection of all the viewports on the screen.
@@ -351,7 +356,7 @@ up.layout = (($) ->
351
356
  @internal
352
357
  ###
353
358
  viewports = ->
354
- viewportSelector().select()
359
+ $(document).add(viewportSelector())
355
360
 
356
361
  scrollTopKey = (viewport) ->
357
362
  $viewport = $(viewport)
@@ -497,6 +502,56 @@ up.layout = (($) ->
497
502
  selector += ", a[name='#{selector}']"
498
503
  selector
499
504
 
505
+ ###**
506
+ @internal
507
+ ###
508
+ absolutize = ($element, options) ->
509
+ options = u.options(options, afterMeasure: u.noop)
510
+ $viewport = up.layout.viewportOf($element)
511
+ originalDims = u.measure($element, relative: true, inner: true)
512
+ originalOffset = $element.offset()
513
+ options.afterMeasure()
514
+
515
+ u.writeInlineStyle $element,
516
+ # If the element had a layout context before, make sure the
517
+ # ghost will have layout context as well (and vice versa).
518
+ position: if u.readComputedStyle($element, 'position') == 'static' then 'static' else 'relative'
519
+ top: 'auto'
520
+ right: 'auto'
521
+ bottom: 'auto'
522
+ left: 'auto'
523
+ width: '100%'
524
+ height: '100%'
525
+
526
+ # Wrap the ghost in another container so its margin can expand
527
+ # freely. If we would position the element directly (old implementation),
528
+ # it would gain a layout context which cannot be crossed by margins.
529
+ $bounds = $('<div class="up-bounds"></div>')
530
+ boundsStyle = u.merge(originalDims, position: 'absolute')
531
+ u.writeInlineStyle($bounds, boundsStyle)
532
+ $bounds.insertBefore($element)
533
+ $element.appendTo($bounds)
534
+
535
+ top = originalDims.top
536
+
537
+ moveTop = (diff) ->
538
+ if diff != 0
539
+ top += diff
540
+ u.writeInlineStyle($bounds, { top })
541
+
542
+ # In theory, $element should not have moved visually.
543
+ # However, $element (or a child of $element) might collapse its margin
544
+ # against a previous sibling element, and now that it is absolute it does
545
+ # not have the same sibling. So we manually correct $element's top
546
+ # position so it aligns with the previous top position.
547
+ moveTop(originalOffset.top - $element.offset().top)
548
+
549
+ $fixedElements = up.layout.fixedChildren($element)
550
+ for fixedElement in $fixedElements
551
+ u.fixedToAbsolute(fixedElement, $viewport)
552
+
553
+ { $element, $bounds, moveTop }
554
+
500
555
  ###**
501
556
  Marks this element as a scrolling container ("viewport").
502
557
 
@@ -653,6 +708,7 @@ up.layout = (($) ->
653
708
  revealOrRestoreScroll: revealOrRestoreScroll
654
709
  anchoredRight: anchoredRight
655
710
  fixedChildren: fixedChildren
711
+ absolutize: absolutize
656
712
 
657
713
  )(jQuery)
658
714