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,105 @@
1
+ u = up.util
2
+ e = up.element
3
+
4
+ class up.Tether
5
+
6
+ constructor: (options) ->
7
+ @anchor = options.anchor
8
+
9
+ [@position, @align] = options.position.split('-')
10
+ if @align
11
+ up.legacy.warn('The position value %o is deprecated. Use %o instead.', options.position, @describeConstraints())
12
+ else
13
+ @align = options.align
14
+
15
+ @alignAxis = if @position == 'top' || @position == 'bottom' then 'horizontal' else 'vertical'
16
+
17
+ @viewport = up.viewport.closest(@anchor)
18
+ # The document viewport is <html> on some browsers, and we cannot attach children to that.
19
+ @parent = if @viewport == e.root() then document.body else @viewport
20
+
21
+ # If the offsetParent is within the viewport (or is the viewport)
22
+ # we can simply absolutely position it and it will move as the viewport scrolls.
23
+ # If not however, we have no choice but to move it on every scroll event.
24
+ @syncOnScroll = !@viewport.contains(@anchor.offsetParent)
25
+
26
+ @root = e.affix(@parent, '.up-bounds')
27
+ @setBoundsOffset(0, 0)
28
+
29
+ @changeEventSubscription('on')
30
+
31
+ destroy: ->
32
+ e.remove(@root)
33
+ @changeEventSubscription('off')
34
+
35
+ changeEventSubscription: (fn) ->
36
+ up[fn](window, 'resize', @scheduleSync)
37
+ up[fn](@viewport, 'scroll', @scheduleSync) if @syncOnScroll
38
+
39
+ scheduleSync: =>
40
+ clearTimeout(@syncTimer)
41
+ @syncTimer = u.task(@sync)
42
+
43
+ sync: =>
44
+ rootBox = @root.getBoundingClientRect()
45
+ anchorBox = @anchor.getBoundingClientRect()
46
+
47
+ left = undefined
48
+ top = undefined
49
+
50
+ switch @alignAxis
51
+ when 'horizontal'
52
+ top = switch @position
53
+ when 'top'
54
+ anchorBox.top - rootBox.height
55
+ when 'bottom'
56
+ anchorBox.top + anchorBox.height
57
+
58
+ left = switch @align
59
+ when 'left'
60
+ # anchored to anchor's left, grows to the right
61
+ anchorBox.left
62
+ when 'center'
63
+ # anchored to anchor's horizontal center, grows equally to left/right
64
+ anchorBox.left + 0.5 * (anchorBox.width - rootBox.width)
65
+ when 'right'
66
+ # anchored to anchor's right, grows to the left
67
+ anchorBox.left + anchorBox.width - rootBox.width
68
+
69
+ when 'vertical'
70
+ top = switch @align
71
+ when 'top'
72
+ # anchored to the top, grows to the bottom
73
+ anchorBox.top
74
+ when 'center'
75
+ # anchored to anchor's vertical center, grows equally to left/right
76
+ anchorBox.top + 0.5 * (anchorBox.height - rootBox.height)
77
+ when 'bottom'
78
+ # anchored to the bottom, grows to the top
79
+ anchorBox.top + anchorBox.height - rootBox.height
80
+
81
+ left = switch @position
82
+ when 'left'
83
+ anchorBox.left - rootBox.width
84
+ when 'right'
85
+ anchorBox.left + anchorBox.width
86
+
87
+ if u.isDefined(left) || u.isDefined(top)
88
+ @moveTo(left, top)
89
+ else
90
+ up.fail('Invalid tether constraints: %o', @describeConstraints())
91
+
92
+ describeConstraints: ->
93
+ { @position, @align }
94
+
95
+ moveTo: (targetLeft, targetTop) ->
96
+ rootBox = @root.getBoundingClientRect()
97
+ @setBoundsOffset(
98
+ targetLeft - rootBox.left + @offsetLeft,
99
+ targetTop - rootBox.top + @offsetTop
100
+ )
101
+
102
+ setBoundsOffset: (left, top) ->
103
+ @offsetLeft = left
104
+ @offsetTop = top
105
+ e.setStyle(@root, { left, top })
@@ -8,20 +8,25 @@ class up.UrlSet
8
8
  @urls = u.compact(@urls)
9
9
 
10
10
  matches: (testUrl) =>
11
- if testUrl.substr(-1) == '*'
12
- @doesMatchPrefix(testUrl.slice(0, -1))
11
+ if testUrl.indexOf('*') >= 0
12
+ @doesMatchPattern(testUrl)
13
13
  else
14
14
  @doesMatchFully(testUrl)
15
15
 
16
16
  doesMatchFully: (testUrl) =>
17
17
  u.contains(@urls, testUrl)
18
18
 
19
- doesMatchPrefix: (prefix) =>
20
- u.detect @urls, (url) ->
21
- url.indexOf(prefix) == 0
19
+ doesMatchPattern: (pattern) =>
20
+ placeholder = "__ASTERISK__"
21
+ pattern = pattern.replace(/\*/g, placeholder)
22
+ pattern = u.escapeRegexp(pattern)
23
+ pattern = pattern.replace(new RegExp(placeholder, 'g'), '.*?')
24
+ pattern = new RegExp('^' + pattern + '$')
25
+
26
+ u.find @urls, (url) -> pattern.test(url)
22
27
 
23
28
  matchesAny: (testUrls) =>
24
- u.detect(testUrls, @matches)
29
+ u.find(testUrls, @matches)
25
30
 
26
- isEqual: (otherSet) =>
31
+ "#{u.isEqual.key}": (otherSet) =>
27
32
  u.isEqual(@urls, otherSet?.urls)
