upjs-rails 0.17.0 → 0.18.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/dist/up.js +929 -374
  4. data/dist/up.min.js +2 -2
  5. data/lib/assets/javascripts/up/browser.js.coffee +31 -14
  6. data/lib/assets/javascripts/up/bus.js.coffee +87 -22
  7. data/lib/assets/javascripts/up/flow.js.coffee +119 -43
  8. data/lib/assets/javascripts/up/form.js.coffee +188 -57
  9. data/lib/assets/javascripts/up/link.js.coffee +57 -21
  10. data/lib/assets/javascripts/up/modal.js.coffee +77 -63
  11. data/lib/assets/javascripts/up/motion.js.coffee +10 -9
  12. data/lib/assets/javascripts/up/popup.js.coffee +54 -40
  13. data/lib/assets/javascripts/up/proxy.js.coffee +46 -17
  14. data/lib/assets/javascripts/up/rails.js.coffee +22 -4
  15. data/lib/assets/javascripts/up/syntax.js.coffee +2 -2
  16. data/lib/assets/javascripts/up/util.js.coffee +100 -16
  17. data/lib/upjs/rails/inspector.rb +3 -3
  18. data/lib/upjs/rails/version.rb +1 -1
  19. data/spec_app/Gemfile.lock +1 -4
  20. data/spec_app/app/controllers/test_controller.rb +2 -2
  21. data/spec_app/spec/controllers/test_controller_spec.rb +5 -5
  22. data/spec_app/spec/javascripts/helpers/browser_switches.js.coffee +9 -0
  23. data/spec_app/spec/javascripts/helpers/knife.js.coffee +0 -1
  24. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +4 -5
  25. data/spec_app/spec/javascripts/helpers/to_be_present.js.coffee +5 -0
  26. data/spec_app/spec/javascripts/helpers/to_have_request_method.js.coffee +8 -0
  27. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +26 -0
  28. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +203 -91
  29. data/spec_app/spec/javascripts/up/form_spec.js.coffee +244 -49
  30. data/spec_app/spec/javascripts/up/history_spec.js.coffee +8 -2
  31. data/spec_app/spec/javascripts/up/link_spec.js.coffee +83 -30
  32. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +23 -17
  33. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +4 -4
  34. data/spec_app/spec/javascripts/up/navigation_spec.js.coffee +1 -1
  35. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +26 -16
  36. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +45 -13
  37. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +48 -0
  38. data/spec_app/spec/javascripts/up/util_spec.js.coffee +48 -0
  39. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eec276bb6f7dc7dfdf81aebd61dbed1565b0c5f0
4
- data.tar.gz: 4afc1f416a3d147376fe884e7d2091d83ae2309b
3
+ metadata.gz: 0282df3df026ad3d48b9f10614eb73790b146243
4
+ data.tar.gz: 1fd5ddad76f7ab45668da13b90e7727a24d7f4ac
5
5
  SHA512:
6
- metadata.gz: f0166f436863417fa66826fb4978d72087454a0cd4d8cfa43070fdadc42a0da029ca4cc6115a9f4b4d3ea06fdee35de4d87e537ea71ef691a191df62dbffba42
7
- data.tar.gz: 141cf7507e68a2ab6409dda692c8cca53912ab0e47061d391169780447027188de354faba9aec422437d61e36a51f1e702a98674e466f26c4f9cbbd17773a3ec
6
+ metadata.gz: 5b2c33b0c3835ebac5f42bd58f90536da481cce4ab252b3295f1b6d727ba875ec2a0543fc3e28baa16500ae8cd6767d3fe1af41625afd9740017c5f982dd24b9
7
+ data.tar.gz: 10773603d25db7f0aaf99eea17926ab4b253378834900780f95627c4b71eeec7f87e9cbb515eee1151b7ff4b26b682ff9fa5a4a1f36c2ed3e8e88876c7b2c8d6
data/CHANGELOG.md CHANGED
@@ -8,6 +8,51 @@ This project mostly adheres to [Semantic Versioning](http://semver.org/).
8
8
  Unreleased
9
9
  ----------
10
10
 
11
+ ### Compatible changes
12
+
13
+ ### Breaking changes
14
+
15
+
16
+ 0.18.0
17
+ ------
18
+
19
+ ### Compatible changes
20
+
21
+ - New UJS attribute [`[up-toggle]`](/up-toggle) to show or hide part of a form if certain options are selected or boxes are checked.
22
+ - Links can now have an optional `up-confirm` attribute. This opens a confirmation dialog with the given message
23
+ before the link is followed or the modal/popup is opened.
24
+ - New function [`up.off`](/up.off). This unregisters an event listener previously bound with [`up.on`](/up.on).
25
+ - If a container contains more than one link, you can now set the value of the [`up-expand`](/up-expand)
26
+ attribute to a CSS selector to define which link should be expanded.
27
+ - You can now configure a list of idempotent HTTP methods in [`up.proxy.config.safeMethods`](/up.proxy.config).
28
+ The proxy cache will only cache idempotent requests and will clear the entire
29
+ cache after a non-idempotent request.
30
+ - Loading modals and popups will now open if there is a fragment update between the modal/popup's
31
+ request and response.
32
+ - [`up.follow`](/up.follow) and [`up.replace`](/up.replace) now have an option `{ failTarget }`.
33
+ Use it to define the selector to replace if the server responds with a non-200 status code.
34
+ - [`[up-target]`](/up-target) and [`up-follow`](/up.replace) now have a modifying attribute `up-fail-target`.
35
+ Use it to define the selector to replace if the server responds with a non-200 status code.
36
+ - New utility method [`up.util.reject`](/up.util.reject)
37
+ - New utility method [`up.util.only`](/up.util.only)
38
+ - New utility method [`up.util.except`](/up.util.except)
39
+ - Fix a bug where modals could no longer be opened on some browsers
40
+
41
+ ### Breaking changes
42
+
43
+ - By default Up.js now converts `PUT`, `PATCH` and `DELETE` requests to `POST` requests
44
+ that carry their original method in a form parameter named `_method`.
45
+ This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347).
46
+
47
+ Web frameworks like Ruby on Rails or Sinatra are aware of the `_method` parameter and use
48
+ its value as the method for routing.
49
+
50
+ You can configure this behavior in [`up.proxy.config.wrapMethods`](/up.proxy.config)
51
+ and [`up.proxy.config.wrapMethodParam`](/up.proxy.config).
52
+ - The requested selector is now sent to the server as a request header `X-Up-Target`
53
+ (this used to be `X-Up-Selector`). If you are using `upjs-rails`, you can access it
54
+ through `up.target` (this used to be `up.selector`).
55
+
11
56
 
12
57
  0.17.0
13
58
  ------
@@ -67,7 +112,7 @@ Unreleased
67
112
 
68
113
  ### Compatible changes
69
114
 
70
- - New function [`up.autosubmit`](/up.autosubmit) and select [`[up-autosubmit]`] to
115
+ - New function [`up.autosubmit`](/up.autosubmit) and selector [`[up-autosubmit]`](/up-autosubmit) to
71
116
  observe a form or field and submit the form when a value changes.
72
117
  - [`up.observe`](/up.observe) and [`[up-observe]`](/up-observe) can now be applied
73
118
  to `<form>` tags. The callback is run when any field in the form changes.
