opal-browser 0.1.0.beta1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (257) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/build.yml +95 -0
  3. data/.gitignore +3 -0
  4. data/.yardopts +1 -1
  5. data/Gemfile +22 -3
  6. data/LICENSE +20 -0
  7. data/README.md +200 -20
  8. data/Rakefile +29 -1
  9. data/config.ru +20 -2
  10. data/docs/polyfills.md +24 -0
  11. data/examples/2048/Gemfile +6 -0
  12. data/examples/2048/README.md +13 -0
  13. data/examples/2048/app/application.rb +169 -0
  14. data/examples/2048/config.ru +9 -0
  15. data/examples/canvas/Gemfile +6 -0
  16. data/examples/canvas/README.md +9 -0
  17. data/examples/canvas/app/application.rb +55 -0
  18. data/examples/canvas/config.ru +9 -0
  19. data/examples/component/Gemfile +6 -0
  20. data/examples/component/README.md +10 -0
  21. data/examples/component/app/application.rb +66 -0
  22. data/examples/component/config.ru +9 -0
  23. data/examples/integrations/README.md +24 -0
  24. data/examples/integrations/dynamic-rack-opal-sprockets-server/Gemfile +6 -0
  25. data/examples/integrations/dynamic-rack-opal-sprockets-server/README.md +16 -0
  26. data/examples/integrations/dynamic-rack-opal-sprockets-server/app/application.rb +6 -0
  27. data/examples/integrations/dynamic-rack-opal-sprockets-server/config.ru +9 -0
  28. data/examples/integrations/dynamic-roda-roda-sprockets/.gitignore +1 -0
  29. data/examples/integrations/dynamic-roda-roda-sprockets/Gemfile +7 -0
  30. data/examples/integrations/dynamic-roda-roda-sprockets/README.md +22 -0
  31. data/examples/integrations/dynamic-roda-roda-sprockets/Rakefile +4 -0
  32. data/examples/integrations/dynamic-roda-roda-sprockets/app/application.rb +6 -0
  33. data/examples/integrations/dynamic-roda-roda-sprockets/app.rb +32 -0
  34. data/examples/integrations/dynamic-roda-roda-sprockets/config.ru +3 -0
  35. data/examples/integrations/dynamic-roda-tilt/.gitignore +1 -0
  36. data/examples/integrations/dynamic-roda-tilt/Gemfile +8 -0
  37. data/examples/integrations/dynamic-roda-tilt/README.md +17 -0
  38. data/examples/integrations/dynamic-roda-tilt/Rakefile +6 -0
  39. data/examples/integrations/dynamic-roda-tilt/app/application.rb +6 -0
  40. data/examples/integrations/dynamic-roda-tilt/app.rb +50 -0
  41. data/examples/integrations/dynamic-roda-tilt/config.ru +3 -0
  42. data/examples/integrations/dynamic-sinatra-opal-sprockets-server/Gemfile +7 -0
  43. data/examples/integrations/dynamic-sinatra-opal-sprockets-server/README.md +16 -0
  44. data/examples/integrations/dynamic-sinatra-opal-sprockets-server/app/application.rb +6 -0
  45. data/examples/integrations/dynamic-sinatra-opal-sprockets-server/config.ru +29 -0
  46. data/examples/integrations/static-bash/.gitignore +2 -0
  47. data/examples/integrations/static-bash/Gemfile +3 -0
  48. data/examples/integrations/static-bash/README.md +8 -0
  49. data/examples/integrations/static-bash/app/application.rb +6 -0
  50. data/examples/integrations/static-bash/build.sh +4 -0
  51. data/examples/integrations/static-bash/index.html +10 -0
  52. data/examples/integrations/static-bash-opal-parser/.gitignore +3 -0
  53. data/examples/integrations/static-bash-opal-parser/Gemfile +3 -0
  54. data/examples/integrations/static-bash-opal-parser/README.md +10 -0
  55. data/examples/integrations/static-bash-opal-parser/build.sh +4 -0
  56. data/examples/integrations/static-bash-opal-parser/index.html +19 -0
  57. data/examples/integrations/static-rake/.gitignore +1 -0
  58. data/examples/integrations/static-rake/Gemfile +4 -0
  59. data/examples/integrations/static-rake/README.md +7 -0
  60. data/examples/integrations/static-rake/Rakefile +10 -0
  61. data/examples/integrations/static-rake/app/application.rb +6 -0
  62. data/examples/integrations/static-rake/index.html +9 -0
  63. data/examples/integrations/static-rake-guard/.gitignore +1 -0
  64. data/examples/integrations/static-rake-guard/Gemfile +6 -0
  65. data/examples/integrations/static-rake-guard/Guardfile +3 -0
  66. data/examples/integrations/static-rake-guard/README.md +10 -0
  67. data/examples/integrations/static-rake-guard/Rakefile +10 -0
  68. data/examples/integrations/static-rake-guard/app/application.rb +6 -0
  69. data/examples/integrations/static-rake-guard/index.html +9 -0
  70. data/examples/svg/.gitignore +1 -0
  71. data/examples/svg/Gemfile +4 -0
  72. data/examples/svg/README.md +7 -0
  73. data/examples/svg/Rakefile +10 -0
  74. data/examples/svg/app/application.rb +11 -0
  75. data/examples/svg/index.html +17 -0
  76. data/examples/svg/index.svg +6 -0
  77. data/index.html.erb +24 -0
  78. data/lib/opal-browser.rb +1 -0
  79. data/opal/browser/animation_frame.rb +92 -10
  80. data/opal/browser/audio/node.rb +121 -0
  81. data/opal/browser/audio/param_schedule.rb +43 -0
  82. data/opal/browser/audio.rb +66 -0
  83. data/opal/browser/blob.rb +94 -0
  84. data/opal/browser/canvas/data.rb +2 -12
  85. data/opal/browser/canvas/gradient.rb +1 -11
  86. data/opal/browser/canvas/style.rb +3 -11
  87. data/opal/browser/canvas/text.rb +1 -11
  88. data/opal/browser/canvas.rb +86 -28
  89. data/opal/browser/console.rb +6 -38
  90. data/opal/browser/cookies.rb +90 -27
  91. data/opal/browser/crypto.rb +79 -0
  92. data/opal/browser/css/declaration.rb +1 -6
  93. data/opal/browser/css/rule.rb +1 -1
  94. data/opal/browser/css/style_sheet.rb +2 -2
  95. data/opal/browser/css.rb +23 -7
  96. data/opal/browser/database/sql.rb +193 -0
  97. data/opal/browser/delay.rb +94 -0
  98. data/opal/browser/dom/attribute.rb +16 -9
  99. data/opal/browser/dom/builder.rb +35 -25
  100. data/opal/browser/dom/character_data.rb +43 -7
  101. data/opal/browser/dom/document.rb +171 -37
  102. data/opal/browser/dom/document_fragment.rb +18 -0
  103. data/opal/browser/dom/document_or_shadow_root.rb +19 -0
  104. data/opal/browser/dom/element/attributes.rb +111 -0
  105. data/opal/browser/dom/element/button.rb +31 -0
  106. data/opal/browser/dom/element/custom.rb +177 -0
  107. data/opal/browser/dom/element/data.rb +82 -0
  108. data/opal/browser/dom/element/editable.rb +47 -0
  109. data/opal/browser/dom/element/form.rb +38 -0
  110. data/opal/browser/dom/element/iframe.rb +37 -0
  111. data/opal/browser/dom/element/image.rb +25 -0
  112. data/opal/browser/dom/element/input.rb +48 -1
  113. data/opal/browser/dom/element/media.rb +17 -0
  114. data/opal/browser/dom/element/offset.rb +32 -10
  115. data/opal/browser/dom/element/position.rb +11 -2
  116. data/opal/browser/dom/element/scroll.rb +139 -20
  117. data/opal/browser/dom/element/select.rb +42 -0
  118. data/opal/browser/dom/element/size.rb +46 -0
  119. data/opal/browser/dom/element/template.rb +11 -0
  120. data/opal/browser/dom/element/textarea.rb +26 -0
  121. data/opal/browser/dom/element.rb +496 -168
  122. data/opal/browser/dom/mutation_observer.rb +69 -9
  123. data/opal/browser/dom/node.rb +270 -83
  124. data/opal/browser/dom/node_set.rb +74 -41
  125. data/opal/browser/dom/shadow_root.rb +12 -0
  126. data/opal/browser/dom/text.rb +18 -3
  127. data/opal/browser/dom.rb +40 -18
  128. data/opal/browser/effects.rb +180 -3
  129. data/opal/browser/event/all.rb +26 -0
  130. data/opal/browser/event/animation.rb +40 -0
  131. data/opal/browser/{dom/event → event}/audio_processing.rb +10 -6
  132. data/opal/browser/event/base.rb +461 -0
  133. data/opal/browser/event/before_unload.rb +17 -0
  134. data/opal/browser/event/clipboard.rb +37 -0
  135. data/opal/browser/event/close.rb +49 -0
  136. data/opal/browser/event/composition.rb +52 -0
  137. data/opal/browser/event/custom.rb +65 -0
  138. data/opal/browser/event/data_transfer.rb +95 -0
  139. data/opal/browser/event/device_light.rb +25 -0
  140. data/opal/browser/{dom/event → event}/device_motion.rb +21 -6
  141. data/opal/browser/event/device_orientation.rb +50 -0
  142. data/opal/browser/{dom/event → event}/device_proximity.rb +10 -6
  143. data/opal/browser/event/drag.rb +123 -0
  144. data/opal/browser/event/focus.rb +41 -0
  145. data/opal/browser/event/gamepad.rb +62 -0
  146. data/opal/browser/{dom/event → event}/hash_change.rb +10 -6
  147. data/opal/browser/event/keyboard.rb +128 -0
  148. data/opal/browser/event/message.rb +72 -0
  149. data/opal/browser/{dom/event → event}/mouse.rb +37 -32
  150. data/opal/browser/event/page_transition.rb +25 -0
  151. data/opal/browser/event/pop_state.rb +35 -0
  152. data/opal/browser/event/progress.rb +45 -0
  153. data/opal/browser/event/sensor.rb +17 -0
  154. data/opal/browser/{dom/event → event}/storage.rb +10 -6
  155. data/opal/browser/{dom/event → event}/touch.rb +14 -21
  156. data/opal/browser/event/ui.rb +38 -0
  157. data/opal/browser/{dom/event → event}/wheel.rb +6 -4
  158. data/opal/browser/event.rb +163 -0
  159. data/opal/browser/event_source.rb +7 -4
  160. data/opal/browser/form_data.rb +225 -0
  161. data/opal/browser/history.rb +53 -21
  162. data/opal/browser/http/binary.rb +1 -0
  163. data/opal/browser/http/headers.rb +21 -2
  164. data/opal/browser/http/request.rb +83 -55
  165. data/opal/browser/http/response.rb +5 -1
  166. data/opal/browser/http.rb +47 -9
  167. data/opal/browser/immediate.rb +128 -10
  168. data/opal/browser/interval.rb +41 -23
  169. data/opal/browser/location.rb +20 -4
  170. data/opal/browser/navigator.rb +136 -13
  171. data/opal/browser/polyfill/visual_viewport.rb +216 -0
  172. data/opal/browser/screen.rb +34 -8
  173. data/opal/browser/setup/base.rb +6 -0
  174. data/opal/browser/setup/full.rb +13 -0
  175. data/opal/browser/setup/large.rb +17 -0
  176. data/opal/browser/setup/mini.rb +8 -0
  177. data/opal/browser/setup/traditional.rb +10 -0
  178. data/opal/browser/socket.rb +16 -8
  179. data/opal/browser/storage.rb +155 -52
  180. data/opal/browser/support.rb +299 -0
  181. data/opal/browser/utils.rb +116 -18
  182. data/opal/browser/version.rb +1 -1
  183. data/opal/browser/visual_viewport.rb +39 -0
  184. data/opal/browser/window/size.rb +47 -9
  185. data/opal/browser/window/view.rb +37 -4
  186. data/opal/browser/window.rb +46 -26
  187. data/opal/browser.rb +1 -10
  188. data/opal/opal-browser.rb +1 -0
  189. data/opal-browser.gemspec +10 -12
  190. data/spec/database/sql_spec.rb +139 -0
  191. data/spec/delay_spec.rb +41 -0
  192. data/spec/dom/attribute_spec.rb +49 -0
  193. data/spec/dom/builder_spec.rb +36 -19
  194. data/spec/dom/document_spec.rb +28 -6
  195. data/spec/dom/element/attributes_spec.rb +52 -0
  196. data/spec/dom/element/custom_spec.rb +106 -0
  197. data/spec/dom/element/subclass_spec.rb +144 -0
  198. data/spec/dom/element_spec.rb +184 -7
  199. data/spec/dom/mutation_observer_spec.rb +13 -9
  200. data/spec/dom/node_set_spec.rb +44 -0
  201. data/spec/dom/node_spec.rb +87 -27
  202. data/spec/dom_spec.rb +19 -9
  203. data/spec/event_source_spec.rb +18 -15
  204. data/spec/{dom/event_spec.rb → event_spec.rb} +55 -26
  205. data/spec/history_spec.rb +32 -19
  206. data/spec/http_spec.rb +25 -36
  207. data/spec/immediate_spec.rb +10 -7
  208. data/spec/interval_spec.rb +59 -0
  209. data/spec/native_cached_wrapper_spec.rb +46 -0
  210. data/spec/runner.rb +107 -0
  211. data/spec/socket_spec.rb +18 -14
  212. data/spec/spec_helper.rb +2 -4
  213. data/spec/spec_helper_promise.rb.erb +25 -0
  214. data/spec/storage_spec.rb +7 -7
  215. data/spec/wgxpath.install.js +49 -0
  216. data/spec/window_spec.rb +2 -2
  217. metadata +181 -93
  218. data/opal/browser/compatibility/animation_frame.rb +0 -93
  219. data/opal/browser/compatibility/dom/document/window.rb +0 -15
  220. data/opal/browser/compatibility/dom/element/css.rb +0 -15
  221. data/opal/browser/compatibility/dom/element/matches.rb +0 -31
  222. data/opal/browser/compatibility/dom/element/offset.rb +0 -20
  223. data/opal/browser/compatibility/dom/element/scroll.rb +0 -25
  224. data/opal/browser/compatibility/dom/element/style.rb +0 -15
  225. data/opal/browser/compatibility/dom/mutation_observer.rb +0 -47
  226. data/opal/browser/compatibility/http/request.rb +0 -15
  227. data/opal/browser/compatibility/immediate.rb +0 -107
  228. data/opal/browser/compatibility/window/scroll.rb +0 -27
  229. data/opal/browser/compatibility/window/size.rb +0 -13
  230. data/opal/browser/compatibility/window/view.rb +0 -13
  231. data/opal/browser/compatibility.rb +0 -59
  232. data/opal/browser/dom/compatibility.rb +0 -8
  233. data/opal/browser/dom/event/animation.rb +0 -26
  234. data/opal/browser/dom/event/base.rb +0 -207
  235. data/opal/browser/dom/event/before_unload.rb +0 -13
  236. data/opal/browser/dom/event/clipboard.rb +0 -26
  237. data/opal/browser/dom/event/close.rb +0 -35
  238. data/opal/browser/dom/event/composition.rb +0 -38
  239. data/opal/browser/dom/event/custom.rb +0 -30
  240. data/opal/browser/dom/event/device_light.rb +0 -21
  241. data/opal/browser/dom/event/device_orientation.rb +0 -36
  242. data/opal/browser/dom/event/drag.rb +0 -113
  243. data/opal/browser/dom/event/focus.rb +0 -23
  244. data/opal/browser/dom/event/gamepad.rb +0 -47
  245. data/opal/browser/dom/event/keyboard.rb +0 -93
  246. data/opal/browser/dom/event/message.rb +0 -50
  247. data/opal/browser/dom/event/page_transition.rb +0 -21
  248. data/opal/browser/dom/event/pop_state.rb +0 -21
  249. data/opal/browser/dom/event/progress.rb +0 -31
  250. data/opal/browser/dom/event/sensor.rb +0 -13
  251. data/opal/browser/dom/event/ui.rb +0 -22
  252. data/opal/browser/dom/event.rb +0 -240
  253. data/opal/browser/http/compatibility.rb +0 -1
  254. data/opal/browser/http/parameters.rb +0 -8
  255. data/opal/browser/timeout.rb +0 -60
  256. data/opal/browser/window/compatibility.rb +0 -3
  257. data/opal/browser/window/scroll.rb +0 -49
