unpoly-rails 0.56.7 → 0.57.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1
  3. data/dist/unpoly.js +1569 -793
  4. data/dist/unpoly.min.js +4 -4
  5. data/lib/assets/javascripts/unpoly.coffee +2 -0
  6. data/lib/assets/javascripts/unpoly/browser.coffee.erb +25 -41
  7. data/lib/assets/javascripts/unpoly/bus.coffee.erb +20 -6
  8. data/lib/assets/javascripts/unpoly/classes/cache.coffee +23 -13
  9. data/lib/assets/javascripts/unpoly/classes/compile_pass.coffee +87 -0
  10. data/lib/assets/javascripts/unpoly/classes/focus_tracker.coffee +29 -0
  11. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +7 -4
  12. data/lib/assets/javascripts/unpoly/classes/record.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/request.coffee +38 -45
  14. data/lib/assets/javascripts/unpoly/classes/response.coffee +16 -1
  15. data/lib/assets/javascripts/unpoly/classes/store/memory.coffee +26 -0
  16. data/lib/assets/javascripts/unpoly/classes/store/session.coffee +59 -0
  17. data/lib/assets/javascripts/unpoly/cookie.coffee +56 -0
  18. data/lib/assets/javascripts/unpoly/dom.coffee.erb +67 -39
  19. data/lib/assets/javascripts/unpoly/feedback.coffee +2 -2
  20. data/lib/assets/javascripts/unpoly/form.coffee.erb +23 -12
  21. data/lib/assets/javascripts/unpoly/history.coffee +2 -2
  22. data/lib/assets/javascripts/unpoly/layout.coffee.erb +118 -99
  23. data/lib/assets/javascripts/unpoly/link.coffee.erb +12 -5
  24. data/lib/assets/javascripts/unpoly/log.coffee +6 -5
  25. data/lib/assets/javascripts/unpoly/modal.coffee.erb +9 -2
  26. data/lib/assets/javascripts/unpoly/motion.coffee.erb +2 -6
  27. data/lib/assets/javascripts/unpoly/namespace.coffee.erb +2 -2
  28. data/lib/assets/javascripts/unpoly/params.coffee.erb +522 -0
  29. data/lib/assets/javascripts/unpoly/popup.coffee.erb +3 -3
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +42 -34
  31. data/lib/assets/javascripts/unpoly/{syntax.coffee → syntax.coffee.erb} +59 -117
  32. data/lib/assets/javascripts/unpoly/{util.coffee → util.coffee.erb} +206 -171
  33. data/lib/unpoly/rails/version.rb +1 -1
  34. data/package.json +1 -1
  35. data/spec_app/Gemfile.lock +1 -1
  36. data/spec_app/app/assets/javascripts/integration_test.coffee +0 -4
  37. data/spec_app/app/assets/stylesheets/integration_test.sass +7 -1
  38. data/spec_app/app/controllers/pages_controller.rb +4 -0
  39. data/spec_app/app/views/form_test/basics/new.erb +34 -5
  40. data/spec_app/app/views/form_test/submission_result.erb +2 -2
  41. data/spec_app/app/views/form_test/uploads/new.erb +15 -2
  42. data/spec_app/app/views/hash_test/unpoly.erb +30 -0
  43. data/spec_app/app/views/pages/start.erb +2 -1
  44. data/spec_app/spec/javascripts/helpers/parse_form_data.js.coffee +17 -2
  45. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +5 -0
  46. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +1 -1
  47. data/spec_app/spec/javascripts/helpers/to_match_selector.coffee +5 -0
  48. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +8 -8
  49. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +58 -20
  50. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +78 -0
  51. data/spec_app/spec/javascripts/up/classes/focus_tracker_spec.coffee +31 -0
  52. data/spec_app/spec/javascripts/up/classes/request_spec.coffee +50 -0
  53. data/spec_app/spec/javascripts/up/classes/store/memory_spec.js.coffee +67 -0
  54. data/spec_app/spec/javascripts/up/classes/store/session_spec.js.coffee +113 -0
  55. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +133 -45
  56. data/spec_app/spec/javascripts/up/form_spec.js.coffee +13 -13
  57. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +110 -26
  58. data/spec_app/spec/javascripts/up/link_spec.js.coffee +1 -1
  59. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +1 -0
  60. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +52 -51
  61. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +2 -2
  62. data/spec_app/spec/javascripts/up/params_spec.coffee +768 -0
  63. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +75 -36
  64. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +48 -15
  65. data/spec_app/spec/javascripts/up/util_spec.js.coffee +148 -131
  66. metadata +17 -5
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
@@ -6,7 +6,7 @@ class up.Record
6
6
  throw 'Return an array of property names'
