unpoly-rails 0.56.7 → 0.57.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 (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'