unpoly-rails 0.57.0 → 0.60.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +393 -1
  3. data/Gemfile.lock +5 -2
  4. data/README.md +1 -1
  5. data/README_RAILS.md +1 -1
  6. data/Rakefile +10 -1
  7. data/design/es6.js +32 -0
  8. data/design/ie11.txt +9 -0
  9. data/design/measure_jquery/element_list.js +41 -0
  10. data/design/measure_jquery/up.on_vs_addEventListener.js +56 -0
  11. data/design/todo_jquery.txt +13 -0
  12. data/dist/unpoly-bootstrap3.js +8 -8
  13. data/dist/unpoly-bootstrap3.min.js +1 -1
  14. data/dist/unpoly.css +22 -20
  15. data/dist/unpoly.js +6990 -5336
  16. data/dist/unpoly.min.css +1 -1
  17. data/dist/unpoly.min.js +4 -4
  18. data/lib/assets/javascripts/unpoly-bootstrap3/viewport-ext.coffee +5 -0
  19. data/lib/assets/javascripts/unpoly.coffee +8 -6
  20. data/lib/assets/javascripts/unpoly/browser.coffee.erb +23 -118
  21. data/lib/assets/javascripts/unpoly/classes/body_shifter.coffee +36 -0
  22. data/lib/assets/javascripts/unpoly/classes/cache.coffee +4 -4
  23. data/lib/assets/javascripts/unpoly/classes/compile_pass.coffee +45 -39
  24. data/lib/assets/javascripts/unpoly/classes/config.coffee +9 -0
  25. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +18 -27
  26. data/lib/assets/javascripts/unpoly/classes/divertible_chain.coffee +39 -0
  27. data/lib/assets/javascripts/unpoly/classes/event_listener.coffee +116 -0
  28. data/lib/assets/javascripts/unpoly/classes/extract_cascade.coffee +8 -8
  29. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +19 -19
  30. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +54 -31
  31. data/lib/assets/javascripts/unpoly/classes/{focus_tracker.coffee → focus_follower.coffee} +2 -2
  32. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +25 -25
  33. data/lib/assets/javascripts/unpoly/classes/html_parser.coffee +4 -11
  34. data/lib/assets/javascripts/unpoly/classes/motion_controller.coffee +157 -0
  35. data/lib/assets/javascripts/unpoly/classes/params.coffee.erb +525 -0
  36. data/lib/assets/javascripts/unpoly/classes/record.coffee +8 -2
  37. data/lib/assets/javascripts/unpoly/classes/rect.js +21 -0
  38. data/lib/assets/javascripts/unpoly/classes/request.coffee +41 -35
  39. data/lib/assets/javascripts/unpoly/classes/response.coffee +7 -3
  40. data/lib/assets/javascripts/unpoly/classes/reveal_motion.coffee +102 -0
  41. data/lib/assets/javascripts/unpoly/classes/scroll_motion.coffee +67 -0
  42. data/lib/assets/javascripts/unpoly/classes/selector.coffee +60 -0
  43. data/lib/assets/javascripts/unpoly/classes/tether.coffee +105 -0
  44. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +12 -7
  45. data/lib/assets/javascripts/unpoly/element.coffee.erb +1126 -0
  46. data/lib/assets/javascripts/unpoly/event.coffee.erb +437 -0
  47. data/lib/assets/javascripts/unpoly/feedback.coffee +73 -94
  48. data/lib/assets/javascripts/unpoly/form.coffee.erb +188 -181
  49. data/lib/assets/javascripts/unpoly/{dom.coffee.erb → fragment.coffee.erb} +250 -283
  50. data/lib/assets/javascripts/unpoly/framework.coffee +67 -0
  51. data/lib/assets/javascripts/unpoly/history.coffee +29 -28
  52. data/lib/assets/javascripts/unpoly/legacy.coffee +60 -0
  53. data/lib/assets/javascripts/unpoly/link.coffee.erb +127 -119
  54. data/lib/assets/javascripts/unpoly/log.coffee +99 -19
  55. data/lib/assets/javascripts/unpoly/modal.coffee.erb +95 -118
  56. data/lib/assets/javascripts/unpoly/motion.coffee.erb +158 -138
  57. data/lib/assets/javascripts/unpoly/namespace.coffee.erb +0 -5
  58. data/lib/assets/javascripts/unpoly/popup.coffee.erb +119 -102
  59. data/lib/assets/javascripts/unpoly/protocol.coffee +11 -15
  60. data/lib/assets/javascripts/unpoly/proxy.coffee +62 -65
  61. data/lib/assets/javascripts/unpoly/radio.coffee +3 -5
  62. data/lib/assets/javascripts/unpoly/rails.coffee +8 -9
  63. data/lib/assets/javascripts/unpoly/syntax.coffee.erb +173 -125
  64. data/lib/assets/javascripts/unpoly/toast.coffee +25 -24
  65. data/lib/assets/javascripts/unpoly/tooltip.coffee +89 -79
  66. data/lib/assets/javascripts/unpoly/util.coffee.erb +579 -1074
  67. data/lib/assets/javascripts/unpoly/{layout.coffee.erb → viewport.coffee.erb} +334 -264
  68. data/lib/assets/stylesheets/unpoly/dom.sass +1 -1
  69. data/lib/assets/stylesheets/unpoly/layout.sass +2 -0
  70. data/lib/assets/stylesheets/unpoly/popup.sass +0 -1
  71. data/lib/assets/stylesheets/unpoly/tooltip.sass +17 -12
  72. data/lib/unpoly/rails/version.rb +1 -1
  73. data/package.json +1 -2
  74. data/spec_app/Gemfile +2 -1
  75. data/spec_app/Gemfile.lock +38 -27
  76. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  77. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -2
  78. data/spec_app/app/assets/stylesheets/integration_test.sass +14 -1
  79. data/spec_app/app/controllers/scroll_test_controller.rb +5 -0
  80. data/spec_app/app/views/css_test/modal.erb +6 -6
  81. data/spec_app/app/views/css_test/popup.erb +44 -18
  82. data/spec_app/app/views/css_test/tooltip.erb +23 -4
  83. data/spec_app/app/views/error_test/trigger.erb +1 -1
  84. data/spec_app/app/views/form_test/basics/new.erb +1 -3
  85. data/spec_app/app/views/pages/start.erb +9 -2
  86. data/spec_app/app/views/reveal_test/long1.erb +1 -1
  87. data/spec_app/app/views/reveal_test/long2.erb +1 -1
  88. data/spec_app/app/views/reveal_test/within_document_viewport.erb +24 -0
  89. data/spec_app/app/views/reveal_test/within_overflowing_div_viewport.erb +28 -0
  90. data/spec_app/app/views/scroll_test/long1.erb +30 -0
  91. data/spec_app/config/routes.rb +1 -0
  92. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +3 -0
  93. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +1 -0
  94. data/spec_app/spec/javascripts/helpers/browser_switches.js.coffee +17 -5
  95. data/spec_app/spec/javascripts/helpers/enable_logging.js.coffee +1 -1
  96. data/spec_app/spec/javascripts/helpers/fixture.js.coffee +25 -0
  97. data/spec_app/spec/javascripts/helpers/jquery_no_conflict.js +1 -0
  98. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -0
  99. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +1 -1
  100. data/spec_app/spec/javascripts/helpers/parse_form_data.js.coffee +2 -2
  101. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +4 -1
  102. data/spec_app/spec/javascripts/helpers/remove_body_margin.js.coffee +3 -0
  103. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +2 -1
  104. data/spec_app/spec/javascripts/helpers/reset_knife.js.coffee +2 -2
  105. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +18 -11
  106. data/spec_app/spec/javascripts/helpers/restore_body_scroll.js.coffee +3 -0
  107. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +3 -0
  108. data/spec_app/spec/javascripts/helpers/spec_util.coffee +47 -0
  109. data/spec_app/spec/javascripts/helpers/to_be_around.js.coffee +3 -0
  110. data/spec_app/spec/javascripts/helpers/to_be_array.coffee +5 -0
  111. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +6 -2
  112. data/spec_app/spec/javascripts/helpers/to_be_blank.js.coffee +3 -0
  113. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +6 -2
  114. data/spec_app/spec/javascripts/helpers/to_be_element.js.coffee +8 -0
  115. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +3 -0
  116. data/spec_app/spec/javascripts/helpers/to_be_given.js.coffee +3 -0
  117. data/spec_app/spec/javascripts/helpers/to_be_hidden.js.coffee +8 -0
  118. data/spec_app/spec/javascripts/helpers/to_be_missing.js.coffee +3 -0
  119. data/spec_app/spec/javascripts/helpers/to_be_present.js.coffee +3 -0
  120. data/spec_app/spec/javascripts/helpers/to_be_scrolled_to.coffee +3 -0
  121. data/spec_app/spec/javascripts/helpers/to_be_visible.js.coffee +9 -0
  122. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +3 -0
  123. data/spec_app/spec/javascripts/helpers/to_end_with.js.coffee +3 -0
  124. data/spec_app/spec/javascripts/helpers/to_equal_jquery.js.coffee +1 -2
  125. data/spec_app/spec/javascripts/helpers/to_equal_node_list.coffee +7 -0
  126. data/spec_app/spec/javascripts/helpers/to_equal_via_is_equal.js.coffee +7 -0
  127. data/spec_app/spec/javascripts/helpers/to_have_class.js.coffee +10 -0
  128. data/spec_app/spec/javascripts/helpers/to_have_descendant.js.coffee +10 -0
  129. data/spec_app/spec/javascripts/helpers/to_have_length.js.coffee +8 -0
  130. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +7 -3
  131. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +3 -0
  132. data/spec_app/spec/javascripts/helpers/to_have_request_method.js.coffee +1 -0
  133. data/spec_app/spec/javascripts/helpers/to_have_text.js.coffee +9 -0
  134. data/spec_app/spec/javascripts/helpers/to_have_unhandled_rejections.coffee +0 -21
  135. data/spec_app/spec/javascripts/helpers/to_match_list.coffee +14 -0
  136. data/spec_app/spec/javascripts/helpers/to_match_selector.coffee +3 -0
  137. data/spec_app/spec/javascripts/helpers/to_match_text.js.coffee +4 -1
  138. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +1 -0
  139. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +91 -7
  140. data/spec_app/spec/javascripts/helpers/wait_until_dom_ready.js.coffee +3 -0
  141. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +23 -90
  142. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +3 -0
  143. data/spec_app/spec/javascripts/up/classes/config_spec.coffee +24 -0
  144. data/spec_app/spec/javascripts/up/classes/divertible_chain_spec.coffee +45 -0
  145. data/spec_app/spec/javascripts/up/classes/focus_tracker_spec.coffee +5 -2
  146. data/spec_app/spec/javascripts/up/classes/params_spec.coffee +557 -0
  147. data/spec_app/spec/javascripts/up/classes/request_spec.coffee +7 -4
  148. data/spec_app/spec/javascripts/up/classes/scroll_motion_spec.js.coffee +51 -0
  149. data/spec_app/spec/javascripts/up/classes/store/memory_spec.js.coffee +3 -0
  150. data/spec_app/spec/javascripts/up/classes/store/session_spec.js.coffee +3 -2
  151. data/spec_app/spec/javascripts/up/element_spec.coffee +897 -0
  152. data/spec_app/spec/javascripts/up/event_spec.js.coffee +496 -0
  153. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +69 -48
  154. data/spec_app/spec/javascripts/up/form_spec.js.coffee +252 -194
  155. data/spec_app/spec/javascripts/up/{dom_spec.js.coffee → fragment_spec.js.coffee} +381 -388
  156. data/spec_app/spec/javascripts/up/history_spec.js.coffee +21 -19
  157. data/spec_app/spec/javascripts/up/jquery_spec.js.coffee +4 -0
  158. data/spec_app/spec/javascripts/up/legacy_spec.js.coffee +27 -0
  159. data/spec_app/spec/javascripts/up/link_spec.js.coffee +163 -160
  160. data/spec_app/spec/javascripts/up/log_spec.js.coffee +85 -12
  161. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +141 -123
  162. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +117 -113
  163. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +60 -77
  164. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +1 -0
  165. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +85 -78
  166. data/spec_app/spec/javascripts/up/radio_spec.js.coffee +29 -22
  167. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +14 -13
  168. data/spec_app/spec/javascripts/up/spec_spec.js.coffee +9 -0
  169. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +96 -66
  170. data/spec_app/spec/javascripts/up/toast_spec.js.coffee +37 -0
  171. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +31 -47
  172. data/spec_app/spec/javascripts/up/util_spec.js.coffee +725 -562
  173. data/spec_app/spec/javascripts/up/{layout_spec.js.coffee → viewport_spec.js.coffee} +175 -149
  174. metadata +57 -19
  175. data/lib/assets/javascripts/unpoly-bootstrap3/layout-ext.coffee +0 -5
  176. data/lib/assets/javascripts/unpoly/bus.coffee.erb +0 -518
  177. data/lib/assets/javascripts/unpoly/classes/extract_step.coffee +0 -4
  178. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +0 -125
  179. data/lib/assets/javascripts/unpoly/params.coffee.erb +0 -522
  180. data/spec_app/spec/javascripts/helpers/append_fixture.js.coffee +0 -8
  181. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +0 -210
  182. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +0 -9
  183. data/spec_app/spec/javascripts/up/params_spec.coffee +0 -768
  184. data/spec_app/vendor/asset-libs/jasmine-fixture-1.3.4/jasmine-fixture.js +0 -433
  185. data/spec_app/vendor/asset-libs/jasmine-jquery-2.1.1/.bower.json +0 -26
  186. data/spec_app/vendor/asset-libs/jasmine-jquery-2.1.1/jasmine-jquery.js +0 -838
