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
@@ -1,13 +1,12 @@
1
1
  u = up.util
2
+ e = up.element
2
3
 
3
4
  class up.CssTransition
4
5
 
5
- constructor: ($element, lastFrame, options) ->
6
- @$element = $element
7
- @element = u.element($element)
8
- @lastFrameCamel = u.camelCaseKeys(lastFrame)
9
- @lastFrameKebab = u.kebabCaseKeys(lastFrame)
6
+ constructor: (@element, @lastFrameKebab, options) ->
10
7
  @lastFrameKeysKebab = Object.keys(@lastFrameKebab)
8
+ if u.some(@lastFrameKeysKebab, (key) -> key.match(/A-Z/))
9
+ up.fail('Animation keys must be kebab-case')
11
10
  @finishEvent = options.finishEvent
12
11
  @duration = options.duration
13
12
  @delay = options.delay
@@ -35,11 +34,7 @@ class up.CssTransition
35
34
 
36
35
  listenToFinishEvent: =>
37
36
  if @finishEvent
38
- @$element.on(@finishEvent, @onFinishEvent)
39
-
40
- stopListenToFinishEvent: =>
41
- if @finishEvent
42
- @$element.off(@finishEvent, @onFinishEvent)
37
+ @stopListenToFinishEvent = @element.addEventListener(@finishEvent, @onFinishEvent)
43
38
 
44
39
  onFinishEvent: (event) =>
45
40
  # don't waste time letting the event bubble up the DOM
@@ -48,17 +43,14 @@ class up.CssTransition
48
43
 
49
44
  startFallbackTimer: =>
50
45
  timingTolerance = 100
51
- @fallbackTimer = u.setTimer (@totalDuration + timingTolerance), =>
46
+ @fallbackTimer = u.timer (@totalDuration + timingTolerance), =>
52
47
  @finish()
53
48
 
54
49
  stopFallbackTimer: =>
55
50
  clearTimeout(@fallbackTimer)
56
51
 
57
52
  listenToTransitionEnd: =>
58
- @$element.on 'transitionend', @onTransitionEnd
59
-
60
- stopListenToTransitionEnd: =>
61
- @$element.off 'transitionend', @onTransitionEnd
53
+ @stopListenToTransitionEnd = up.on(@element, 'transitionend', @onTransitionEnd)
62
54
 
63
55
  onTransitionEnd: (event) =>
64
56
  # Check if the transitionend event was caused by our own transition,
@@ -70,7 +62,7 @@ class up.CssTransition
70
62
  elapsed = new Date() - @startTime
71
63
  return unless elapsed > 0.25 * @totalDuration
72
64
 
73
- completedPropertyKebab = event.originalEvent.propertyName
65
+ completedPropertyKebab = event.propertyName
74
66
  return unless u.contains(@lastFrameKeysKebab, completedPropertyKebab)
75
67
 
76
68
  @finish()
@@ -81,47 +73,46 @@ class up.CssTransition
81
73
  @finished = true
82
74
 
83
75
  @stopFallbackTimer()
84
- @stopListenToFinishEvent()
85
- @stopListenToTransitionEnd()
76
+ @stopListenToFinishEvent?()
77
+ @stopListenToTransitionEnd?()
86
78
 
87
79
  # Cleanly finish our own transition so the old transition
88
80
  # (or any other transition set right after that) will be able to take effect.
89
- u.concludeCssTransition(@element)
81
+ e.concludeCssTransition(@element)
90
82
 
91
83
  @resumeOldTransition()
92
84
 
93
85
  @deferred.resolve()
94
86
 
95
87
  pauseOldTransition: =>
