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
@@ -0,0 +1,437 @@
1
+ ###**
2
+ Events
3
+ ======
4
+
5
+ Most Unpoly interactions emit DOM events that are prefixed with `up:`.
6
+
7
+ document.addEventListener('up:modal:opened', (event) => {
8
+ console.log('A new modal has just opened!')
9
+ })
10
+
11
+ Events often have both present and past forms. For example,
12
+ `up:modal:open` is emitted before a modal starts to open.
13
+ `up:modal:opened` is emitted when the modal has finished its
14
+ opening animation.
15
+
16
+ \#\#\# Preventing events
17
+
18
+ You can prevent most present form events by calling `preventDefault()`:
19
+
20
+ document.addEventListener('up:modal:open', (event) => {
21
+ if (event.url == '/evil') {
22
+ // Prevent the modal from opening
23
+ event.preventDefault()
24
+ }
25
+ })
26
+
27
+
28
+ \#\#\# A better way to bind event listeners
29
+
30
+ Instead of using [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener),
31
+ you may find it convenient to use [`up.on()`](/up.on) instead:
32
+
33
+ up.on('click', 'button', function(event, button, data) {
34
+ // button is the clicked element
35
+ // data is the parsed [`up-data`](/up-data) attribute
36
+ })
37
+
38
+ There are some advantages to using `up.on()`:
39
+
40
+ - You may pass a selector for [event delegation](https://davidwalsh.name/event-delegate).
41
+ - The event target is automatically passed as a second argument.
42
+ - You may register a listener to multiple events by passing a space-separated list of event name (e.g. `"click mousedown"`)
43
+ - You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements.
44
+ - You use an [`[up-data]`](/up-data) attribute to [attach structured data](/up.on#attaching-structured-data)
45
+ to observed elements. If an `[up-data]` attribute is set, its value will automatically be
46
+ parsed as JSON and passed as a third argument.
47
+ - Event listeners on [unsupported browsers](/up.browser.isSupported) are silently discarded,
48
+ leaving you with an application without JavaScript. This is typically preferable to
49
+ a soup of randomly broken JavaScript in ancient browsers.
50
+
51
+ @module up.event
52
+ ###
53
+ up.event = do ->
54
+
55
+ u = up.util
56
+ e = up.element
57
+
58
+ reset = ->
59
+ # Resets the list of registered event listeners to the
60
+ # moment when the framework was booted.
61
+ for element in [window, document, document.documentElement, document.body]
62
+ up.EventListener.unbindNonDefault(element)
63
+
64
+ ###**
65
+ Listens to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events)
66
+ on `document` or a given element.
67
+
68
+ `up.on()` has some quality of life improvements over
69
+ [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener):
70
+
71
+ - You may pass a selector for [event delegation](https://davidwalsh.name/event-delegate).
72
+ - The event target is automatically passed as a second argument.
73
+ - You may register a listener to multiple events by passing a space-separated list of event name (e.g. `"click mousedown"`)
74
+ - You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements.
75
+ - You use an [`[up-data]`](/up-data) attribute to [attach structured data](/up.on#attaching-structured-data)
76
+ to observed elements. If an `[up-data]` attribute is set, its value will automatically be
77
+ parsed as JSON and passed as a third argument.
78
+ - Event listeners on [unsupported browsers](/up.browser.isSupported) are silently discarded,
79
+ leaving you with an application without JavaScript. This is typically preferable to
80
+ a soup of randomly broken JavaScript in ancient browsers.
81
+
82
+ \#\#\# Examples
83
+
84
+ The code below will call the listener when a `<a>` is clicked
85
+ anywhere in the `document`:
86
+
87
+ up.on('click', 'a', function(event, element) {
88
+ console.log("Click on a link %o", element)
89
+ })
90
+
91
+ You may also bind the listener to a given element instead of `document`:
92
+
93
+ var form = document.querySelector('form')
94
+ up.on(form, 'click', function(event, form) {
95
+ console.log("Click within %o", form)
96
+ })
97
+
98
+ You may also pass both an element and a selector
99
+ for [event delegation](https://davidwalsh.name/event-delegate):
100
+
101
+ var form = document.querySelector('form')
102
+ document.addEventListener(form, 'click', 'a', function(event, link) {
103
+ console.log("Click on a link %o within %o", link, form)
104
+ })
105
+
106
+ \#\#\# Attaching structured data
107
+
108
+ In case you want to attach structured data to the event you're observing,
109
+ you can serialize the data to JSON and put it into an `[up-data]` attribute:
110
+
111
+ <span class='person' up-data='{ "age": 18, "name": "Bob" }'>Bob</span>
112
+ <span class='person' up-data='{ "age": 22, "name": "Jim" }'>Jim</span>
113
+
114
+ The JSON will parsed and handed to your event handler as a third argument:
115
+
116
+ up.on('click', '.person', function(event, element, data) {
117
+ console.log("This is %o who is %o years old", data.name, data.age)
118
+ })
119
+
120
+ \#\#\# Unbinding an event listener
121
+
122
+ `up.on()` returns a function that unbinds the event listeners when called:
123
+
124
+ // Define the listener
125
+ var listener = function(event) { ... }
126
+
127
+ // Binding the listener returns an unbind function
128
+ var unbind = up.on('click', listener)
129
+
130
+ // Unbind the listener
131
+ unbind()
132
+
133
+ There is also a function [`up.off()`](/up.off) which you can use for the same purpose:
134
+
135
+ // Define the listener
136
+ var listener = function(event) { ... }
137
+
138
+ // Bind the listener
139
+ up.on('click', listener)
140
+
141
+ // Unbind the listener
142
+ up.off('click', listener)
143
+
144
+ @function up.on
145
+ @param {Element|jQuery} [element=document]
146
+ The element on which to register the event listener.
147
+
148
+ If no element is given, the listener is registered on the `document`.
149
+ @param {string} events
150
+ A space-separated list of event names to bind to.
151
+ @param {string} [selector]
152
+ The selector of an element on which the event must be triggered.
153
+ Omit the selector to listen to all events with that name, regardless
154
+ of the event target.
155
+ @param {Function(event, [element], [data])} listener
156
+ The listener function that should be called.
157
+
158
+ The function takes the affected element as the first argument).
159
+ If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON
160
+ and passed as a second argument.
161
+ @return {Function()}
162
+ A function that unbinds the event listeners when called.
163
+ @stable
164
+ ###
165
+ bind = (args...) ->
166
+ bindNow(args)
167
+
168
+ ###**
169
+ Listens to an event on `document` or a given element.
170
+ The event handler is called with the event target as a
171
+ [jQuery collection](https://learn.jquery.com/using-jquery-core/jquery-object/).
172
+
173
+ If you're not using jQuery, use `up.on()` instead, which calls
174
+ event handlers with a native element.
175
+
176
+ \#\#\# Example
177
+
178
+ ```
179
+ up.$on('click', 'a', function(event, $link) {
180
+ console.log("Click on a link with destination %s", $element.attr('href'))
181
+ })
182
+ ```
183
+
184
+ @function up.$on
185
+ @param {Element|jQuery} [element=document]
186
+ The element on which to register the event listener.
187
+
188
+ If no element is given, the listener is registered on the `document`.
189
+ @param {string} events
190
+ A space-separated list of event names to bind to.
191
+ @param {string} [selector]
192
+ The selector of an element on which the event must be triggered.
193
+ Omit the selector to listen to all events with that name, regardless
194
+ of the event target.
195
+ @param {Function(event, [element], [data])} listener
196
+ The listener function that should be called.
197
+
198
+ The function takes the affected element as the first argument).
199
+ If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON
200
+ and passed as a second argument.
201
+ @return {Function()}
202
+ A function that unbinds the event listeners when called.
203
+ @stable
204
+ ###
205
+ $bind = (args...) ->
206
+ bindNow(args, jQuery: true)
207
+
208
+ bindNow = (args, options) ->
209
+ # Silently discard any event handlers that are registered on unsupported
210
+ # browsers and return a no-op destructor
211
+ return (->) unless up.browser.isSupported()
212
+
213
+ up.EventListener.bind(args, options)
214
+
215
+ ###**
216
+ Unbinds an event listener previously bound with [`up.on()`](/up.on).
217
+
218
+ \#\#\# Example
219
+
220
+ Let's say you are listing to clicks on `.button` elements:
221
+
222
+ var listener = function() { ... }
223
+ up.on('click', '.button', listener)
224
+
225
+ You can stop listening to these events like this:
226
+
227
+ up.off('click', '.button', listener)
228
+
229
+ Note that you need to pass `up.off()` a reference to the same listener function
230
+ that was passed to `up.on()` earlier.
231
+
232
+ @function up.off
233
+ @stable
234
+ ###
235
+ unbind = (args...) ->
236
+ up.EventListener.unbind(args)
237
+
238
+ ###**
239
+ Emits a event with the given name and properties.
240
+
241
+ The event will be triggered as an event on `document` or on the given element.
242
+
243
+ Other code can subscribe to events with that name using
244
+ [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
245
+ or [`up.on()`](/up.on).
246
+
247
+ \#\#\# Example
248
+
249
+ up.on('my:event', function(event) {
250
+ console.log(event.foo)
251
+ })
252
+
253
+ up.emit('my:event', { foo: 'bar' })
254
+ // Prints "bar" to the console
255
+
256
+ @function up.emit
257
+ @param {Element|jQuery} [target=document]
258
+ The element on which the event is triggered.
259
+
260
+ If omitted, the event will be emitted on the `document`.
261
+ @param {string} eventName
262
+ The name of the event.
263
+ @param {Object} [eventProps={}]
264
+ A list of properties to become part of the event object
265
+ that will be passed to listeners. Note that the event object
266
+ will by default include properties like `preventDefault()`
267
+ or `stopPropagation()`.
268
+ @param {string|Array} [eventProps.log=false]
269
+ A message to print to the console when the event is emitted.
270
+
271
+ Pass `true` to print a default message
272
+ @param {Element|jQuery} [eventProps.target=document]
273
+ The element on which the event is triggered.
274
+ @stable
275
+ ###
276
+ emit = (args...) ->
277
+ if args[0].addEventListener
278
+ target = args.shift()
279
+ else if u.isJQuery(args[0])
280
+ target = e.get(args.shift())
281
+
282
+ eventName = args[0]
283
+ eventProps = args[1] || {}
284
+
285
+ if targetFromProps = u.pluckKey(eventProps, 'target')
286
+ target = targetFromProps
287
+ target ?= document
288
+
289
+ logEmission(eventName, eventProps)
290
+
291
+ event = buildEvent(eventName, eventProps)
292
+ target.dispatchEvent(event)
293
+
294
+ return event
295
+
296
+ buildEvent = (name, props) ->
297
+ event = document.createEvent('Event')
298
+ event.initEvent(name, true, true) # name, bubbles, cancelable
299
+ u.assign(event, props)
300
+
301
+ # IE11 does not set { defaultPrevented: true } after #preventDefault()
302
+ # was called on a custom event.
303
+ # See discussion here: https://stackoverflow.com/questions/23349191
304
+ if up.browser.isIE11()
305
+ event.preventDefault = ->
306
+ Object.defineProperty(event, 'defaultPrevented', get: -> true)
307
+
308
+ event
309
+
310
+ logEmission = (eventName, eventProps) ->
311
+ return unless up.log.isEnabled()
312
+
313
+ message = u.pluckKey(eventProps, 'log')
314
+
315
+ if u.isArray(message)
316
+ [message, messageArgs...] = message
317
+ else
318
+ messageArgs = []
319
+
320
+ if u.isString(message)
321
+ if u.isPresent(eventProps)
322
+ up.puts "#{message} (%s (%o))", messageArgs..., eventName, eventProps
323
+ else
324
+ up.puts "#{message} (%s)", messageArgs..., eventName
325
+ else if message == true
326
+ if u.isPresent(eventProps)
327
+ up.puts 'Event %s (%o)', eventName, eventProps
328
+ else
329
+ up.puts 'Event %s', eventName
330
+
331
+ ###**
332
+ [Emits an event](/up.emit) and returns whether no listener
333
+ has prevented the default action.
334
+
335
+ @function up.event.nobodyPrevents
336
+ @param {string} eventName
337
+ @param {Object} eventProps
338
+ @param {string|Array} [eventProps.log]
339
+ @return {boolean}
340
+ whether no listener has prevented the default action
341
+ @experimental
342
+ ###
343
+ nobodyPrevents = (args...) ->
344
+ event = emit(args...)
345
+ not event.defaultPrevented
346
+
347
+ ###**
348
+ [Emits](/up.emit) the given event and returns a promise
349
+ that will be fulfilled if no listener has prevented the default action.
350
+
351
+ If any listener prevented the default listener
352
+ the returned promise will never be resolved.
353
+
354
+ @function up.event.whenEmitted
355
+ @param {string} eventName
356
+ @param {Object} eventProps
357
+ @param {string|Array} [eventProps.message]
358
+ @return {Promise}
359
+ @internal
360
+ ###
361
+ whenEmitted = (args...) ->
362
+ new Promise (resolve, reject) ->
363
+ if nobodyPrevents(args...)
364
+ resolve()
365
+ else
366
+ reject(new Error("Event #{args[0]} was prevented"))
367
+
368
+ ###**
369
+ Registers an event listener to be called when the user
370
+ presses the `Escape` key.
371
+
372
+ @function up.event.onEscape
373
+ @param {Function(event)} listener
374
+ The listener function to register.
375
+ @return {Function()}
376
+ A function that unbinds the event listeners when called.
377
+ @experimental
378
+ ###
379
+ onEscape = (listener) ->
380
+ bind('keydown', 'body', (event) ->
381
+ if u.escapePressed(event)
382
+ listener(event)
383
+ )
384
+
385
+ ###**
386
+ Stops the given event from propagating and prevents the default action.
387
+
388
+ @function up.event.halt
389
+ @internal
390
+ ###
391
+ halt = (event) ->
392
+ event.stopImmediatePropagation()
393
+ event.stopPropagation()
394
+ event.preventDefault()
395
+
396
+ ###**
397
+ @function up.event.consumeAction
398
+ @internal
399
+ ###
400
+ consumeAction = (event) ->
401
+ # Halt the event chain to stop duplicate processing of this user interaction.
402
+ halt(event)
403
+ unless event.type == 'up:action:consumed'
404
+ # Although we have consumed this action and halted the event chain,
405
+ # other components might still need to react. E.g. a popup needs to close when
406
+ # an outside link consumes the user click. So we emit another event for that.
407
+ emit(event.target, 'up:action:consumed', log: false)
408
+
409
+ onReady = (callback) ->
410
+ # Values are "loading", "interactive" and "completed".
411
+ # https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
412
+ if document.readyState != 'loading'
413
+ callback()
414
+ else
415
+ document.addEventListener('DOMContentLoaded', callback)
416
+
417
+ bind 'up:framework:reset', reset
418
+
419
+ <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %>
420
+ on: bind # can't name symbols `on` in Coffeescript
421
+ $on: $bind
422
+ off: unbind # can't name symbols `off` in Coffeescript
423
+ emit: emit
424
+ nobodyPrevents: nobodyPrevents
425
+ whenEmitted: whenEmitted
426
+ onEscape: onEscape
427
+ halt: halt
428
+ consumeAction: consumeAction
429
+ onReady: onReady
430
+
431
+ up.on = up.event.on
432
+ up.$on = up.event.$on
433
+ up.off = up.event.off
434
+ up.$off = up.event.off # it's the same as up.off()
435
+ up.emit = up.event.emit
436
+
437
+ up.legacy.renamedModule 'bus', 'event'
@@ -2,43 +2,49 @@
2
2
  Navigation feedback
3
3
  ===================
4
4
 
5
- Unpoly automatically adds the class [`.up-active`](/a.up-active) to links or forms while they are loading.
6
-
7
- By marking navigation elements as [`[up-nav]`](/up-nav), contained links that point to the current location
8
- automatically get the [`.up-current`](/up-nav-a.up-current) class.
9
-
10
- You should style [`.up-active`](/a.up-active) and [`.up-current`](/up-nav a.up-current) with CSS to
5
+ The `up.feedback` module adds useful CSS classes to links while they are loading,
6
+ or when they point to the current URL. By styling these classes you may
11
7
  provide instant feedback to user interactions. This improves the perceived speed of your interface.
12
8
 
9
+
13
10
  \#\#\# Example
14
11
 
15
12
  Let's say we have an navigation bar with two links, pointing to `/foo` and `/bar` respectively:
16
13
 
17
- <a href="/foo" up-follow>Foo</a>
18
- <a href="/bar" up-follow>Bar</a>
14
+ <div up-nav>
15
+ <a href="/foo" up-follow>Foo</a>
16
+ <a href="/bar" up-follow>Bar</a>
17
+ </div>
19
18
 
20
- If the current URL is `/foo`, the first link is automatically marked with an [`up-current`](/a.up-current) class:
19
+ If the current URL is `/foo`, the first link is automatically marked with an [`.up-current`](/a.up-current) class:
21
20
 
22
- <a href="/foo" up-follow class="up-current">Foo</a>
23
- <a href="/bar" up-follow>Bar</a>
21
+ <div up-nav>
22
+ <a href="/foo" up-follow class="up-current">Foo</a>
23
+ <a href="/bar" up-follow>Bar</a>
24
+ </div>
24
25
 
25
26
  When the user clicks on the `/bar` link, the link will receive the [`up-active`](/a.up-active) class while it is waiting
26
27
  for the server to respond:
27
28
 
28
- <a href="/foo" up-follow class="up-current">Foo</a>
29
- <a href="/bar" up-follow class="up-active">Bar</a>
29
+ <div up-nav>
30
+ <a href="/foo" up-follow class="up-current">Foo</a>
31
+ <a href="/bar" up-follow class="up-active">Bar</a>
32
+ </div>
30
33
 
31
34
  Once the response is received the URL will change to `/bar` and the `up-active` class is removed:
32
35
 
33
- <a href="/foo" up-follow>Foo</a>
34
- <a href="/bar" up-follow class="up-current">Bar</a>
36
+ <div up-nav>
37
+ <a href="/foo" up-follow>Foo</a>
38
+ <a href="/bar" up-follow class="up-current">Bar</a>
39
+ </div>
35
40
 
36
41
 
37
- @class up.feedback
42
+ @module up.feedback
38
43
  ###
39
- up.feedback = (($) ->
44
+ up.feedback = do ->
40
45
 
41
46
  u = up.util
47
+ e = up.element
42
48
 
43
49
  ###**
44
50
  Sets default options for this module.
@@ -50,7 +56,7 @@ up.feedback = (($) ->
50
56
  An array of CSS selectors that match [navigation components](/up-nav).
51
57
  @stable
52
58
  ###
53
- config = u.config
59
+ config = new up.Config
54
60
  currentClasses: ['up-current']
55
61
  navs: ['[up-nav]']
56
62
 
@@ -72,25 +78,23 @@ up.feedback = (($) ->
72
78
  if u.isPresent(url)
73
79
  u.normalizeUrl(url, stripTrailingSlash: true)
74
80
 
75
- NORMALIZED_SECTION_URLS_KEY = 'up-normalized-urls'
76
-
77
- sectionUrls = ($section) ->
81
+ sectionUrls = (section) ->
78
82
  # Check if we have computed the URLs before.
79
83
  # Computation is sort of expensive (multiplied by number of links),
80
84
  # so we cache the results in a data attribute.
81
- unless urls = $section.data(NORMALIZED_SECTION_URLS_KEY)
82
- urls = buildSectionUrls($section)
83
- $section.data(NORMALIZED_SECTION_URLS_KEY, urls)
85
+ unless urls = section.upNormalizedUrls
86
+ urls = buildSectionUrls(section)
87
+ section.upNormalizedUrls = urls
84
88
  urls
85
89
 
86
- buildSectionUrls = ($section) ->
90
+ buildSectionUrls = (section) ->
87
91
  urls = []
88
92
 
89
93
  # A link with an unsafe method will never be higlighted with .up-current,
90
94
  # so we cache an empty array.
91
- if up.link.isSafe($section)
95
+ if up.link.isSafe(section)
92
96
  for attr in ['href', 'up-href', 'up-alias']
93
- if value = u.presentAttr($section, attr)
97
+ if value = section.getAttribute(attr)
94
98
  # Allow to include multiple space-separated URLs in [up-alias]
95
99
  for url in u.splitValues(value)
96
100
  unless url == '#'
@@ -111,37 +115,33 @@ up.feedback = (($) ->
111
115
  updateAllNavigationSectionsIfLocationChanged = ->
112
116
  previousUrlSet = currentUrlSet
113
117
  currentUrlSet = buildCurrentUrlSet()
114
- unless currentUrlSet.isEqual(previousUrlSet)
115
- updateAllNavigationSections($('body'))
118
+ unless u.isEqual(currentUrlSet, previousUrlSet)
119
+ updateAllNavigationSections(document.body)
116
120
 
117
- updateAllNavigationSections = ($root) ->
118
- $navs = u.selectInSubtree($root, navSelector())
119
- $sections = u.selectInSubtree($navs, SELECTOR_LINK)
120
- updateCurrentClassForLinks($sections)
121
+ updateAllNavigationSections = (root) ->
122
+ navs = e.subtree(root, navSelector())
123
+ sections = u.flatMap navs, (nav) -> e.subtree(nav, SELECTOR_LINK)
124
+ updateCurrentClassForLinks(sections)
121
125
 
122
- updateNavigationSectionsInNewFragment = ($fragment) ->
123
- if $fragment.closest(navSelector()).length
126
+ updateNavigationSectionsInNewFragment = (fragment) ->
127
+ if e.closest(fragment, navSelector())
124
128
  # If the new fragment is an [up-nav], or if the new fragment is a child of an [up-nav],
125
129
  # all links in the new fragment are considered sections that we need to update.
126
130
  # Note that:
127
131
  # - The [up-nav] element might not be part of this update.
128
132
  # It might already be in the DOM, and only a child was updated.
129
- # - The $fragment might be a link itself
130
- # - We do not need to update sibling links of $fragment that have been processed before.
131
- $sections = u.selectInSubtree($fragment, SELECTOR_LINK)
132
- updateCurrentClassForLinks($sections)
133
+ # - The fragment might be a link itself
134
+ # - We do not need to update sibling links of fragment that have been processed before.
135
+ sections = e.subtree(fragment, SELECTOR_LINK)
136
+ updateCurrentClassForLinks(sections)
133
137
  else
134
- updateAllNavigationSections($fragment)
138
+ updateAllNavigationSections(fragment)
135
139
 
136
- updateCurrentClassForLinks = ($links) ->
140
+ updateCurrentClassForLinks = (links) ->
137
141
  currentUrlSet ||= buildCurrentUrlSet()
138
- u.each $links, (link) ->
139
- $link = $(link)
140
- urls = sectionUrls($link)
142
+ u.each links, (link) ->
143
+ urls = sectionUrls(link)
141
144
 
142
- # We use Element#classList to manipulate classes instead of jQuery's
143
- # addClass and removeClass. Since we are in an inner loop, we want to
144
- # be as fast as we can.
145
145
  classList = link.classList
146
146
  if currentUrlSet.matchesAny(urls)
147
147
  for klass in config.currentClasses
@@ -157,12 +157,11 @@ up.feedback = (($) ->
157
157
  @param {string|Element|jQuery} elementOrSelector
158
158
  @internal
159
159
  ###
160
- findActivatableArea = (elementOrSelector) ->
161
- $area = $(elementOrSelector)
162
- if $area.is(SELECTOR_LINK)
163
- # Try to enlarge links that are expanded with [up-expand] on a surrounding container.
164
- $area = u.presence($area.parent(SELECTOR_LINK)) || $area
165
- $area
160
+ findActivatableArea = (element) ->
161
+ element = e.get(element)
162
+ # Try to enlarge links that are expanded with [up-expand] on a surrounding container.
163
+ # Note that the expression below is not the same as e.closest(area, SELECTOR_LINK)!
164
+ e.ancestor(element, SELECTOR_LINK) || element
166
165
 
167
166
  ###**
168
167
  Marks the given element as currently loading, by assigning the CSS class [`up-active`](/a.up-active).
@@ -175,39 +174,22 @@ up.feedback = (($) ->
175
174
 
176
175
  \#\#\# Example
177
176
 
178
- var $button = $('button');
179
- $button.on('click', function() {
180
- up.feedback.start($button, function() {
181
- // the .up-active class will be removed when this promise resolves:
182
- return up.request(...);
183
- });
184
- });
177
+ var button = document.querySelector('button')
178
+
179
+ button.addEventListener('click', () => {
180
+ up.feedback.start(button)
181
+ up.request(...).then(() => {
182
+ up.feedback.stop(button)
183
+ })
184
+ })
185
185
 
186
186
  @method up.feedback.start
187
- @param {Element|jQuery|string} elementOrSelector
187
+ @param {Element|jQuery|string} element
188
188
  The element to mark as active
189
- @param {Object} [options.preload]
190
- If set to `false`, the element will not be marked as loading.
191
- @param {Function} [action]
192
- An optional function to run while the element is marked as loading.
193
- The function must return a promise.
194
- Once the promise resolves, the element will be [marked as no longer loading](/up.feedback.stop).
195
189
  @internal
196
190
  ###
197
- start = (args...) ->
198
- elementOrSelector = args.shift()
199
- action = args.pop()
200
- options = u.options(args[0])
201
- $element = findActivatableArea(elementOrSelector)
202
- unless options.preload
203
- $element.addClass(CLASS_ACTIVE)
204
- if action
205
- promise = action()
206
- if u.isPromise(promise)
207
- u.always promise, -> stop($element)
208
- else
209
- up.warn('Expected block to return a promise, but got %o', promise)
210
- promise
191
+ start = (element) ->
192
+ findActivatableArea(element).classList.add(CLASS_ACTIVE)
211
193
 
212
194
  ###**
213
195
  Links that are currently [loading through Unpoly](/form-up-target)
@@ -275,13 +257,12 @@ up.feedback = (($) ->
275
257
  Use this function if you make custom network calls from your own JavaScript code.
276
258
 
277
259
  @function up.feedback.stop
278
- @param {jQuery} event.$element
260
+ @param {Element|jQuery|string} element
279
261
  The link or form that has finished loading.
280
262
  @internal
281
263
  ###
282
- stop = (elementOrSelector) ->
283
- $element = findActivatableArea(elementOrSelector)
284
- $element.removeClass(CLASS_ACTIVE)
264
+ stop = (element) ->
265
+ findActivatableArea(element).classList.remove(CLASS_ACTIVE)
285
266
 
286
267
  ###**
287
268
  Marks this element as a navigation component, such as a menu or navigation bar.
@@ -333,10 +314,10 @@ up.feedback = (($) ->
333
314
  - the link's `up-href` attribute
334
315
  - a space-separated list of URLs in the link's `up-alias` attribute
335
316
 
336
- \#\#\# Matching URL by prefix
317
+ \#\#\# Matching URL by pattern
337
318
 
338
- You can mark a link as `.up-current` whenever the current URL matches a prefix.
339
- To do so, end the `up-alias` attribute in an asterisk (`*`).
319
+ You can mark a link as `.up-current` whenever the current URL matches a prefix or suffix.
320
+ To do so, include an asterisk (`*`) in the `up-alias` attribute.
340
321
 
341
322
  For instance, the following `[up-nav]` link is highlighted for both `/reports` and `/reports/123`:
342
323
 
@@ -356,11 +337,11 @@ up.feedback = (($) ->
356
337
  ###
357
338
 
358
339
  # Even when the modal or popup does not change history, we consider the URLs of the content it displays.
359
- up.on 'up:history:pushed up:history:replaced up:history:restored up:modal:opened up:modal:closed up:popup:opened up:popup:closed', (event) ->
340
+ up.on 'up:history:pushed up:history:replaced up:history:restored up:modal:opened up:modal:closed up:popup:opened up:popup:closed', (event) -> # take 1 arg to prevent data parsing
360
341
  updateAllNavigationSectionsIfLocationChanged()
361
342
 
362
- up.on 'up:fragment:inserted', (event, $newFragment) ->
363
- updateNavigationSectionsInNewFragment($newFragment)
343
+ up.on 'up:fragment:inserted', (event, newFragment) ->
344
+ updateNavigationSectionsInNewFragment(newFragment)
364
345
 
365
346
  # The framework is reset between tests
366
347
  up.on 'up:framework:reset', reset
@@ -369,6 +350,4 @@ up.feedback = (($) ->
369
350
  start: start
370
351
  stop: stop
371
352
 
372
- )(jQuery)
373
-
374
- up.deprecateRenamedModule 'navigation', 'feedback'
353
+ up.legacy.renamedModule 'navigation', 'feedback'