@@ -6,11 +6,12 @@ Unpoly comes with functionality to [submit](/form-up-target) and [validate](/inp
6
6
  forms without leaving the current page. This means you can replace page fragments,
7
7
  open dialogs with sub-forms, etc. all without losing form state.
8
8
 
9
- @class up.form
9
+ @module up.form
10
10
  ###
11
- up.form = (($) ->
11
+ up.form = do ->
12
12
 
13
13
  u = up.util
14
+ e = up.element
14
15
 
15
16
  ###**
16
17
  Sets default options for form submission and validation.
@@ -26,16 +27,15 @@ up.form = (($) ->
26
27
  will be updated with the validation messages from the server.
27
28
 
28
29
  By default this looks for a `<fieldset>`, `<label>` or `<form>`
29
- around the validating input field, or any element with an
30
- `up-fieldset` attribute.
30
+ around the validating input field.
31
31
  @param {string} [config.fields]
32
32
  An array of CSS selectors that represent form fields, such as `input` or `select`.
33
33
  @param {string} [config.submitButtons]
34
34
  An array of CSS selectors that represent submit buttons, such as `input[type=submit]`.
35
35
  @stable
36
36
  ###
37
- config = u.config
38
- validateTargets: ['[up-fieldset]:has(&)', 'fieldset:has(&)', 'label:has(&)', 'form:has(&)']
37
+ config = new up.Config
38
+ validateTargets: ['fieldset:has(&)', 'label:has(&)', 'form:has(&)']
39
39
  fields: ['select', 'input:not([type=submit]):not([type=image])', 'button[type]:not([type=submit])', 'textarea'],
40
40
  submitButtons: ['input[type=submit]', 'input[type=image]', 'button[type=submit]', 'button:not([type])']
41
41
  observeDelay: 0
@@ -50,6 +50,41 @@ up.form = (($) ->
50
50
  fieldSelector = ->
51
51
  config.fields.join(',')
52
52
 
53
+ ###**
54
+ @function up.form.fields
55
+ @internal
56
+ ###
57
+ findFields = (root) ->
58
+ e.subtree(root, fieldSelector())
59
+
60
+ # findFields = (rootOrRoots) ->
61
+ # rootOrRoots = e.list(rootOrRoots)
62
+ # u.flatMap rootOrRoots, (root) -> e.subtree(root, fieldSelector())
63
+
64
+ ###***
65
+ @function up.form.submissionFields
66
+ @internal
67
+ ###
68
+ findSubmissionFields = (root) ->
69
+ fields = findFields(root)
70
+ if button = submittingButton(root)
71
+ fields = u.toArray(fields)
72
+ fields.push(button)
73
+ fields
74
+
75
+ ###**
76
+ @function up.form.submittingButton
77
+ @internal
78
+ ###
79
+ submittingButton = (form) ->
80
+ selector = submitButtonSelector()
81
+ focusedElement = document.activeElement
82
+ if focusedElement && e.matches(focusedElement, selector) && form.contains(focusedElement)
83
+ return focusedElement
84
+ else
85
+ # If no button is focused, we assume the first button in the form.
86
+ return e.first(form, selector)
87
+
53
88
  ###**
54
89
  @function up.form.submitButtonSelector
55
90
  @internal
@@ -149,27 +184,28 @@ up.form = (($) ->
149
184
  @stable
150
185
  ###
151
186
  submit = (formOrSelector, options) ->
152
- $form = $(formOrSelector).closest('form')
153
-
154
187
  options = u.options(options)
155
- target = u.option(options.target, $form.attr('up-target'), 'body')
156
- url = u.option(options.url, $form.attr('action'), up.browser.url())
157
- options.failTarget = u.option(options.failTarget, $form.attr('up-fail-target')) || u.selectorForElement($form)
158
- options.reveal = u.option(options.reveal, u.castedAttr($form, 'up-reveal'), true)
159
- options.failReveal = u.option(options.failReveal, u.castedAttr($form, 'up-fail-reveal'), true)
160
- options.fallback = u.option(options.fallback, $form.attr('up-fallback'))
161
- options.history = u.option(options.history, u.castedAttr($form, 'up-history'), true)
162
- options.transition = u.option(options.transition, u.castedAttr($form, 'up-transition'), 'none')
163
- options.failTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), 'none')
164
- options.method = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase()
165
- options.headers = u.option(options.headers, {})
166
- options.cache = u.option(options.cache, u.castedAttr($form, 'up-cache'))
167
- options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($form, 'up-restore-scroll'))
168
- options.origin = u.option(options.origin, $form)
169
- options.layer = u.option(options.layer, $form.attr('up-layer'), 'auto')
170
- options.failLayer = u.option(options.failLayer, $form.attr('up-fail-layer'), 'auto')
171
- options.params = up.params.fromForm($form)
172
- options = u.merge(options, up.motion.animateOptions(options, $form))
188
+
189
+ form = e.get(formOrSelector)
190
+ form = e.closest(form, 'form')
191
+
192
+ target = options.target ? form.getAttribute('up-target') ? 'body'
193
+ url = options.url ? form.getAttribute('action') ? up.browser.url()
194
+ options.failTarget ?= form.getAttribute('up-fail-target') ? e.toSelector(form)
195
+ options.reveal ?= e.booleanOrStringAttr(form, 'up-reveal') ? true
196
+ options.failReveal ?= e.booleanOrStringAttr(form, 'up-fail-reveal') ? true
197
+ options.fallback ?= form.getAttribute('up-fallback')
198
+ options.history ?= e.booleanOrStringAttr(form, 'up-history') ? true
199
+ options.transition ?= e.booleanOrStringAttr(form, 'up-transition')
200
+ options.failTransition ?= e.booleanOrStringAttr(form, 'up-fail-transition')
201
+ options.method ?= form.getAttribute('up-method') ? form.getAttribute('data-method') ? form.getAttribute('method') ? 'post'
202
+ options.cache ?= e.booleanAttr(form, 'up-cache')
203
+ options.restoreScroll ?= e.booleanAttr(form, 'up-restore-scroll') # the option supports an object value, but not the attr
204
+ options.origin ?= form
205
+ options.layer ?= form.getAttribute('up-layer')
206
+ options.failLayer ?= form.getAttribute('up-fail-layer')
207
+ options.params = up.Params.fromForm(form)
208
+ options = u.merge(options, up.motion.animateOptions(options, form))
173
209
 
174
210
  if options.validate
175
211
  options.headers ||= {}
@@ -177,24 +213,26 @@ up.form = (($) ->
177
213
  options.failTransition = false
178
214
  options.headers[up.protocol.config.validateHeader] = options.validate
179
215
 
180
- up.bus.whenEmitted('up:form:submit', message: 'Submitting form', $form: $form, $element: $form).then ->
181
- up.feedback.start($form)
216
+ up.event.whenEmitted('up:form:submit', log: 'Submitting form', target: form).then ->
217
+ up.feedback.start(form)
182
218
 
183
219
  # If we can't update the location URL, fall back to a vanilla form submission.
184
220
  unless up.browser.canPushState() || options.history == false
185
221
  # Don't use up.browser.navigate(); It cannot deal with file inputs.
186
- $form.get(0).submit()
222
+ form.submit()
187
223
  return u.unresolvablePromise()
188
224
 
189
225
  promise = up.replace(target, url, options)
190
- u.always promise, -> up.feedback.stop($form)
226
+ u.always promise, -> up.feedback.stop(form)
191
227
  promise
192
228
 
193
229
  ###**
194
230
  This event is [emitted](/up.emit) when a form is [submitted](/up.submit) through Unpoly.
195
231
 
232
+ The event is emitted on the`<form>` element.
233
+
196
234
  @event up:form:submit
197
- @param {jQuery} event.$form
235
+ @param {Element} event.target
198
236
  The `<form>` element that will be submitted.
199
237
  @param event.preventDefault()
200
238
  Event listeners may call this method to prevent the form from being submitted.
@@ -206,105 +244,71 @@ up.form = (($) ->
206
244
 
207
245
  This is useful for observing text fields while the user is typing.
208
246
 
209
- The unobtrusive variant of this is the [`up-observe`](/up-observe) attribute.
247
+ The unobtrusive variant of this is the [`[up-observe]`](/up-observe) attribute.
210
248
 
211
249
  \#\#\# Example
212
250
 
213
251
  The following would print to the console whenever an input field changes:
214
252
 
215
- up.observe('input.query', function(value, $input) {
216
- console.log('Query is now ' + value);
217
- });
253
+ up.observe('input.query', function(value) {
254
+ console.log('Query is now %o', value)
255
+ })
218
256
 
219
257
  Instead of a single form field, you can also pass multiple fields,
220
258
  a `<form>` or any container that contains form fields.
221
- The callback will be run if any of the given fields change.
222
-
223
- \#\#\# Preventing concurrency
224
-
225
- Making network requests whenever a form field changes can cause
226
- [concurrency issues](https://makandracards.com/makandra/961-concurrency-issues-with-find-as-you-type-boxes).
227
- Since `up.observe()` can trigger many requests in a short period of time,
228
- the responses might not arrive in the same order.
229
-
230
- To mitigate this, `up.observe()` will try to never run a callback
231
- before the previous callback has completed.
232
- For this your callback code must return a promise that resolves
233
- when your request completes.
234
-
235
- The following would submit a form whenever an input field changes,
236
- but never make more than one request at a time:
237
-
238
- up.observe('input.query', function(value, $input) {
239
- var submitDone = up.submit($input);
240
- return submitDone;
241
- });
242
-
243
- Note that many Unpoly functions like [`up.submit()`](/up.submit) or
244
- [`up.replace()`](/up.replace) return promises.
259
+ The callback will be run if any of the given fields change:
245
260
 
246
- \#\#\# Debouncing
261
+ up.observe('form', function(value, name) {
262
+ console.log('The value of %o is now %o', name, value)
263
+ })
247
264
 
248
- If you are concerned about fast typists causing too much
249
- load on your server, you can use a `delay` option to wait
250
- a few miliseconds before executing the callback:
265
+ You may also pass the `{ batch: true }` option to receive all
266
+ changes since the last callback in a single object:
251
267
 
252
- up.observe('input', { delay: 100 }, function(value, $input) {
253
- up.submit($input)
254
- });
268
+ up.observe('form', { batch: true }, function(diff) {
269
+ console.log('Observed one or more changes: %o', diff)
270
+ })
255
271
 
256
272
  @function up.observe
257
- @param {Element|jQuery|string} selectorOrElement
258
- The form fields that wiill be observed.
273
+ @param {string|Element|Array<Element>|jQuery} elements
274
+ The form fields that will be observed.
259
275
 
260
276
  You can pass one or more fields, a `<form>` or any container that contains form fields.
261
277
  The callback will be run if any of the given fields change.
278
+ @param {boolean} [options.batch=false]
279
+ If set to `true`, the `onChange` callback will receive multiple
280
+ detected changes in a single diff object as its argument.
262
281
  @param {number} [options.delay=up.form.config.observeDelay]
263
282
  The number of miliseconds to wait before executing the callback
264
283
  after the input value changes. Use this to limit how often the callback
265
284
  will be invoked for a fast typist.
266
- @param {Function(value, $field)|string} onChange
285
+ @param {Function(value, name): string} onChange
267
286
  The callback to run when the field's value changes.
268
- If given as a function, it must take two arguments (`value`, `$field`).
287
+
288
+ If given as a function, it receives two arguments (`value`, `name`).
289
+ `value` is a string with the new attribute value and `string` is the name
290
+ of the form field that changed.
291
+
269
292
  If given as a string, it will be evaled as JavaScript code in a context where
270
- (`value`, `$field`) are set.
271
- @return {Function}
293
+ (`value`, `name`) are set.
294
+ @return {Function()}
272
295
  A destructor function that removes the observe watch when called.
273
296
  @stable
274
297
  ###
275
- observe = (selectorOrElement, extraArgs...) ->
276
- options = {}
277
- callbackArg = undefined
278
- if extraArgs.length == 1
279
- callbackArg = extraArgs[0]
280
- else if extraArgs.length > 1
281
- options = u.options(extraArgs[0])
282
- callbackArg = extraArgs[1]
283
-
284
- $element = $(selectorOrElement)
285
-
286
- callback = null
287
- rawCallback = u.option(callbackArg, u.presentAttr($element, 'up-observe'))
288
- if u.isString(rawCallback)
289
- callback = new Function('value', '$field', rawCallback)
290
- else
291
- callback = rawCallback or up.fail('up.observe: No change callback given')
292
-
293
- delay = u.option(u.presentAttr($element, 'up-delay'), options.delay, config.observeDelay)
294
- delay = parseInt(delay)
295
-
296
- $fields = u.selectInSubtree($element, fieldSelector())
297
-
298
- destructors = u.map $fields, (field) ->
299
- observeField($(field), delay, callback)
300
-
301
- u.sequence(destructors...)
302
-
303
- observeField = ($field, delay, callback) ->
304
- observer = new up.FieldObserver($field, { delay, callback })
298
+ observe = (elements, args...) ->
299
+ elements = e.list(elements)
300
+ fields = u.flatMap(elements, findFields)
301
+ callback = u.extractCallback(args) ? observeCallbackFromElement(elements[0]) ? up.fail('up.observe: No change callback given')
302
+ options = u.extractOptions(args)
303
+ options.delay = options.delay ? e.numberAttr(elements[0], 'up-delay') ? config.observeDelay
304
+ observer = new up.FieldObserver(fields, options, callback)
305
305
  observer.start()
306
306
  return observer.stop
307
307
 
308
+ observeCallbackFromElement = (element) ->
309
+ if rawCallback = element.getAttribute('up-observe')
310
+ new Function('value', 'name', rawCallback)
311
+
308
312
  ###**
309
313
  [Observes](/up.observe) a field or form and submits the form when a value changes.
310
314
 
@@ -318,28 +322,27 @@ up.form = (($) ->
318
322
  The field or form to observe.
319
323
  @param {Object} [options]
320
324
  See options for [`up.observe()`](/up.observe)
321
- @return {Function}
325
+ @return {Function()}
322
326
  A destructor function that removes the observe watch when called.
323
327
  @stable
324
328
  ###
325
329
  autosubmit = (selectorOrElement, options) ->
326
- observe(selectorOrElement, options, (value, $field) ->
327
- $form = $field.closest('form')
328
- up.feedback.start $field, -> submit($form)
329
- )
330
-
331
- resolveValidateTarget = ($field, options) ->
332
- target = u.option(options.target, $field.attr('up-validate'))
333
- if u.isBlank(target)
334
- target ||= u.detect(config.validateTargets, (defaultTarget) ->
335
- resolvedDefault = up.dom.resolveSelector(defaultTarget, options.origin)
336
- $field.closest(resolvedDefault).length
337
- )
338
- if u.isBlank(target)
339
- up.fail('Could not find default validation target for %o (tried ancestors %o)', $field.get(0), config.validateTargets)
340
- unless u.isString(target)
341
- target = u.selectorForElement(target)
342
- target
330
+ observe(selectorOrElement, options, -> submit(selectorOrElement))
331
+
332
+ findValidateTarget = (field, options) ->
333
+ option = options.target ? field.getAttribute('up-validate')
334
+ option ||= u.findResult config.validateTargets, (defaultTarget) ->
335
+ resolvedDefault = e.resolveSelector(defaultTarget, options.origin)
336
+ if e.first(resolvedDefault)
337
+ # We want to return the selector, *not* the element. If we returned the element
338
+ # and derive a selector from that, any :has() expression would be lost.
339
+ return resolvedDefault
340
+
341
+ unless option
342
+ up.fail('Could not find validation target for %o (tried defaults %o)', field, config.validateTargets)
343
+
344
+ # resolveSelector() also creates a selector string if given an element
345
+ return e.resolveSelector(option, options.origin)
343
346
 
344
347
  ###**
345
348
  Performs a server-side validation of a form field.
@@ -366,38 +369,42 @@ up.form = (($) ->
366
369
  @stable
367
370
  ###
368
371
  validate = (fieldOrSelector, options) ->
369
- $field = $(fieldOrSelector)
372
+ field = e.get(fieldOrSelector)
370
373
  options = u.options(options)
371
- options.origin = $field
372
- options.target = resolveValidateTarget($field, options)
374
+ options.origin = field
375
+ options.target = findValidateTarget(field, options)
373
376
  options.failTarget = options.target
374
- options.reveal = u.option(options.reveal, u.castedAttr($field, 'up-reveal'), false)
377
+ options.reveal ?= e.booleanOrStringAttr(field, 'up-reveal') ? false
375
378
  options.history = false
376
- options.headers = u.option(options.headers, {})
377
379
  # Make sure the X-Up-Validate header is present, so the server-side
378
380
  # knows that it should not persist the form submission
379
- options.validate = ($field.attr('name') || '__none__')
380
- options = u.merge(options, up.motion.animateOptions(options, $field))
381
- $form = $field.closest('form')
382
- promise = up.submit($form, options)
381
+ options.validate = field.getAttribute('name') || ':none'
382
+ options = u.merge(options, up.motion.animateOptions(options, field))
383
+ promise = up.submit(field, options)
383
384
  promise
384
385
 
385
- switcherValues = ($field) ->
386
- if $field.is('input[type=checkbox]')
387
- if $field.is(':checked')
388
- value = $field.val()
386
+ switcherValues = (field) ->
387
+ value = undefined
388
+ meta = undefined
389
+
390
+ if e.matches(field, 'input[type=checkbox]')
391
+ if field.checked
392
+ value = field.value
389
393
  meta = ':checked'
390
394
  else
391
395
  meta = ':unchecked'
392
- else if $field.is('input[type=radio]')
393
- $checkedButton = $field.closest('form, body').find("input[type='radio'][name='#{$field.attr('name')}']:checked")
394
- if $checkedButton.length
396
+ else if e.matches(field, 'input[type=radio]')
397
+ form = closestContainer(field)
398
+ groupName = field.getAttribute('name')
399
+ checkedButton = form.querySelector("input[type=radio]#{e.attributeSelector('name', groupName)}:checked")
400
+ if checkedButton
395
401
  meta = ':checked'
396
- value = $checkedButton.val()
402
+ value = checkedButton.value
397
403
  else
398
404
  meta = ':unchecked'
399
405
  else
400
- value = $field.val()
406
+ value = field.value
407
+
401
408
  values = []
402
409
  if u.isPresent(value)
403
410
  values.push(value)
@@ -418,32 +425,32 @@ up.form = (($) ->
418
425
  still marked `@internal`.
419
426
 
420
427
  @function up.form.switchTargets
421
- @param {string|Element|jQuery} fieldOrSelector
428
+ @param {Element} switcher
422
429
  @param {string} [options.target]
423
430
  The target selectors to switch.
424
- Defaults to an `up-switch` attribute on the given field.
431
+ Defaults to an `[up-switch]` attribute on the given field.
425
432
  @internal
426
433
  ###
427
- switchTargets = (fieldOrSelector, options) ->
428
- $switcher = $(fieldOrSelector)
429
- options = u.options(options)
430
- targetSelector = u.option(options.target, $switcher.attr('up-switch'))
431
- u.isPresent(targetSelector) or up.fail("No switch target given for %o", $switcher.get(0))
432
- fieldValues = switcherValues($switcher)
433
- $(targetSelector).each ->
434
- switchTarget($(this), fieldValues)
434
+ switchTargets = (switcher, options = {}) ->
435
+ targetSelector = options.target ? switcher.getAttribute('up-switch')
436
+ form = closestContainer(switcher)
437
+ u.isPresent(targetSelector) or up.fail("No switch target given for %o", switcher)
438
+ fieldValues = switcherValues(switcher)
439
+
440
+ u.each e.all(form, targetSelector), (target) ->
441
+ switchTarget(target, fieldValues)
435
442
 
436
443
  ###**
437
444
  @internal
438
445
  ###
439
446
  switchTarget = (target, fieldValues) ->
440
- $target = $(target)
441
- fieldValues ||= switcherValues(findSwitcherForTarget($target))
442
- if hideValues = $target.attr('up-hide-for')
447
+ fieldValues ||= switcherValues(findSwitcherForTarget(target))
448
+
449
+ if hideValues = target.getAttribute('up-hide-for')
443
450
  hideValues = u.splitValues(hideValues)
444
451
  show = u.intersect(fieldValues, hideValues).length == 0
445
452
  else
446
- if showValues = $target.attr('up-show-for')
453
+ if showValues = target.getAttribute('up-show-for')
447
454
  showValues = u.splitValues(showValues)
448
455
  else
449
456
  # If the target has neither up-show-for or up-hide-for attributes,
@@ -451,21 +458,23 @@ up.form = (($) ->
451
458
  # is checked or entered.
452
459
  showValues = [':present', ':checked']
453
460
  show = u.intersect(fieldValues, showValues).length > 0
454
- $target.toggle(show)
455
- $target.addClass('up-switched')
461
+
462
+ e.toggle(target, show)
463
+ target.classList.add('up-switched')
456
464
 
457
465
  ###**
458
466
  @internal
459
467
  ###
460
- findSwitcherForTarget = ($target) ->
461
- $switchers = $('[up-switch]')
462
- switcher = u.detect $switchers, (switcher) ->
463
- target = $(switcher).attr('up-switch')
464
- $target.is(target)
465
- if switcher
466
- $(switcher)
467
- else
468
- u.fail('Could not find [up-switch] field for %o', $target.get(0))
468
+ findSwitcherForTarget = (target) ->
469
+ form = closestContainer(target)
470
+ switchers = e.all(form, '[up-switch]')
471
+ switcher = u.find switchers, (switcher) ->
472
+ targetSelector = switcher.getAttribute('up-switch')
473
+ e.matches(target, targetSelector)
474
+ return switcher or u.fail('Could not find [up-switch] field for %o', target)
475
+
476
+ closestContainer = (element) ->
477
+ e.closest(element, 'form, body')
469
478
 
470
479
  ###**
471
480
  Forms with an `up-target` attribute are [submitted via AJAX](/up.submit)
@@ -600,9 +609,9 @@ up.form = (($) ->
600
609
  By default only responses to `GET` requests are cached for a few minutes.
601
610
  @stable
602
611
  ###
603
- up.on 'submit', 'form[up-target]', (event, $form) ->
604
- up.bus.consumeAction(event)
605
- u.muteRejection submit($form)
612
+ up.on 'submit', 'form[up-target]', (event, form) ->
613
+ up.event.consumeAction(event)
614
+ u.muteRejection submit(form)
606
615
 
607
616
  ###**
608
617
  When a form field with this attribute is changed, the form is validated on the server
@@ -749,8 +758,8 @@ up.form = (($) ->
749
758
  This defaults to a fieldset or form group around the validating field.
750
759
  @stable
751
760
  ###
752
- up.on 'change', '[up-validate]', (event, $field) ->
753
- u.muteRejection validate($field)
761
+ up.on 'change', '[up-validate]', (event, field) ->
762
+ u.muteRejection validate(field)
754
763
 
755
764
  ###**
756
765
  Show or hide elements when a `<select>` or `<input>` has a given value.
@@ -848,14 +857,14 @@ up.form = (($) ->
848
857
  A space-separated list of input values for which this element should be hidden.
849
858
  @stable
850
859
  ###
851
- up.compiler '[up-switch]', ($field) ->
852
- switchTargets($field)
860
+ up.compiler '[up-switch]', (switcher) ->
861
+ switchTargets(switcher)
853
862
 
854
- up.on 'change', '[up-switch]', (event, $field) ->
855
- switchTargets($field)
863
+ up.on 'change', '[up-switch]', (event, switcher) ->
864
+ switchTargets(switcher)
856
865
 
857
- up.compiler '[up-show-for]:not(.up-switched), [up-hide-for]:not(.up-switched)', ($element) ->
858
- switchTarget($element)
866
+ up.compiler '[up-show-for]:not(.up-switched), [up-hide-for]:not(.up-switched)', (element) ->
867
+ switchTarget(element)
859
868
 
860
869
  ###**
861
870
  Observes this field and runs a callback when a value changes.
@@ -927,7 +936,7 @@ up.form = (($) ->
927
936
  The number of miliseconds to wait after a change before the code is run.
928
937
  @stable
929
938
  ###
930
- up.compiler '[up-observe]', ($formOrField) -> observe($formOrField)
939
+ up.compiler '[up-observe]', (formOrField) -> observe(formOrField)
931
940
 
932
941
  ###**
933
942
  [Observes](/up.observe) this form field and submits the form when its value changes.
@@ -974,9 +983,9 @@ up.form = (($) ->
974
983
  The number of miliseconds to wait after a change before the form is submitted.
975
984
  @stable
976
985
  ###
977
- up.compiler '[up-autosubmit]', ($formOrField) -> autosubmit($formOrField)
986
+ up.compiler '[up-autosubmit]', (formOrField) -> autosubmit(formOrField)
978
987
 
979
- up.compiler '[autofocus]', { batch: true }, ($input) -> $input.last().focus()
988
+ up.compiler '[autofocus]', { batch: true }, (inputs) -> u.last(inputs).focus()
980
989
 
981
990
  up.on 'up:framework:reset', reset
982
991
 
@@ -985,12 +994,10 @@ up.form = (($) ->
985
994
  submit: submit
986
995
  observe: observe
987
996
  validate: validate
988
- switchTargets: switchTargets
989
997
  autosubmit: autosubmit
990
998
  fieldSelector: fieldSelector
991
- submitButtonSelector: submitButtonSelector
992
-
993
- )(jQuery)
999
+ fields: findFields
1000
+ submissionFields: findSubmissionFields
994
1001
 
995
1002
  up.submit = up.form.submit
996
1003
  up.observe = up.form.observe