unpoly-rails 0.37.0 → 0.50.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -25
  3. data/LICENSE +1 -1
  4. data/README_RAILS.md +4 -2
  5. data/Rakefile +6 -1
  6. data/dist/unpoly.js +3192 -2198
  7. data/dist/unpoly.min.js +4 -3
  8. data/lib/assets/javascripts/unpoly/browser.coffee +51 -63
  9. data/lib/assets/javascripts/unpoly/bus.coffee +58 -33
  10. data/lib/assets/javascripts/unpoly/classes/cache.coffee +117 -0
  11. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_cascade.coffee +3 -3
  12. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_plan.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +57 -0
  14. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +52 -0
  15. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +95 -0
  16. data/lib/assets/javascripts/unpoly/classes/record.coffee +16 -0
  17. data/lib/assets/javascripts/unpoly/classes/request.coffee +228 -0
  18. data/lib/assets/javascripts/unpoly/classes/response.coffee +138 -0
  19. data/lib/assets/javascripts/unpoly/dom.coffee +151 -142
  20. data/lib/assets/javascripts/unpoly/feedback.coffee +67 -38
  21. data/lib/assets/javascripts/unpoly/form.coffee +156 -139
  22. data/lib/assets/javascripts/unpoly/history.coffee +22 -19
  23. data/lib/assets/javascripts/unpoly/layout.coffee +108 -90
  24. data/lib/assets/javascripts/unpoly/link.coffee +159 -158
  25. data/lib/assets/javascripts/unpoly/log.coffee +5 -5
  26. data/lib/assets/javascripts/unpoly/modal.coffee +93 -81
  27. data/lib/assets/javascripts/unpoly/motion.coffee +291 -250
  28. data/lib/assets/javascripts/unpoly/popup.coffee +67 -53
  29. data/lib/assets/javascripts/unpoly/protocol.coffee +67 -16
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +282 -211
  31. data/lib/assets/javascripts/unpoly/rails.coffee +3 -14
  32. data/lib/assets/javascripts/unpoly/syntax.coffee +54 -49
  33. data/lib/assets/javascripts/unpoly/tooltip.coffee +18 -25
  34. data/lib/assets/javascripts/unpoly/util.coffee +236 -477
  35. data/lib/assets/javascripts/unpoly.coffee +1 -1
  36. data/lib/unpoly/rails/inspector.rb +67 -22
  37. data/lib/unpoly/rails/version.rb +1 -1
  38. data/package.json +1 -1
  39. data/spec_app/Gemfile.lock +13 -13
  40. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  41. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -1
  42. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +10 -0
  43. data/spec_app/app/controllers/binding_test_controller.rb +19 -2
  44. data/spec_app/app/controllers/method_test_controller.rb +16 -0
  45. data/spec_app/app/views/layouts/jasmine_rails/spec_runner.html.erb +20 -0
  46. data/spec_app/app/views/method_test/form_target.erb +17 -0
  47. data/spec_app/app/views/method_test/page1.erb +11 -0
  48. data/spec_app/app/views/method_test/page2.erb +6 -0
  49. data/spec_app/app/views/pages/start.erb +33 -19
  50. data/spec_app/config/initializers/assets.rb +5 -0
  51. data/spec_app/config/routes.rb +3 -0
  52. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -27
  53. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +17 -0
  54. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +102 -0
  55. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -1
  56. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +5 -2
  57. data/spec_app/spec/javascripts/helpers/promise_state.js +18 -0
  58. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +9 -0
  59. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +22 -0
  60. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +11 -3
  61. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +10 -0
  62. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +5 -0
  63. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +13 -0
  64. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +13 -6
  65. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +92 -33
  66. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +64 -15
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
  68. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +1 -0
  69. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +759 -551
  70. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +155 -82
  71. data/spec_app/spec/javascripts/up/form_spec.js.coffee +490 -349
  72. data/spec_app/spec/javascripts/up/history_spec.js.coffee +226 -179
  73. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +253 -185
  74. data/spec_app/spec/javascripts/up/link_spec.js.coffee +416 -270
  75. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +459 -330
  76. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +198 -153
  77. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +9 -0
  78. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +240 -175
  79. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +38 -0
  80. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +777 -303
  81. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +24 -8
  82. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +40 -23
  83. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +80 -66
  84. data/spec_app/spec/javascripts/up/util_spec.js.coffee +227 -201
  85. data/spec_app/vendor/asset-libs/es6-promise-4.1.6/es6-promise.auto.js +1159 -0
  86. metadata +30 -7
  87. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +0 -7
  88. data/spec_app/spec/javascripts/helpers/to_equal_url.coffee +0 -11