7
7
 
8
8
  constructor: (options) ->
9
- u.assign(@, @attributes(options))
9
+ u.assign(this, @attributes(options))
10
10
 
11
11
  attributes: (source = @) =>
12
12
  u.only(source, @fields()...)
@@ -27,16 +27,10 @@ class up.Request extends up.Record
27
27
  ###
28
28
 
29
29
  ###**
30
- Parameters that should be sent as the request's payload.
30
+ [Parameters](/up.params) that should be sent as the request's payload.
31
31
 
32
- Parameters may be passed as one of the following forms:
33
-
34
- 1. An object where keys are param names and the values are param values
35
- 2. An array of `{ name: 'param-name', value: 'param-value' }` objects
36
- 3. A [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object
37
-
38
- @property up.Request#data
39
- @param {String} data
32
+ @property up.Request#params
33
+ @param {object|FormData|string|Array} params
40
34
  @stable
41
35
  ###
42
36
 
@@ -78,11 +72,14 @@ class up.Request extends up.Record
78
72
  [
79
73
  'method',
80
74
  'url',
81
- 'data',
75
+ 'params',
76
+ 'data', # deprecated. use #params.
82
77
  'target',
83
78
  'failTarget',
84
79
  'headers',
85
- 'timeout'
80
+ 'timeout',
81
+ 'preload' # since up.proxy.request() options are sometimes wrapped in this class
82
+ 'cache' # since up.proxy.request() options are sometimes wrapped in this class
86
83
  ]
87
84
 
88
85
  ###**
@@ -94,37 +91,34 @@ class up.Request extends up.Record
94
91
  @normalize()
95
92
 
96
93
  normalize: =>
94
+ u.deprecateRenamedKey(@, 'data', 'params')
97
95
  @method = u.normalizeMethod(@method)
98
96
  @headers ||= {}
99
97
  @extractHashFromUrl()
100
98
 
101
99
  if u.methodAllowsPayload(@method)
102
- @transferSearchToData()
100
+ @transferSearchToParams()
103
101
  else
104
- @transferDataToUrl()
102
+ @transferParamsToUrl()
105
103
 
106
104
  extractHashFromUrl: =>
107
105
  urlParts = u.parseUrl(@url)
108
106
  # Remember the #hash for later revealing.
109
107
  # It will be lost during normalization.
110
- @hash = urlParts.hash
108
+ @hash = u.presence(urlParts.hash)
111
109
  @url = u.normalizeUrl(urlParts, hash: false)
112
110
 
113
- transferDataToUrl: =>
114
- if @data && !u.isFormData(@data)
115
- # GET methods are not allowed to have a payload, so we transfer { data } params to the URL.
116
- query = u.requestDataAsQuery(@data)
117
- separator = if u.contains(@url, '?') then '&' else '?'
118
- @url += separator + query
119
- # Now that we have transfered the params into the URL, we delete them from the { data } option.
120
- @data = undefined
111
+ transferParamsToUrl: =>
112
+ if @params && !u.isFormData(@params)
113
+ # GET methods are not allowed to have a payload, so we transfer { params } params to the URL.
114
+ @url = up.params.buildURL(@url, @params)
115
+ # Now that we have transfered the params into the URL, we delete them from the { params } option.
116
+ @params = undefined
121
117
 
122
- transferSearchToData: =>
123
- urlParts = u.parseUrl(@url)
124
- query = urlParts.search
125
- if query
126
- @data = u.mergeRequestData(@data, query)
127
- @url = u.normalizeUrl(urlParts, search: false)
118
+ transferSearchToParams: =>
119
+ if query = up.params.fromURL(@url)
120
+ @params = up.params.merge(@params, query)
121
+ @url = u.normalizeUrl(@url, search: false)
128
122
 
129
123
  isSafe: =>
130
124
  up.proxy.isSafeMethod(@method)
@@ -136,27 +130,25 @@ class up.Request extends up.Record
136
130
  xhr = new XMLHttpRequest()
137
131
 
138
132
  xhrHeaders = u.copy(@headers)
139
- xhrData = @data
133
+ xhrPayload = @params
140
134
  xhrMethod = @method
141
135
  xhrUrl = @url
142
136
 
143
- [xhrMethod, xhrData] = up.proxy.wrapMethod(xhrMethod, xhrData)
137
+ [xhrMethod, xhrPayload] = up.proxy.wrapMethod(xhrMethod, xhrPayload)
144
138
 
145
- if u.isFormData(xhrData)
139
+ if xhrPayload
146
140
  delete xhrHeaders['Content-Type'] # let the browser set the content type
147
- else if u.isPresent(xhrData)
148
- xhrData = u.requestDataAsQuery(xhrData, purpose: 'form')
149
- xhrHeaders['Content-Type'] = 'application/x-www-form-urlencoded'
141
+ xhrPayload = up.params.toFormData(xhrPayload)
150
142
  else
151
143
  # XMLHttpRequest expects null for an empty body
152
- xhrData = null
144
+ xhrPayload = null
153
145
 
154
- xhrHeaders[up.protocol.config.targetHeader] = @target if @target
155
- xhrHeaders[up.protocol.config.failTargetHeader] = @failTarget if @failTarget
146
+ pc = up.protocol.config
147
+ xhrHeaders[pc.targetHeader] = @target if @target
148
+ xhrHeaders[pc.failTargetHeader] = @failTarget if @failTarget
156
149
  xhrHeaders['X-Requested-With'] ||= 'XMLHttpRequest' unless @isCrossDomain()
157
-
158
150
  if csrfToken = @csrfToken()
159
- xhrHeaders[up.protocol.config.csrfHeader] = csrfToken
151
+ xhrHeaders[pc.csrfHeader] = csrfToken
160
152
 
161
153
  xhr.open(xhrMethod, xhrUrl)
162
154
 
@@ -177,12 +169,12 @@ class up.Request extends up.Record
177
169
 
178
170
  xhr.timeout = @timeout if @timeout
179
171
 
180
- xhr.send(xhrData)
172
+ xhr.send(xhrPayload)
181
173
 
182
174
  navigate: =>
183
175
  # GET forms cannot have an URL with a query section in their [action] attribute.
184
176
  # The query section would be overridden by the serialized input values on submission.
185
- @transferSearchToData()
177
+ @transferSearchToParams()
186
178
 
187
179
  $form = $('<form class="up-page-loader"></form>')
188
180
 
@@ -202,9 +194,9 @@ class up.Request extends up.Record
202
194
  if (csrfParam = up.protocol.csrfParam()) && (csrfToken = @csrfToken())
203
195
  addField(name: csrfParam, value: csrfToken)
204
196
 
205
- # @data will be undefined for GET requests, since we have already
197
+ # @params will be undefined for GET requests, since we have already
206
198
  # transfered all params to the URL during normalize().
207
- u.each u.requestDataAsArray(@data), addField
199
+ u.each(up.params.toArray(@params), addField)
208
200
 
209
201
  $form.hide().appendTo('body')
210
202
  up.browser.submitForm($form)
@@ -236,10 +228,11 @@ class up.Request extends up.Record
236
228
  new up.Response(responseAttrs)
237
229
 
238
230
  isCachable: =>
239
- @isSafe() && !u.isFormData(@data)
231
+ @isSafe() && !u.isFormData(@params)
240
232
 
241
233
  cacheKey: =>
242
- [@url, @method, u.requestDataAsQuery(@data), @target].join('|')
234
+ query = up.params.toQuery(@params)
235
+ [@url, @method, query, @target].join('|')
243
236
 
244
237
  @wrap: (object) ->
245
238
  if object instanceof @
@@ -94,7 +94,7 @@ class up.Response extends up.Record
94
94
  'status',
95
95
  'request',
96
96
  'xhr',
97
- 'title'
97
+ 'title',
98
98
  ]