@@ -0,0 +1,1126 @@
1
+ #= require ./classes/selector
2
+
3
+ ###**
4
+ DOM helpers
5
+ ===========
6
+
7
+ The `up.element` module offers functions for DOM manipulation and traversal.
8
+
9
+ It complements [native `Element` methods](https://www.w3schools.com/jsref/dom_obj_all.asp) and works across all [supported browsers](/up.browser).
10
+
11
+ @module up.element
12
+ ###
13
+ up.element = do ->
14
+
15
+ u = up.util
16
+
17
+ ###**
18
+ Returns a null-object that mostly behaves like an `Element`.
19
+
20
+ @function up.element.none()
21
+ @internal
22
+ ###
23
+ NONE = { getAttribute: -> undefined }
24
+
25
+ ###**
26
+ Matches all elements that have a descendant matching the given selector.
27
+
28
+ \#\#\# Example
29
+
30
+ `up.element.all('div:has(span)')` matches all `<div>` elements with at least one `<span>` among its descendants:
31
+
32
+ ```html
33
+ <div>
34
+ <span>Will be matched</span>
35
+ </div>
36
+ <div>
37
+ Will NOT be matched
38
+ </div>
39
+ <div>
40
+ <span>Will be matched</span>
41
+ </div>
42
+ ```
43
+
44
+ \#\#\# Compatibility
45
+
46
+ `:has()` is supported by all Unpoly functions (like `up.element.all()`) and
47
+ selectors (like `a[up-target]`).
48
+
49
+ As a [level 4 CSS selector](https://drafts.csswg.org/selectors-4/#relational),
50
+ `:has()` [has yet to be implemented](https://caniuse.com/#feat=css-has)
51
+ in native browser functions like [`document.querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll).
52
+
53
+ You can also use [`:has()` in jQuery](https://api.jquery.com/has-selector/).
54
+
55
+ @selector :has()
56
+ @experimental
57
+ ###
58
+
59
+ parseSelector = (selector) ->
60
+ up.Selector.parse(selector)
61
+
62
+ ###**
63
+ Returns the first descendant element matching the given selector.
64
+
65
+ It is similar to [`element.querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector),
66
+ but also supports the [`:has()`](/has) selector.
67
+
68
+ @function up.element.first
69
+ @param {Element} [parent=document]
70
+ The parent element whose descendants to search.
71
+
72
+ If omitted, all elements in the `document` will be searched.
73
+ @param {string} selector
74
+ The CSS selector to match.
75
+ @return {Element|undefined|null}
76
+ The first element matching the selector.
77
+
78
+ Returns `null` or `undefined` if no element macthes.
79
+ @experimental
80
+ ###
81
+ first = (args...) ->
82
+ selector = args.pop()
83
+ parent = args[0] ? document
84
+ parseSelector(selector).descendant(parent)
85
+
86
+ ###**
87
+ Returns all descendant elements matching the given selector.
88
+
89
+ @function up.element.all
90
+ @param {Element} [parent=document]
91
+ The parent element whose descendants to search.
92
+
93
+ If omitted, all elements in the `document` will be searched.
94
+ @param {string} selector
95
+ The CSS selector to match.
96
+ @return {NodeList<Element>|Array<Element>}
97
+ A list of all elements matching the selector.
98
+
99
+ Returns an empty list if there are no matches.
100
+ @experimental
101
+ ###
102
+ all = (args...) ->
103
+ selector = args.pop()
104
+ parent = args[0] ? document
105
+ parseSelector(selector).descendants(parent)
106
+
107
+ ###**
108
+ Returns a list of the given parent's descendants matching the given selector.
109
+ The list will also include the parent element if it matches the selector itself.
110
+
111
+ @function up.element.subtree
112
+ @param {Element} parent
113
+ The parent element for the search.
114
+ @param {string} selector
115
+ The CSS selector to match.
116
+ @return {NodeList<Element>|Array<Element>}
117
+ A list of all matching elements.
118
+ @experimental
119
+ ###
120
+ subtree = (root, selector) ->
121
+ parseSelector(selector).subtree(root)
122
+
123
+ ###**
124
+ Returns the first element that matches the selector by testing the element itself
125
+ and traversing up through its ancestors in the DOM tree.
126
+
127
+ @function up.element.closest
128
+ @param {Element} element
129
+ The element on which to start the search.
130
+ @param {string}
131
+ The CSS selector to match.
132
+ @return {Element|null|undefined} element
133
+ The matching element.
134
+
135
+ Returns `null` or `undefined` if no element matches.
136
+ @experimental
137
+ ###
138
+ closest = (element, selector) ->
139
+ parseSelector(selector).closest(element)
140
+
141
+ ###**
142
+ Returns whether the given element matches the given CSS selector.
143
+
144
+ @function up.element.matches
145
+ @param {Element} element
146
+ The element to check.
147
+ @param {string} selector
148
+ The CSS selector to match.
149
+ @return {boolean}
150
+ Whether `element` matches `selector`.
151
+ @experimental
152
+ ###
153
+ matches = (element, selector) ->
154
+ parseSelector(selector).matches(element)
155
+
156
+ ###**
157
+ @function up.element.ancestor
158
+ @internal
159
+ ###
160
+ ancestor = (element, selector) ->
161
+ parseSelector(selector).ancestor(element)
162
+
163
+ ###**
164
+ Casts the given value to a native [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element).
165
+
166
+ This is useful when working with jQuery values, or to allow callers to pass CSS selectors
167
+ instead of elements.
168
+
169
+ \#\#\# Casting rules
170
+
171
+ - If given an element, returns that element.
172
+ - If given a CSS selector string, returns the [first element matching](/up.element.first) that selector.
173
+ - If given a jQuery collection , returns the first element in the collection.
174
+ Throws an error if the collection contains more than one element.
175
+ - If given any other argument (`undefined`, `null`, `document`, `window`…), returns the argument unchanged.
176
+
177
+ @function up.element.get
178
+ @param {Element|jQuery|string} value
179
+ The value to cast.
180
+ @return {Element}
181
+ The obtained `Element`.
182
+ @experimental
183
+ ###
184
+ getOne = (value) ->
185
+ if u.isElement(value) # Return an element before we run any other expensive checks
186
+ value
187
+ else if u.isString(value)
188
+ first(value)
189
+ else if u.isJQuery(value)
190
+ if value.length > 1
191
+ up.fail('up.element.get(): Cannot cast multiple elements (%o) to a single element', value)
192
+ value[0]
193
+ else
194
+ # undefined, null, Window, Document, DocumentFragment, ...
195
+ value
196
+
197
+ ###**
198
+ Composes a list of elements from the given arguments.
199
+
200
+ \#\#\# Casting rules
201
+
202
+ - If given a string, returns the all elements matching that string.
203
+ - If given any other argument, returns the argument [wrapped as a list](/up.util.wrapList).
204
+
205
+ \#\#\# Example
206
+
207
+ ```javascript
208
+ $jquery = $('.jquery') // returns jQuery (2) [div.jquery, div.jquery]
209
+ nodeList = document.querySelectorAll('.node') // returns NodeList (2) [div.node, div.node]
210
+ element = document.querySelector('.element') // returns Element div.element
211
+ selector = '.selector' // returns String '.selector'
212
+
213
+ elements = up.element.list($jquery, nodeList, undefined, element, selector)
214
+ // returns [div.jquery, div.jquery, div.node, div.node, div.element, div.selector]
215
+ ```
216
+
217
+ @function up.element.list
218
+ @param {Array<jQuery|Element|Array<Element>|String|undefined|null>} ...args
219
+ @return {Array<Element>}
220
+ @internal
221
+ ###
222
+ getList = (args...) ->
223
+ u.flatMap args, valueToList
224
+
225
+ valueToList = (value) ->
226
+ if u.isString(value)
227
+ all(value)
228
+ else
229
+ u.wrapList(value)
230
+
231
+ # assertIsElement = (element) ->
232
+ # unless u.isElement(element)
233
+ # up.fail('Not an element: %o', element)
234
+
235
+ ###**
236
+ Removes the given element from the DOM tree.
237
+
238
+ If you don't need IE11 support you may also use the built-in
239
+ [`Element#remove()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove) to the same effect.
240
+
241
+ @function up.element.remove
242
+ @param {Element} element
243
+ The element to remove.
244
+ @experimental
245
+ ###
246
+ remove = (element) ->
247
+ if element.remove
248
+ element.remove()
249
+ # IE does not support Element#remove()
250
+ else if parent = element.parentNode
251
+ parent.removeChild(element)
252
+
253
+ ###**
254
+ Hides the given element.
255
+
256
+ The element is hidden by setting an [inline style](https://www.codecademy.com/articles/html-inline-styles)
257
+ of `{ display: none }`.
258
+
259
+ Also see `up.element.show()`.
260
+
261
+ @function up.element.hide
262
+ @param {Element} element
263
+ @experimental
264
+ ###
265
+ hide = (element) ->
266
+ element.style.display = 'none'
267
+
268
+ ###**
269
+ Shows the given element.
270
+
271
+ Also see `up.element.hide()`.
272
+
273
+ \#\#\# Limitations
274
+
275
+ The element is shown by setting an [inline style](https://www.codecademy.com/articles/html-inline-styles)
276
+ of `{ display: '' }`.
277
+
278
+ You might have CSS rules causing the element to remain hidden after calling `up.element.show(element)`.
279
+ Unpoly will not handle such cases in order to keep this function performant. As a workaround, you may
280
+ manually set the `element.style.display` property. Also see discussion
281
+ in jQuery issues [#88](https://github.com/jquery/jquery.com/issues/88),
282
+ [#2057](https://github.com/jquery/jquery/issues/2057) and
283
+ [this WHATWG mailing list post](http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Apr/0094.html).
284
+
285
+ @function up.element.show
286
+ @experimental
287
+ ###
288
+ show = (element) ->
289
+ element.style.display = ''
290
+
291
+ ###**
292
+ Display or hide the given element, depending on its current visibility.
293
+
294
+ @function up.element.toggle
295
+ @param {Element} element
296
+ @param {Boolean} [newVisible]
297
+ Pass `true` to show the element or `false` to hide it.
298
+
299
+ If omitted, the element will be hidden if shown and shown if hidden.
300
+ @experimental
301
+ ###
302
+ toggle = (element, newVisible) ->
303
+ newVisible ?= !isVisible(element)
304
+ if newVisible
305
+ show(element)
306
+ else
307
+ hide(element)
308
+
309
+ # trace = (fn) ->
310
+ # (args...) ->
311
+ # console.debug("Calling %o with %o", fn, args)
312
+ # fn(args...)
313
+
314
+ ###**
315
+ Adds or removes the given class from the given element.
316
+
317
+ If you don't need IE11 support you may also use the built-in
318
+ [`Element#classList.toggle(className)`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) to the same effect.
319
+
320
+ @function up.element.toggleClass
321
+ @param {Element} element
322
+ The element for which to add or remove the class.
323
+ @param {String} className
324
+ A boolean value to determine whether the class should be added or removed.
325
+ @param {String} state
326
+ If omitted, the class will be added if missing and removed if present.
327
+ @experimental
328
+ ###
329
+ toggleClass = (element, klass, newPresent) ->
330
+ list = element.classList
331
+ newPresent ?= !list.contains(klass)
332
+ if newPresent
333
+ list.add(klass)
334
+ else
335
+ list.remove(klass)
336
+
337
+ ###**
338
+ Sets all key/values from the given object as attributes on the given element.
339
+
340
+ \#\#\# Example
341
+
342
+ up.element.setAttrs(element, { title: 'Tooltip', tabindex: 1 })
343
+
344
+ @function up.element.setAttrs
345
+ @param {Element} element
346
+ The element on which to set attributes.
347
+ @param {object} attributes
348
+ An object of attributes to set.
349
+ @experimental
350
+ ###
351
+ setAttrs = (element, attributes) ->
352
+ for key, value of attributes
353
+ element.setAttribute(key, value)
354
+
355
+ ###**
356
+ @function up.element.metaContent
357
+ @internal
358
+ ###
359
+ metaContent = (name) ->
360
+ selector = "meta" + attributeSelector('name', name)
361
+ first(selector)?.getAttribute('content')
362
+
363
+ ###**
364
+ @function up.element.insertBefore
365
+ @internal
366
+ ###
367
+ insertBefore = (existingElement, newElement) ->
368
+ existingElement.insertAdjacentElement('beforebegin', newElement)
369
+
370
+ # insertAfter = (existingElement, newElement) ->
371
+ # existingElement.insertAdjacentElement('afterend', newElement)
372
+
373
+ ###**
374
+ Replaces the given old element with the given new element.
375
+
376
+ The old element will be removed from the DOM tree.
377
+
378
+ If you don't need IE11 support you may also use the built-in
379
+ [`Element#replaceWith()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith) to the same effect.
380
+
381
+ @function up.element.replace
382
+ @param {Element} oldElement
383
+ @param {Element} newElement
384
+ @experimental
385
+ ###
386
+ replace = (oldElement, newElement) ->
387
+ oldElement.parentElement.replaceChild(newElement, oldElement)
388
+
389
+ ###**
390
+ Creates an element matching the given CSS selector.
391
+
392
+ The created element will not yet be attached to the DOM tree.
393
+ Attach it with [`Element#appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)
394
+ or use `up.element.affix()` to create an attached element.
395
+
396
+ \#\#\# Examples
397
+
398
+ To create an element with a given tag name:
399
+
400
+ element = up.element.createFromSelector('span')
401
+ // element is <span></span>
402
+
403
+ To create an element with a given class:
404
+
405
+ element = up.element.createFromSelector('.klass')
406
+ // element is <div class="klass"></div>
407
+
408
+ To create an element with a given ID:
409
+
410
+ element = up.element.createFromSelector('#foo')
411
+ // element is <div id="foo"></div>
412
+
413
+ To create an element with a given boolean attribute:
414
+
415
+ element = up.element.createFromSelector('[attr]')
416
+ // element is <div attr></div>
417
+
418
+ To create an element with a given attribute value:
419
+
420
+ element = up.element.createFromSelector('[attr="value"]')
421
+ // element is <div attr="value"></div>
422
+
423
+ You may also pass an object of attribute names/values as a second argument:
424
+
425
+ element = up.element.createFromSelector('div', { attr: 'value' })
426
+ // element is <div attr="value"></div>
427
+
428
+ You may set the element's inner text by passing a `{ text }` option:
429
+
430
+ element = up.element.createFromSelector('div', { text: 'inner text' })
431
+ // element is <div>inner text</div>
432
+
433
+ You may set inline styles by passing an object of CSS properties as a second argument:
434
+
435
+ element = up.element.createFromSelector('div', { style: { color: 'red' }})
436
+ // element is <div style="color: red"></div>
437
+
438
+ @function up.element.createFromSelector
439
+ @param {string} selector
440
+ The CSS selector from which to create an element.
441
+ @param {Object} [attrs]
442
+ An object of attributes to set on the created element.
443
+ @param {Object} [attrs.text]
444
+ The [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) of the created element.
445
+ @param {Object} [attrs.style]
446
+ An object of CSS properties that will be set as the inline style
447
+ of the created element.
448
+ @return {Element}
449
+ The created element.
450
+ @experimental
451
+ ###
452
+ createFromSelector = (selector, attrs) ->
453
+ # Extract attribute values before we do anything else.
454
+ # Attribute values might contain spaces, and then we would incorrectly
455
+ # split depths at that space.
456
+ attrValues = []
457
+ selectorWithoutAttrValues = selector.replace /\[([\w-]+)(?:=(["'])?([^"'\]]*?)\2)?\]/g, (_match, attrName, _quote, attrValue) ->
458
+ attrValues.push(attrValue || '')
459
+ "[#{attrName}]"
460
+
461
+ depths = selectorWithoutAttrValues.split(/[ >]+/)
462
+ rootElement = undefined
463
+ depthElement = undefined
464
+ previousElement = undefined
465
+
466
+ for depthSelector in depths
467
+ tagName = undefined
468
+
469
+ depthSelector = depthSelector.replace /^[\w-]+/, (match) ->
470
+ tagName = match
471
+ ''
472
+
473
+ depthElement = document.createElement(tagName || 'div')
474
+ rootElement ||= depthElement
475
+
476
+ depthSelector = depthSelector.replace /\#([\w-]+)/, (_match, id) ->
477
+ depthElement.id = id
478
+ ''
479
+
480
+ depthSelector = depthSelector.replace /\.([\w-]+)/g, (_match, className) ->
481
+ depthElement.classList.add(className)
482
+ ''
483
+
484
+ # If we have stripped out attrValues at the beginning of the function,
485
+ # they have been replaced with the attribute name only (as "[name]").
486
+ if attrValues.length
487
+ depthSelector = depthSelector.replace /\[([\w-]+)\]/g, (_match, attrName) ->
488
+ depthElement.setAttribute(attrName, attrValues.shift())
489
+ ''
490
+
491
+ unless depthSelector == ''
492
+ throw new Error('Cannot parse selector: ' + selector)
493
+
494
+ previousElement?.appendChild(depthElement)
495
+ previousElement = depthElement
496
+
497
+ if attrs
498
+ if classValue = u.pluckKey(attrs, 'class')
499
+ for klass in u.wrapList(classValue)
500
+ rootElement.classList.add(klass)
501
+ if styleValue = u.pluckKey(attrs, 'style')
502
+ setInlineStyle(rootElement, styleValue)
503
+ if textValue = u.pluckKey(attrs, 'text')
504
+ rootElement.innerText = textValue
505
+ setAttrs(rootElement, attrs)
506
+
507
+ rootElement
508
+
509
+ ###**
510
+ Creates an element matching the given CSS selector and attaches it to the given parent element.
511
+
512
+ To create a detached element from a selector,
513
+ see `up.element.createFromSelector()`.
514
+
515
+ \#\#\# Example
516
+
517
+ element = up.element.affix(document.body, '.klass')
518
+ element.parentElement // returns document.body
519
+ element.className // returns 'klass'
520
+
521
+ @function up.element.affix
522
+ @params {Element} parent
523
+ The parent to which to attach the created element.
524
+ @params {string} selector
525
+ The CSS selector from which to create an element.
526
+ @params {Object} attrs
527
+ An object of attributes to set on the created element.
528
+ @param {Object} attrs.text
529
+ The [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) of the created element.
530
+ @param {Object} attrs.style
531
+ An object of CSS properties that will be set as the inline style
532
+ of the created element.
533
+ @return {Element}
534
+ The created element.
535
+ @experimental
536
+ ###
537
+ affix = (parent, selector, attributes) ->
538
+ element = createFromSelector(selector, attributes)
539
+ parent.appendChild(element)
540
+ element
541
+
542
+ ###**
543
+ Returns a CSS selector that matches the given element as good as possible.
544
+
545
+ To build the selector, the following element properties are used in decreasing
546
+ order of priority:
547
+
548
+ - The element's `[up-id]` attribute
549
+ - The element's `[id]` attribute
550
+ - The element's `[name]` attribute
551
+ - The element's `[class]` names
552
+ - The element's [`[aria-label]`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute) attribute
553
+ - The element's tag name
554
+
555
+ \#\#\# Example
556
+
557
+ element = document.createElement('span')
558
+ element.className = 'klass'
559
+ selector = up.element.toSelector(element) // returns '.klass'
560
+
561
+ @function up.element.toSelector
562
+ @param {string|Element|jQuery}
563
+ The element for which to create a selector.
564
+ @experimental
565
+ ###
566
+ toSelector = (element) ->
567
+ return element if u.isString(element)
568
+
569
+ # resolveSelector() might be called with jQuery collections
570
+ element = getOne(element)
571
+
572
+ selector = undefined
573
+
574
+ if isSingleton(element)
575
+ selector = elementTagName(element)
576
+ else if upId = element.getAttribute("up-id")
577
+ selector = attributeSelector('up-id', upId)
578
+ else if id = element.getAttribute("id")
579
+ if id.match(/^[a-z0-9\-_]+$/i)
580
+ selector = "##{id}"
581
+ else
582
+ selector = attributeSelector('id', id)
583
+ else if name = element.getAttribute("name")
584
+ selector = elementTagName(element) + attributeSelector('name', name)
585
+ else if classes = u.presence(nonUpClasses(element))
586
+ selector = ''
587
+ for klass in classes
588
+ selector += ".#{klass}"
589
+ else if ariaLabel = element.getAttribute("aria-label")
590
+ selector = attributeSelector('aria-label', ariaLabel)
591
+ else
592
+ selector = elementTagName(element)
593
+
594
+ return selector
595
+
596
+ ###**
597
+ Sets an unique identifier for this element.
598
+
599
+ This identifier is used by `up.element.toSelector()`
600
+ to create a CSS selector that matches this element precisely.
601
+
602
+ If the element already has other attributes that make a good identifier,
603
+ like a `[id]`, `[class]` or `[aria-label]`, it is not necessary to
604
+ set `[up-id]`.
605
+
606
+ \#\#\# Example
607
+
608
+ Take this element:
609
+
610
+ <a href="/">Homepage</a>
611
+
612
+ Unpoly cannot generate a good CSS selector for this element:
613
+
614
+ up.element.toSelector(element)
615
+ // returns 'a'
616
+
617
+ We can improve this by assigning an `[up-id]`:
618
+
619
+ <a href="/" up-id="link-to-home">Open user 4</a>
620
+
621
+ The attribute value is used to create a better selector:
622
+
623
+ up.element.toSelector(element)
624
+ // returns '[up-id="link-to-home"]'
625
+
626
+ @selector [up-id]
627
+ @param {string} up-id
628
+ A string that uniquely identifies this element.
629
+ @stable
630
+ ###
631
+
632
+ ###**
633
+ @function up.element.isSingleton
634
+ @internal
635
+ ###
636
+ isSingleton = (element) ->
637
+ matches(element, 'html, body, head, title')
638
+
639
+ elementTagName = (element) ->
640
+ element.tagName.toLowerCase()
641
+
642
+ ###**
643
+ @function up.element.attributeSelector
644
+ @internal
645
+ ###
646
+ attributeSelector = (attribute, value) ->
647
+ value = value.replace(/"/g, '\\"')
648
+ "[#{attribute}=\"#{value}\"]"
649
+
650
+ nonUpClasses = (element) ->
651
+ classString = element.className
652
+ classes = u.splitValues(classString)
653
+ u.reject classes, (klass) -> klass.match(/^up-/)
654
+
655
+ ###**
656
+ @function up.element.createDocumentFromHtml
657
+ @internal
658
+ ###
659
+ createDocumentFromHtml = (html) ->
660
+ # IE9 cannot set innerHTML on a <html> or <head> element.
661
+ parser = new DOMParser()
662
+ return parser.parseFromString(html, 'text/html')
663
+
664
+ ###**
665
+ Creates an element from the given HTML fragment.
666
+
667
+ \#\#\# Example
668
+
669
+ element = up.element.createFromHtml('<div class="foo"><span>text</span></div>')
670
+ element.className // returns 'foo'
671
+ element.children[0] // returns <span> element
672
+ element.children[0].textContent // returns 'text'
673
+
674
+ @function up.element.createFromHtml
675
+ @experimental
676
+ ###
677
+ createFromHtml = (html) ->
678
+ doc = createDocumentFromHtml(html)
679
+ return doc.body.children[0]
680
+
681
+ ###**
682
+ @function up.element.root
683
+ @internal
684
+ ###
685
+ getRoot = ->
686
+ document.documentElement
687
+
688
+ ###**
689
+ Forces the browser to paint the given element now.
690
+
691
+ @function up.element.paint
692
+ @internal
693
+ ###
694
+ paint = (element) ->
695
+ element.offsetHeight
696
+
697
+ ###**
698
+ @function up.element.concludeCssTransition
699
+ @internal
700
+ ###
701
+ concludeCssTransition = (element) ->
702
+ undo = setTemporaryStyle(element, transition: 'none')
703
+ # Browsers need to paint at least one frame without a transition to stop the
704
+ # animation. In theory we could just wait until the next paint, but in case
705
+ # someone will set another transition after us, let's force a repaint here.
706
+ paint(element)
707
+ return undo
708
+
709
+ ###**
710
+ Returns whether the given element has a CSS transition set.
711
+
712
+ @function up.element.hasCssTransition
713
+ @return {boolean}
714
+ @internal
715
+ ###
716
+ hasCssTransition = (elementOrStyleHash) ->
717
+ if u.isOptions(elementOrStyleHash)
718
+ styleHash = elementOrStyleHash
719
+ else
720
+ styleHash = computedStyle(elementOrStyleHash)
721
+
722
+ prop = styleHash.transitionProperty
723
+ duration = styleHash.transitionDuration
724
+ # The default transition for elements is actually "all 0s ease 0s"
725
+ # instead of "none", although that has the same effect as "none".
726
+ noTransition = (prop == 'none' || (prop == 'all' && duration == 0))
727
+ not noTransition
728
+
729
+ ###**
730
+ @function up.element.fixedToAbsolute
731
+ @internal
732
+ ###
733
+ fixedToAbsolute = (element) ->
734
+ elementRectAsFixed = element.getBoundingClientRect()
735
+
736
+ # Set the position to 'absolute' so it gains an offsetParent
737
+ element.style.position = 'absolute'
738
+
739
+ offsetParentRect = element.offsetParent.getBoundingClientRect()
740
+
741
+ setInlineStyle element,
742
+ left: elementRectAsFixed.left - computedStyleNumber(element, 'margin-left') - offsetParentRect.left
743
+ top: elementRectAsFixed.top - computedStyleNumber(element, 'margin-top') - offsetParentRect.top
744
+ right: ''
745
+ bottom: ''
746
+
747
+ ###**
748
+ On the given element, set attributes that are still missing.
749
+
750
+ @function up.element.setMissingAttrs
751
+ @internal
752
+ ###
753
+ setMissingAttrs = (element, attrs) ->
754
+ for key, value of attrs
755
+ if u.isMissing(element.getAttribute(key))
756
+ element.setAttribute(key, value)
757
+
758
+ ###**
759
+ @function up.element.unwrap
760
+ @internal
761
+ ###
762
+ unwrap = (wrapper) ->
763
+ parent = wrapper.parentNode;
764
+ wrappedNodes = u.toArray(wrapper.childNodes)
765
+ u.each wrappedNodes, (wrappedNode) ->
766
+ parent.insertBefore(wrappedNode, wrapper)
767
+ parent.removeChild(wrapper)
768
+
769
+ # ###**
770
+ # Returns the value of the given attribute on the given element, if the value is [present](/up.util.isPresent).
771
+ #
772
+ # Returns `undefined` if the attribute is not set, or if it is set to an empty string.
773
+ #
774
+ # @function up.element.presentAttr
775
+ # @param {Element} element
776
+ # The element from which to retrieve the attribute value.
777
+ # @param {String} attribute
778
+ # The attribute name.
779
+ # @return {string|undefined}
780
+ # The attribute value, if present.
781
+ # @experimental
782
+ # ###
783
+ # presentAttr = (element, attribute) ->
784
+ # value = element.getAttribute(attribute)
785
+ # u.presence(value)
786
+
787
+ ###**
788
+ Returns the value of the given attribute on the given element, cast as a boolean value.
789
+
790
+ If the attribute value cannot be cast to `true` or `false`, `undefined` is returned.
791
+
792
+ \#\#\# Casting rules
793
+
794
+ This function deviates from the
795
+ [HTML Standard for boolean attributes](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes)
796
+ in order to allow `undefined` values. When an attribute is missing, Unpoly considers the value to be `undefined`
797
+ (where the standard would assume `false`).
798
+
799
+ Unpoly also allows `"true"` and `"false"` as attribute values.
800
+
801
+ The table below shows return values for `up.element.booleanAttr(element, 'foo')` given different elements:
802
+
803
+ | Element | Return value |
804
+ |---------------------|--------------|
805
+ | `<div foo>` | `true` |
806
+ | `<div foo="foo">` | `true` |
807
+ | `<div foo="true">` | `true` |
808
+ | `<div foo="">` | `true` |
809
+ | `<div foo="false">` | `false` |
810
+ | `<div>` | `undefined` |
811
+ | `<div foo="bar">` | `undefined` |
812
+
813
+ @function up.element.booleanAttr
814
+ @param {Element} element
815
+ The element from which to retrieve the attribute value.
816
+ @param {String} attribute
817
+ The attribute name.
818
+ @return {boolean|undefined}
819
+ The cast attribute value.
820
+ @experimental
821
+ ###
822
+ booleanAttr = (element, attribute, pass) ->
823
+ value = element.getAttribute(attribute)
824
+ switch value
825
+ when 'false'
826
+ false
827
+ when 'true', '', attribute
828
+ true
829
+ else
830
+ value if pass
831
+
832
+ ###**
833
+ Returns the given attribute value cast as boolean.
834
+
835
+ If the attribute value cannot be cast, returns the attribute value unchanged.
836
+
837
+ @internal
838
+ ###
839
+ booleanOrStringAttr = (element, attribute) ->
840
+ booleanAttr(element, attribute, true)
841
+
842
+ ###**
843
+ Returns the value of the given attribute on the given element, cast to a number.
844
+
845
+ If the attribute value cannot be cast to a number, `undefined` is returned.
846
+
847
+ @function up.element.numberAttr
848
+ @param {Element} element
849
+ The element from which to retrieve the attribute value.
850
+ @param {String} attribute
851
+ The attribute name.
852
+ @return {number|undefined}
853
+ The cast attribute value.
854
+ @experimental
855
+ ###
856
+ numberAttr = (element, attribute) ->
857
+ value = element.getAttribute(attribute)
858
+ if value?.match(/^[\d\.]+$/)
859
+ parseFloat(value)
860
+
861
+ ###**
862
+ Reads the given attribute from the element, parsed as [JSON](https://www.json.org/).
863
+
864
+ Returns `undefined` if the attribute value is [blank](/up.util.isBlank).
865
+
866
+ Throws a `SyntaxError` if the attribute value is an invalid JSON string.
867
+
868
+ @function up.element.jsonAttr
869
+ @param {Element} element
870
+ The element from which to retrieve the attribute value.
871
+ @param {String} attribute
872
+ The attribute name.
873
+ @return {Object|undefined}
874
+ The cast attribute value.
875
+ @experimental
876
+ ###
877
+ jsonAttr = (element, attribute) ->
878
+ # The document does not respond to #getAttribute()
879
+ if json = element.getAttribute?(attribute)?.trim()
880
+ JSON.parse(json)
881
+
882
+ ###**
883
+ Temporarily sets the inline CSS styles on the given element.
884
+
885
+ Returns a function that restores the original inline styles when called.
886
+
887
+ \#\#\# Example
888
+
889
+ element = document.querySelector('div')
890
+ unhide = up.element.setTemporaryStyle(element, { 'visibility': 'hidden' })
891
+ // do things while element is invisible
892
+ unhide()
893
+ // element is visible again
894
+
895
+ @function up.element.setTemporaryStyle
896
+ @param {Element} element
897
+ The element to style.
898
+ @param {Object} styles
899
+ An object of CSS property names and values.
900
+ @return {Function()}
901
+ A function that restores the original inline styles when called.
902
+ @internal
903
+ ###
904
+ setTemporaryStyle = (element, newStyles, block) ->
905
+ oldStyles = inlineStyle(element, Object.keys(newStyles))
906
+ setInlineStyle(element, newStyles)
907
+ return -> setInlineStyle(element, oldStyles)
908
+
909
+ ###**
910
+ Receives [computed CSS styles](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle)
911
+ for the given element.
912
+
913
+ \#\#\# Examples
914
+
915
+ When requesting a single CSS property, its value will be returned as a string:
916
+
917
+ value = up.element.style(element, 'font-size')
918
+ // value is '16px'
919
+
920
+ When requesting multiple CSS properties, the function returns an object of property names and values:
921
+
922
+ value = up.element.style(element, ['font-size', 'margin-top'])
923
+ // value is { 'font-size': '16px', 'margin-top': '10px' }
924
+
925
+ @function up.element.style
926
+ @param {Element} element
927
+ @param {String|Array} propOrProps
928
+ One or more CSS property names in kebab-case or camelCase.
929
+ @return {string|object}
930
+ @experimental
931
+ ###
932
+ computedStyle = (element, props) ->
933
+ style = window.getComputedStyle(element)
934
+ extractFromStyleObject(style, props)
935
+
936
+ ###**
937
+ Receives a [computed CSS property value](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle)
938
+ for the given element, casted as a number.
939
+
940
+ The value is casted by removing the property's [unit](https://www.w3schools.com/cssref/css_units.asp) (which is usually `px` for computed properties).
941
+ The result is then parsed as a floating point number.
942
+
943
+ Returns `undefined` if the property value is missing, or if it cannot
944
+ be parsed as a number.
945
+
946
+ \#\#\# Examples
947
+
948
+ When requesting a single CSS property, its value will be returned as a string:
949
+
950
+ value = up.element.style(element, 'font-size')
951
+ // value is '16px'
952
+
953
+ value = up.element.styleNumber(element, 'font-size')
954
+ // value is 16
955
+
956
+ @function up.element.styleNumber
957
+ @param {Element} element
958
+ @param {String} prop
959
+ A single property name in kebab-case or camelCase.
960
+ @return {number|undefined}
961
+ @experimental
962
+ ###
963
+ computedStyleNumber = (element, prop) ->
964
+ rawValue = computedStyle(element, prop)
965
+ if u.isGiven(rawValue)
966
+ parseFloat(rawValue)
967
+ else
968
+ undefined
969
+
970
+ ###**
971
+ Gets the given inline style(s) from the given element's `[style]` attribute.
972
+
973
+ @function up.element.inlineStyle
974
+ @param {Element} element
975
+ @param {String|Array} propOrProps
976
+ One or more CSS property names in kebab-case or camelCase.
977
+ @return {string|object}
978
+ @internal
979
+ ###
980
+ inlineStyle = (element, props) ->
981
+ style = element.style
982
+ extractFromStyleObject(style, props)
983
+
984
+ extractFromStyleObject = (style, keyOrKeys) ->
985
+ if u.isString(keyOrKeys)
986
+ style[keyOrKeys]
987
+ else # array
988
+ u.only(style, keyOrKeys...)
989
+
990
+ ###**
991
+ Sets the given CSS properties as inline styles on the given element.
992
+
993
+ @function up.element.setStyle
994
+ @param {Element} element
995
+ @param {Object} props
996
+ One or more CSS properties with kebab-case keys or camelCase keys.
997
+ @return {string|object}
998
+ @experimental
999
+ ###
1000
+ setInlineStyle = (element, props) ->
1001
+ style = element.style
1002
+ for key, value of props
1003
+ value = normalizeStyleValueForWrite(key, value)
1004
+ style[key] = value
1005
+
1006
+ normalizeStyleValueForWrite = (key, value) ->
1007
+ if u.isMissing(value)
1008
+ value = ''
1009
+ else if CSS_LENGTH_PROPS.has(key.toLowerCase().replace(/-/, ''))
1010
+ value = cssLength(value)
1011
+ value
1012
+
1013
+ CSS_LENGTH_PROPS = u.arrayToSet [
1014
+ 'top', 'right', 'bottom', 'left',
1015
+ 'padding', 'paddingtop', 'paddingright', 'paddingbottom', 'paddingleft',
1016
+ 'margin', 'margintop', 'marginright', 'marginbottom', 'marginleft',
1017
+ 'borderwidth', 'bordertopwidth', 'borderrightwidth', 'borderbottomwidth', 'borderleftwidth'
1018
+ 'width', 'height',
1019
+ 'maxwidth', 'maxheight',
1020
+ 'minwidth', 'minheight',
1021
+ ]
1022
+
1023
+ ###**
1024
+ Converts the given value to a CSS length value, adding a `px` unit if required.
1025
+
1026
+ @function cssLength
1027
+ @internal
1028
+ ###
1029
+ cssLength = (obj) ->
1030
+ if u.isNumber(obj) || (u.isString(obj) && /^\d+$/.test(obj))
1031
+ obj.toString() + "px"
1032
+ else
1033
+ obj
1034
+
1035
+ ###**
1036
+ Resolves the given CSS selector (which might contain `&` references)
1037
+ to a full CSS selector without ampersands.
1038
+
1039
+ If passed an `Element` or `jQuery` element, returns a CSS selector string
1040
+ for that element.
1041
+
1042
+ @function up.element.resolveSelector
1043
+ @param {string|Element|jQuery} selectorOrElement
1044
+ @param {string|Element|jQuery} origin
1045
+ The element that this selector resolution is relative to.
1046
+ That element's selector will be substituted for `&` ([like in Sass](https://sass-lang.com/documentation/file.SASS_REFERENCE.html#parent-selector)).
1047
+ @return {string}
1048
+ @internal
1049
+ ###
1050
+ resolveSelector = (selectorOrElement, origin) ->
1051
+ if u.isString(selectorOrElement)
1052
+ selector = selectorOrElement
1053
+ if u.contains(selector, '&')
1054
+ if u.isPresent(origin) # isPresent returns false for empty jQuery collection
1055
+ originSelector = toSelector(origin)
1056
+ selector = selector.replace(/\&/, originSelector)
1057
+ else
1058
+ up.fail("Found origin reference (%s) in selector %s, but no origin was given", '&', selector)
1059
+ else
1060
+ selector = toSelector(selectorOrElement)
1061
+ selector
1062
+
1063
+ ###**
1064
+ Returns whether the given element is currently visible.
1065
+
1066
+ An element is considered visible if it consumes space in the document.
1067
+ Elements with `{ visibility: hidden }` or `{ opacity: 0 }` are considered visible, since they still consume space in the layout.
1068
+
1069
+ Elements not attached to the DOM are considered hidden.
1070
+
1071
+ @function up.element.isVisible
1072
+ @param {Element} element
1073
+ The element to check.
1074
+ @experimental
1075
+ ###
1076
+ isVisible = (element) ->
1077
+ # From https://github.com/jquery/jquery/blame/9cb162f6b62b6d4403060a0f0d2065d3ae96bbcc/src/css/hiddenVisibleSelectors.js#L12
1078
+ !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
1079
+
1080
+ <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %>
1081
+ # also document :has()!
1082
+ first: first # same as document.querySelector
1083
+ all: all # same as document.querySelectorAll
1084
+ subtree: subtree # practical
1085
+ closest: closest # needed for IE11
1086
+ matches: matches # needed for IE11
1087
+ ancestor: ancestor # not practical. we use it to implement closest
1088
+ get: getOne # practical for code that also works with jQuery
1089
+ list: getList # practical for composing multiple collections, or wrapping.
1090
+ remove: remove # needed for IE11
1091
+ toggle: toggle # practical
1092
+ toggleClass: toggleClass # practical
1093
+ hide: hide # practical
1094
+ show: show # practical
1095
+ metaContent: metaContent # internal
1096
+ replace: replace # needed for IE11
1097
+ insertBefore: insertBefore # internal shortcut, people can use insertAdjacentElement and i don't want to support insertAfter when I don't need it.
1098
+ createFromSelector: createFromSelector # practical for element creation.
1099
+ setAttrs: setAttrs # practical
1100
+ affix: affix # practical for element creation
1101
+ toSelector: toSelector # practical
1102
+ isSingleton: isSingleton # internal
1103
+ attributeSelector: attributeSelector # internal
1104
+ createDocumentFromHtml: createDocumentFromHtml # internal
1105
+ createFromHtml: createFromHtml # practical for element creation
1106
+ root: getRoot # internal
1107
+ paint: paint # internal
1108
+ concludeCssTransition: concludeCssTransition # internal
1109
+ hasCssTransition: hasCssTransition # internal
1110
+ fixedToAbsolute: fixedToAbsolute # internal
1111
+ setMissingAttrs: setMissingAttrs # internal
1112
+ unwrap: unwrap # practical for jQuery migration
1113
+ # presentAttr: presentAttr # experimental
1114
+ booleanAttr: booleanAttr # it's practical, but i cannot find a good name. people might expect it to cast to number, too. but i don't need that for my own code. maybe booleanAttr?
1115
+ numberAttr: numberAttr # practical
1116
+ jsonAttr: jsonAttr # practical
1117
+ booleanOrStringAttr: booleanOrStringAttr
1118
+ setTemporaryStyle: setTemporaryStyle # practical
1119
+ style: computedStyle # practical.
1120
+ styleNumber: computedStyleNumber # practical.
1121
+ inlineStyle: inlineStyle # internal
1122
+ setStyle: setInlineStyle # practical.
1123
+ resolveSelector: resolveSelector # internal
1124
+ none: -> NONE # internal
1125
+ isVisible: isVisible # practical
1126
+