data/dist/up.js CHANGED
@@ -27,7 +27,7 @@ that might save you from loading something like [Underscore.js](http://underscor
27
27
  @function up.util.memoize
28
28
  @internal
29
29
  */
30
- var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, any, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, cssAnimate, debug, detect, each, error, escapePressed, evalConsoleTemplate, extend, findWithSelf, finishCssAnimate, fixedToAbsolute, forceCompositing, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, multiSelector, nextFrame, normalizeMethod, normalizeUrl, nullJQuery, offsetParent, once, only, option, options, parseUrl, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, selectorForElement, setMissingAttrs, temporaryCss, times, titleFromXhr, toArray, trim, unJQuery, uniq, unresolvableDeferred, unresolvablePromise, unwrapElement, warn;
30
+ var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, any, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, cssAnimate, debug, detect, each, error, escapePressed, evalConsoleTemplate, except, extend, findWithSelf, finishCssAnimate, fixedToAbsolute, forceCompositing, intersect, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, multiSelector, nextFrame, nonUpClasses, normalizeMethod, normalizeUrl, nullJQuery, offsetParent, once, only, option, options, parseUrl, presence, presentAttr, reject, remove, requestDataAsArray, requestDataAsQueryString, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, selectorForElement, setMissingAttrs, temporaryCss, times, titleFromXhr, toArray, trim, unJQuery, uniq, unresolvableDeferred, unresolvablePromise, unwrapElement, warn;
31
31
  memoize = function(func) {
32
32
  var cache, cached;
33
33
  cache = void 0;
@@ -44,19 +44,6 @@ that might save you from loading something like [Underscore.js](http://underscor
44
44
  };
45
45
  };
46
46
 
47
- /**
48
- @function up.util.ajax
49
- @internal
50
- */
51
- ajax = function(request) {
52
- request = copy(request);
53
- if (request.selector) {
54
- request.headers || (request.headers = {});
55
- request.headers['X-Up-Selector'] = request.selector;
56
- }
57
- return $.ajax(request);
58
- };
59
-
60
47
  /**
61
48
  Returns if the given port is the default port for the given protocol.
62
49
 
@@ -307,7 +294,7 @@ that might save you from loading something like [Underscore.js](http://underscor
307
294
  @experimental
308
295
  */
309
296
  selectorForElement = function(element) {
310
- var $element, classString, classes, id, j, klass, len, name, selector, upId;
297
+ var $element, classes, id, j, klass, len, name, selector, upId;
311
298
  $element = $(element);
312
299
  selector = void 0;
313
300
  debug("Creating selector from element %o", $element.get(0));
@@ -317,8 +304,8 @@ that might save you from loading something like [Underscore.js](http://underscor
317
304
  selector = "#" + id;
318
305
  } else if (name = presence($element.attr("name"))) {
319
306
  selector = "[name='" + name + "']";
320
- } else if (classString = presence($element.attr("class"))) {
321
- classes = classString.split(' ');
307
+ } else if (classes = presence(nonUpClasses($element))) {
308
+ console.log("using klass!", classes);
322
309
  selector = '';
323
310
  for (j = 0, len = classes.length; j < len; j++) {
324
311
  klass = classes[j];
@@ -329,6 +316,14 @@ that might save you from loading something like [Underscore.js](http://underscor
329
316
  }
330
317
  return selector;
331
318
  };
319
+ nonUpClasses = function($element) {
320
+ var classString, classes;
321
+ classString = $element.attr('class') || '';
322
+ classes = classString.split(' ');
323
+ return select(classes, function(klass) {
324
+ return isPresent(klass) && !klass.match(/^up-/);
325
+ });
326
+ };
332
327
  createElementFromHtml = function(html) {
333
328
  var anything, bodyElement, bodyMatch, bodyPattern, capture, closeTag, headElement, htmlElement, openTag, titleElement, titleMatch, titlePattern;
334
329
  openTag = function(tag) {
@@ -890,6 +885,35 @@ that might save you from loading something like [Underscore.js](http://underscor
890
885
  return matches;
891
886
  };
892
887
 
888
+ /**
889
+ Returns all elements from the given array that do not return
890
+ a truthy value when passed to the given function.
891
+
892
+ @function up.util.reject
893
+ @param {Array<T>} array
894
+ @return {Array<T>}
895
+ @stable
896
+ */
897
+ reject = function(array, tester) {
898
+ return select(array, function(element) {
899
+ return !tester(element);
900
+ });
901
+ };
902
+
903
+ /**
904
+ Returns the intersection of the given two arrays.
905
+
906
+ Implementation is not optimized. Don't use it for large arrays.
907
+
908
+ @function up.util.intersect
909
+ @internal
910
+ */
911
+ intersect = function(array1, array2) {
912
+ return select(array1, function(element) {
913
+ return contains(array2, element);
914
+ });
915
+ };
916
+
893
917
  /**
894
918
  Returns the first [present](/up.util.isPresent) element attribute
895
919
  among the given list of attribute names.
@@ -1299,6 +1323,26 @@ that might save you from loading something like [Underscore.js](http://underscor
1299
1323
  return filtered;
1300
1324
  };
1301
1325
 
1326
+ /**
1327
+ Returns a copy of the given object that contains all except
1328
+ the given properties.
1329
+
1330
+ @function up.util.except
1331
+ @param {Object} object
1332
+ @param {Array} keys...
1333
+ @stable
1334
+ */
1335
+ except = function() {
1336
+ var filtered, j, len, object, properties, property;
1337
+ object = arguments[0], properties = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1338
+ filtered = copy(object);
1339
+ for (j = 0, len = properties.length; j < len; j++) {
1340
+ property = properties[j];
1341
+ delete filtered[property];
1342
+ }
1343
+ return filtered;
1344
+ };
1345
+
1302
1346
  /**
1303
1347
  @function up.util.isUnmodifiedKeyEvent
1304
1348
  @internal
@@ -1720,7 +1764,61 @@ that might save you from loading something like [Underscore.js](http://underscor
1720
1764
  bottom: ''
1721
1765
  });
1722
1766
  };
1767
+
1768
+ /**
1769
+ Normalizes the given params object to the form returned by
1770
+ [`jQuery.serializeArray`](https://api.jquery.com/serializeArray/).
1771
+
1772
+ @function up.util.requestDataAsArray
1773
+ @param {Object|Array|Undefined|Null} data
1774
+ @internal
1775
+ */
1776
+ requestDataAsArray = function(data) {
1777
+ var name, results, value;
1778
+ if (isMissing(data)) {
1779
+ return [];
1780
+ } else if (isArray(data)) {
1781
+ return data;
1782
+ } else if (isObject(data)) {
1783
+ results = [];
1784
+ for (name in data) {
1785
+ value = data[name];
1786
+ results.push({
1787
+ name: name,
1788
+ value: value
1789
+ });
1790
+ }
1791
+ return results;
1792
+ } else {
1793
+ return error('Unknown options.data type for %o', data);
1794
+ }
1795
+ };
1796
+
1797
+ /**
1798
+ Returns an URL-encoded query string for the given params object.
1799
+
1800
+ @function up.util.requestDataAsQueryString
1801
+ @param {Object|Array|Undefined|Null} data
1802
+ @internal
1803
+ */
1804
+ requestDataAsQueryString = function(data) {
1805
+ var array, query;
1806
+ array = requestDataAsArray(data);
1807
+ query = '';
1808
+ if (isPresent(array)) {
1809
+ query += '?';
1810
+ each(array, function(field, index) {
1811
+ if (index !== 0) {
1812
+ query += '&';
1813
+ }
1814
+ return query += encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value);
1815
+ });
1816
+ }
1817
+ return query;
1818
+ };
1723
1819
  return {
1820
+ requestDataAsArray: requestDataAsArray,
1821
+ requestDataAsQueryString: requestDataAsQueryString,
1724
1822
  offsetParent: offsetParent,
1725
1823
  fixedToAbsolute: fixedToAbsolute,
1726
1824
  presentAttr: presentAttr,
@@ -1731,7 +1829,6 @@ that might save you from loading something like [Underscore.js](http://underscor
1731
1829
  createElementFromHtml: createElementFromHtml,
1732
1830
  $createElementFromSelector: $createElementFromSelector,
1733
1831
  selectorForElement: selectorForElement,
1734
- ajax: ajax,
1735
1832
  extend: extend,
1736
1833
  copy: copy,
1737
1834
  merge: merge,
@@ -1746,6 +1843,8 @@ that might save you from loading something like [Underscore.js](http://underscor
1746
1843
  any: any,
1747
1844
  detect: detect,
1748
1845
  select: select,
1846
+ reject: reject,
1847
+ intersect: intersect,
1749
1848
  compact: compact,
1750
1849
  uniq: uniq,
1751
1850
  last: last,
@@ -1787,6 +1886,7 @@ that might save you from loading something like [Underscore.js](http://underscor
1787
1886
  methodFromXhr: methodFromXhr,
1788
1887
  clientSize: clientSize,
1789
1888
  only: only,
1889
+ except: except,
1790
1890
  trim: trim,
1791
1891
  unresolvableDeferred: unresolvableDeferred,
1792
1892
  unresolvablePromise: unresolvablePromise,
@@ -1827,32 +1927,42 @@ we can't currently get rid off.
1827
1927
  var slice = [].slice;
1828
1928
 
1829
1929
  up.browser = (function($) {
1830
- var canCssTransition, canInputEvent, canLogSubstitution, canPushState, initialRequestMethod, isIE8OrWorse, isIE9OrWorse, isRecentJQuery, isSupported, loadPage, popCookie, puts, u, url;
1930
+ var canCssTransition, canInputEvent, canLogSubstitution, canPushState, confirm, initialRequestMethod, isIE8OrWorse, isIE9OrWorse, isRecentJQuery, isSupported, loadPage, popCookie, puts, u, url;
1831
1931
  u = up.util;
1932
+
1933
+ /**
1934
+ @method up.browser.loadPage
1935
+ @param {String} url
1936
+ @param {String} [options.method='get']
1937
+ @param {Object|Array} [options.data]
1938
+ @internal
1939
+ */
1832
1940
  loadPage = function(url, options) {
1833
- var $form, csrfParam, csrfToken, metadataInput, method, target;
1941
+ var $form, addField, csrfField, method;
1834
1942
  if (options == null) {
1835
1943
  options = {};
1836
1944
  }
1837
1945
  method = u.option(options.method, 'get').toLowerCase();
1838
1946
  if (method === 'get') {
1839
- return location.href = url;
1840
- } else if ($.rails) {
1841
- target = options.target;
1842
- csrfToken = $.rails.csrfToken();
1843
- csrfParam = $.rails.csrfParam();
1947
+ return location.href = url + u.requestDataAsQueryString(options.data);
1948
+ } else {
1844
1949
  $form = $("<form method='post' action='" + url + "'></form>");
1845
- metadataInput = "<input name='_method' value='" + method + "' type='hidden' />";
1846
- if (u.isDefined(csrfParam) && u.isDefined(csrfToken)) {
1847
- metadataInput += "<input name='" + csrfParam + "' value='" + csrfToken + "' type='hidden' />";
1848
- }
1849
- if (target) {
1850
- $form.attr('target', target);
1950
+ addField = function(field) {
1951
+ var $field;
1952
+ $field = $('<input type="hidden">');
1953
+ $field.attr(field.name, field.value);
1954
+ return $field.appendTo($form);
1955
+ };
1956
+ addField({
1957
+ name: up.proxy.config.wrapMethodParam,
1958
+ value: method
1959
+ });
1960
+ if (csrfField = up.rails.csrfField()) {
1961
+ addField(csrfField);
1851
1962
  }
1852
- $form.hide().append(metadataInput).appendTo('body');
1963
+ u.each(u.requestDataAsArray(options.data), addField);
1964
+ $form.hide().appendTo('body');
1853
1965
  return $form.submit();
1854
- } else {
1855
- return error("Can't fake a " + (method.toUpperCase()) + " request without Rails UJS");
1856
1966
  }
1857
1967
  };
1858
1968
 
@@ -1945,7 +2055,7 @@ we can't currently get rid off.
1945
2055
  console.log("Hello %o!", "Judy");
1946
2056
 
1947
2057
  @function up.browser.canLogSubstitution
1948
- @return boolean
2058
+ @return {Boolean}
1949
2059
  @internal
1950
2060
  */
1951
2061
  canLogSubstitution = u.memoize(function() {
@@ -1967,6 +2077,19 @@ we can't currently get rid off.
1967
2077
  }
1968
2078
  return value;
1969
2079
  };
2080
+
2081
+ /**
2082
+ @function up,browser.confirm
2083
+ @return {Promise}
2084
+ @internal
2085
+ */
2086
+ confirm = function(message) {
2087
+ if (u.isBlank(message) || confirm(message)) {
2088
+ return u.resolvedPromise();
2089
+ } else {
2090
+ return u.unresolvablePromise();
2091
+ }
2092
+ };
1970
2093
  initialRequestMethod = u.memoize(function() {
1971
2094
  return (popCookie('_up_request_method') || 'get').toLowerCase();
1972
2095
  });
@@ -1993,6 +2116,7 @@ we can't currently get rid off.
1993
2116
  return {
1994
2117
  url: url,
1995
2118
  loadPage: loadPage,
2119
+ confirm: confirm,
1996
2120
  canPushState: canPushState,
1997
2121
  canCssTransition: canCssTransition,
1998
2122
  canInputEvent: canInputEvent,
@@ -2049,14 +2173,17 @@ and call `preventDefault()` on the `event` object:
2049
2173
  var slice = [].slice;
2050
2174
 
2051
2175
  up.bus = (function($) {
2052
- var boot, defaultLiveDescriptions, emit, emitReset, live, liveDescriptions, nobodyPrevents, onEscape, restoreSnapshot, snapshot, u, upListenerToJqueryListener;
2176
+ var boot, emit, emitReset, forgetUpDescription, live, liveUpDescriptions, nextUpDescriptionNumber, nobodyPrevents, onEscape, rememberUpDescription, restoreSnapshot, snapshot, u, unbind, upDescriptionNumber, upDescriptionToJqueryDescription, upListenerToJqueryListener;
2053
2177
  u = up.util;
2054
- liveDescriptions = [];
2055
- defaultLiveDescriptions = null;
2178
+ liveUpDescriptions = {};
2179
+ nextUpDescriptionNumber = 0;
2056
2180
 
2057
2181
  /**
2058
- * Convert an Up.js style listener (second argument is the event target
2059
- * as a jQuery collection) to a vanilla jQuery listener
2182
+ Convert an Up.js style listener (second argument is the event target
2183
+ as a jQuery collection) to a vanilla jQuery listener
2184
+
2185
+ @function upListenerToJqueryListener
2186
+ @internal
2060
2187
  */
2061
2188
  upListenerToJqueryListener = function(upListener) {
2062
2189
  return function(event) {
@@ -2066,6 +2193,30 @@ and call `preventDefault()` on the `event` object:
2066
2193
  };
2067
2194
  };
2068
2195
 
2196
+ /**
2197
+ Converts an argument list for `up.on` to an argument list for `jQuery.on`.
2198
+ This involves rewriting the listener signature in the last argument slot.
2199
+
2200
+ @function upDescriptionToJqueryDescription
2201
+ @internal
2202
+ */
2203
+ upDescriptionToJqueryDescription = function(upDescription, isNew) {
2204
+ var jqueryDescription, jqueryListener, upListener;
2205
+ jqueryDescription = u.copy(upDescription);
2206
+ upListener = jqueryDescription.pop();
2207
+ jqueryListener = void 0;
2208
+ if (isNew) {
2209
+ jqueryListener = upListenerToJqueryListener(upListener);
2210
+ upListener._asJqueryListener = jqueryListener;
2211
+ upListener._descriptionNumber = ++nextUpDescriptionNumber;
2212
+ } else {
2213
+ jqueryListener = upListener._asJqueryListener;
2214
+ jqueryListener || u.error('up.off: The event listener %o was never registered through up.on');
2215
+ }
2216
+ jqueryDescription.push(jqueryListener);
2217
+ return jqueryDescription;
2218
+ };
2219
+
2069
2220
  /**
2070
2221
  Listens to an event on `document`.
2071
2222
 
@@ -2085,7 +2236,6 @@ and call `preventDefault()` on the `event` object:
2085
2236
  Other than jQuery, Up.js will silently discard event listeners
2086
2237
  on [unsupported browsers](/up.browser.isSupported).
2087
2238
 
2088
-
2089
2239
  \#\#\#\# Attaching structured data
2090
2240
 
2091
2241
  In case you want to attach structured data to the event you're observing,
@@ -2100,7 +2250,6 @@ and call `preventDefault()` on the `event` object:
2100
2250
  console.log("This is %o who is %o years old", data.name, data.age);
2101
2251
  });
2102
2252
 
2103
-
2104
2253
  \#\#\#\# Migrating jQuery event handlers to `up.on`
2105
2254
 
2106
2255
  Within the event handler, Up.js will bind `this` to the
@@ -2119,6 +2268,12 @@ and call `preventDefault()` on the `event` object:
2119
2268
  $(this).something();
2120
2269
  });
2121
2270
 
2271
+ \#\#\#\# Stopping to listen
2272
+
2273
+ `up.on` returns a function that unbinds the event listeners when called.
2274
+
2275
+ There is also a function [`up.off`](/up.off) which you can use for the same purpose.
2276
+
2122
2277
  @function up.on
2123
2278
  @param {String} events
2124
2279
  A space-separated list of event names to bind.
@@ -2136,23 +2291,60 @@ and call `preventDefault()` on the `event` object:
2136
2291
  @stable
2137
2292
  */
2138
2293
  live = function() {
2139
- var $document, args, behavior, description, lastIndex;
2140
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
2294
+ var jqueryDescription, ref, upDescription;
2295
+ upDescription = 1 <= arguments.length ? slice.call(arguments, 0) : [];
2141
2296
  if (!up.browser.isSupported()) {
2142
2297
  return (function() {});
2143
2298
  }
2144
- description = u.copy(args);
2145
- lastIndex = description.length - 1;
2146
- behavior = description[lastIndex];
2147
- description[lastIndex] = upListenerToJqueryListener(behavior);
2148
- liveDescriptions.push(description);
2149
- $document = $(document);
2150
- $document.on.apply($document, description);
2299
+ jqueryDescription = upDescriptionToJqueryDescription(upDescription, true);
2300
+ rememberUpDescription(upDescription);
2301
+ (ref = $(document)).on.apply(ref, jqueryDescription);
2151
2302
  return function() {
2152
- return $document.off.apply($document, description);
2303
+ return unbind.apply(null, upDescription);
2153
2304
  };
2154
2305
  };
2155
2306
 
2307
+ /**
2308
+ Unregisters an event listener previously bound with [`up.on`](/up.on).
2309
+
2310
+ \#\#\#\# Example
2311
+
2312
+ Let's say you are listing to clicks on `.button` elements:
2313
+
2314
+ var listener = function() { };
2315
+ up.on('click', '.button', listener);
2316
+
2317
+ You can stop listening to these events like this:
2318
+
2319
+ up.off('click', '.button', listener);
2320
+
2321
+ Note that you need to pass `up.off` a reference to the same listener function
2322
+ that was passed to `up.on` earlier.
2323
+
2324
+ @function up.off
2325
+ @stable
2326
+ */
2327
+ unbind = function() {
2328
+ var jqueryDescription, ref, upDescription;
2329
+ upDescription = 1 <= arguments.length ? slice.call(arguments, 0) : [];
2330
+ jqueryDescription = upDescriptionToJqueryDescription(upDescription, false);
2331
+ forgetUpDescription(upDescription);
2332
+ return (ref = $(document)).off.apply(ref, jqueryDescription);
2333
+ };
2334
+ rememberUpDescription = function(upDescription) {
2335
+ var number;
2336
+ number = upDescriptionNumber(upDescription);
2337
+ return liveUpDescriptions[number] = upDescription;
2338
+ };
2339
+ forgetUpDescription = function(upDescription) {
2340
+ var number;
2341
+ number = upDescriptionNumber(upDescription);
2342
+ return delete liveUpDescriptions[number];
2343
+ };
2344
+ upDescriptionNumber = function(upDescription) {
2345
+ return u.last(upDescription)._descriptionNumber;
2346
+ };
2347
+
2156
2348
  /**
2157
2349
  Emits a event with the given name and properties.
2158
2350
 
@@ -2179,7 +2371,7 @@ and call `preventDefault()` on the `event` object:
2179
2371
  will by default include properties like `preventDefault()`
2180
2372
  or `stopPropagation()`.
2181
2373
  @param {jQuery} [eventProps.$element=$(document)]
2182
- The element on which the event is trigered.
2374
+ The element on which the event is triggered.
2183
2375
  @experimental
2184
2376
  */
2185
2377
  emit = function(eventName, eventProps) {
@@ -2236,7 +2428,13 @@ and call `preventDefault()` on the `event` object:
2236
2428
  @internal
2237
2429
  */
2238
2430
  snapshot = function() {
2239
- return defaultLiveDescriptions = u.copy(liveDescriptions);
2431
+ var description, i, len, results;
2432
+ results = [];
2433
+ for (i = 0, len = liveUpDescriptions.length; i < len; i++) {
2434
+ description = liveUpDescriptions[i];
2435
+ results.push(description._isDefault = true);
2436
+ }
2437
+ return results;
2240
2438
  };
2241
2439
 
2242
2440
  /**
@@ -2246,14 +2444,16 @@ and call `preventDefault()` on the `event` object:
2246
2444
  @internal
2247
2445
  */
2248
2446
  restoreSnapshot = function() {
2249
- var description, i, len, ref;
2250
- for (i = 0, len = liveDescriptions.length; i < len; i++) {
2251
- description = liveDescriptions[i];
2252
- if (!u.contains(defaultLiveDescriptions, description)) {
2253
- (ref = $(document)).off.apply(ref, description);
2254
- }
2447
+ var description, doomedDescriptions, i, len, results;
2448
+ doomedDescriptions = u.reject(liveUpDescriptions, function(description) {
2449
+ return description._isDefault;
2450
+ });
2451
+ results = [];
2452
+ for (i = 0, len = doomedDescriptions.length; i < len; i++) {
2453
+ description = doomedDescriptions[i];
2454
+ results.push(unbind.apply(null, description));
2255
2455
  }
2256
- return liveDescriptions = u.copy(defaultLiveDescriptions);
2456
+ return results;
2257
2457
  };
2258
2458
 
2259
2459
  /**
@@ -2306,7 +2506,9 @@ and call `preventDefault()` on the `event` object:
2306
2506
  live('up:framework:boot', snapshot);
2307
2507
  live('up:framework:reset', restoreSnapshot);
2308
2508
  return {
2509
+ knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0),
2309
2510
  on: live,
2511
+ off: unbind,
2310
2512
  emit: emit,
2311
2513
  nobodyPrevents: nobodyPrevents,
2312
2514
  onEscape: onEscape,
@@ -2317,6 +2519,8 @@ and call `preventDefault()` on the `event` object:
2317
2519
 
2318
2520
  up.on = up.bus.on;
2319
2521
 
2522
+ up.off = up.bus.off;
2523
+
2320
2524
  up.emit = up.bus.emit;
2321
2525
 
2322
2526
  up.reset = up.bus.emitReset;
@@ -2692,11 +2896,11 @@ later.
2692
2896
  up.on('ready', (function() {
2693
2897
  return hello(document.body);
2694
2898
  }));
2695
- up.on('up:fragment:inserted', function(event) {
2696
- return compile(event.$element);
2899
+ up.on('up:fragment:inserted', function(event, $element) {
2900
+ return compile($element);
2697
2901
  });
2698
- up.on('up:fragment:destroy', function(event) {
2699
- return runDestroyers(event.$element);
2902
+ up.on('up:fragment:destroy', function(event, $element) {
2903
+ return runDestroyers($element);
2700
2904
  });
2701
2905
  up.on('up:framework:boot', snapshot);
2702
2906
  up.on('up:framework:reset', reset);
@@ -3561,7 +3765,7 @@ are based on this module.
3561
3765
 
3562
3766
  (function() {
3563
3767
  up.flow = (function($) {
3564
- var autofocus, destroy, elementsInserted, findOldFragment, first, implant, isRealElement, oldFragmentNotFound, parseImplantSteps, parseResponse, reload, replace, resolveSelector, setSource, source, swapElements, u;
3768
+ var autofocus, destroy, elementsInserted, findOldFragment, first, implant, isRealElement, oldFragmentNotFound, parseImplantSteps, parseResponse, processResponse, reload, replace, resolveSelector, setSource, source, swapElements, u;
3565
3769
  u = up.util;
3566
3770
  setSource = function(element, sourceUrl) {
3567
3771
  var $element;
@@ -3571,6 +3775,14 @@ are based on this module.
3571
3775
  }
3572
3776
  return $element.attr("up-source", sourceUrl);
3573
3777
  };
3778
+
3779
+ /**
3780
+ Returns the URL the given element was retrieved from.
3781
+
3782
+ @method up.flow.source
3783
+ @param {String|Element|jQuery} selectorOrElement
3784
+ @experimental
3785
+ */
3574
3786
  source = function(selectorOrElement) {
3575
3787
  var $element;
3576
3788
  $element = $(selectorOrElement).closest('[up-source]');
@@ -3578,15 +3790,22 @@ are based on this module.
3578
3790
  };
3579
3791
 
3580
3792
  /**
3793
+ Resolves the given selector (which might contain `&` references)
3794
+ to an absolute selector.
3795
+
3581
3796
  @function up.flow.resolveSelector
3797
+ @param {String|Element|jQuery} selectorOrElement
3798
+ @param {String|Element|jQuery} origin
3799
+ The element that this selector resolution is relative to.
3800
+ That element's selector will be substituted for `&`.
3582
3801
  @internal
3583
3802
  */
3584
- resolveSelector = function(selectorOrElement, options) {
3585
- var origin, originSelector, selector;
3803
+ resolveSelector = function(selectorOrElement, origin) {
3804
+ var originSelector, selector;
3586
3805
  if (u.isString(selectorOrElement)) {
3587
3806
  selector = selectorOrElement;
3588
3807
  if (u.contains(selector, '&')) {
3589
- if (origin = u.presence(options.origin)) {
3808
+ if (origin) {
3590
3809
  originSelector = u.selectorForElement(origin);
3591
3810
  selector = selector.replace(/\&/, originSelector);
3592
3811
  } else {
@@ -3662,7 +3881,7 @@ are based on this module.
3662
3881
  \#\#\#\# Optimizing response rendering
3663
3882
 
3664
3883
  The server is free to optimize Up.js requests by only rendering the HTML fragment
3665
- that is being updated. The request's `X-Up-Selector` header will contain
3884
+ that is being updated. The request's `X-Up-Target` header will contain
3666
3885
  the CSS selector for the updating fragment.
3667
3886
 
3668
3887
  If you are using the `upjs-rails` gem you can also access the selector via
@@ -3680,8 +3899,17 @@ are based on this module.
3680
3899
  here, in which case a selector will be inferred from the element's class and ID.
3681
3900
  @param {String} url
3682
3901
  The URL to fetch from the server.
3683
- @param {String} [options.method='get']
3902
+ @param {String} [options.failTarget='body']
3903
+ The CSS selector to update if the server sends a non-200 status code.
3684
3904
  @param {String} [options.title]
3905
+ @param {String} [options.method='get']
3906
+ @param {Object|Array} [options.data]
3907
+ Parameters that should be sent as the request's payload.
3908
+
3909
+ Parameters can either be passed as an object (where the property names become
3910
+ the param names and the property values become the param values) or as
3911
+ an array of `{ name: 'param-name', value: 'param-value' }` objects
3912
+ (compare to jQuery's [`serializeArray`](https://api.jquery.com/serializeArray/)).
3685
3913
  @param {String} [options.transition='none']
3686
3914
  @param {String|Boolean} [options.history=true]
3687
3915
  If a `String` is given, it is used as the URL the browser's location bar and history.
@@ -3713,50 +3941,95 @@ are based on this module.
3713
3941
  @stable
3714
3942
  */
3715
3943
  replace = function(selectorOrElement, url, options) {
3716
- var promise, request, selector;
3944
+ var failTarget, promise, request, target;
3717
3945
  u.debug("Replace %o with %o (options %o)", selectorOrElement, url, options);
3718
3946
  options = u.options(options);
3719
- selector = resolveSelector(selectorOrElement, options);
3947
+ target = resolveSelector(selectorOrElement, options.origin);
3948
+ failTarget = u.option(options.failTarget, 'body');
3949
+ failTarget = resolveSelector(failTarget, options.origin);
3720
3950
  if (!up.browser.canPushState() && options.history !== false) {
3721
3951
  if (!options.preload) {
3722
- up.browser.loadPage(url, u.only(options, 'method'));
3952
+ up.browser.loadPage(url, u.only(options, 'method', 'data'));
3723
3953
  }
3724
3954
  return u.unresolvablePromise();
3725
3955
  }
3726
3956
  request = {
3727
3957
  url: url,
3728
3958
  method: options.method,
3729
- selector: selector,
3959
+ data: options.data,
3960
+ target: target,
3961
+ failTarget: failTarget,
3730
3962
  cache: options.cache,
3731
3963
  preload: options.preload,
3732
3964
  headers: options.headers
3733
3965
  };
3734
3966
  promise = up.proxy.ajax(request);
3735
3967
  promise.done(function(html, textStatus, xhr) {
3736
- var currentLocation, newRequest;
3737
- if (currentLocation = u.locationFromXhr(xhr)) {
3738
- u.debug('Location from server: %o', currentLocation);
3968
+ return processResponse(true, target, url, request, xhr, options);
3969
+ });
3970
+ promise.fail(function(xhr, textStatus, errorThrown) {
3971
+ return processResponse(false, failTarget, url, request, xhr, options);
3972
+ });
3973
+ return promise;
3974
+ };
3975
+
3976
+ /**
3977
+ @internal
3978
+ */
3979
+ processResponse = function(isSuccess, selector, url, request, xhr, options) {
3980
+ var isReloadable, newRequest, urlFromServer;
3981
+ options.method = u.normalizeMethod(u.option(u.methodFromXhr(xhr), options.method));
3982
+ options.title = u.option(u.titleFromXhr(xhr), options.title);
3983
+ isReloadable = options.method === 'GET';
3984
+ if (urlFromServer = u.locationFromXhr(xhr)) {
3985
+ url = urlFromServer;
3986
+ if (isSuccess) {
3739
3987
  newRequest = {
3740
- url: currentLocation,
3988
+ url: url,
3741
3989
  method: u.methodFromXhr(xhr),
3742
- selector: selector
3990
+ target: selector
3743
3991
  };
3744
3992
  up.proxy.alias(request, newRequest);
3745
- url = currentLocation;
3746
- }
3747
- if (options.history !== false) {
3748
- options.history = url;
3749
3993
  }
3750
- if (options.source !== false) {
3751
- options.source = url;
3994
+ } else if (isReloadable) {
3995
+ url = url + u.requestDataAsQueryString(options.data);
3996
+ }
3997
+ if (isSuccess) {
3998
+ if (isReloadable) {
3999
+ if (!(options.history === false || u.isString(options.history))) {
4000
+ options.history = url;
4001
+ }
4002
+ if (!(options.source === false || u.isString(options.source))) {
4003
+ options.source = url;
4004
+ }
4005
+ } else {
4006
+ if (!u.isString(options.history)) {
4007
+ options.history = false;
4008
+ }
4009
+ if (!u.isString(options.source)) {
4010
+ options.source = 'keep';
4011
+ }
3752
4012
  }
3753
- options.title || (options.title = u.titleFromXhr(xhr));
3754
- if (!options.preload) {
3755
- return implant(selector, html, options);
4013
+ } else {
4014
+ options.transition = options.failTransition;
4015
+ options.failTransition = void 0;
4016
+ if (isReloadable) {
4017
+ if (options.history !== false) {
4018
+ options.history = url;
4019
+ }
4020
+ if (options.source !== false) {
4021
+ options.source = url;
4022
+ }
4023
+ } else {
4024
+ options.source = 'keep';
4025
+ options.history = false;
3756
4026
  }
3757
- });
3758
- promise.fail(u.error);
3759
- return promise;
4027
+ }
4028
+ if (options.preload) {
4029
+ return u.resolvedPromise();
4030
+ } else {
4031
+ return implant(selector, xhr.responseText, options);
4032
+ }
3760
4033
  };
3761
4034
 
3762
4035
  /**
@@ -3792,34 +4065,41 @@ are based on this module.
3792
4065
  @param {String} html
3793
4066
  @param {Object} [options]
3794
4067
  See options for [`up.replace`](/up.replace).
4068
+ @return {Promise}
4069
+ A promise that will be resolved then the selector was updated
4070
+ and all animation has finished.
3795
4071
  @experimental
3796
4072
  */
3797
4073
  implant = function(selectorOrElement, html, options) {
3798
- var $new, $old, j, len, ref, ref1, response, results, selector, step;
3799
- selector = resolveSelector(selectorOrElement, options);
4074
+ var $new, $old, deferred, deferreds, j, len, ref, ref1, ref2, response, selector, step;
3800
4075
  options = u.options(options, {
3801
4076
  historyMethod: 'push',
3802
4077
  requireMatch: true
3803
4078
  });
3804
- options.source = u.option(options.source, options.history);
4079
+ selector = resolveSelector(selectorOrElement, options.origin);
3805
4080
  response = parseResponse(html, options);
3806
4081
  options.title || (options.title = response.title());
3807
4082
  if (options.saveScroll !== false) {
3808
4083
  up.layout.saveScroll();
3809
4084
  }
4085
+ if (typeof options.beforeSwap === "function") {
4086
+ options.beforeSwap($old, $new);
4087
+ }
4088
+ deferreds = [];
3810
4089
  ref = parseImplantSteps(selector, options);
3811
- results = [];
3812
4090
  for (j = 0, len = ref.length; j < len; j++) {
3813
4091
  step = ref[j];
3814
4092
  $old = findOldFragment(step.selector, options);
3815
4093
  $new = (ref1 = response.find(step.selector)) != null ? ref1.first() : void 0;
3816
4094
  if ($old && $new) {
3817
- results.push(swapElements($old, $new, step.pseudoClass, step.transition, options));
3818
- } else {
3819
- results.push(void 0);
4095
+ deferred = swapElements($old, $new, step.pseudoClass, step.transition, options);
4096
+ deferreds.push(deferred);
3820
4097
  }
3821
4098
  }
3822
- return results;
4099
+ if (typeof options.afterSwap === "function") {
4100
+ options.afterSwap($old, $new);
4101
+ }
4102
+ return (ref2 = up.motion).when.apply(ref2, deferreds);
3823
4103
  };
3824
4104
  findOldFragment = function(selector, options) {
3825
4105
  return first(".up-popup " + selector) || first(".up-modal " + selector) || first(selector) || oldFragmentNotFound(selector, options);
@@ -3868,8 +4148,13 @@ are based on this module.
3868
4148
  });
3869
4149
  };
3870
4150
  swapElements = function($old, $new, pseudoClass, transition, options) {
3871
- var $wrapper, insertionMethod;
4151
+ var $wrapper, insertionMethod, promise, replacement;
3872
4152
  transition || (transition = 'none');
4153
+ if (options.source === 'keep') {
4154
+ options = u.merge(options, {
4155
+ source: source($old)
4156
+ });
4157
+ }
3873
4158
  up.motion.finish($old);
3874
4159
  if (pseudoClass) {
3875
4160
  insertionMethod = pseudoClass === 'before' ? 'prepend' : 'append';
@@ -3877,21 +4162,25 @@ are based on this module.
3877
4162
  $old[insertionMethod]($wrapper);
3878
4163
  u.copyAttributes($new, $old);
3879
4164
  elementsInserted($wrapper.children(), options);
3880
- return up.layout.revealOrRestoreScroll($wrapper, options).then(function() {
4165
+ promise = up.layout.revealOrRestoreScroll($wrapper, options);
4166
+ promise = promise.then(function() {
3881
4167
  return up.animate($wrapper, transition, options);
3882
- }).then(function() {
3883
- u.unwrapElement($wrapper);
3884
4168
  });
4169
+ promise = promise.then(function() {
4170
+ return u.unwrapElement($wrapper);
4171
+ });
4172
+ return promise;
3885
4173
  } else {
3886
- return destroy($old, {
3887
- animation: function() {
3888
- $new.insertBefore($old);
3889
- elementsInserted($new, options);
3890
- if ($old.is('body') && transition !== 'none') {
3891
- u.error('Cannot apply transitions to body-elements (%o)', transition);
3892
- }
3893
- return up.morph($old, $new, transition, options);
4174
+ replacement = function() {
4175
+ $new.insertBefore($old);
4176
+ elementsInserted($new, options);
4177
+ if ($old.is('body') && transition !== 'none') {
4178
+ u.error('Cannot apply transitions to body-elements (%o)', transition);
3894
4179
  }
4180
+ return up.morph($old, $new, transition, options);
4181
+ };
4182
+ return destroy($old, {
4183
+ animation: replacement
3895
4184
  });
3896
4185
  }
3897
4186
  };
@@ -3907,6 +4196,7 @@ are based on this module.
3907
4196
  for (i = j = 0, len = disjunction.length; j < len; i = ++j) {
3908
4197
  selectorAtom = disjunction[i];
3909
4198
  selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/);
4199
+ selectorParts || u.error('Could not parse selector atom %o', selectorAtom);
3910
4200
  selector = selectorParts[1];
3911
4201
  if (selector === 'html') {
3912
4202
  selector = 'body';
@@ -4006,7 +4296,7 @@ are based on this module.
4006
4296
  $element: $element
4007
4297
  })) {
4008
4298
  options = u.options(options, {
4009
- animation: 'none'
4299
+ animation: false
4010
4300
  });
4011
4301
  animateOptions = up.motion.animateOptions(options);
4012
4302
  $element.addClass('up-destroying');
@@ -4090,11 +4380,13 @@ are based on this module.
4090
4380
  return setSource(document.body, up.browser.url());
4091
4381
  });
4092
4382
  return {
4383
+ knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0),
4093
4384
  replace: replace,
4094
4385
  reload: reload,
4095
4386
  destroy: destroy,
4096
4387
  implant: implant,
4097
4388
  first: first,
4389
+ source: source,
4098
4390
  resolveSelector: resolveSelector
4099
4391
  };
4100
4392
  })(jQuery);
@@ -4272,9 +4564,8 @@ or [transitions](/up.transition) using Javascript or CSS.
4272
4564
  finish($element);
4273
4565
  options = animateOptions(options);
4274
4566
  if (animation === 'none' || animation === false) {
4275
- none();
4276
- }
4277
- if (u.isFunction(animation)) {
4567
+ return none();
4568
+ } else if (u.isFunction(animation)) {
4278
4569
  return assertIsDeferred(animation($element, options), animation);
4279
4570
  } else if (u.isString(animation)) {
4280
4571
  return animate($element, findAnimation(animation), options);
@@ -4286,7 +4577,7 @@ or [transitions](/up.transition) using Javascript or CSS.
4286
4577
  return u.resolvedDeferred();
4287
4578
  }
4288
4579
  } else {
4289
- return u.error("Unknown animation type %o", animation);
4580
+ return u.error("Unknown animation type for %o", animation);
4290
4581
  }
4291
4582
  };
4292
4583
 
@@ -4453,7 +4744,7 @@ or [transitions](/up.transition) using Javascript or CSS.
4453
4744
  @stable
4454
4745
  */
4455
4746
  morph = function(source, target, transitionOrName, options) {
4456
- var $new, $old, animation, deferred, parsedOptions, parts, transition;
4747
+ var $new, $old, animation, parsedOptions, parts, transition;
4457
4748
  u.debug('Morphing %o to %o (using %o)', source, target, transitionOrName);
4458
4749
  $old = $(source);
4459
4750
  $new = $(target);
@@ -4462,12 +4753,11 @@ or [transitions](/up.transition) using Javascript or CSS.
4462
4753
  if (isEnabled()) {
4463
4754
  finish($old);
4464
4755
  finish($new);
4465
- if (transitionOrName === 'none' || transitionOrName === false || (animation = animations[transitionOrName])) {
4466
- deferred = skipMorph($old, $new, parsedOptions);
4467
- deferred.then(function() {
4468
- return animate($new, animation || 'none', options);
4469
- });
4470
- return deferred;
4756
+ if (transitionOrName === 'none' || transitionOrName === false) {
4757
+ return skipMorph($old, $new, parsedOptions);
4758
+ } else if (animation = animations[transitionOrName]) {
4759
+ skipMorph($old, $new, parsedOptions);
4760
+ return animate($new, animation, parsedOptions);
4471
4761
  } else if (transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]) {
4472
4762
  return withGhosts($old, $new, parsedOptions, function($oldGhost, $newGhost) {
4473
4763
  var transitionPromise;
@@ -4871,7 +5161,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4871
5161
  var slice = [].slice;
4872
5162
 
4873
5163
  up.proxy = (function($) {
4874
- var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isIdempotent, load, loadEnded, loadOrQueue, loadStarted, normalizeRequest, pendingCount, pokeQueue, preload, preloadDelayTimer, queue, queuedRequests, remove, reset, set, startPreloadDelay, u;
5164
+ var $waitingLink, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isIdempotent, load, loadEnded, loadOrQueue, loadStarted, normalizeRequest, pendingCount, pokeQueue, preload, preloadDelayTimer, queue, queuedRequests, remove, reset, set, startPreloadDelay, u;
4875
5165
  u = up.util;
4876
5166
  $waitingLink = void 0;
4877
5167
  preloadDelayTimer = void 0;
@@ -4903,6 +5193,16 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4903
5193
 
4904
5194
  Note that your browser might [impose its own request limit](http://www.browserscope.org/?category=network)
4905
5195
  regardless of what you configure here.
5196
+ @param {Array<String>} [config.wrapMethods]
5197
+ An array of uppercase HTTP method names. AJAX requests with one of these methods
5198
+ will be converted into a `POST` request and carry their original method as a `_method`
5199
+ parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347).
5200
+ @param {String} [config.wrapMethodParam]
5201
+ The name of the POST parameter when wrapping HTTP methods in a `POST` request.
5202
+ @param {Array<String>} [config.safeMethods]
5203
+ An array of uppercase HTTP method names that are considered idempotent.
5204
+ The proxy cache will only cache idempotent requests and will clear the entire
5205
+ cache after a non-idempotent request.
4906
5206
  @stable
4907
5207
  */
4908
5208
  config = u.config({
@@ -4910,11 +5210,14 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4910
5210
  preloadDelay: 75,
4911
5211
  cacheSize: 70,
4912
5212
  cacheExpiry: 1000 * 60 * 5,
4913
- maxRequests: 4
5213
+ maxRequests: 4,
5214
+ wrapMethods: ['PATCH', 'PUT', 'DELETE'],
5215
+ wrapMethodParam: '_method',
5216
+ safeMethods: ['GET', 'OPTIONS', 'HEAD']
4914
5217
  });
4915
5218
  cacheKey = function(request) {
4916
5219
  normalizeRequest(request);
4917
- return [request.url, request.method, request.data, request.selector].join('|');
5220
+ return [request.url, request.method, request.data, request.target].join('|');
4918
5221
  };
4919
5222
  cache = u.cache({
4920
5223
  size: function() {
@@ -4942,14 +5245,14 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4942
5245
  var candidate, candidates, i, len, requestForBody, requestForHtml, response;
4943
5246
  request = normalizeRequest(request);
4944
5247
  candidates = [request];
4945
- if (request.selector !== 'html') {
5248
+ if (request.target !== 'html') {
4946
5249
  requestForHtml = u.merge(request, {
4947
- selector: 'html'
5250
+ target: 'html'
4948
5251
  });
4949
5252
  candidates.push(requestForHtml);
4950
- if (request.selector !== 'body') {
5253
+ if (request.target !== 'body') {
4951
5254
  requestForBody = u.merge(request, {
4952
- selector: 'body'
5255
+ target: 'body'
4953
5256
  });
4954
5257
  candidates.push(requestForBody);
4955
5258
  }
@@ -4968,7 +5271,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4968
5271
  @function up.proxy.set
4969
5272
  @param {String} request.url
4970
5273
  @param {String} [request.method='GET']
4971
- @param {String} [request.selector='body']
5274
+ @param {String} [request.target='body']
4972
5275
  @param {Promise} response
4973
5276
  A promise for the response that is API-compatible with the
4974
5277
  promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
@@ -4985,7 +5288,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
4985
5288
  @function up.proxy.remove
4986
5289
  @param {String} request.url
4987
5290
  @param {String} [request.method='GET']
4988
- @param {String} [request.selector='body']
5291
+ @param {String} [request.target='body']
4989
5292
  @experimental
4990
5293
  */
4991
5294
  remove = cache.remove;
@@ -5026,7 +5329,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5026
5329
  if (request.url) {
5027
5330
  request.url = u.normalizeUrl(request.url);
5028
5331
  }
5029
- request.selector || (request.selector = 'body');
5332
+ request.target || (request.target = 'body');
5030
5333
  request._normalized = true;
5031
5334
  }
5032
5335
  return request;
@@ -5049,13 +5352,15 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5049
5352
  @function up.proxy.ajax
5050
5353
  @param {String} request.url
5051
5354
  @param {String} [request.method='GET']
5052
- @param {String} [request.selector='body']
5355
+ @param {String} [request.target='body']
5053
5356
  @param {Boolean} [request.cache]
5054
5357
  Whether to use a cached response, if available.
5055
5358
  If set to `false` a network connection will always be attempted.
5056
5359
  @param {Object} [request.headers={}]
5057
5360
  An object of additional header key/value pairs to send along
5058
5361
  with the request.
5362
+ @param {Object} [request.data={}]
5363
+ An object of request parameters.
5059
5364
  @return
5060
5365
  A promise for the response that is API-compatible with the
5061
5366
  promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
@@ -5065,7 +5370,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5065
5370
  var forceCache, ignoreCache, pending, promise, request;
5066
5371
  forceCache = options.cache === true;
5067
5372
  ignoreCache = options.cache === false;
5068
- request = u.only(options, 'url', 'method', 'data', 'selector', 'headers', '_normalized');
5373
+ request = u.only(options, 'url', 'method', 'data', 'target', 'headers', '_normalized');
5069
5374
  pending = true;
5070
5375
  if (!isIdempotent(request) && !forceCache) {
5071
5376
  clear();
@@ -5085,7 +5390,6 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5085
5390
  }
5086
5391
  return promise;
5087
5392
  };
5088
- SAFE_HTTP_METHODS = ['GET', 'OPTIONS', 'HEAD'];
5089
5393
 
5090
5394
  /**
5091
5395
  Returns `true` if the proxy is not currently waiting
@@ -5190,9 +5494,20 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5190
5494
  };
5191
5495
  load = function(request) {
5192
5496
  var promise;
5193
- u.debug('Loading URL %o', request.url);
5497
+ u.debug('Fetching %o via %o', request.url, request.method);
5194
5498
  up.emit('up:proxy:load', request);
5195
- promise = u.ajax(request);
5499
+ request = u.copy(request);
5500
+ request.headers || (request.headers = {});
5501
+ request.headers['X-Up-Target'] = request.target;
5502
+ request.data = u.requestDataAsArray(request.data);
5503
+ if (u.contains(config.wrapMethods, request.method)) {
5504
+ request.data.push({
5505
+ name: config.wrapMethodParam,
5506
+ value: request.method
5507
+ });
5508
+ request.method = 'POST';
5509
+ }
5510
+ promise = $.ajax(request);
5196
5511
  promise.always(function() {
5197
5512
  up.emit('up:proxy:received', request);
5198
5513
  return pokeQueue();
@@ -5223,7 +5538,7 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5223
5538
  @event up:proxy:load
5224
5539
  @param event.url
5225
5540
  @param event.method
5226
- @param event.selector
5541
+ @param event.target
5227
5542
  @experimental
5228
5543
  */
5229
5544
 
@@ -5234,12 +5549,12 @@ You can change (or remove) this delay by [configuring `up.proxy`](/up.proxy.conf
5234
5549
  @event up:proxy:received
5235
5550
  @param event.url
5236
5551
  @param event.method
5237
- @param event.selector
5552
+ @param event.target
5238
5553
  @experimental
5239
5554
  */
5240
5555
  isIdempotent = function(request) {
5241
5556
  normalizeRequest(request);
5242
- return u.contains(SAFE_HTTP_METHODS, request.method);
5557
+ return u.contains(config.safeMethods, request.method);
5243
5558
  };
5244
5559
  checkPreload = function($link) {
5245
5560
  var curriedPreload, delay;
@@ -5407,7 +5722,7 @@ Read on
5407
5722
 
5408
5723
  (function() {
5409
5724
  up.link = (function($) {
5410
- var allowDefault, childClicked, follow, followMethod, followVariantSelectors, isFollowable, makeFollowable, registerFollowVariant, shouldProcessLinkEvent, u, visit;
5725
+ var allowDefault, childClicked, follow, followMethod, followVariantSelectors, isFollowable, makeFollowable, onAction, shouldProcessLinkEvent, u, visit;
5411
5726
  u = up.util;
5412
5727
 
5413
5728
  /**
@@ -5456,8 +5771,13 @@ Read on
5456
5771
  or any element that is marked up with an `up-href` attribute.
5457
5772
  @param {String} [options.target]
5458
5773
  The selector to replace.
5459
- Defaults to the `up-target` attribute on `link`,
5460
- or to `body` if such an attribute does not exist.
5774
+ Defaults to the `up-target` attribute on `link`, or to `body` if such an attribute does not exist.
5775
+ @param {String} [options.failTarget]
5776
+ The selector to replace if the server responds with a non-200 status code.
5777
+ Defaults to the `up-fail-target` attribute on `link`, or to `body` if such an attribute does not exist.
5778
+ @param {String} [options.confirm]
5779
+ A message that will be displayed in a cancelable confirmation dialog
5780
+ before the link is followed.
5461
5781
  @param {Function|String} [options.transition]
5462
5782
  A transition function or name.
5463
5783
  @param {Number} [options.duration]
@@ -5484,20 +5804,25 @@ Read on
5484
5804
  @stable
5485
5805
  */
5486
5806
  follow = function(linkOrSelector, options) {
5487
- var $link, selector, url;
5807
+ var $link, target, url;
5488
5808
  $link = $(linkOrSelector);
5489
5809
  options = u.options(options);
5490
5810
  url = u.option($link.attr('up-href'), $link.attr('href'));
5491
- selector = u.option(options.target, $link.attr('up-target'), 'body');
5492
- options.transition = u.option(options.transition, u.castedAttr($link, 'up-transition'), u.castedAttr($link, 'up-animation'));
5811
+ target = u.option(options.target, $link.attr('up-target'), 'body');
5812
+ options.failTarget = u.option(options.failTarget, $link.attr('up-fail-target'), 'body');
5813
+ options.transition = u.option(options.transition, u.castedAttr($link, 'up-transition'), 'none');
5814
+ options.failTransition = u.option(options.failTransition, u.castedAttr($link, 'up-fail-transition'), 'none');
5493
5815
  options.history = u.option(options.history, u.castedAttr($link, 'up-history'));
5494
5816
  options.reveal = u.option(options.reveal, u.castedAttr($link, 'up-reveal'), true);
5495
5817
  options.cache = u.option(options.cache, u.castedAttr($link, 'up-cache'));
5496
5818
  options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($link, 'up-restore-scroll'));
5497
5819
  options.method = followMethod($link, options);
5498
5820
  options.origin = u.option(options.origin, $link);
5821
+ options.confirm = u.option(options.confirm, $link.attr('up-confirm'));
5499
5822
  options = u.merge(options, up.motion.animateOptions(options, $link));
5500
- return up.replace(selector, url, options);
5823
+ return up.browser.confirm(options.confirm).then(function() {
5824
+ return up.replace(target, url, options);
5825
+ });
5501
5826
  };
5502
5827
 
5503
5828
  /**
@@ -5541,7 +5866,7 @@ Read on
5541
5866
  @internal
5542
5867
  */
5543
5868
  allowDefault = function(event) {};
5544
- registerFollowVariant = function(selector, handler) {
5869
+ onAction = function(selector, handler) {
5545
5870
  followVariantSelectors.push(selector);
5546
5871
  up.on('click', "a" + selector + ", [up-href]" + selector, function(event, $link) {
5547
5872
  if (shouldProcessLinkEvent(event, $link)) {
@@ -5635,9 +5960,14 @@ Read on
5635
5960
  @selector a[up-target]
5636
5961
  @param {String} up-target
5637
5962
  The CSS selector to replace
5963
+ @param [up-fail-target='body']
5964
+ The selector to replace if the server responds with a non-200 status code.
5638
5965
  @param {String} [up-href]
5639
5966
  The destination URL to follow.
5640
5967
  If omitted, the the link's `href` attribute will be used.
5968
+ @param {String} [up-confirm]
5969
+ A message that will be displayed in a cancelable confirmation dialog
5970
+ before the link is followed.
5641
5971
  @param {String} [up-reveal='true']
5642
5972
  Whether to reveal the target element within its viewport before updating.
5643
5973
  @param {String} [up-restore-scroll='false']
@@ -5651,7 +5981,7 @@ Read on
5651
5981
  Set this to `'false'` to prevent the current URL from being updated.
5652
5982
  @stable
5653
5983
  */
5654
- registerFollowVariant('[up-target]', function($link) {
5984
+ onAction('[up-target]', function($link) {
5655
5985
  return follow($link);
5656
5986
  });
5657
5987
 
@@ -5699,9 +6029,14 @@ Read on
5699
6029
  opening the destination in a new tab.
5700
6030
 
5701
6031
  @selector a[up-follow]
6032
+ @param [up-fail-target='body']
6033
+ The selector to replace if the server responds with a non-200 status code.
5702
6034
  @param [up-href]
5703
6035
  The destination URL to follow.
5704
6036
  If omitted, the the link's `href` attribute will be used.
6037
+ @param {String} [up-confirm]
6038
+ A message that will be displayed in a cancelable confirmation dialog
6039
+ before the link is followed.
5705
6040
  @param [up-history]
5706
6041
  Set this to `'false'` to prevent the current URL from being updated.
5707
6042
  @param [up-restore-scroll='false']
@@ -5709,7 +6044,7 @@ Read on
5709
6044
  within the response.
5710
6045
  @stable
5711
6046
  */
5712
- registerFollowVariant('[up-follow]', function($link) {
6047
+ onAction('[up-follow]', function($link) {
5713
6048
  return follow($link);
5714
6049
  });
5715
6050
 
@@ -5732,27 +6067,46 @@ Read on
5732
6067
 
5733
6068
  `up-expand` also expands links that open [modals](/up.modal) or [popups](/up.popup).
5734
6069
 
6070
+ \#\#\#\# Elements with multiple contained links
6071
+
6072
+ If a container contains more than one link, you can set the value of the
6073
+ `up-expand` attribute to a CSS selector to define which link should be expanded:
6074
+
6075
+ <div class="notification" up-expand=".close">
6076
+ Record was saved!
6077
+ <a class="details" href="/records/5">Details</a>
6078
+ <a class="close" href="/records">Close</a>
6079
+ </div>
6080
+
5735
6081
  @selector [up-expand]
6082
+ @param {String} [up-expand]
6083
+ A CSS selector that defines which containing link should be expanded.
6084
+
6085
+ If omitted, the first contained link will be expanded.
5736
6086
  @stable
5737
6087
  */
5738
6088
  up.compiler('[up-expand]', function($area) {
5739
- var attribute, i, len, link, name, newAttrs, ref, upAttributePattern;
5740
- link = $area.find('a, [up-href]').get(0);
5741
- link || u.error('No link to expand within %o', $area);
5742
- upAttributePattern = /^up-/;
5743
- newAttrs = {};
5744
- newAttrs['up-href'] = $(link).attr('href');
5745
- ref = link.attributes;
5746
- for (i = 0, len = ref.length; i < len; i++) {
5747
- attribute = ref[i];
5748
- name = attribute.name;
5749
- if (name.match(upAttributePattern)) {
5750
- newAttrs[name] = attribute.value;
6089
+ var $childLinks, attribute, i, len, link, name, newAttrs, ref, selector, upAttributePattern;
6090
+ $childLinks = $area.find('a, [up-href]');
6091
+ if (selector = $area.attr('up-expand')) {
6092
+ $childLinks = $childLinks.filter(selector);
6093
+ }
6094
+ if (link = $childLinks.get(0)) {
6095
+ upAttributePattern = /^up-/;
6096
+ newAttrs = {};
6097
+ newAttrs['up-href'] = $(link).attr('href');
6098
+ ref = link.attributes;
6099
+ for (i = 0, len = ref.length; i < len; i++) {
6100
+ attribute = ref[i];
6101
+ name = attribute.name;
6102
+ if (name.match(upAttributePattern)) {
6103
+ newAttrs[name] = attribute.value;
6104
+ }
5751
6105
  }
6106
+ u.setMissingAttrs($area, newAttrs);
6107
+ $area.removeAttr('up-expand');
6108
+ return makeFollowable($area);
5752
6109
  }
5753
- u.setMissingAttrs($area, newAttrs);
5754
- $area.removeAttr('up-expand');
5755
- return makeFollowable($area);
5756
6110
  });
5757
6111
 
5758
6112
  /**
@@ -5797,7 +6151,7 @@ Read on
5797
6151
  shouldProcessLinkEvent: shouldProcessLinkEvent,
5798
6152
  childClicked: childClicked,
5799
6153
  followMethod: followMethod,
5800
- registerFollowVariant: registerFollowVariant
6154
+ onAction: onAction
5801
6155
  };
5802
6156
  })(jQuery);
5803
6157
 
@@ -5822,7 +6176,7 @@ open dialogs with sub-forms, etc. all without losing form state.
5822
6176
  var slice = [].slice;
5823
6177
 
5824
6178
  up.form = (function($) {
5825
- var autosubmit, config, observe, observeForm, reset, resolveValidateTarget, submit, u, validate;
6179
+ var autosubmit, config, currentValuesForToggle, observe, observeForm, reset, resolveValidateTarget, submit, toggleTargets, u, validate;
5826
6180
  u = up.util;
5827
6181
 
5828
6182
  /**
@@ -5924,77 +6278,41 @@ open dialogs with sub-forms, etc. all without losing form state.
5924
6278
  @stable
5925
6279
  */
5926
6280
  submit = function(formOrSelector, options) {
5927
- var $form, failureSelector, failureTransition, hasFileInputs, headers, historyOption, httpMethod, implantOptions, request, successSelector, successTransition, successUrl, url, useCache;
6281
+ var $form, hasFileInputs, promise, target, url;
5928
6282
  $form = $(formOrSelector).closest('form');
5929
6283
  options = u.options(options);
5930
- successSelector = u.option(options.target, $form.attr('up-target'), 'body');
5931
- successSelector = up.flow.resolveSelector(successSelector, options);
5932
- failureSelector = u.option(options.failTarget, $form.attr('up-fail-target')) || u.selectorForElement($form);
5933
- failureSelector = up.flow.resolveSelector(failureSelector, options);
5934
- historyOption = u.option(options.history, u.castedAttr($form, 'up-history'), true);
5935
- successTransition = u.option(options.transition, u.castedAttr($form, 'up-transition'));
5936
- failureTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), successTransition);
5937
- httpMethod = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase();
5938
- headers = u.option(options.headers, {});
5939
- implantOptions = {};
5940
- implantOptions.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true);
5941
- implantOptions.cache = u.option(options.cache, u.castedAttr($form, 'up-cache'));
5942
- implantOptions.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll'));
5943
- implantOptions.origin = u.option(options.origin, $form);
5944
- implantOptions = u.extend(implantOptions, up.motion.animateOptions(options, $form));
5945
- useCache = u.option(options.cache, u.castedAttr($form, 'up-cache'));
6284
+ target = u.option(options.target, $form.attr('up-target'), 'body');
5946
6285
  url = u.option(options.url, $form.attr('action'), up.browser.url());
6286
+ options.failTarget = u.option(options.failTarget, $form.attr('up-fail-target')) || u.selectorForElement($form);
6287
+ options.history = u.option(options.history, u.castedAttr($form, 'up-history'), true);
6288
+ options.transition = u.option(options.transition, u.castedAttr($form, 'up-transition'), 'none');
6289
+ options.failTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), 'none');
6290
+ options.method = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase();
6291
+ options.headers = u.option(options.headers, {});
6292
+ options.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true);
6293
+ options.cache = u.option(options.cache, u.castedAttr($form, 'up-cache'));
6294
+ options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll'));
6295
+ options.origin = u.option(options.origin, $form);
6296
+ options.data = $form.serializeArray();
6297
+ options = u.merge(options, up.motion.animateOptions(options, $form));
5947
6298
  hasFileInputs = $form.find('input[type=file]').length;
5948
6299
  if (options.validate) {
5949
- headers['X-Up-Validate'] = options.validate;
6300
+ options.headers || (options.headers = {});
6301
+ options.headers['X-Up-Validate'] = options.validate;
5950
6302
  if (hasFileInputs) {
5951
6303
  return u.unresolvablePromise();
5952
6304
  }
5953
6305
  }
5954
6306
  $form.addClass('up-active');
5955
- if (hasFileInputs || (!up.browser.canPushState() && historyOption !== false)) {
6307
+ if (hasFileInputs || (!up.browser.canPushState() && options.history !== false)) {
5956
6308
  $form.get(0).submit();
5957
6309
  return u.unresolvablePromise();
5958
6310
  }
5959
- request = {
5960
- url: url,
5961
- method: httpMethod,
5962
- data: $form.serialize(),
5963
- selector: successSelector,
5964
- cache: useCache,
5965
- headers: headers
5966
- };
5967
- successUrl = function(xhr) {
5968
- var currentLocation;
5969
- url = void 0;
5970
- if (u.isGiven(historyOption)) {
5971
- if (historyOption === false || u.isString(historyOption)) {
5972
- url = historyOption;
5973
- } else if (currentLocation = u.locationFromXhr(xhr)) {
5974
- url = currentLocation;
5975
- } else if (request.type === 'GET') {
5976
- url = request.url + '?' + request.data;
5977
- }
5978
- }
5979
- return u.option(url, false);
5980
- };
5981
- return up.proxy.ajax(request).always(function() {
6311
+ promise = up.replace(target, url, options);
6312
+ promise.always(function() {
5982
6313
  return $form.removeClass('up-active');
5983
- }).done(function(html, textStatus, xhr) {
5984
- var successOptions;
5985
- successOptions = u.merge(implantOptions, {
5986
- history: successUrl(xhr),
5987
- transition: successTransition
5988
- });
5989
- return up.flow.implant(successSelector, html, successOptions);
5990
- }).fail(function(xhr, textStatus, errorThrown) {
5991
- var failureOptions, html;
5992
- html = xhr.responseText;
5993
- failureOptions = u.merge(implantOptions, {
5994
- transition: failureTransition
5995
- });
5996
- return up.flow.implant(failureSelector, html, failureOptions);
5997
6314
  });
6315
+ return promise;
5998
6316
  };
5999
6317
 
6000
6318
  /**
@@ -6172,7 +6490,6 @@ open dialogs with sub-forms, etc. all without losing form state.
6172
6490
  @stable
6173
6491
  */
6174
6492
  autosubmit = function(selectorOrElement, options) {
6175
- console.log("autosubmit %o", selectorOrElement);
6176
6493
  return observe(selectorOrElement, options, function(value, $field) {
6177
6494
  var $form;
6178
6495
  $form = $field.closest('form');
@@ -6188,7 +6505,7 @@ open dialogs with sub-forms, etc. all without losing form state.
6188
6505
  if (u.isBlank(target)) {
6189
6506
  target || (target = u.detect(config.validateTargets, function(defaultTarget) {
6190
6507
  var resolvedDefault;
6191
- resolvedDefault = up.flow.resolveSelector(defaultTarget, options);
6508
+ resolvedDefault = up.flow.resolveSelector(defaultTarget, options.origin);
6192
6509
  return $field.closest(resolvedDefault).length;
6193
6510
  }));
6194
6511
  }
@@ -6240,6 +6557,109 @@ open dialogs with sub-forms, etc. all without losing form state.
6240
6557
  promise = up.submit($form, options);
6241
6558
  return promise;
6242
6559
  };
6560
+ currentValuesForToggle = function($field) {
6561
+ var $checkedButton, value, values;
6562
+ values = void 0;
6563
+ if ($field.is('input[type=checkbox]')) {
6564
+ if ($field.is(':checked')) {
6565
+ values = [':checked', ':present', $field.val()];
6566
+ } else {
6567
+ values = [':unchecked', ':blank'];
6568
+ }
6569
+ } else if ($field.is('input[type=radio]')) {
6570
+ console.log('-- it is a radio button --');
6571
+ $checkedButton = $field.closest('form, body').find("input[type='radio'][name='" + ($field.attr('name')) + "']:checked");
6572
+ console.log('checked button is %o', $checkedButton);
6573
+ console.log('checked button val is %o', $checkedButton.val());
6574
+ if ($checkedButton.length) {
6575
+ values = [':checked', ':present', $checkedButton.val()];
6576
+ } else {
6577
+ values = [':unchecked', ':blank'];
6578
+ }
6579
+ } else {
6580
+ console.log('-- else -- for %o', $field);
6581
+ value = $field.val();
6582
+ if (u.isPresent(value)) {
6583
+ values = [':present', value];
6584
+ } else {
6585
+ values = [':blank'];
6586
+ }
6587
+ }
6588
+ return values;
6589
+ };
6590
+ currentValuesForToggle = function($field) {
6591
+ var $checkedButton, meta, value, values;
6592
+ if ($field.is('input[type=checkbox]')) {
6593
+ if ($field.is(':checked')) {
6594
+ value = $field.val();
6595
+ meta = ':checked';
6596
+ } else {
6597
+ meta = ':unchecked';
6598
+ }
6599
+ } else if ($field.is('input[type=radio]')) {
6600
+ $checkedButton = $field.closest('form, body').find("input[type='radio'][name='" + ($field.attr('name')) + "']:checked");
6601
+ if ($checkedButton.length) {
6602
+ meta = ':checked';
6603
+ value = $checkedButton.val();
6604
+ } else {
6605
+ meta = ':unchecked';
6606
+ }
6607
+ } else {
6608
+ value = $field.val();
6609
+ }
6610
+ values = [];
6611
+ if (u.isPresent(value)) {
6612
+ values.push(value);
6613
+ values.push(':present');
6614
+ } else {
6615
+ values.push(':blank');
6616
+ }
6617
+ if (u.isPresent(meta)) {
6618
+ values.push(meta);
6619
+ }
6620
+ return values;
6621
+ };
6622
+
6623
+ /**
6624
+ Shows or hides a target selector depending on the value.
6625
+
6626
+ See [`[up-toggle]`](/up-toggle) for more documentation and examples.
6627
+
6628
+ This function does not currently have a very useful API outside
6629
+ of our use for `up-toggle`'s UJS behavior, that's why it's currently
6630
+ still marked `@internal`.
6631
+
6632
+ @function up.form.toggle
6633
+ @param {String|Element|jQuery} fieldOrSelector
6634
+ @param {String} [options.target]
6635
+ The target selectors to toggle.
6636
+ Defaults to an `up-toggle` attribute on the given field.
6637
+ @internal
6638
+ */
6639
+ toggleTargets = function(fieldOrSelector, options) {
6640
+ var $field, fieldValues, targets;
6641
+ $field = $(fieldOrSelector);
6642
+ options = u.options(options);
6643
+ targets = u.option(options.target, $field.attr('up-toggle'));
6644
+ u.isPresent(targets) || u.error("No toggle target given for %o", $field);
6645
+ fieldValues = currentValuesForToggle($field);
6646
+ return $(targets).each(function() {
6647
+ var $target, hideValues, show, showValues;
6648
+ $target = $(this);
6649
+ if (hideValues = $target.attr('up-hide-for')) {
6650
+ hideValues = hideValues.split(' ');
6651
+ show = u.intersect(fieldValues, hideValues).length === 0;
6652
+ } else {
6653
+ if (showValues = $target.attr('up-show-for')) {
6654
+ showValues = showValues.split(' ');
6655
+ } else {
6656
+ showValues = [':present', ':checked'];
6657
+ }
6658
+ show = u.intersect(fieldValues, showValues).length > 0;
6659
+ }
6660
+ return $target.toggle(show);
6661
+ });
6662
+ };
6243
6663
 
6244
6664
  /**
6245
6665
  Forms with an `up-target` attribute are [submitted via AJAX](/up.submit)
@@ -6294,7 +6714,7 @@ open dialogs with sub-forms, etc. all without losing form state.
6294
6714
 
6295
6715
  When the form's action performs a redirect, the server should echo
6296
6716
  the new request's URL as a response header `X-Up-Location`
6297
- and the request's HTTP method as `X-Up-Method`.
6717
+ and the request's HTTP method as `X-Up-Method: GET`.
6298
6718
 
6299
6719
  If you are using Up.js via the `upjs-rails` gem, these headers
6300
6720
  are set automatically for every request.
@@ -6490,6 +6910,83 @@ open dialogs with sub-forms, etc. all without losing form state.
6490
6910
  return validate($field);
6491
6911
  });
6492
6912
 
6913
+ /**
6914
+ Show or hide part of a form if certain options are selected or boxes are checked.
6915
+
6916
+ \#\#\#\# Example
6917
+
6918
+ The triggering input gets an `up-toggle` attribute with a selector for the elements to show or hide:
6919
+
6920
+ <select name="advancedness" up-toggle=".target">
6921
+ <option value="basic">Basic parts</option>
6922
+ <option value="advanced">Advanced parts</option>
6923
+ <option value="very-advanced">Very advanced parts</option>
6924
+ </select>
6925
+
6926
+ The target elements get a space-separated list of select values for which they are shown or hidden:
6927
+
6928
+ <div class="target" up-show-for="basic">
6929
+ only shown for advancedness = basic
6930
+ </div>
6931
+
6932
+ <div class="target" up-hide-for="basic">
6933
+ hidden for advancedness = basic
6934
+ </div>
6935
+
6936
+ <div class="target" up-show-for="advanced very-advanced">
6937
+ shown for advancedness = advanced or very-advanced
6938
+ </div>
6939
+
6940
+ For checkboxes you can also use the pseudo-values `:checked` or `:unchecked` like so:
6941
+
6942
+ <input type="checkbox" name="flag" up-toggle=".target">
6943
+
6944
+ <div class="target" up-show-for=":checked">
6945
+ only shown when checkbox is checked
6946
+ </div>
6947
+
6948
+ You can also use the pseudo-values `:blank` to match an empty input value,
6949
+ or `:present` to match a non-empty input value:
6950
+
6951
+ <input type="text" name="email" up-toggle=".target">
6952
+
6953
+ <div class="target" up-show-for=":blank">
6954
+ please enter an email address
6955
+ </div>
6956
+
6957
+ @selector [up-toggle]
6958
+ @stable
6959
+ */
6960
+
6961
+ /**
6962
+ Show this element only if a form field has a given value.
6963
+
6964
+ See [`[up-toggle]`](/up-toggle) for more documentation and examples.
6965
+
6966
+ @selector [up-show-for]
6967
+ @param up-show-for
6968
+ A space-separated list of values for which to show this element.
6969
+ @stable
6970
+ */
6971
+
6972
+ /**
6973
+ Hide this element if a form field has a given value.
6974
+
6975
+ See [`[up-toggle]`](/up-toggle) for more documentation and examples.
6976
+
6977
+ @selector [up-hide-for]
6978
+ @param up-hide-for
6979
+ A space-separated list of values for which to show this element.
6980
+ @stable
6981
+ */
6982
+ up.on('change', '[up-toggle]', function(event, $field) {
6983
+ console.log("CHANGE EVENT");
6984
+ return toggleTargets($field);
6985
+ });
6986
+ up.compiler('[up-toggle]', function($field) {
6987
+ return toggleTargets($field);
6988
+ });
6989
+
6493
6990
  /**
6494
6991
  Observes this field or form and runs a callback when a value changes.
6495
6992
 
@@ -6558,7 +7055,8 @@ open dialogs with sub-forms, etc. all without losing form state.
6558
7055
  config: config,
6559
7056
  submit: submit,
6560
7057
  observe: observe,
6561
- validate: validate
7058
+ validate: validate,
7059
+ toggleTargets: toggleTargets
6562
7060
  };
6563
7061
  })(jQuery);
6564
7062
 
@@ -6619,7 +7117,7 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6619
7117
 
6620
7118
  (function() {
6621
7119
  up.popup = (function($) {
6622
- var attach, autoclose, close, config, contains, coveredUrl, createHiddenPopup, currentUrl, discardHistory, ensureInViewport, rememberHistory, reset, setPosition, u, updated;
7120
+ var attach, autoclose, close, config, contains, coveredUrl, createFrame, currentUrl, discardHistory, ensureInViewport, isOpen, reset, setPosition, u;
6623
7121
  u = up.util;
6624
7122
 
6625
7123
  /**
@@ -6641,9 +7139,7 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6641
7139
  @experimental
6642
7140
  */
6643
7141
  coveredUrl = function() {
6644
- var $popup;
6645
- $popup = $('.up-popup');
6646
- return $popup.attr('up-covered-url');
7142
+ return $('.up-popup').attr('up-covered-url');
6647
7143
  };
6648
7144
 
6649
7145
  /**
@@ -6669,11 +7165,13 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6669
7165
  history: false
6670
7166
  });
6671
7167
  reset = function() {
6672
- close();
7168
+ close({
7169
+ animation: false
7170
+ });
6673
7171
  return config.reset();
6674
7172
  };
6675
- setPosition = function($link, $popup, position) {
6676
- var css, linkBox;
7173
+ setPosition = function($link, position) {
7174
+ var $popup, css, linkBox;
6677
7175
  linkBox = u.measure($link, {
6678
7176
  full: true
6679
7177
  });
@@ -6703,6 +7201,7 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6703
7201
  return u.error("Unknown position %o", position);
6704
7202
  }
6705
7203
  })();
7204
+ $popup = $('.up-popup');
6706
7205
  $popup.attr('up-position', position);
6707
7206
  $popup.css(css);
6708
7207
  return ensureInViewport($popup);
@@ -6741,42 +7240,34 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6741
7240
  }
6742
7241
  }
6743
7242
  };
6744
- rememberHistory = function() {
6745
- var $popup;
6746
- $popup = $('.up-popup');
6747
- $popup.attr('up-covered-url', up.browser.url());
6748
- return $popup.attr('up-covered-title', document.title);
6749
- };
6750
7243
  discardHistory = function() {
6751
7244
  var $popup;
6752
7245
  $popup = $('.up-popup');
6753
7246
  $popup.removeAttr('up-covered-url');
6754
7247
  return $popup.removeAttr('up-covered-title');
6755
7248
  };
6756
- createHiddenPopup = function($link, selector, sticky) {
7249
+ createFrame = function(target, options) {
6757
7250
  var $placeholder, $popup;
6758
7251
  $popup = u.$createElementFromSelector('.up-popup');
6759
- if (sticky) {
7252
+ if (options.sticky) {
6760
7253
  $popup.attr('up-sticky', '');
6761
7254
  }
6762
- $placeholder = u.$createElementFromSelector(selector);
7255
+ $popup.attr('up-covered-url', up.browser.url());
7256
+ $popup.attr('up-covered-title', document.title);
7257
+ $placeholder = u.$createElementFromSelector(target);
6763
7258
  $placeholder.appendTo($popup);
6764
7259
  $popup.appendTo(document.body);
6765
- rememberHistory();
6766
- $popup.hide();
6767
7260
  return $popup;
6768
7261
  };
6769
- updated = function($link, position, animation, animateOptions) {
6770
- var $popup, deferred;
6771
- $popup = $('.up-popup');
6772
- if ($popup.is(':hidden')) {
6773
- $popup.show();
6774
- setPosition($link, $popup, position);
6775
- deferred = up.animate($popup, animation, animateOptions);
6776
- return deferred.then(function() {
6777
- return up.emit('up:popup:opened');
6778
- });
6779
- }
7262
+
7263
+ /**
7264
+ Returns whether popup modal is currently open.
7265
+
7266
+ @function up.popup.isOpen
7267
+ @stable
7268
+ */
7269
+ isOpen = function() {
7270
+ return $('.up-popup').length > 0;
6780
7271
  };
6781
7272
 
6782
7273
  /**
@@ -6791,6 +7282,9 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6791
7282
  Defines where the popup is attached to the opening element.
6792
7283
 
6793
7284
  Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`.
7285
+ @param {String} [options.confirm]
7286
+ A message that will be displayed in a cancelable confirmation dialog
7287
+ before the modal is being opened.
6794
7288
  @param {String} [options.animation]
6795
7289
  The animation to use when opening the popup.
6796
7290
  @param {Number} [options.duration]
@@ -6804,37 +7298,56 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6804
7298
  open even if the page changes in the background.
6805
7299
  @param {Object} [options.history=false]
6806
7300
  @return {Promise}
6807
- A promise that will be resolved when the popup has been loaded and rendered.
7301
+ A promise that will be resolved when the popup has been loaded and
7302
+ the opening animation has completed.
6808
7303
  @stable
6809
7304
  */
6810
7305
  attach = function(linkOrSelector, options) {
6811
- var $link, animateOptions, animation, history, position, promise, selector, sticky, url;
7306
+ var $link, animateOptions, target, url;
6812
7307
  $link = $(linkOrSelector);
6813
7308
  $link.length || u.error('Cannot attach popup to non-existing element %o', linkOrSelector);
6814
7309
  options = u.options(options);
6815
7310
  url = u.option(options.url, $link.attr('href'));
6816
- selector = u.option(options.target, $link.attr('up-popup'), 'body');
6817
- position = u.option(options.position, $link.attr('up-position'), config.position);
6818
- animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
6819
- sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
6820
- history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), config.history) : false;
7311
+ target = u.option(options.target, $link.attr('up-popup'), 'body');
7312
+ options.position = u.option(options.position, $link.attr('up-position'), config.position);
7313
+ options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
7314
+ options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
7315
+ options.history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), config.history) : false;
7316
+ options.confirm = u.option(options.confirm, $link.attr('up-confirm'));
6821
7317
  animateOptions = up.motion.animateOptions(options, $link);
6822
- close();
6823
- if (up.bus.nobodyPrevents('up:popup:open', {
6824
- url: url
6825
- })) {
6826
- createHiddenPopup($link, selector, sticky);
6827
- promise = up.replace(selector, url, {
6828
- history: history,
6829
- requireMatch: false
6830
- });
6831
- promise.then(function() {
6832
- return updated($link, position, animation, animateOptions);
6833
- });
6834
- return promise;
6835
- } else {
6836
- return u.unresolvableDeferred();
6837
- }
7318
+ return up.browser.confirm(options.confirm).then(function() {
7319
+ var promise, wasOpen;
7320
+ if (up.bus.nobodyPrevents('up:popup:open', {
7321
+ url: url
7322
+ })) {
7323
+ wasOpen = isOpen();
7324
+ if (wasOpen) {
7325
+ close({
7326
+ animation: false
7327
+ });
7328
+ }
7329
+ options.beforeSwap = function() {
7330
+ return createFrame(target, options);
7331
+ };
7332
+ promise = up.replace(target, url, u.merge(options, {
7333
+ animation: false
7334
+ }));
7335
+ promise = promise.then(function() {
7336
+ return setPosition($link, options.position);
7337
+ });
7338
+ if (!wasOpen) {
7339
+ promise = promise.then(function() {
7340
+ return up.animate($('.up-popup'), options.animation, animateOptions);
7341
+ });
7342
+ }
7343
+ promise = promise.then(function() {
7344
+ return up.emit('up:popup:opened');
7345
+ });
7346
+ return promise;
7347
+ } else {
7348
+ return u.unresolvableDeferred();
7349
+ }
7350
+ });
6838
7351
  };
6839
7352
 
6840
7353
  /**
@@ -6948,12 +7461,15 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
6948
7461
  Defines where the popup is attached to the opening element.
6949
7462
 
6950
7463
  Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`.
7464
+ @param {String} [up-confirm]
7465
+ A message that will be displayed in a cancelable confirmation dialog
7466
+ before the popup is opened.
6951
7467
  @param [up-sticky]
6952
7468
  If set to `true`, the popup remains
6953
7469
  open even if the page changes in the background.
6954
7470
  @stable
6955
7471
  */
6956
- up.link.registerFollowVariant('[up-popup]', function($link) {
7472
+ up.link.onAction('[up-popup]', function($link) {
6957
7473
  if ($link.is('.up-current')) {
6958
7474
  return close();
6959
7475
  } else {
@@ -7020,7 +7536,8 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7020
7536
  },
7021
7537
  source: function() {
7022
7538
  return up.error('up.popup.source no longer exists. Please use up.popup.url instead.');
7023
- }
7539
+ },
7540
+ isOpen: isOpen
7024
7541
  };
7025
7542
  })(jQuery);
7026
7543
 
@@ -7086,7 +7603,7 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7086
7603
 
7087
7604
  (function() {
7088
7605
  up.modal = (function($) {
7089
- var autoclose, close, config, contains, coveredUrl, createHiddenModal, currentUrl, discardHistory, follow, open, rememberHistory, reset, shiftElements, templateHtml, u, unshiftElements, updated, visit;
7606
+ var autoclose, close, config, contains, coveredUrl, createFrame, currentUrl, discardHistory, follow, isOpen, open, reset, shiftElements, templateHtml, u, unshiftElements, unshifters, visit;
7090
7607
  u = up.util;
7091
7608
 
7092
7609
  /**
@@ -7161,12 +7678,12 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7161
7678
  @experimental
7162
7679
  */
7163
7680
  coveredUrl = function() {
7164
- var $modal;
7165
- $modal = $('.up-modal');
7166
- return $modal.attr('up-covered-url');
7681
+ return $('.up-modal').attr('up-covered-url');
7167
7682
  };
7168
7683
  reset = function() {
7169
- close();
7684
+ close({
7685
+ animation: false
7686
+ });
7170
7687
  currentUrl = void 0;
7171
7688
  return config.reset();
7172
7689
  };
@@ -7179,20 +7696,15 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7179
7696
  return template;
7180
7697
  }
7181
7698
  };
7182
- rememberHistory = function() {
7183
- var $modal;
7184
- $modal = $('.up-modal');
7185
- $modal.attr('up-covered-url', up.browser.url());
7186
- return $modal.attr('up-covered-title', document.title);
7187
- };
7188
7699
  discardHistory = function() {
7189
7700
  var $modal;
7190
7701
  $modal = $('.up-modal');
7191
7702
  $modal.removeAttr('up-covered-url');
7192
7703
  return $modal.removeAttr('up-covered-title');
7193
7704
  };
7194
- createHiddenModal = function(options) {
7705
+ createFrame = function(target, options) {
7195
7706
  var $content, $dialog, $modal, $placeholder;
7707
+ shiftElements();
7196
7708
  $modal = $(templateHtml());
7197
7709
  if (options.sticky) {
7198
7710
  $modal.attr('up-sticky', '');
@@ -7210,14 +7722,12 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7210
7722
  $dialog.css('height', options.height);
7211
7723
  }
7212
7724
  $content = $modal.find('.up-modal-content');
7213
- $placeholder = u.$createElementFromSelector(options.selector);
7725
+ $placeholder = u.$createElementFromSelector(target);
7214
7726
  $placeholder.appendTo($content);
7215
7727
  $modal.appendTo(document.body);
7216
- rememberHistory();
7217
- $modal.hide();
7218
7728
  return $modal;
7219
7729
  };
7220
- unshiftElements = [];
7730
+ unshifters = [];
7221
7731
  shiftElements = function() {
7222
7732
  var bodyRightPadding, bodyRightShift, scrollbarWidth, unshiftBody;
7223
7733
  scrollbarWidth = u.scrollbarWidth();
@@ -7227,29 +7737,35 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7227
7737
  'padding-right': bodyRightShift + "px",
7228
7738
  'overflow-y': 'hidden'
7229
7739
  });
7230
- unshiftElements.push(unshiftBody);
7740
+ unshifters.push(unshiftBody);
7231
7741
  return up.layout.anchoredRight().each(function() {
7232
- var $element, elementRight, elementRightShift, unshiftElement;
7742
+ var $element, elementRight, elementRightShift, unshifter;
7233
7743
  $element = $(this);
7234
7744
  elementRight = parseInt($element.css('right'));
7235
7745
  elementRightShift = scrollbarWidth + elementRight;
7236
- unshiftElement = u.temporaryCss($element, {
7746
+ unshifter = u.temporaryCss($element, {
7237
7747
  'right': elementRightShift
7238
7748
  });
7239
- return unshiftElements.push(unshiftElement);
7749
+ return unshifters.push(unshifter);
7240
7750
  });
7241
7751
  };
7242
- updated = function(animation, animateOptions) {
7243
- var $modal, deferred;
7244
- $modal = $('.up-modal');
7245
- if ($modal.is(':hidden')) {
7246
- shiftElements();
7247
- $modal.show();
7248
- deferred = up.animate($modal, animation, animateOptions);
7249
- return deferred.then(function() {
7250
- return up.emit('up:modal:opened');
7251
- });
7752
+ unshiftElements = function() {
7753
+ var results, unshifter;
7754
+ results = [];
7755
+ while (unshifter = unshifters.pop()) {
7756
+ results.push(unshifter());
7252
7757
  }
7758
+ return results;
7759
+ };
7760
+
7761
+ /**
7762
+ Returns whether a modal is currently open.
7763
+
7764
+ @function up.modal.isOpen
7765
+ @stable
7766
+ */
7767
+ isOpen = function() {
7768
+ return $('.up-modal').length > 0;
7253
7769
  };
7254
7770
 
7255
7771
  /**
@@ -7276,6 +7792,9 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7276
7792
  @param {Boolean} [options.sticky=false]
7277
7793
  If set to `true`, the modal remains
7278
7794
  open even if the page changes in the background.
7795
+ @param {String} [options.confirm]
7796
+ A message that will be displayed in a cancelable confirmation dialog
7797
+ before the modal is being opened.
7279
7798
  @param {Object} [options.history=true]
7280
7799
  Whether to add a browser history entry for the modal's source URL.
7281
7800
  @param {String} [options.animation]
@@ -7287,7 +7806,8 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7287
7806
  @param {String} [options.easing]
7288
7807
  The timing function that controls the animation's acceleration. [`up.animate`](/up.animate).
7289
7808
  @return {Promise}
7290
- A promise that will be resolved when the popup has been loaded and rendered.
7809
+ A promise that will be resolved when the modal has been loaded and
7810
+ the opening animation has completed.
7291
7811
  @stable
7292
7812
  */
7293
7813
  follow = function(linkOrSelector, options) {
@@ -7317,7 +7837,8 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7317
7837
  @param {Object} options
7318
7838
  See options for [`up.modal.follow`](/up.modal.follow).
7319
7839
  @return {Promise}
7320
- A promise that will be resolved when the popup has been loaded and rendered.
7840
+ A promise that will be resolved when the modal has been loaded and the opening
7841
+ animation has completed.
7321
7842
  @stable
7322
7843
  */
7323
7844
  visit = function(url, options) {
@@ -7331,40 +7852,49 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7331
7852
  @internal
7332
7853
  */
7333
7854
  open = function(options) {
7334
- var $link, animateOptions, animation, height, history, maxWidth, promise, selector, sticky, url, width;
7855
+ var $link, animateOptions, target, url;
7335
7856
  options = u.options(options);
7336
7857
  $link = u.option(options.$link, u.nullJQuery());
7337
7858
  url = u.option(options.url, $link.attr('up-href'), $link.attr('href'));
7338
- selector = u.option(options.target, $link.attr('up-modal'), 'body');
7339
- width = u.option(options.width, $link.attr('up-width'), config.width);
7340
- maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), config.maxWidth);
7341
- height = u.option(options.height, $link.attr('up-height'), config.height);
7342
- animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
7343
- sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
7344
- history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), config.history) : false;
7859
+ target = u.option(options.target, $link.attr('up-modal'), 'body');
7860
+ options.width = u.option(options.width, $link.attr('up-width'), config.width);
7861
+ options.maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), config.maxWidth);
7862
+ options.height = u.option(options.height, $link.attr('up-height'), config.height);
7863
+ options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
7864
+ options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
7865
+ options.history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), config.history) : false;
7866
+ options.confirm = u.option(options.confirm, $link.attr('up-confirm'));
7345
7867
  animateOptions = up.motion.animateOptions(options, $link);
7346
- close();
7347
- if (up.bus.nobodyPrevents('up:modal:open', {
7348
- url: url
7349
- })) {
7350
- createHiddenModal({
7351
- selector: selector,
7352
- width: width,
7353
- maxWidth: maxWidth,
7354
- height: height,
7355
- sticky: sticky
7356
- });
7357
- promise = up.replace(selector, url, {
7358
- history: history,
7359
- requireMatch: false
7360
- });
7361
- promise.then(function() {
7362
- return updated(animation, animateOptions);
7363
- });
7364
- return promise;
7365
- } else {
7366
- return u.unresolvableDeferred();
7367
- }
7868
+ return up.browser.confirm(options.confirm).then(function() {
7869
+ var promise, wasOpen;
7870
+ if (up.bus.nobodyPrevents('up:modal:open', {
7871
+ url: url
7872
+ })) {
7873
+ wasOpen = isOpen();
7874
+ if (wasOpen) {
7875
+ close({
7876
+ animation: false
7877
+ });
7878
+ }
7879
+ options.beforeSwap = function() {
7880
+ return createFrame(target, options);
7881
+ };
7882
+ promise = up.replace(target, url, u.merge(options, {
7883
+ animation: false
7884
+ }));
7885
+ if (!wasOpen) {
7886
+ promise = promise.then(function() {
7887
+ return up.animate($('.up-modal'), options.animation, animateOptions);
7888
+ });
7889
+ }
7890
+ promise = promise.then(function() {
7891
+ return up.emit('up:modal:opened');
7892
+ });
7893
+ return promise;
7894
+ } else {
7895
+ return u.unresolvablePromise();
7896
+ }
7897
+ });
7368
7898
  };
7369
7899
 
7370
7900
  /**
@@ -7399,7 +7929,7 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7399
7929
  @stable
7400
7930
  */
7401
7931
  close = function(options) {
7402
- var $modal, deferred;
7932
+ var $modal, promise;
7403
7933
  $modal = $('.up-modal');
7404
7934
  if ($modal.length) {
7405
7935
  if (up.bus.nobodyPrevents('up:modal:close', {
@@ -7411,15 +7941,12 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7411
7941
  title: $modal.attr('up-covered-title')
7412
7942
  });
7413
7943
  currentUrl = void 0;
7414
- deferred = up.destroy($modal, options);
7415
- deferred.then(function() {
7416
- var unshifter;
7417
- while (unshifter = unshiftElements.pop()) {
7418
- unshifter();
7419
- }
7944
+ promise = up.destroy($modal, options);
7945
+ promise = promise.then(function() {
7946
+ unshiftElements();
7420
7947
  return up.emit('up:modal:closed');
7421
7948
  });
7422
- return deferred;
7949
+ return promise;
7423
7950
  } else {
7424
7951
  return u.unresolvableDeferred();
7425
7952
  }
@@ -7480,20 +8007,26 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7480
8007
  a modal dialog.
7481
8008
 
7482
8009
  @selector a[up-modal]
7483
- @param [up-sticky]
7484
- @param [up-animation]
7485
- @param [up-height]
8010
+ @param {String} [up-confirm]
8011
+ A message that will be displayed in a cancelable confirmation dialog
8012
+ before the modal is opened.
8013
+ @param {String} [up-sticky]
8014
+ If set to `"true"`, the modal remains
8015
+ open even if the page changes in the background.
8016
+ @param {String} [up-animation]
8017
+ The animation to use when opening the modal.
8018
+ @param {String} [up-height]
8019
+ The width of the dialog in pixels.
8020
+ By [default](/up.modal.config) the dialog will grow to fit its contents.
7486
8021
  @param [up-width]
7487
- @param [up-history]
8022
+ The width of the dialog in pixels.
8023
+ By [default](/up.modal.config) the dialog will grow to fit its contents.
8024
+ @param [up-history="true"]
8025
+ Whether to add a browser history entry for the modal's source URL.
7488
8026
  @stable
7489
8027
  */
7490
- up.link.registerFollowVariant('[up-modal]', function($link) {
7491
- event.preventDefault();
7492
- if ($link.is('.up-current')) {
7493
- return close();
7494
- } else {
7495
- return follow($link);
7496
- }
8028
+ up.link.onAction('[up-modal]', function($link) {
8029
+ return follow($link);
7497
8030
  });
7498
8031
  up.on('click', 'body', function(event, $body) {
7499
8032
  var $target;
@@ -7554,8 +8087,9 @@ To disable this behavior, give the opening link an `up-sticky` attribute:
7554
8087
  },
7555
8088
  contains: contains,
7556
8089
  source: function() {
7557
- return up.error('up.popup.source no longer exists. Please use up.popup.url instead.');
7558
- }
8090
+ return up.error('up.modal.source no longer exists. Please use up.popup.url instead.');
8091
+ },
8092
+ isOpen: isOpen
7559
8093
  };
7560
8094
  })(jQuery);
7561
8095
 
@@ -8011,19 +8545,40 @@ Play nice with Rails UJS
8011
8545
 
8012
8546
  (function() {
8013
8547
  up.rails = (function($) {
8014
- var u, willHandle;
8548
+ var csrfField, isRails, u, willHandle;
8015
8549
  u = up.util;
8016
8550
  willHandle = function($element) {
8017
8551
  return $element.is('[up-follow], [up-target], [up-modal], [up-popup]');
8018
8552
  };
8019
- return up.compiler('[data-method]', function($element) {
8020
- if ($.rails && willHandle($element)) {
8021
- u.setMissingAttrs($element, {
8022
- 'up-method': $element.attr('data-method')
8023
- });
8024
- return $element.removeAttr('data-method');
8025
- }
8553
+ isRails = function() {
8554
+ return u.isGiven($.rails);
8555
+ };
8556
+ u.each(['method', 'confirm'], function(feature) {
8557
+ var dataAttribute, upAttribute;
8558
+ dataAttribute = "data-" + feature;
8559
+ upAttribute = "up-" + feature;
8560
+ return up.compiler("[" + dataAttribute + "]", function($element) {
8561
+ var replacement;
8562
+ if (isRails() && willHandle($element)) {
8563
+ replacement = {};
8564
+ replacement[upAttribute] = $element.attr(dataAttribute);
8565
+ u.setMissingAttrs($element, replacement);
8566
+ return $element.removeAttr(dataAttribute);
8567
+ }
8568
+ });
8026
8569
  });
8570
+ csrfField = function() {
8571
+ if (isRails()) {
8572
+ return {
8573
+ name: $.rails.csrfParam(),
8574
+ value: $.rails.csrfToken()
8575
+ };
8576
+ }
8577
+ };
8578
+ return {
8579
+ csrfField: csrfField,
8580
+ isRails: isRails
8581
+ };
8027
8582
  })(jQuery);
8028
8583
 
8029
8584
  }).call(this);