@@ -1,114 +1,398 @@
1
- require 'browser/dom/element/position'
2
- require 'browser/dom/element/offset'
3
- require 'browser/dom/element/scroll'
4
-
5
- require 'browser/dom/element/input'
1
+ # Requires are moved to the bottom of this file.
6
2
 
7
3
  module Browser; module DOM
8
4
 
9
5
  class Element < Node
10
- def self.create(*args)
11
- $document.create_element(*args)
12
- end
6
+ def self.create(*args, &block)
7
+ if Document === args.first
8
+ document = args.shift
9
+ else
10
+ document = $document
11
+ end
13
12
 
14
- def self.new(node)
15
13
  if self == Element
16
- case `node.nodeName`.downcase
17
- when :input
18
- Input.new(node)
14
+ document.create_element(*args, &block)
15
+ elsif @tag_name
16
+ document.create_element(@tag_name, *args, &block)
17
+ elsif @selector
18
+ # That's crude, but should cover the most basic cases.
19
+ # Just in case, you can override it safely. To reiterate:
20
+ # .create is not to be used inside libraries, those are
21
+ # expected to use the Document#create_element API.
22
+ kwargs = {}
23
+ kwargs = args.pop if Hash === args.last
24
+ custom_attrs, custom_id, custom_classes = nil, nil, nil
25
+ tag_name = (@selector.scan(/^[\w-]+/).first || "div").upcase
26
+ classes = @selector.scan(/\.([\w-]+)/).flatten
27
+ classes |= custom_classes if custom_classes = kwargs.delete(:classes)
28
+ id = @selector.scan(/#([\w-]+)/).flatten.first
29
+ id = custom_id if custom_id = kwargs.delete(:id)
30
+ attrs = @selector.scan(/\[([\w-]+)=((["'])(.*?)\3|[\w_-]*)\]/).map { |a,b,_,d| [a,d||b] }.to_h
31
+ attrs = attrs.merge(custom_attrs) if custom_attrs = kwargs.delete(:attrs)
32
+ document.create_element(tag_name, *args, classes: classes, id: id, attrs: attrs, **kwargs, &block)
33
+ else
34
+ raise NotImplementedError
35
+ end
36
+ end
37
+
38
+ def self.subclasses
39
+ @subclasses ||= []
40
+ end
41
+
42
+ # Define a selector for subclass dispatch
43
+ #
44
+ # Example:
45
+ # ```
46
+ # class CustomElement < Browser::DOM::Element
47
+ # def_selector "div.hello-world"
48
+ # end
49
+ # ```
50
+ def self.def_selector(selector)
51
+ Element.subclasses << self
52
+
53
+ @selector = selector
54
+
55
+ # A special case to speedup dispatch
56
+ @tag_name = selector.upcase unless selector =~ /[^\w-]/
57
+ end
58
+
59
+ def self.selector
60
+ @selector
61
+ end
62
+
63
+ def self.tag_name
64
+ @tag_name
65
+ end
66
+
67
+ def self.new(*args, &block)
68
+ if args.length == 1 && !block_given? && Opal.native?(args[0])
69
+ # Use `.new` as a wrapping method.
70
+ node = args[0]
71
+ else
72
+ # Use `.new` as an alias for `.create`.
73
+ return create(*args, &block)
74
+ end
19
75
 
20
- else super
76
+ if self == Element
77
+ subclass = Element.subclasses.select do |subclass|
78
+ Element.native_is?(node, subclass)
79
+ end.last
80
+
81
+ if subclass
82
+ subclass.new(node)
83
+ else
84
+ super(node)
21
85
  end
22
86
  else
23
- super
87
+ super(node)
24
88
  end
25
89
  end
26
90
 
27
91
  include Event::Target
28
92
 
29
93
  target {|value|
30
- DOM(value) rescue nil
94
+ begin
95
+ DOM(value)
96
+ rescue StandardError, JS::Error
97
+ nil
98
+ end
31
99
  }
32
100
 
33
- alias_native :id
101
+ def self.native_is? (native, klass)
102
+ if tag_name = klass.tag_name
103
+ is = `(#{native}.getAttribute("is") || "")`
104
+ `#{tag_name} === #{is}.toUpperCase() || #{tag_name} === #{native}.nodeName`
105
+ else
106
+ Element.native_matches?(native, klass.selector)
107
+ end
108
+ end
34
109
 
35
- def add_class(*names)
36
- `#@native.className = #{(class_names + names).uniq.join ' '}`
110
+ if Browser.supports? 'Element.matches'
111
+ def self.native_matches? (native, selector)
112
+ `#{native}.matches(#{selector})`
113
+ end
114
+ elsif Browser.supports? 'Element.matches (Opera)'
115
+ def self.native_matches? (native, selector)
116
+ `#{native}.oMatchesSelector(#{selector})`
117
+ end
118
+ elsif Browser.supports? 'Element.matches (Internet Explorer)'
119
+ def self.native_matches? (native, selector)
120
+ `#{native}.msMatchesSelector(#{selector})`
121
+ end
122
+ elsif Browser.supports? 'Element.matches (Firefox)'
123
+ def self.native_matches? (native, selector)
124
+ `#{native}.mozMatchesSelector(#{selector})`
125
+ end
126
+ elsif Browser.supports? 'Element.matches (Chrome)'
127
+ def self.native_matches? (native, selector)
128
+ `#{native}.webkitMatchesSelector(#{selector})`
129
+ end
130
+ elsif Browser.loaded? 'Sizzle'
131
+ def self.native_matches? (native, selector)
132
+ `Sizzle.matchesSelector(#{native}, #{selector})`
133
+ end
134
+ else
135
+ def self.native_matches? (native, selector)
136
+ raise NotImplementedError, 'selector matching unsupported'
137
+ end
138
+ end
37
139
 
38
- self
140
+ # Check whether the element matches the given selector.
141
+ #
142
+ # @param selector [String] the CSS selector
143
+ def =~(selector)
144
+ Element.native_matches?(@native, selector)
39
145
  end
40
146
 
41
- def remove_class(*names)
42
- `#@native.className = #{(class_names - names).join ' '}`
147
+ # Allow for case expressions
148
+ alias === =~
149
+
150
+ # Query for children with the given XPpaths.
151
+ #
152
+ # @param paths [Array<String>] the XPaths to look for
153
+ #
154
+ # @return [NodeSet]
155
+ def /(*paths)
156
+ NodeSet[paths.map { |path| xpath(path) }]
157
+ end
158
+
159
+ # Get the attribute with the given name.
160
+ #
161
+ # @param name [String] the attribute name
162
+ # @param options [Hash] options for the attribute
163
+ #
164
+ # @option options [String] :namespace the namespace for the attribute
165
+ #
166
+ # @return [String?]
167
+ def [](name, options = {})
168
+ attributes.get(name, options)
169
+ end
170
+
171
+ # Set the attribute with the given name and value.
172
+ #
173
+ # @param name [String] the attribute name
174
+ # @param value [Object] the attribute value
175
+ # @param options [Hash] the options for the attribute
176
+ #
177
+ # @option options [String] :namespace the namespace for the attribute
178
+ def []=(name, value, options = {})
179
+ attributes.set(name, value, options)
180
+ end
181
+
182
+ # Add class names to the element.
183
+ #
184
+ # @param names [Array<String>] class names to add
185
+ #
186
+ # @return [self]
187
+ def add_class(*names)
188
+ classes = class_names + names
189
+
190
+ unless classes.empty?
191
+ `#@native.className = #{classes.uniq.join ' '}`
192
+ end
43
193
 
44
194
  self
45
195
  end
46
196
 
47
- alias_native :class_name, :className
197
+ # Get the first node that matches the given CSS selector or XPath.
198
+ #
199
+ # @param path_or_selector [String] an XPath or CSS selector
200
+ #
201
+ # @return [Node?]
202
+ def at(path_or_selector)
203
+ xpath(path_or_selector).first || css(path_or_selector).first
204
+ end
48
205
 
49
- def class_names
50
- `#@native.className`.split(/\s+/).reject(&:empty?)
206
+ # Get the first node matching the given CSS selectors.
207
+ #
208
+ # @param rules [Array<String>] the CSS selectors to match with
209
+ #
210
+ # @return [Node?]
211
+ def at_css(*rules)
212
+ result = nil
213
+
214
+ rules.each {|rule|
215
+ if result = css(rule).first
216
+ break
217
+ end
218
+ }
219
+
220
+ result
51
221
  end
52
222
 
53
- alias attribute attr
223
+ # Get the first node matching the given XPath.
224
+ #
225
+ # @param paths [Array<String>] the XPath to match with
226
+ #
227
+ # @return [Node?]
228
+ def at_xpath(*paths)
229
+ result = nil
54
230
 
55
- def attribute_nodes
56
- Native::Array.new(`#@native.attributes`, get: :item) { |e| DOM(e) }
231
+ paths.each {|path|
232
+ if result = xpath(path).first
233
+ break
234
+ end
235
+ }
236
+
237
+ result
57
238
  end
58
239
 
240
+ alias attr []
241
+
242
+ alias attribute []
243
+
244
+ # @!attribute [r] attributes
245
+ # @return [Attributes] the attributes for the element
59
246
  def attributes(options = {})
60
247
  Attributes.new(self, options)
61
248
  end
62
249
 
63
- def get(name, options = {})
64
- if namespace = options[:namespace]
65
- `#@native.getAttributeNS(#{namespace.to_s}, #{name.to_s}) || nil`
66
- else
67
- `#@native.getAttribute(#{name.to_s}) || nil`
250
+ # @!attribute [r] attribute_nodes
251
+ # @return [NodeSet] the attribute nodes for the element
252
+ def attribute_nodes
253
+ NodeSet[Native::Array.new(`#@native.attributes`, get: :item)]
254
+ end
255
+
256
+ # @!attribute [r] class_name
257
+ # @return [String] all the element class names
258
+ alias_native :class_name, :className
259
+
260
+ # @!attribute [r] class_names
261
+ # @return [Array<String>] all the element class names
262
+ def class_names
263
+ `#@native.className`.split(/\s+/).reject(&:empty?)
264
+ end
265
+
266
+ if Browser.supports? 'Query.css'
267
+ def css(path)
268
+ NodeSet[Native::Array.new(`#@native.querySelectorAll(path)`)]
269
+ rescue StandardError, JS::Error
270
+ NodeSet[]
271
+ end
272
+ elsif Browser.loaded? 'Sizzle'
273
+ def css(path)
274
+ NodeSet[`Sizzle(path, #@native)`]
275
+ rescue StandardError, JS::Error
276
+ NodeSet[]
68
277
  end
278
+ else
279
+ # Query for children matching the given CSS selector.
280
+ #
281
+ # @param selector [String] the CSS selector
282
+ #
283
+ # @return [NodeSet]
284
+ def css(selector)
285
+ raise NotImplementedError, 'query by CSS selector unsupported'
286
+ end
287
+ end
288
+
289
+ # Click the element. it fires the element's click event.
290
+ def click
291
+ `#@native.click()`
292
+ self
69
293
  end
70
294
 
71
- def set(name, value, options = {})
72
- if namespace = options[:namespace]
73
- `#@native.setAttributeNS(#{namespace.to_s}, #{name.to_s}, #{value})`
295
+ # @overload data()
296
+ #
297
+ # Return the data for the element.
298
+ #
299
+ # @return [Data]
300
+ #
301
+ # @overload data(hash)
302
+ #
303
+ # Set data on the element.
304
+ #
305
+ # @param hash [Hash] the data to set
306
+ #
307
+ # @return [self]
308
+ def data(value = nil)
309
+ data = Data.new(self)
310
+
311
+ return data unless value
312
+
313
+ if Hash === value
314
+ data.assign(value)
74
315
  else
75
- `#@native.setAttribute(#{name.to_s}, #{value.to_s})`
316
+ raise ArgumentError, 'unknown data type'
76
317
  end
318
+
319
+ self
77
320
  end
78
321
 
79
- alias [] get
80
- alias []= set
322
+ alias get_attribute []
81
323
 
82
- alias attr get
83
- alias attribute get
324
+ alias get []
84
325
 
85
- alias get_attribute get
86
- alias set_attribute set
326
+ # @!attribute height
327
+ # @return [Integer] the height of the element
328
+ def height
329
+ size.height
330
+ end
87
331
 
88
- def key?(name)
89
- !!self[name]
332
+ def height=(value)
333
+ size.height = value
90
334
  end
91
335
 
92
- def keys
93
- attributes_nodesmap(&:name)
336
+ # @!attribute id
337
+ # @return [String?] the ID of the element
338
+ def id
339
+ %x{
340
+ var id = #@native.id;
341
+
342
+ if (id === "") {
343
+ return nil;
344
+ }
345
+ else {
346
+ return id;
347
+ }
348
+ }
94
349
  end
95
350
 
96
- def values
97
- attribute_nodes.map(&:value)
351
+ def id=(value)
352
+ `#@native.id = #{value.to_s}`
98
353
  end
99
354
 
100
- def remove_attribute(name)
101
- `#@native.removeAttribute(name)`
355
+ # Set the inner DOM of the element using the {Builder}.
356
+ def inner_dom(builder=nil, &block)
357
+ self.inner_dom = Builder.new(document, builder, &block).to_a
358
+ self
102
359
  end
103
360
 
104
- def size(*inc)
105
- Size.new(`#@native.offsetWidth`, `#@native.offsetHeight`)
361
+ # Set the inner DOM with the given node.
362
+ #
363
+ # (see #append_child)
364
+ def inner_dom=(node)
365
+ clear
366
+
367
+ self << node
106
368
  end
107
369
 
108
- def position
109
- Position.new(self)
370
+ # @!attribute inner_html
371
+ # @return [String] the inner HTML of the element
372
+ def inner_html
373
+ `#@native.innerHTML`
374
+ end
375
+
376
+ def inner_html=(value)
377
+ `#@native.innerHTML = #{value}`
378
+ end
379
+
380
+ def inspect
381
+ inspect = name.downcase
382
+
383
+ if id
384
+ inspect += '.' + id + '!'
385
+ end
386
+
387
+ unless class_names.empty?
388
+ inspect += '.' + class_names.join('.')
389
+ end
390
+
391
+ "#<#{self.class.name.gsub("Browser::","")}: #{inspect}>"
110
392
  end
111
393
 
394
+ # @!attribute offset
395
+ # @return [Offset] the offset of the element
112
396
  def offset(*values)
113
397
  off = Offset.new(self)
114
398
 
@@ -123,168 +407,212 @@ class Element < Node
123
407
  offset.set(*value)
124
408
  end
125
409
 
126
- def scroll
127
- Scroll.new(self)
128
- end
129
-
130
- def inner_dom(&block)
131
- # FIXME: when block passing is fixed
132
- doc = document
133
- clear; Builder.new(doc, self, &block)
134
-
135
- self
136
- end
137
-
138
- def inner_dom=(node)
139
- clear; self << node
140
- end
141
-
142
- def /(*paths)
143
- paths.map { |path| xpath(path) }.flatten.uniq
144
- end
145
-
146
- def at(path)
147
- xpath(path).first || css(path).first
410
+ # @!attribute outer_html
411
+ # @return [String] the outer HTML of the element
412
+ def outer_html
413
+ `#@native.outerHTML`
148
414
  end
149
415
 
150
- def at_css(*rules)
151
- rules.each {|rule|
152
- found = css(rule).first
153
-
154
- return found if found
155
- }
156
-
157
- nil
416
+ # @!attribute [r] position
417
+ # @return [Position] the position of the element
418
+ def position
419
+ @position ||= Position.new(self)
158
420
  end
159
421
 
160
- def at_xpath(*paths)
161
- paths.each {|path|
162
- found = xpath(path).first
163
-
164
- return found if found
165
- }
166
-
167
- nil
422
+ # @!attribute [r] scroll
423
+ # @return [Scroll] the scrolling for the element
424
+ def scroll
425
+ @scroll ||= Scroll.new(self)
168
426
  end
169
427
 
428
+ # Search for all the children matching the given XPaths or CSS selectors.
429
+ #
430
+ # @param selectors [Array<String>] mixed list of XPaths and CSS selectors
431
+ #
432
+ # @return [NodeSet]
170
433
  def search(*selectors)
171
- NodeSet.new document, selectors.map {|selector|
434
+ NodeSet.new selectors.map {|selector|
172
435
  xpath(selector).to_a.concat(css(selector).to_a)
173
436
  }.flatten.uniq
174
437
  end
175
438
 
176
- def css(path)
177
- NodeSet.new(document, Native::Array.new(`#@native.querySelectorAll(path)`))
178
- end
179
-
180
- def xpath(path)
181
- result = []
182
-
183
- begin
184
- %x{
185
- var tmp = (#@native.ownerDocument || #@native).evaluate(
186
- path, #@native, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
439
+ alias set []=
187
440
 
188
- result = #{Native::Array.new(`tmp`, get: :snapshotItem, length: :snapshotLength)};
189
- }
190
- rescue; end
441
+ alias set_attribute []=
191
442
 
192
- NodeSet.new(document, result)
443
+ # Creates or accesses the shadow root of this element
444
+ #
445
+ # @param open [Boolean] set to false if you want to create a closed
446
+ # shadow root
447
+ #
448
+ # @return [ShadowRoot]
449
+ def shadow (open = true)
450
+ if root = `#@native.shadowRoot`
451
+ DOM(root)
452
+ else
453
+ DOM(`#@native.attachShadow({mode: #{open ? "open" : "closed"}})`)
454
+ end
193
455
  end
194
456
 
457
+ # Checks for a presence of a shadow root of this element
458
+ #
459
+ # @return [Boolean]
460
+ def shadow?
461
+ `!!#@native.shadowRoot`
462
+ end
463
+
464
+ # @overload style()
465
+ #
466
+ # Return the style for the element.
467
+ #
468
+ # @return [CSS::Declaration]
469
+ #
470
+ # @overload style(data)
471
+ #
472
+ # Set the CSS style as string or set of values.
473
+ #
474
+ # @param data [String, Hash] the new style
475
+ #
476
+ # @return [self]
477
+ #
478
+ # @overload style(&block)
479
+ #
480
+ # Set the CSS style from a CSS builder DSL.
481
+ #
482
+ # @return [self]
195
483
  def style(data = nil, &block)
196
484
  style = CSS::Declaration.new(`#@native.style`)
197
485
 
198
486
  return style unless data || block
199
487
 
200
- if data.is_a?(String)
488
+ if String === data
201
489
  style.replace(data)
202
- elsif data.is_a?(Enumerable)
490
+ elsif Hash === data
203
491
  style.assign(data)
204
- end
205
-
206
- if block
492
+ elsif block
207
493
  style.apply(&block)
494
+ else
495
+ raise ArgumentError, 'unknown data type'
208
496
  end
209
497
 
210
498
  self
211
499
  end
212
500
 
213
- def style!
214
- CSS::Declaration.new(`#{window.to_n}.getComputedStyle(#@native, null)`)
215
- end
216
-
217
- def data(what)
218
- unless defined?(`#@native.$data`)
219
- `#@native.$data = {}`
501
+ if Browser.supports? 'CSS.computed'
502
+ def style!
503
+ CSS::Declaration.new(`#{window.to_n}.getComputedStyle(#@native, null)`)
220
504
  end
221
-
222
- if Hash === what
223
- what.each {|name, value|
224
- `#@native.$data[name] = value`
225
- }
226
- else
227
- %x{
228
- var value = #@native.$data[what];
229
-
230
- if (value === undefined) {
231
- return nil;
232
- }
233
- else {
234
- return value;
235
- }
236
- }
505
+ elsif Browser.supports? 'CSS.current'
506
+ def style!
507
+ CSS::Declaration.new(`#@native.currentStyle`)
508
+ end
509
+ else
510
+ # @!attribute [r] style!
511
+ # @return [CSS::Declaration] get the computed style for the element
512
+ def style!
513
+ raise NotImplementedError, 'computed style unsupported'
237
514
  end
238
515
  end
239
516
 
240
- def matches?(selector)
241
- `#@native.matches(#{selector})`
517
+ # Remove an attribute from the element.
518
+ #
519
+ # @param name [String] the attribute name
520
+ def remove_attribute(name)
521
+ `#@native.removeAttribute(name)`
242
522
  end
243
523
 
244
- def window
245
- document.window
246
- end
524
+ # Remove class names from the element.
525
+ #
526
+ # @param names [Array<String>] class names to remove
527
+ #
528
+ # @return [self]
529
+ def remove_class(*names)
530
+ classes = class_names - names
247
531
 
248
- def inspect
249
- "#<DOM::Element: #{name}>"
532
+ if classes.empty?
533
+ `#@native.removeAttribute('class')`
534
+ else
535
+ `#@native.className = #{classes.join ' '}`
536
+ end
537
+
538
+ self
250
539
  end
251
540
 
252
- class Attributes
253
- include Enumerable
541
+ # @!attribute [r] size
542
+ # @return [Size] the size of the element
543
+ def size(*inc)
544
+ Size.new(self, *inc)
545
+ end
254
546
 
255
- attr_reader :namespace
547
+ # Toggle class names of the element.
548
+ #
549
+ # @param names [Array<String>] class names to toggle
550
+ #
551
+ # @return [self]
552
+ def toggle_class(*names)
553
+ to_remove, to_add = names.partition { |name| class_names.include? name }
256
554
 
257
- def initialize(element, options)
258
- @element = element
259
- @namespace = options[:namespace]
260
- end
555
+ add_class(*to_add)
556
+ remove_class(*to_remove)
557
+ end
261
558
 
262
- def each(&block)
263
- return enum_for :each unless block_given?
559
+ # @!attribute width
560
+ # @return [Integer] the width of the element
561
+ def width
562
+ size.width
563
+ end
264
564
 
265
- @element.attribute_nodes.each {|attr|
266
- yield attr.name, attr.value
267
- }
565
+ def width=(value)
566
+ size.width = value
567
+ end
268
568
 
269
- self
270
- end
569
+ # @!attribute [r] window
570
+ # @return [Window] the window for the element
571
+ def window
572
+ document.window
573
+ end
271
574
 
272
- def [](name)
273
- @element.get_attribute(name, namespace: @namespace)
575
+ if Browser.supports?('Query.xpath') || Browser.loaded?('wicked-good-xpath')
576
+ if Browser.loaded? 'wicked-good-xpath'
577
+ `wgxpath.install()`
274
578
  end
275
579
 
276
- def []=(name, value)
277
- @element.set_attribute(name, value, namespace: @namespace)
580
+ def xpath(path)
581
+ NodeSet[Native::Array.new(
582
+ `(#@native.ownerDocument || #@native).evaluate(path,
583
+ #@native, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)`,
584
+ get: :snapshotItem,
585
+ length: :snapshotLength)]
586
+ rescue StandardError, JS::Error
587
+ NodeSet[]
278
588
  end
279
-
280
- def merge!(hash)
281
- hash.each {|name, value|
282
- self[name] = value
283
- }
284
-
285
- self
589
+ else
590
+ # Query for children matching the given XPath.
591
+ #
592
+ # @param path [String] the XPath
593
+ #
594
+ # @return [NodeSet]
595
+ def xpath(path)
596
+ raise NotImplementedError, 'query by XPath unsupported'
286
597
  end
287
598
  end
288
599
  end
289
600
 
290
601
  end; end
602
+
603
+ require 'browser/dom/element/attributes'
604
+ require 'browser/dom/element/data'
605
+ require 'browser/dom/element/position'
606
+ require 'browser/dom/element/offset'
607
+ require 'browser/dom/element/scroll'
608
+ require 'browser/dom/element/size'
609
+
610
+ require 'browser/dom/element/button'
611
+ require 'browser/dom/element/image'
612
+ require 'browser/dom/element/form'
613
+ require 'browser/dom/element/input'
614
+ require 'browser/dom/element/select'
615
+ require 'browser/dom/element/template'
616
+ require 'browser/dom/element/textarea'
617
+ require 'browser/dom/element/iframe'
618
+ require 'browser/dom/element/media'