96
- oldTransition = u.readComputedStyle(@element, [
88
+ oldTransition = e.style(@element, [
97
89
  'transitionProperty',
98
90
  'transitionDuration',
99
91
  'transitionDelay',
100
92
  'transitionTimingFunction'
101
93
  ])
102
94
 
103
- if u.hasCssTransition(oldTransition)
95
+ if e.hasCssTransition(oldTransition)
104
96
  # Freeze the previous transition at its current place, by setting the currently computed,
105
97
  # animated CSS properties as inline styles. Transitions on all properties will not be frozen,
106
98
  # since that would involve setting every single CSS property as an inline style.
107
99
  unless oldTransition.transitionProperty == 'all'
108
100
  oldTransitionProperties = oldTransition.transitionProperty.split(/\s*,\s*/)
109
- oldTransitionFrameKebab = u.readComputedStyle(@element, oldTransitionProperties)
110
- oldTransitionFrameCamel = u.camelCaseKeys(oldTransitionFrameKebab)
111
- @setOldTransitionTargetFrame = u.writeTemporaryStyle(@element, oldTransitionFrameCamel)
101
+ oldTransitionFrameKebab = e.style(@element, oldTransitionProperties)
102
+ @setOldTransitionTargetFrame = e.setTemporaryStyle(@element, oldTransitionFrameKebab)
112
103
 
113
104
  # Stop the existing CSS transition so it does not emit transitionEnd events
114
- @setOldTransition = u.concludeCssTransition(@element)
105
+ @setOldTransition = e.concludeCssTransition(@element)
115
106
 
116
107
  resumeOldTransition: =>
117
108
  @setOldTransitionTargetFrame?()
118
109
  @setOldTransition?()
119
110
 
120
111
  startMotion: =>
121
- u.writeInlineStyle @element,
112
+ e.setStyle @element,
122
113
  transitionProperty: Object.keys(@lastFrameKebab).join(', ')
123
114
  transitionDuration: "#{@duration}ms"
124
115
  transitionDelay: "#{@delay}ms"
125
116
  transitionTimingFunction: @easing
126
- u.writeInlineStyle(@element, @lastFrameCamel)
117
+ e.setStyle(@element, @lastFrameKebab)
127
118
 
@@ -0,0 +1,39 @@
1
+ u = up.util
2
+
3
+ ###**
4
+ A linear task queue whose (2..n)th tasks can be changed at any time.
5
+
6
+ @function up.DivertibleChain
7
+ @internal
8
+ ###
9
+ class up.DivertibleChain
10
+
11
+ constructor: ->
12
+ @reset()
13
+
14
+ reset: =>
15
+ @queue = []
16
+ @currentTask = undefined
17
+
18
+ promise: =>
19
+ lastTask = u.last(@allTasks())
20
+ lastTask?.promise || Promise.resolve()
21
+
22
+ allTasks: =>
23
+ tasks = []
24
+ tasks.push(@currentTask) if @currentTask
25
+ tasks = tasks.concat(@queue)
26
+ tasks
27
+
28
+ poke: =>
29
+ unless @currentTask # don't start a new task while we're still running one
30
+ if @currentTask = @queue.shift()
31
+ promise = @currentTask()
32
+ u.always promise, =>
33
+ @currentTask = undefined
34
+ @poke()
35
+
36
+ asap: (newTasks...) =>
37
+ @queue = u.map(newTasks, u.previewable)
38
+ @poke()
39
+ @promise()
@@ -0,0 +1,116 @@
1
+ u = up.util
2
+ e = up.element
3
+
4
+ class up.EventListener
5
+
6
+ constructor: (@element, @eventName, @selector, @callback, options = {}) ->
7
+ { @jQuery } = options
8
+ @key = @constructor.key(@eventName, @selector, @callback)
9
+ @isDefault = up.framework.isBooting()
10
+
11
+ bind: ->
12
+ map = (@element.upEventListeners ||= {})
13
+ if map[@key]
14
+ up.fail('up.on(): The %o callback %o cannot be registered more than once', @eventName, @callback)
15
+ map[@key] = this
16
+ @element.addEventListener(@eventName, @nativeCallback)
17
+
18
+ unbind: =>
19
+ if map = @element.upEventListeners
20
+ delete map[@key]
21
+ @element.removeEventListener(@eventName, @nativeCallback)
22
+
23
+ nativeCallback: (event) =>
24
+ # 1. Since we're listing on `document`, event.currentTarget is now `document`.
25
+ # 2. event.target is the element that received an event, which might be a
26
+ # child of `selector`.
27
+ # 3. There is only a single event bubbling up the DOM, so we are only called once.
28
+
29
+ element = event.target
30
+ element = e.closest(element, @selector) if @selector
31
+
32
+ if element
33
+ elementArg = if @jQuery then jQuery(element) else element
34
+ args = [event, elementArg]
35
+
36
+ # Do not retrieve and parse [up-data] unless the listener function
37
+ # expects a third argument. Note that we must pass data for an argument
38
+ # count of 0, since then the function might take varargs.
39
+ expectedArgCount = @callback.length
40
+
41
+ unless expectedArgCount == 1 || expectedArgCount == 2
42
+ data = up.syntax.data(element)
43
+ args.push(data)
44
+
45
+ @callback.apply(element, args)
46
+
47
+ ###
48
+ Parses the following arg variants into an object:
49
+
50
+ - [elements, eventNames, selector, callback]
51
+ - [elements, eventNames, callback]
52
+ - [ eventNames, selector, callback]
53
+ - [ eventNames, callback]
54
+
55
+ @function up.EventListener#parseArgs
56
+ @internal
57
+ ###
58
+ @parseArgs = (args) ->
59
+ args = u.copy(args)
60
+
61
+ # A callback function is given in all arg variants.
62
+ callback = args.pop()
63
+ # Give the callback function a numeric identifier so it
64
+ # can become part of the upEventListeners key.
65
+ callback.upUid ||= u.uid()
66
+
67
+ # The user can pass an element (or the document, or the window) as the
68
+ # first argument. If omitted, the listener will bind to the document.
69
+ if args[0].addEventListener
70
+ elements = [args.shift()]
71
+ else if u.isJQuery(args[0]) || (u.isList(args[0]) && args[0][0].addEventListener)
72
+ elements = args.shift()
73
+ else
74
+ elements = [document]
75
+
76
+ # Event names are given in all arg variants
77
+ eventNames = u.splitValues(args.shift())
78
+ # eventNames = u.map(eventNames, up.legacy.fixEventName)
79
+
80
+ # A selector is given if the user wants to delegate events.
81
+ # It might be undefined.
82
+ selector = args[0]
83
+
84
+ { elements, eventNames, selector, callback }
85
+
86
+ @bind: (args, options) ->
87
+ parsed = @parseArgs(args)
88
+ unbindFns = []
89
+
90
+ for element in parsed.elements
91
+ for eventName in parsed.eventNames
92
+ listener = new @(element, eventName, parsed.selector, parsed.callback, options)
93
+ listener.bind()
94
+ unbindFns.push(listener.unbind)
95
+
96
+ u.sequence(unbindFns)
97
+
98
+ @key: (eventName, selector, callback) ->
99
+ [eventName, selector, callback.upUid].join('|')
100
+
101
+ @unbind: (args) ->
102
+ parsed = @parseArgs(args)
103
+ for element in parsed.elements
104
+ map = element.upEventListeners
105
+ for eventName in parsed.eventNames
106
+ key = @key(eventName, parsed.selector, parsed.callback)
107
+ if map && (listener = map[key])
108
+ listener.unbind()
109
+
110
+ @unbindNonDefault: (element) ->
111
+ if map = element.upEventListeners
112
+ listeners = u.values(map)
113
+ for listener in listeners
114
+ # Calling unbind() also removes the listener from element.upEventListeners
115
+ unless listener.isDefault
116
+ listener.unbind()
@@ -2,25 +2,25 @@ u = up.util
2
2
 
3
3
  class up.ExtractCascade
4
4
 
5
- constructor: (selector, options) ->
5
+ constructor: (selectorOrElement, options) ->
6
6
  @options = u.options(options, humanizedTarget: 'selector', layer: 'auto')
7
- @options.transition = u.option(@options.transition, @options.animation)
8
- @options.hungry = u.option(@options.hungry, true)
7
+ @options.transition ?= @options.animation
8
+ @options.hungry ?= true
9
9
 
10
- @candidates = @buildCandidates(selector)
10
+ @candidates = @buildCandidates(selectorOrElement)
11
11
  @plans = u.map @candidates, (candidate, i) =>
12
12
  planOptions = u.copy(@options)
13
13
  if i > 0
14
14
  # If we're using a fallback (any candidate that's not the first),
15
15
  # the original transition might no longer be appropriate.
16
- planOptions.transition = u.option(up.dom.config.fallbackTransition, @options.transition)
16
+ planOptions.transition = up.fragment.config.fallbackTransition ? @options.transition
17
17
  new up.ExtractPlan(candidate, planOptions)
18
18
 
19
19
  buildCandidates: (selector) ->
20
- candidates = [selector, @options.fallback, up.dom.config.fallbacks]
20
+ candidates = [selector, @options.fallback, up.fragment.config.fallbacks]
21
21
  candidates = u.flatten(candidates)
22
22
  # Remove undefined, null and false from the list
23
- candidates = u.select(candidates, u.isTruthy)
23
+ candidates = u.filter(candidates, u.isTruthy)
24
24
  candidates = u.uniq(candidates)
25
25
 
26
26
  if @options.fallback == false || @options.provideTarget
@@ -39,7 +39,7 @@ class up.ExtractCascade
39
39
  @detectPlan('matchExists')
40
40
 
41
41
  detectPlan: (checker) =>
42
- u.detect @plans, (plan) -> plan[checker]()
42
+ u.find @plans, (plan) -> plan[checker]()
43
43
 
44
44
  bestPreflightSelector: =>
45
45
  if @options.provideTarget
@@ -1,4 +1,5 @@
1
1
  u = up.util
2
+ e = up.element
2
3
 
3
4
  class up.ExtractPlan
4
5
 
@@ -9,25 +10,25 @@ class up.ExtractPlan
9
10
  @transition = options.transition
10
11
  @response = options.response
11
12
  @oldLayer = options.layer
12
- originalSelector = up.dom.resolveSelector(selector, @origin)
13
+ originalSelector = e.resolveSelector(selector, @origin)
13
14
  @parseSteps(originalSelector)
14
15
 
15
16
  findOld: =>
16
17
  u.each @steps, (step) =>
17
- step.$old = up.dom.first(step.selector, layer: @oldLayer)
18
+ step.oldElement = up.fragment.first(step.selector, layer: @oldLayer)
18
19
 
19
20
  findNew: =>
20
21
  u.each @steps, (step) =>
21
22
  # The response has no layers. It's always just the page.
22
- step.$new = @response.first(step.selector)
23
+ step.newElement = @response.first(step.selector)
23
24
 
24
25
  oldExists: =>
25
26
  @findOld()
26
- u.all @steps, (step) -> step.$old
27
+ u.every @steps, (step) -> step.oldElement
27
28
 
28
29
  newExists: =>
29
30
  @findNew()
30
- u.all @steps, (step) -> step.$new
31
+ u.every @steps, (step) -> step.newElement
31
32
 
32
33
  matchExists: =>
33
34
  @oldExists() && @newExists()
@@ -42,16 +43,16 @@ class up.ExtractPlan
42
43
 
43
44
  # When two replacements target the same element, we would process
44
45
  # the same content twice. We never want that, so we only keep the first step.
45
- compressed = u.uniqBy(compressed, (step) -> step.$old[0])
46
+ compressed = u.uniqBy(compressed, (step) -> step.oldElement)
46
47
 
47
- compressed = u.select compressed, (candidateStep, candidateIndex) =>
48
- u.all compressed, (rivalStep, rivalIndex) =>
48
+ compressed = u.filter compressed, (candidateStep, candidateIndex) =>
49
+ u.every compressed, (rivalStep, rivalIndex) =>
49
50
  if rivalIndex == candidateIndex
50
51
  true
51
52
  else
52
- candidateElement = candidateStep.$old[0]
53
- rivalElement = rivalStep.$old[0]
54
- rivalStep.pseudoClass || !$.contains(rivalElement, candidateElement)
53
+ candidateElement = candidateStep.oldElement
54
+ rivalElement = rivalStep.oldElement
55
+ rivalStep.pseudoClass || !rivalElement.contains(candidateElement)
55
56
 
56
57
  # If we revealed before, we should reveal now
57
58
  compressed[0].reveal = @steps[0].reveal
@@ -93,16 +94,15 @@ class up.ExtractPlan
93
94
  addHungrySteps: =>
94
95
  hungrySteps = []
95
96
  if @hungry
96
- $hungries = $(up.radio.hungrySelector())
97
- transition = u.option(up.radio.config.hungryTransition, @transition)
98
- for hungry in $hungries
99
- $hungry = $(hungry)
100
- selector = u.selectorForElement($hungry)
101
- if $newHungry = @response.first(selector)
97
+ hungries = e.all(up.radio.hungrySelector())
98
+ transition = up.radio.config.hungryTransition ? @transition
99
+ for hungry in hungries
100
+ selector = e.toSelector(hungry)
101
+ if newHungry = @response.first(selector)
102
102
  hungrySteps.push
103
103
  selector: selector
104
- $old: $hungry
105
- $new: $newHungry
104
+ oldElement: hungry
105
+ newElement: newHungry
106
106
  transition: transition
107
107
  reveal: false # we never auto-reveal a hungry element
108
108
  origin: null # don't let the hungry element auto-close a non-sticky modal or popup
@@ -1,25 +1,24 @@
1
1
  u = up.util
2
+ e = up.element
2
3
 
3
4
  class up.FieldObserver
4
5
 
5
- # Although (depending on the browser) we only need/receive either input or change,
6
- # we always bind to both events in case another script manually triggers it.
7
- CHANGE_EVENTS = 'input change'
8
-
9
- constructor: (@$field, options) ->
6
+ constructor: (fieldOrFields, options, @callback) ->
7
+ @fields = e.list(fieldOrFields)
10
8
  @delay = options.delay
11
- @callback = options.callback
9
+ @batch = options.batch
12
10
 
13
11
  start: =>
14
- # Don't use undefined since an unchecked checkbox actually has an undefined value
15
- @scheduledValue = null
16
- @processedValue = @readFieldValue()
12
+ @scheduledValues = null
13
+ @processedValues = @readFieldValues()
17
14
  @currentTimer = undefined
18
- @currentCallback = undefined
19
- @$field.on(CHANGE_EVENTS, @check)
15
+ @callbackRunning = false
16
+ # Although (depending on the browser) we only need/receive either input or change,
17
+ # we always bind to both events in case another script manually triggers it.
18
+ @unbind = up.on(@fields, 'input change', @check)
20
19
 
21
20
  stop: =>
22
- @$field.off(CHANGE_EVENTS, @check)
21
+ @unbind()
23
22
  @cancelTimer()
24
23
 
25
24
  cancelTimer: =>
@@ -27,31 +26,55 @@ class up.FieldObserver
27
26
  @currentTimer = undefined
28
27
 
29
28
  scheduleTimer: =>
30
- @currentTimer = u.setTimer @delay, =>
29
+ @cancelTimer()
30
+ @currentTimer = u.timer @delay, =>
31
31
  @currentTimer = undefined
32
32
  @requestCallback()
33
33
 
34
- isNewValue: (value) =>
35
- value != @processedValue && (@scheduledValue == null || @scheduledValue != value)
34
+ scheduleValues: (values) =>
35
+ @scheduledValues = values
36
+ @scheduleTimer()
37
+
38
+ isNewValues: (values) =>
39
+ !u.isEqual(values, @processedValues) && !u.isEqual(@scheduledValues, values)
36
40
 
37
41
  requestCallback: =>
38
- if @scheduledValue != null && !@currentTimer && !@currentCallback
39
- @processedValue = @scheduledValue
40
- @scheduledValue = null
41
- @currentCallback = => @callback.call(@$field.get(0), @processedValue, @$field)
42
- # If the callback returns a promise x, Promise.resolve(x) will wait for it
43
- callbackDone = Promise.resolve(@currentCallback())
44
- u.always callbackDone, =>
45
- # Don't use undefined since an unchecked checkbox actually has an undefined value
46
- @currentCallback = undefined
42
+ if @scheduledValues != null && !@currentTimer && !@callbackRunning
43
+ diff = @changedValues(@processedValues, @scheduledValues)
44
+ @processedValues = @scheduledValues
45
+ @scheduledValues = null
46
+ @callbackRunning = true
47
+
48
+ callbackReturnValues = []
49
+ if @batch
50
+ callbackReturnValues.push(@callback(diff))
51
+ else
52
+ for name, value of diff
53
+ callbackReturnValues.push(@callback(value, name))
54
+
55
+ # Promise.all() will wait for any promises that might be
56
+ # contained in the `callbackReturnValues` array.
57
+ callbacksDone = Promise.all(callbackReturnValues)
58
+
59
+ u.always callbacksDone, =>
60
+ @callbackRunning = false
47
61
  @requestCallback()
48
62
 
49
- readFieldValue: =>
50
- u.submittedValue(@$field)
63
+ changedValues: (previous, next) ->
64
+ changes = {}
65
+ keys = Object.keys(previous)
66
+ keys = keys.concat(Object.keys(next))
67
+ keys = u.uniq(keys)
68
+ for key in keys
69
+ previousValue = previous[key]
70
+ nextValue = next[key]
71
+ unless u.isEqual(previousValue, nextValue)
72
+ changes[key] = nextValue
73
+ changes
74
+
75
+ readFieldValues: =>
76
+ up.Params.fromFields(@fields).toObject()
51
77
 
52
78
  check: =>
53
- value = @readFieldValue()
54
- if @isNewValue(value)
55
- @scheduledValue = value
56
- @cancelTimer()
57
- @scheduleTimer()
79
+ values = @readFieldValues()
80
+ @scheduleValues(values) if @isNewValues(values)