99
99
 
100
100
  constructor: (options) ->
@@ -136,3 +136,18 @@ class up.Response extends up.Record
136
136
  ###
137
137
  isFatalError: =>
138
138
  @isError() && u.isBlank(@text)
139
+
140
+ ###**
141
+ Returns the HTTP header value with the given name.
142
+
143
+ The search for the header name is case-insensitive.
144
+
145
+ Returns `undefined` if the given header name was not included in the response.
146
+
147
+ @function up.Response#getHeader
148
+ @param {string} name
149
+ @return {string|undefined} value
150
+ @experimental
151
+ ###
152
+ getHeader: (name) =>
153
+ @xhr.getResponseHeader(name)
@@ -0,0 +1,26 @@
1
+ up.store ||= {}
2
+
3
+ u = up.util
4
+
5
+ class up.store.Memory
6
+
7
+ constructor: ->
8
+ @clear()
9
+
10
+ clear: =>
11
+ @data = {}
12
+
13
+ get: (key) =>
14
+ @data[key]
15
+
16
+ set: (key, value) =>
17
+ @data[key] = value
18
+
19
+ remove: (key) =>
20
+ delete @data[key]
21
+
22
+ keys: =>
23
+ Object.keys(@data)
24
+
25
+ values: =>
26
+ u.values(@data)
@@ -0,0 +1,59 @@
1
+ u = up.util
2
+
3
+ ##
4
+ # Store implementation backed by window.sessionStorage
5
+ # ====================================================
6
+ #
7
+ # This improves plain sessionStorage access in several ways:
8
+ #
9
+ # - Falls back to in-memory storage if window.sessionStorage is not available (see below).
10
+ # - Allows to store other types of values than just strings.
11
+ # - Allows to store structured values.
12
+ # - Allows to invalidate existing data by incrementing a version number on the server.
13
+ #
14
+ # On sessionStorage availability
15
+ # ------------------------------
16
+ #
17
+ # All supported browsers have sessionStorage, but the property is `null`
18
+ # in private browsing mode in Safari and the default Android webkit browser.
19
+ # See https://makandracards.com/makandra/32865-sessionstorage-per-window-browser-storage
20
+ #
21
+ # Also Chrome explodes upon access of window.sessionStorage when
22
+ # user blocks third-party cookies and site data and this page is embedded
23
+ # as an <iframe>. See https://bugs.chromium.org/p/chromium/issues/detail?id=357625
24
+ #
25
+ class up.store.Session extends up.store.Memory
26
+
27
+ constructor: (rootKey) ->
28
+ @rootKey = rootKey
29
+ @loadFromSessionStorage()
30
+
31
+ clear: =>
32
+ super()
33
+ @saveToSessionStorage()
34
+
35
+ set: (key, value) =>
36
+ super(key, value)
37
+ @saveToSessionStorage()
38
+
39
+ remove: (key) =>
40
+ super(key)
41
+ @saveToSessionStorage()
42
+
43
+ loadFromSessionStorage: =>
44
+ try
45
+ if raw = sessionStorage?.getItem(@rootKey)
46
+ @data = JSON.parse(raw)
47
+ catch
48
+ # window.sessionStorage not supported (see class comment)
49
+ # or JSON syntax error. We start with a blank object instead.
50
+
51
+ @data ||= {}
52
+
53
+ saveToSessionStorage: =>
54
+ json = JSON.stringify(@data)
55
+ try
56
+ sessionStorage?.setItem(@rootKey, json)
57
+ catch
58
+ # window.sessionStorage not supported (see class comment).
59
+ # We do nothing and only keep data in-memory.
@@ -0,0 +1,56 @@
1
+ ####**
2
+ #Cookies
3
+ #=======
4
+ #
5
+ #class up.cookies
6
+ ####
7
+ #up.cookie = (->
8
+ # u = up.util
9
+ #
10
+ # escape = encodeURIComponent
11
+ # unescape = decodeURIComponent
12
+ #
13
+ # lastRaw = undefined
14
+ # lastParsed = {}
15
+ #
16
+ # all = ->
17
+ # currentRaw = document.cookie
18
+ # if u.isUndefined(lastRaw) || lastRaw != currentRaw
19
+ # lastParsed = parse()
20
+ # lastRaw = currentRaw
21
+ # lastParsed
22
+ #
23
+ # parse = ->
24
+ # hash = {}
25
+ # pairs = u.splitValues(document.cookie, ';')
26
+ # for pair in pairs
27
+ # parts = u.splitValues(pair, '=')
28
+ # name = unescape(parts[0])
29
+ # value = unescape(parts[1])
30
+ # hash[name] = value
31
+ # hash
32
+ #
33
+ # remove = (name) ->
34
+ # set(name, '', 'expires=Thu, 01-Jan-70 00:00:01 GMT; path=/')
35
+ #
36
+ # get = (name) ->
37
+ # all()[name]
38
+ #
39
+ # set = (name, value, meta) ->
40
+ # str = escape(name) + '=' + escape(value)
41
+ # str += ';' + meta if meta
42
+ # document.cookie = str
43
+ # lastRaw = undefined
44
+ #
45
+ # pop = (name) ->
46
+ # value = get(name)
47
+ # if u.isPresent(value)
48
+ # remove(name)
49
+ # value
50
+ #
51
+ # all: all
52
+ # get: get
53
+ # set: set
54
+ # remove: remove
55
+ # pop: pop
56
+ #)()
@@ -70,6 +70,7 @@ up.dom = (($) ->
70
70
  @param {string|Element|jQuery} origin
71
71
  The element that this selector resolution is relative to.
72
72
  That element's selector will be substituted for `&` ([like in Sass](https://sass-lang.com/documentation/file.SASS_REFERENCE.html#parent-selector)).
73
+ @return {string}
73
74
  @internal
74
75
  ###
75
76
  resolveSelector = (selectorOrElement, origin) ->
@@ -179,13 +180,8 @@ up.dom = (($) ->
179
180
  You can also pass `false` to explicitly prevent the title from being updated.
180
181
  @param {string} [options.method='get']
181
182
  The HTTP method to use for the request.
182
- @param {Object|Array|FormData} [options.data]
183
- Parameters that should be sent as the request's payload.
184
-
185
- Parameters can either be passed as an object (where the property names become
186
- the param names and the property values become the param values) or as
187
- an array of `{ name: 'param-name', value: 'param-value' }` objects
188
-
183
+ @param {Object|FormData|string|Array} [options.params]
184
+ [Parameters](/up.params) that should be sent as the request's payload.
189
185
  @param {string} [options.transition='none']
190
186
  @param {string|boolean} [options.history=true]
191
187
  If a string is given, it is used as the URL the browser's location bar and history.
@@ -211,18 +207,15 @@ up.dom = (($) ->
211
207
  @param {Object} [options.headers={}]
212
208
  An object of additional header key/value pairs to send along
213
209
  with the request.
214
- @param {boolean} [options.requireMatch=true]
215
- Whether to raise an error if the given selector is missing in
216
- either the current page or in the response.
217
210
  @param {Element|jQuery} [options.origin]
218
211
  The element that triggered the replacement.
219
212
 
220
213
  The element's selector will be substituted for the `&` shorthand in the target selector ([like in Sass](https://sass-lang.com/documentation/file.SASS_REFERENCE.html#parent-selector)).
221
214
  @param {string} [options.layer='auto']
222
215
  The name of the layer that ought to be updated. Valid values are
223
- `auto`, `page`, `modal` and `popup`.
216
+ `'auto'`, `'page'`, `'modal'` and `'popup'`.
224
217
 
225
- If set to `auto` (default), Unpoly will try to find a match in the
218
+ If set to `'auto'` (default), Unpoly will try to find a match in the
226
219
  same layer as the element that triggered the replacement (see `options.origin`).
227
220
  If that element is not known, or no match was found in that layer,
228
221
  Unpoly will search in other layers, starting from the topmost layer.
@@ -240,7 +233,7 @@ up.dom = (($) ->
240
233
  replace = (selectorOrElement, url, options) ->
241
234
  options = u.options(options)
242
235
 
243
- options.inspectResponse = fullLoad = -> up.browser.navigate(url, u.only(options, 'method', 'data'))
236
+ options.inspectResponse = fullLoad = -> up.browser.navigate(url, u.only(options, 'method', 'params'))
244
237
 
245
238
  if !up.browser.canPushState() && options.history != false
246
239
  fullLoad() unless options.preload
@@ -266,26 +259,28 @@ up.dom = (($) ->
266
259
  # http://2ality.com/2016/03/promise-rejections-vs-exceptions.html
267
260
  return Promise.reject(e)
268
261
 
269
- request =
262
+ request = new up.Request(
270
263
  url: url
271
264
  method: options.method
272
- data: options.data
265
+ data: options.data # deprecated, use { params }
266
+ params: options.params
273
267
  target: improvedTarget
274
268
  failTarget: improvedFailTarget
275
269
  cache: options.cache
276
270
  preload: options.preload
277
271
  headers: options.headers
278
272
  timeout: options.timeout
273
+ )
279
274
 
280
275
  onSuccess = (response) ->
281
- processResponse(true, improvedTarget, response, successOptions)
276
+ processResponse(true, improvedTarget, request, response, successOptions)
282
277
 
283
278
  onFailure = (response) ->
284
279
  rejection = -> Promise.reject(response)
285
280
  if response.isFatalError()
286
281
  rejection()
287
282
  else
288
- promise = processResponse(false, improvedFailTarget, response, failureOptions)
283
+ promise = processResponse(false, improvedFailTarget, request, response, failureOptions)
289
284
  # Although processResponse() we will perform a successful replacement of options.failTarget,
290
285
  # we still want to reject the promise that's returned to our API client.
291
286
  u.always(promise, rejection)
@@ -297,15 +292,12 @@ up.dom = (($) ->
297
292
  ###**
298
293
  @internal
299
294
  ###
300
- processResponse = (isSuccess, selector, response, options) ->
301
- request = response.request
295
+ processResponse = (isSuccess, selector, request, response, options) ->
302
296
  sourceUrl = response.url
303
297
  historyUrl = sourceUrl
304
- hash = request.hash
305
298
 
306
- if options.reveal == true && hash
307
- # If the request URL had a #hash and options.reveal is not given, we reveal that #hash.
308
- options.reveal = hash
299
+ if hash = request.hash
300
+ options.hash = hash
309
301
  historyUrl += hash
310
302
 
311
303
  isReloadable = (response.method == 'GET')
@@ -376,7 +368,6 @@ up.dom = (($) ->
376
368
  up.log.group 'Extracting %s from %d bytes of HTML', selectorOrElement, html?.length, ->
377
369
  options = u.options options,
378
370
  historyMethod: 'push'
379
- requireMatch: true
380
371
  keep: true
381
372
  layer: 'auto'
382
373
 
@@ -452,7 +443,7 @@ up.dom = (($) ->
452
443
  # Reveal element that was being prepended/appended.
453
444
  # Since we will animate (not morph) it's OK to allow animation of scrolling
454
445
  # if the user has configured up.layout.config.duration.
455
- promise = up.layout.revealOrRestoreScroll($wrapper, options)
446
+ promise = up.layout.scrollAfterInsertFragment($wrapper, options)
456
447
 
457
448
  # Since we're adding content instead of replacing, we'll only
458
449
  # animate $new instead of morphing between $old and $new
@@ -490,6 +481,7 @@ up.dom = (($) ->
490
481
 
491
482
  return up.morph($old, $new, transition, morphOptions)
492
483
 
484
+
493
485
  # This will find all [up-keep] descendants in $old an, overwrite their partner
494
486
  # element in $new and leave a visually identical clone in $old for a later transition.
495
487
  # Returns an array of keepPlans.
@@ -512,6 +504,7 @@ up.dom = (($) ->
512
504
  keepPlans.push(plan)
513
505
  keepPlans
514
506
 
507
+
515
508
  findKeepPlan = ($element, $new, options) ->
516
509
  if options.keep
517
510
  $keepable = $element
@@ -525,8 +518,8 @@ up.dom = (($) ->
525
518
  $partner = $partner.first()
526
519
  if $partner.length && $partner.is('[up-keep]')
527
520
  plan =
528
- $element: $keepable # the element that should be kept
529
- $newElement: $partner # the element that would have replaced it but now does not
521
+ $element: $keepable # the element that should be kept
522
+ $newElement: $partner # the element that would have replaced it but now does not
530
523
  newData: up.syntax.data($partner) # the parsed up-data attribute of the element we will discard
531
524
  keepEventArgs = u.merge(plan, message: ['Keeping element %o', $keepable.get(0)])
532
525
  if up.bus.nobodyPrevents('up:fragment:keep', keepEventArgs)
@@ -652,7 +645,7 @@ up.dom = (($) ->
652
645
  keptElements = []
653
646
  for plan in options.keepPlans
654
647
  emitFragmentKept(plan)
655
- keptElements.push(plan.$element)
648
+ keptElements.push(plan.$element[0])
656
649
  up.syntax.compile($element, skip: keptElements)
657
650
  emitFragmentInserted($element, options)
658
651
  $element
@@ -718,9 +711,10 @@ up.dom = (($) ->
718
711
 
719
712
  @function up.first
720
713
  @param {string|Element|jQuery|Array<Element>} selectorOrElement
721
- @param {string} options.layer
722
- The name of the layer in which to find the element. Valid values are
723
- `auto`, `page`, `modal` and `popup`.
714
+ @param {string} [options.layer='auto']
715
+ The name of the layer in which to find the element.
716
+
717
+ Valid values are `'auto'`, `'page'`, `'modal'` and `'popup'`.
724
718
  @param {string|Element|jQuery} [options.origin]
725
719
  An second element or selector that can be referenced as `&` in the first selector:
726
720
 
@@ -762,17 +756,48 @@ up.dom = (($) ->
762
756
  break
763
757
  $match
764
758
 
759
+ ###**
760
+ @function up.dom.layerOf
761
+ @internal
762
+ ###
765
763
  layerOf = (selectorOrElement) ->
766
764
  $element = $(selectorOrElement)
767
- if up.popup.contains($element)
768
- 'popup'
769
- else if up.modal.contains($element)
770
- 'modal'
771
- else
772
- 'page'
765
+ if $element.length
766
+ if up.popup.contains($element)
767
+ 'popup'
768
+ else if up.modal.contains($element)
769
+ 'modal'
770
+ else
771
+ 'page'
773
772
 
774
773
  matchesLayer = (selectorOrElement, layer) ->
775
- layerOf(selectorOrElement) == layer
774
+ !layer || layerOf(selectorOrElement) == layer
775
+
776
+ ###**
777
+ Returns all elements matching the given selector, but
778
+ ignores elements that are being [destroyed](/up.destroy) or [transitioned](/up.morph).
779
+
780
+ If the given argument is already a jQuery collection (or an array
781
+ of DOM elements), returns the subset of the given list that is matching these conditions.
782
+
783
+ @function up.all
784
+ @param {string|jQuery|Array<Element>} selectorOrElements
785
+ @param {string|Element|jQuery} [options.origin]
786
+ An second element or selector that can be referenced as `&` in the first selector.
787
+ @param {string} [options.layer]
788
+ The name of the layer in which to find the element. Valid values are
789
+ `'page'`, `'modal'` and `'popup'`.
790
+ @return {jQuery}
791
+ A jQuery collection of matching elements.
792
+ @experimental
793
+ ###
794
+ all = (selectorOrElements, options) ->
795
+ options = u.options(options)
796
+ resolved = resolveSelector(selectorOrElements, options.origin)
797
+ $root = $(u.option(options.root, document))
798
+ $root.find(resolved).filter (index, element) ->
799
+ $element = $(element)
800
+ isRealElement($element) && matchesLayer($element, options.layer)
776
801
 
777
802
  ###**
778
803
  Destroys the given element or selector.
@@ -912,10 +937,12 @@ up.dom = (($) ->
912
937
  destroy: destroy
913
938
  extract: extract
914
939
  first: first
940
+ all: all
915
941
  source: source
916
942
  resolveSelector: resolveSelector
917
943
  hello: hello
918
944
  config: config
945
+ layerOf: layerOf
919
946
 
920
947
  )(jQuery)
921
948
 
@@ -924,6 +951,7 @@ up.extract = up.dom.extract
924
951
  up.reload = up.dom.reload
925
952
  up.destroy = up.dom.destroy
926
953
  up.first = up.dom.first
954
+ up.all = up.dom.all
927
955
  up.hello = up.dom.hello
928
956
 
929
- up.renamedModule 'flow', 'dom'
957
+ up.deprecateRenamedModule 'flow', 'dom'