@@ -19,12 +19,12 @@ up.dom = (($) ->
19
19
  Configures defaults for fragment insertion.
20
20
 
21
21
  @property up.dom.config
22
- @param {Boolean} [options.runInlineScripts=true]
22
+ @param {boolean} [options.runInlineScripts=true]
23
23
  Whether inline `<script>` tags inside inserted HTML fragments will be executed.
24
- @param {Boolean} [options.runLinkedScripts=false]
24
+ @param {boolean} [options.runLinkedScripts=false]
25
25
  Whether `<script src='...'>` tags inside inserted HTML fragments will fetch and execute
26
26
  the linked JavaScript file.
27
- @param {String} [options.fallbacks=['body']]
27
+ @param {string} [options.fallbacks=['body']]
28
28
  When a fragment updates cannot find the requested element, Unpoly will try this list of alternative selectors.
29
29
 
30
30
  The first selector that matches an element in the current page (or response) will be used.
@@ -33,7 +33,7 @@ up.dom = (($) ->
33
33
  It is recommend to always keep `'body'` as the last selector in the last in the case
34
34
  your server or load balancer renders an error message that does not contain your
35
35
  application layout.
36
- @param {String} [options.fallbackTransition='none']
36
+ @param {string} [options.fallbackTransition='none']
37
37
  The transition to use when using a fallback target.
38
38
  @stable
39
39
  ###
@@ -55,7 +55,7 @@ up.dom = (($) ->
55
55
  Returns the URL the given element was retrieved from.
56
56
 
57
57
  @method up.dom.source
58
- @param {String|Element|jQuery} selectorOrElement
58
+ @param {string|Element|jQuery} selectorOrElement
59
59
  @experimental
60
60
  ###
61
61
  source = (selectorOrElement) ->
@@ -67,8 +67,8 @@ up.dom = (($) ->
67
67
  to an absolute selector.
68
68
 
69
69
  @function up.dom.resolveSelector
70
- @param {String|Element|jQuery} selectorOrElement
71
- @param {String|Element|jQuery} origin
70
+ @param {string|Element|jQuery} selectorOrElement
71
+ @param {string|Element|jQuery} origin
72
72
  The element that this selector resolution is relative to.
73
73
  That element's selector will be substituted for `&`.
74
74
  @internal
@@ -164,57 +164,59 @@ up.dom = (($) ->
164
164
  element that replaces it.
165
165
 
166
166
  @function up.replace
167
- @param {String|Element|jQuery} selectorOrElement
167
+ @param {string|Element|jQuery} selectorOrElement
168
168
  The CSS selector to update. You can also pass a DOM element or jQuery element
169
169
  here, in which case a selector will be inferred from the element's class and ID.
170
- @param {String} url
170
+ @param {string} url
171
171
  The URL to fetch from the server.
172
- @param {String} [options.failTarget='body']
172
+ @param {string} [options.failTarget='body']
173
173
  The CSS selector to update if the server sends a non-200 status code.
174
- @param {String} [options.fallback]
174
+ @param {string} [options.fallback]
175
175
  The selector to update when the original target was not found in the page.
176
- @param {String} [options.title]
176
+ @param {string} [options.title]
177
177
  The document title after the replacement.
178
178
 
179
179
  If the call pushes an history entry and this option is missing, the title is extracted from the response's `<title>` tag.
180
180
  You can also pass `false` to explicitly prevent the title from being updated.
181
- @param {String} [options.method='get']
181
+ @param {string} [options.method='get']
182
182
  The HTTP method to use for the request.
183
- @param {Object|Array} [options.data]
183
+ @param {Object|Array|FormData} [options.data]
184
184
  Parameters that should be sent as the request's payload.
185
185
 
186
186
  Parameters can either be passed as an object (where the property names become
187
187
  the param names and the property values become the param values) or as
188
188
  an array of `{ name: 'param-name', value: 'param-value' }` objects
189
- (compare to jQuery's [`serializeArray`](https://api.jquery.com/serializeArray/)).
190
- @param {String} [options.transition='none']
191
- @param {String|Boolean} [options.history=true]
192
- If a `String` is given, it is used as the URL the browser's location bar and history.
189
+
190
+ @param {string} [options.transition='none']
191
+ @param {string|boolean} [options.history=true]
192
+ If a string is given, it is used as the URL the browser's location bar and history.
193
193
  If omitted or true, the `url` argument will be used.
194
194
  If set to `false`, the history will remain unchanged.
195
- @param {String|Boolean} [options.source=true]
196
- @param {String} [options.reveal=false]
195
+ @param {boolean|string} [options.source=true]
196
+ @param {boolean|string} [options.reveal=false]
197
197
  Whether to [reveal](/up.reveal) the element being updated, by
198
198
  scrolling its containing viewport.
199
- @param {Boolean} [options.restoreScroll=false]
199
+
200
+ You can also pass a CSS selector for the element to reveal.
201
+ @param {boolean} [options.restoreScroll=false]
200
202
  If set to true, Unpoly will try to restore the scroll position
201
203
  of all the viewports around or below the updated element. The position
202
204
  will be reset to the last known top position before a previous
203
205
  history change for the current URL.
204
- @param {Boolean} [options.cache]
206
+ @param {boolean} [options.cache]
205
207
  Whether to use a [cached response](/up.proxy) if available.
206
- @param {String} [options.historyMethod='push']
208
+ @param {string} [options.historyMethod='push']
207
209
  @param {Object} [options.headers={}]
208
210
  An object of additional header key/value pairs to send along
209
211
  with the request.
210
- @param {Boolean} [options.requireMatch=true]
212
+ @param {boolean} [options.requireMatch=true]
211
213
  Whether to raise an error if the given selector is missing in
212
214
  either the current page or in the response.
213
215
  @param {Element|jQuery} [options.origin]
214
216
  The element that triggered the replacement.
215
217
 
216
218
  The element's selector will be substituted for the `&` shorthand in the target selector.
217
- @param {String} [options.layer='auto']
219
+ @param {string} [options.layer='auto']
218
220
  The name of the layer that ought to be updated. Valid values are
219
221
  `auto`, `page`, `modal` and `popup`.
220
222
 
@@ -222,23 +224,22 @@ up.dom = (($) ->
222
224
  same layer as the element that triggered the replacement (see `options.origin`).
223
225
  If that element is not known, or no match was found in that layer,
224
226
  Unpoly will search in other layers, starting from the topmost layer.
225
- @param {String} [options.failLayer='auto']
227
+ @param {string} [options.failLayer='auto']
226
228
  The name of the layer that ought to be updated if the server sends a non-200 status code.
227
229
 
228
230
  @return {Promise}
229
- A promise that will be resolved when the page has been updated.
231
+ A promise that will be fulfilled when the page has been updated.
230
232
  @stable
231
233
  ###
232
234
  replace = (selectorOrElement, url, options) ->
233
235
  options = u.options(options)
234
236
 
237
+ options.inspectResponse = fullLoad = -> up.browser.navigate(url, u.only(options, 'method', 'data'))
238
+
235
239
  if !up.browser.canPushState() && options.history != false
236
- unless options.preload
237
- up.browser.loadPage(url, u.only(options, 'method', 'data'))
240
+ fullLoad() unless options.preload
238
241
  return u.unresolvablePromise()
239
242
 
240
- options.inspectResponse = -> up.browser.loadPage(url, u.only(options, 'method', 'data'))
241
-
242
243
  successOptions = u.merge options,
243
244
  humanizedTarget: 'target'
244
245
 
@@ -248,80 +249,78 @@ up.dom = (($) ->
248
249
  u.renameKey(failureOptions, 'failTransition', 'transition')
249
250
  u.renameKey(failureOptions, 'failLayer', 'layer')
250
251
 
251
- target = bestPreflightSelector(selectorOrElement, successOptions)
252
- failTarget = bestPreflightSelector(options.failTarget, failureOptions)
252
+ try
253
+ improvedTarget = bestPreflightSelector(selectorOrElement, successOptions)
254
+ improvedFailTarget = bestPreflightSelector(options.failTarget, failureOptions)
255
+ catch e
256
+ # Since we're an async function, we should not throw exceptions but return a rejected promise.
257
+ # http://2ality.com/2016/03/promise-rejections-vs-exceptions.html
258
+ return Promise.reject(e)
253
259
 
254
260
  request =
255
261
  url: url
256
262
  method: options.method
257
263
  data: options.data
258
- target: target
259
- failTarget: failTarget
264
+ target: improvedTarget
265
+ failTarget: improvedFailTarget
260
266
  cache: options.cache
261
267
  preload: options.preload
262
268
  headers: options.headers
263
269
  timeout: options.timeout
264
270
 
265
- onSuccess = (html, textStatus, xhr) ->
266
- processResponse(true, target, url, request, xhr, successOptions)
271
+ onSuccess = (response) ->
272
+ processResponse(true, improvedTarget, response, successOptions)
267
273
 
268
- onFailure = (xhr, textStatus, errorThrown) ->
269
- rejection = -> u.rejectedPromise(xhr, textStatus, errorThrown)
270
- if xhr.responseText
271
- promise = processResponse(false, failTarget, url, request, xhr, failureOptions)
272
- promise.then(rejection, rejection)
273
- else
274
+ onFailure = (response) ->
275
+ rejection = -> Promise.reject(response)
276
+ if response.isFatalError()
274
277
  rejection()
278
+ else
279
+ promise = processResponse(false, improvedFailTarget, response, failureOptions)
280
+ # Although processResponse() we will perform a successful replacement of options.failTarget,
281
+ # we still want to reject the promise that's returned to our API client.
282
+ u.always(promise, rejection)
275
283
 
276
- promise = up.ajax(request)
277
- promise = promise.then(onSuccess, onFailure)
284
+ promise = up.request(request)
285
+ promise = promise.then(onSuccess, onFailure) unless options.preload
278
286
  promise
279
287
 
280
288
  ###*
281
289
  @internal
282
290
  ###
283
- processResponse = (isSuccess, selector, url, request, xhr, options) ->
284
- options.method = u.normalizeMethod(u.option(up.protocol.methodFromXhr(xhr), options.method))
285
-
286
- isReloadable = (options.method == 'GET')
287
-
288
- # The server can send us the current path using a header value.
289
- # This way we know the actual URL if the server has redirected.
290
- # TODO: This logic should be moved to up.proxy.
291
- if urlFromServer = up.protocol.locationFromXhr(xhr)
292
- url = urlFromServer
293
- if isSuccess && up.proxy.isCachable(request)
294
- newRequest =
295
- url: url
296
- method: up.protocol.methodFromXhr(xhr) # If the server redirects, we must use the signaled method (should be GET)
297
- target: selector
298
- up.proxy.alias(request, newRequest)
299
- else if isReloadable
300
- if query = u.requestDataAsQuery(options.data)
301
- url = "#{url}?#{query}"
291
+ processResponse = (isSuccess, selector, response, options) ->
292
+ request = response.request
293
+ sourceUrl = response.url
294
+ historyUrl = sourceUrl
295
+ hash = request.hash
296
+
297
+ if options.reveal == true && hash
298
+ # If the request URL had a #hash and options.reveal is not given, we reveal that #hash.
299
+ options.reveal = hash
300
+ historyUrl += hash
301
+
302
+ isReloadable = (response.method == 'GET')
302
303
 
303
304
  if isSuccess
304
305
  if isReloadable # e.g. GET returns 200 OK
305
- options.history = url unless options.history is false || u.isString(options.history)
306
- options.source = url unless options.source is false || u.isString(options.source)
306
+ options.history = historyUrl unless options.history is false || u.isString(options.history)
307
+ options.source = sourceUrl unless options.source is false || u.isString(options.source)
307
308
  else # e.g. POST returns 200 OK
308
- options.history = false unless u.isString(options.history)
309
+ # We allow the developer to pass GETable URLs as { history } and { source } options.
310
+ options.history = false unless u.isString(options.history)
309
311
  options.source = 'keep' unless u.isString(options.source)
310
312
  else
311
313
  if isReloadable # e.g. GET returns 500 Internal Server Error
312
- options.history = url unless options.history is false
313
- options.source = url unless options.source is false
314
+ options.history = historyUrl unless options.history is false
315
+ options.source = sourceUrl unless options.source is false
314
316
  else # e.g. POST returns 500 Internal Server Error
315
- options.source = 'keep'
316
317
  options.history = false
318
+ options.source = 'keep'
317
319
 
318
- if shouldExtractTitle(options) && titleFromXhr = up.protocol.titleFromXhr(xhr)
319
- options.title = titleFromXhr
320
+ if shouldExtractTitle(options) && response.title
321
+ options.title = response.title
320
322
 
321
- if options.preload
322
- u.resolvedPromise()
323
- else
324
- extract(selector, xhr.responseText, options)
323
+ extract(selector, response.text, options)
325
324
 
326
325
  shouldExtractTitle = (options) ->
327
326
  not (options.title is false || u.isString(options.title) || (options.history is false && options.title isnt true))
@@ -355,12 +354,12 @@ up.dom = (($) ->
355
354
  discarded, since it didn't match the selector.
356
355
 
357
356
  @function up.extract
358
- @param {String|Element|jQuery} selectorOrElement
359
- @param {String} html
357
+ @param {string|Element|jQuery} selectorOrElement
358
+ @param {string} html
360
359
  @param {Object} [options]
361
360
  See options for [`up.replace()`](/up.replace).
362
361
  @return {Promise}
363
- A promise that will be resolved then the selector was updated
362
+ A promise that will be fulfilled then the selector was updated
364
363
  and all animation has finished.
365
364
  @stable
366
365
  ###
@@ -374,39 +373,45 @@ up.dom = (($) ->
374
373
 
375
374
  up.layout.saveScroll() unless options.saveScroll == false
376
375
 
377
- # Allow callers to create the targeted element right before we swap.
378
- options.provideTarget?()
379
- response = parseResponse(html)
380
- implantSteps = bestMatchingSteps(selectorOrElement, response, options)
381
-
382
- if shouldExtractTitle(options) && responseTitle = response.title()
383
- options.title = responseTitle
384
- updateHistoryAndTitle(options)
385
-
386
- swapPromises = []
387
- for step in implantSteps
388
- up.log.group 'Updating %s', step.selector, ->
389
- filterScripts(step.$new, options)
390
- swapPromise = swapElements(step.$old, step.$new, step.pseudoClass, step.transition, options)
391
- swapPromises.push(swapPromise)
392
- options.reveal = false # only reveal the first selector atom in the union
393
-
394
- # Delay all further links in the promise chain until all fragments have been swapped
395
- $.when(swapPromises...)
376
+ u.rejectOnError ->
377
+ # Allow callers to create the targeted element right before we swap.
378
+ options.provideTarget?()
379
+ responseDoc = parseResponseDoc(html)
380
+ extractSteps = bestMatchingSteps(selectorOrElement, responseDoc, options)
381
+
382
+ if shouldExtractTitle(options) && responseTitle = responseDoc.title()
383
+ options.title = responseTitle
384
+ updateHistoryAndTitle(options)
385
+
386
+ swapPromises = []
387
+ for step in extractSteps
388
+ up.log.group 'Updating %s', step.selector, ->
389
+ filterScripts(step.$new, options)
390
+ swapPromise = swapElements(step.$old, step.$new, step.pseudoClass, step.transition, options)
391
+ swapPromises.push(swapPromise)
392
+ # When extracting multiple selectors, we only want to reveal the first element.
393
+ # So we set the { reveal } option to false for the next iteration.
394
+ # Note that we must copy the options hash instead of changing it in-place, since the
395
+ # async swapElements() is scheduled for the next microtask and we must not change the options
396
+ # for the previous iteration.
397
+ options = u.merge(options, reveal: false)
398
+
399
+ # Delay all further links in the promise chain until all fragments have been swapped
400
+ Promise.all(swapPromises)
396
401
 
397
402
  bestPreflightSelector = (selector, options) ->
398
- cascade = new up.dom.ExtractCascade(selector, options)
403
+ cascade = new up.ExtractCascade(selector, options)
399
404
  cascade.bestPreflightSelector()
400
405
 
401
406
  bestMatchingSteps = (selector, response, options) ->
402
407
  options = u.merge(options, response: response)
403
- cascade = new up.dom.ExtractCascade(selector, options)
408
+ cascade = new up.ExtractCascade(selector, options)
404
409
  cascade.bestMatchingSteps()
405
410
 
406
411
  filterScripts = ($element, options) ->
407
412
  runInlineScripts = u.option(options.runInlineScripts, config.runInlineScripts)
408
413
  runLinkedScripts = u.option(options.runLinkedScripts, config.runLinkedScripts)
409
- $scripts = u.findWithSelf($element, 'script')
414
+ $scripts = u.selectInSubtree($element, 'script')
410
415
  for script in $scripts
411
416
  $script = $(script)
412
417
  isLinked = u.isPresent($script.attr('src'))
@@ -414,7 +419,7 @@ up.dom = (($) ->
414
419
  unless (isLinked && runLinkedScripts) || (isInline && runInlineScripts)
415
420
  $script.remove()
416
421
 
417
- parseResponse = (html) ->
422
+ parseResponseDoc = (html) ->
418
423
  # jQuery cannot construct transient elements that contain <html> or <body> tags
419
424
  htmlElement = u.createElementFromHtml(html)
420
425
  title: ->
@@ -471,7 +476,7 @@ up.dom = (($) ->
471
476
  # Since we're keeping the element that was requested to be swapped,
472
477
  # there is nothing left to do here.
473
478
  emitFragmentKept(keepPlan)
474
- promise = u.resolvedPromise()
479
+ promise = Promise.resolve()
475
480
 
476
481
  else
477
482
  # This needs to happen before prepareClean() below.
@@ -509,7 +514,7 @@ up.dom = (($) ->
509
514
 
510
515
  # Wrap the replacement as a destroy animation, so $old will
511
516
  # get marked as .up-destroying right away.
512
- promise = destroy($old, { clean, animation: replacement })
517
+ promise = destroy($old, { clean, beforeWipe: replacement, log: false })
513
518
 
514
519
  promise
515
520
 
@@ -550,7 +555,7 @@ up.dom = (($) ->
550
555
  if options.descendantsOnly
551
556
  $partner = $new.find(partnerSelector)
552
557
  else
553
- $partner = u.findWithSelf($new, partnerSelector)
558
+ $partner = u.selectInSubtree($new, partnerSelector)
554
559
  $partner = $partner.first()
555
560
  if $partner.length && $partner.is('[up-keep]')
556
561
  plan =
@@ -624,9 +629,9 @@ up.dom = (($) ->
624
629
  Event listeners may call this method to prevent the element from being preserved.
625
630
  @param {jQuery} event.$element
626
631
  The fragment that will be kept.
627
- @param {jqQuery} event.$newElement
632
+ @param {jQuery} event.$newElement
628
633
  The discarded element.
629
- @param {jQuery} event.newData
634
+ @param {Object} event.newData
630
635
  The value of the [`up-data`](/up-data) attribute of the discarded element,
631
636
  parsed as a JSON object.
632
637
  @stable
@@ -642,9 +647,9 @@ up.dom = (($) ->
642
647
  @event up:fragment:kept
643
648
  @param {jQuery} event.$element
644
649
  The fragment that has been kept.
645
- @param {jqQuery} event.$newElement
650
+ @param {jQuery} event.$newElement
646
651
  The discarded element.
647
- @param {jQuery} event.newData
652
+ @param {Object} event.newData
648
653
  The value of the [`up-data`](/up-data) attribute of the discarded element,
649
654
  parsed as a JSON object.
650
655
  @stable
@@ -668,9 +673,9 @@ up.dom = (($) ->
668
673
  event.
669
674
 
670
675
  @function up.hello
671
- @param {String|Element|jQuery} selectorOrElement
672
- @param {String|Element|jQuery} [options.origin]
673
- @param {String|Element|jQuery} [options.kept]
676
+ @param {string|Element|jQuery} selectorOrElement
677
+ @param {string|Element|jQuery} [options.origin]
678
+ @param {string|Element|jQuery} [options.kept]
674
679
  @return {jQuery}
675
680
  The compiled element
676
681
  @stable
@@ -714,7 +719,7 @@ up.dom = (($) ->
714
719
 
715
720
  autofocus = ($element) ->
716
721
  selector = '[autofocus]:last'
717
- $control = u.findWithSelf($element, selector)
722
+ $control = u.selectInSubtree($element, selector)
718
723
  if $control.length && $control.get(0) != document.activeElement
719
724
  $control.focus()
720
725
 
@@ -735,16 +740,16 @@ up.dom = (($) ->
735
740
  Returns `undefined` if no element matches these conditions.
736
741
 
737
742
  @function up.first
738
- @param {String|Element|jQuery|Array<Element>} selectorOrElement
739
- @param {String} options.layer
743
+ @param {string|Element|jQuery|Array<Element>} selectorOrElement
744
+ @param {string} options.layer
740
745
  The name of the layer in which to find the element. Valid values are
741
746
  `auto`, `page`, `modal` and `popup`.
742
- @param {String|Element|jQuery} [options.origin]
747
+ @param {string|Element|jQuery} [options.origin]
743
748
  An second element or selector that can be referenced as `&` in the first selector:
744
749
 
745
750
  $input = $('input.email');
746
751
  up.first('.field:has(&)', $input); // returns the .field containing $input
747
- @return {jQuery|Undefined}
752
+ @return {jQuery|undefined}
748
753
  The first element that is neither a ghost or being destroyed,
749
754
  or `undefined` if no such element was found.
750
755
  @experimental
@@ -804,55 +809,61 @@ up.dom = (($) ->
804
809
  Emits events [`up:fragment:destroy`](/up:fragment:destroy) and [`up:fragment:destroyed`](/up:fragment:destroyed).
805
810
 
806
811
  @function up.destroy
807
- @param {String|Element|jQuery} selectorOrElement
808
- @param {String} [options.history]
812
+ @param {string|Element|jQuery} selectorOrElement
813
+ @param {string} [options.history]
809
814
  A URL that will be pushed as a new history entry when the element begins destruction.
810
- @param {String} [options.title]
815
+ @param {string} [options.title]
811
816
  The document title to set when the element begins destruction.
812
- @param {String|Function} [options.animation='none']
817
+ @param {string|Function} [options.animation='none']
813
818
  The animation to use before the element is removed from the DOM.
814
- @param {Number} [options.duration]
819
+ @param {number} [options.duration]
815
820
  The duration of the animation. See [`up.animate()`](/up.animate).
816
- @param {Number} [options.delay]
821
+ @param {number} [options.delay]
817
822
  The delay before the animation starts. See [`up.animate()`](/up.animate).
818
- @param {String} [options.easing]
823
+ @param {string} [options.easing]
819
824
  The timing function that controls the animation's acceleration. [`up.animate()`](/up.animate).
820
- @return {Deferred}
821
- A promise that will be resolved once the element has been removed from the DOM.
825
+ @return {Promise}
826
+ A promise that will be fulfilled once the element has been removed from the DOM.
822
827
  @stable
823
828
  ###
824
829
  destroy = (selectorOrElement, options) ->
830
+ $element = $(selectorOrElement)
825
831
  options = u.options(options, animation: false)
826
832
 
827
- $element = $(selectorOrElement)
828
- unless $element.is('.up-placeholder, .up-tooltip, .up-modal, .up-popup')
833
+ if shouldLogDestruction($element, options)
829
834
  destroyMessage = ['Destroying fragment %o', $element.get(0)]
830
835
  destroyedMessage = ['Destroyed fragment %o', $element.get(0)]
836
+
831
837
  if $element.length == 0
832
- u.resolvedDeferred()
833
- else if up.bus.nobodyPrevents('up:fragment:destroy', $element: $element, message: destroyMessage)
834
- animateOptions = up.motion.animateOptions(options)
838
+ Promise.resolve()
839
+ else
840
+ up.emit 'up:fragment:destroy', $element: $element, message: destroyMessage
835
841
  $element.addClass('up-destroying')
836
842
  # If e.g. a modal or popup asks us to restore a URL, do this
837
843
  # before emitting `fragment:destroy`. This way up.navigate sees the
838
844
  # new URL and can assign/remove .up-current classes accordingly.
839
845
  updateHistoryAndTitle(options)
840
846
 
841
- animationDeferred = u.presence(options.animation, u.isDeferred) ||
847
+ animate = ->
848
+ animateOptions = up.motion.animateOptions(options)
842
849
  up.motion.animate($element, options.animation, animateOptions)
843
850
 
844
- animationDeferred.then ->
851
+ beforeWipe = options.beforeWipe || Promise.resolve()
852
+
853
+ wipe = ->
845
854
  options.clean ||= -> up.syntax.clean($element)
846
855
  options.clean()
856
+ up.syntax.clean($element)
847
857
  # Emit this while $element is still part of the DOM, so event
848
858
  # listeners bound to the document will receive the event.
849
859
  up.emit 'up:fragment:destroyed', $element: $element, message: destroyedMessage
850
860
  $element.remove()
851
- animationDeferred
852
- else
853
- # Although someone prevented the destruction, keep a uniform API for
854
- # callers by returning a Deferred that will never be resolved.
855
- u.unresolvableDeferred()
861
+
862
+ animate().then(beforeWipe).then(wipe)
863
+
864
+ shouldLogDestruction = ($element, options) ->
865
+ # Don't log destruction for elements that are either Unpoly internals or frequently destroyed
866
+ options.log != false && !$element.is('.up-placeholder, .up-tooltip, .up-modal, .up-popup')
856
867
 
857
868
  ###*
858
869
  Before a page fragment is being [destroyed](/up.destroy), this
@@ -864,8 +875,6 @@ up.dom = (($) ->
864
875
  @event up:fragment:destroy
865
876
  @param {jQuery} event.$element
866
877
  The page fragment that is about to be destroyed.
867
- @param event.preventDefault()
868
- Event listeners may call this method to prevent the fragment from being destroyed.
869
878
  @stable
870
879
  ###
871
880
 
@@ -895,10 +904,10 @@ up.dom = (($) ->
895
904
  don't usually need to give an URL when reloading.
896
905
 
897
906
  @function up.reload
898
- @param {String|Element|jQuery} selectorOrElement
907
+ @param {string|Element|jQuery} selectorOrElement
899
908
  @param {Object} [options]
900
909
  See options for [`up.replace()`](/up.replace)
901
- @param {String} [options.url]
910
+ @param {string} [options.url]
902
911
  The URL from which to reload the fragment.
903
912
  This defaults to the URL from which the fragment was originally loaded.
904
913
  @stable