opal-browser 0.2.0.beta1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/build.yml +95 -0
  3. data/.gitignore +3 -0
  4. data/CHANGELOG.md +8 -0
  5. data/Gemfile +17 -3
  6. data/LICENSE +2 -1
  7. data/README.md +183 -52
  8. data/Rakefile +29 -1
  9. data/config.ru +20 -3
  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 +8 -6
  78. data/lib/opal-browser.rb +1 -0
  79. data/opal/browser/animation_frame.rb +26 -1
  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 +1 -11
  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 +17 -13
  89. data/opal/browser/console.rb +3 -1
  90. data/opal/browser/cookies.rb +78 -42
  91. data/opal/browser/crypto.rb +79 -0
  92. data/opal/browser/css/declaration.rb +1 -1
  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 +41 -7
  98. data/opal/browser/dom/attribute.rb +13 -12
  99. data/opal/browser/dom/builder.rb +31 -17
  100. data/opal/browser/dom/document.rb +174 -42
  101. data/opal/browser/dom/document_fragment.rb +18 -0
  102. data/opal/browser/dom/document_or_shadow_root.rb +19 -0
  103. data/opal/browser/dom/element/attributes.rb +111 -0
  104. data/opal/browser/dom/element/button.rb +31 -0
  105. data/opal/browser/dom/element/custom.rb +177 -0
  106. data/opal/browser/dom/element/data.rb +82 -0
  107. data/opal/browser/dom/element/editable.rb +47 -0
  108. data/opal/browser/dom/element/form.rb +38 -0
  109. data/opal/browser/dom/element/iframe.rb +37 -0
  110. data/opal/browser/dom/element/image.rb +2 -0
  111. data/opal/browser/dom/element/input.rb +48 -1
  112. data/opal/browser/dom/element/media.rb +17 -0
  113. data/opal/browser/dom/element/offset.rb +5 -0
  114. data/opal/browser/dom/element/position.rb +11 -2
  115. data/opal/browser/dom/element/scroll.rb +123 -24
  116. data/opal/browser/dom/element/select.rb +42 -0
  117. data/opal/browser/dom/element/size.rb +17 -0
  118. data/opal/browser/dom/element/template.rb +11 -0
  119. data/opal/browser/dom/element/textarea.rb +26 -0
  120. data/opal/browser/dom/element.rb +468 -238
  121. data/opal/browser/dom/mutation_observer.rb +4 -4
  122. data/opal/browser/dom/node.rb +142 -60
  123. data/opal/browser/dom/node_set.rb +73 -44
  124. data/opal/browser/dom/shadow_root.rb +12 -0
  125. data/opal/browser/dom/text.rb +2 -2
  126. data/opal/browser/dom.rb +40 -16
  127. data/opal/browser/effects.rb +180 -3
  128. data/opal/browser/event/all.rb +26 -0
  129. data/opal/browser/{dom/event → event}/animation.rb +4 -2
  130. data/opal/browser/{dom/event → event}/audio_processing.rb +4 -2
  131. data/opal/browser/{dom/event → event}/base.rb +98 -9
  132. data/opal/browser/{dom/event → event}/before_unload.rb +4 -2
  133. data/opal/browser/{dom/event → event}/clipboard.rb +11 -2
  134. data/opal/browser/{dom/event → event}/close.rb +4 -2
  135. data/opal/browser/{dom/event → event}/composition.rb +4 -2
  136. data/opal/browser/{dom/event → event}/custom.rb +3 -3
  137. data/opal/browser/event/data_transfer.rb +95 -0
  138. data/opal/browser/{dom/event → event}/device_light.rb +4 -2
  139. data/opal/browser/{dom/event → event}/device_motion.rb +4 -2
  140. data/opal/browser/{dom/event → event}/device_orientation.rb +4 -2
  141. data/opal/browser/{dom/event → event}/device_proximity.rb +4 -2
  142. data/opal/browser/{dom/event → event}/drag.rb +11 -7
  143. data/opal/browser/{dom/event → event}/focus.rb +4 -2
  144. data/opal/browser/{dom/event → event}/gamepad.rb +5 -3
  145. data/opal/browser/{dom/event → event}/hash_change.rb +4 -2
  146. data/opal/browser/{dom/event → event}/keyboard.rb +16 -3
  147. data/opal/browser/{dom/event → event}/message.rb +4 -2
  148. data/opal/browser/{dom/event → event}/mouse.rb +12 -8
  149. data/opal/browser/{dom/event → event}/page_transition.rb +4 -2
  150. data/opal/browser/{dom/event → event}/pop_state.rb +4 -2
  151. data/opal/browser/{dom/event → event}/progress.rb +4 -2
  152. data/opal/browser/{dom/event → event}/sensor.rb +4 -2
  153. data/opal/browser/{dom/event → event}/storage.rb +4 -2
  154. data/opal/browser/{dom/event → event}/touch.rb +4 -2
  155. data/opal/browser/{dom/event → event}/ui.rb +2 -2
  156. data/opal/browser/{dom/event → event}/wheel.rb +4 -2
  157. data/opal/browser/event.rb +163 -0
  158. data/opal/browser/event_source.rb +2 -2
  159. data/opal/browser/form_data.rb +225 -0
  160. data/opal/browser/history.rb +4 -8
  161. data/opal/browser/http/binary.rb +1 -0
  162. data/opal/browser/http/headers.rb +16 -2
  163. data/opal/browser/http/request.rb +46 -48
  164. data/opal/browser/http/response.rb +5 -1
  165. data/opal/browser/http.rb +25 -2
  166. data/opal/browser/immediate.rb +9 -5
  167. data/opal/browser/interval.rb +34 -11
  168. data/opal/browser/location.rb +7 -1
  169. data/opal/browser/navigator.rb +127 -7
  170. data/opal/browser/polyfill/visual_viewport.rb +216 -0
  171. data/opal/browser/screen.rb +3 -3
  172. data/opal/browser/setup/base.rb +6 -0
  173. data/opal/browser/setup/full.rb +13 -0
  174. data/opal/browser/setup/large.rb +17 -0
  175. data/opal/browser/setup/mini.rb +8 -0
  176. data/opal/browser/setup/traditional.rb +10 -0
  177. data/opal/browser/socket.rb +8 -4
  178. data/opal/browser/storage.rb +53 -35
  179. data/opal/browser/support.rb +72 -5
  180. data/opal/browser/utils.rb +94 -14
  181. data/opal/browser/version.rb +1 -1
  182. data/opal/browser/visual_viewport.rb +39 -0
  183. data/opal/browser/window/size.rb +31 -3
  184. data/opal/browser/window/view.rb +15 -0
  185. data/opal/browser/window.rb +46 -25
  186. data/opal/browser.rb +1 -10
  187. data/opal/opal-browser.rb +1 -0
  188. data/opal-browser.gemspec +3 -3
  189. data/spec/database/sql_spec.rb +139 -0
  190. data/spec/delay_spec.rb +41 -0
  191. data/spec/dom/attribute_spec.rb +49 -0
  192. data/spec/dom/builder_spec.rb +25 -8
  193. data/spec/dom/document_spec.rb +22 -0
  194. data/spec/dom/element/attributes_spec.rb +52 -0
  195. data/spec/dom/element/custom_spec.rb +106 -0
  196. data/spec/dom/element/subclass_spec.rb +144 -0
  197. data/spec/dom/element_spec.rb +181 -4
  198. data/spec/dom/mutation_observer_spec.rb +12 -8
  199. data/spec/dom/node_set_spec.rb +44 -0
  200. data/spec/dom/node_spec.rb +48 -0
  201. data/spec/dom_spec.rb +8 -0
  202. data/spec/event_source_spec.rb +15 -12
  203. data/spec/{dom/event_spec.rb → event_spec.rb} +44 -15
  204. data/spec/history_spec.rb +23 -19
  205. data/spec/http_spec.rb +19 -31
  206. data/spec/immediate_spec.rb +5 -4
  207. data/spec/interval_spec.rb +59 -0
  208. data/spec/native_cached_wrapper_spec.rb +46 -0
  209. data/spec/runner.rb +62 -69
  210. data/spec/socket_spec.rb +16 -12
  211. data/spec/spec_helper.rb +2 -5
  212. data/spec/spec_helper_promise.rb.erb +25 -0
  213. data/spec/storage_spec.rb +1 -1
  214. metadata +172 -50
  215. data/.travis.yml +0 -60
  216. data/opal/browser/dom/event.rb +0 -253
  217. data/opal/browser/http/parameters.rb +0 -8
  218. data/opal/browser/window/scroll.rb +0 -59
@@ -1,40 +1,189 @@
1
- require 'browser/dom/element/position'
2
- require 'browser/dom/element/offset'
3
- require 'browser/dom/element/scroll'
4
- require 'browser/dom/element/size'
5
-
6
- require 'browser/dom/element/input'
7
- require 'browser/dom/element/image'
1
+ # Requires are moved to the bottom of this file.
8
2
 
9
3
  module Browser; module DOM
10
4
 
11
5
  class Element < Node
12
- def self.create(*args)
13
- $document.create_element(*args)
6
+ def self.create(*args, &block)
7
+ if Document === args.first
8
+ document = args.shift
9
+ else
10
+ document = $document
11
+ end
12
+
13
+ if self == Element
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
14
65
  end
15
66
 
16
- def self.new(node)
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
75
+
17
76
  if self == Element
18
- name = `node.nodeName`.capitalize
77
+ subclass = Element.subclasses.select do |subclass|
78
+ Element.native_is?(node, subclass)
79
+ end.last
19
80
 
20
- if Element.const_defined?(name)
21
- Element.const_get(name).new(node)
81
+ if subclass
82
+ subclass.new(node)
22
83
  else
23
- super
84
+ super(node)
24
85
  end
25
86
  else
26
- super
87
+ super(node)
27
88
  end
28
89
  end
29
90
 
30
91
  include Event::Target
31
92
 
32
93
  target {|value|
33
- DOM(value) rescue nil
94
+ begin
95
+ DOM(value)
96
+ rescue StandardError, JS::Error
97
+ nil
98
+ end
34
99
  }
35
100
 
36
- 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
37
109
 
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
139
+
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)
145
+ end
146
+
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]
38
187
  def add_class(*names)
39
188
  classes = class_names + names
40
189
 
@@ -45,99 +194,205 @@ class Element < Node
45
194
  self
46
195
  end
47
196
 
48
- def remove_class(*names)
49
- classes = class_names - names
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
50
205
 
51
- if classes.empty?
52
- `#@native.removeAttribute('class')`
53
- else
54
- `#@native.className = #{classes.join ' '}`
55
- end
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
56
213
 
57
- self
214
+ rules.each {|rule|
215
+ if result = css(rule).first
216
+ break
217
+ end
218
+ }
219
+
220
+ result
58
221
  end
59
222
 
60
- alias_native :class_name, :className
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
61
230
 
62
- def class_names
63
- `#@native.className`.split(/\s+/).reject(&:empty?)
231
+ paths.each {|path|
232
+ if result = xpath(path).first
233
+ break
234
+ end
235
+ }
236
+
237
+ result
64
238
  end
65
239
 
66
- alias attribute attr
240
+ alias attr []
67
241
 
68
- def attribute_nodes
69
- Native::Array.new(`#@native.attributes`, get: :item) { |e| DOM(e) }
70
- end
242
+ alias attribute []
71
243
 
244
+ # @!attribute [r] attributes
245
+ # @return [Attributes] the attributes for the element
72
246
  def attributes(options = {})
73
247
  Attributes.new(self, options)
74
248
  end
75
249
 
76
- def get(name, options = {})
77
- if namespace = options[:namespace]
78
- `#@native.getAttributeNS(#{namespace.to_s}, #{name.to_s}) || nil`
79
- else
80
- `#@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[]
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'
81
286
  end
82
287
  end
83
288
 
84
- def set(name, value, options = {})
85
- if namespace = options[:namespace]
86
- `#@native.setAttributeNS(#{namespace.to_s}, #{name.to_s}, #{value})`
289
+ # Click the element. it fires the element's click event.
290
+ def click
291
+ `#@native.click()`
292
+ self
293
+ end
294
+
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)
87
315
  else
88
- `#@native.setAttribute(#{name.to_s}, #{value.to_s})`
316
+ raise ArgumentError, 'unknown data type'
89
317
  end
90
- end
91
318
 
92
- alias [] get
93
- alias []= set
319
+ self
320
+ end
94
321
 
95
- alias attr get
96
- alias attribute get
322
+ alias get_attribute []
97
323
 
98
- alias get_attribute get
99
- alias set_attribute set
324
+ alias get []
100
325
 
101
- def key?(name)
102
- !!self[name]
326
+ # @!attribute height
327
+ # @return [Integer] the height of the element
328
+ def height
329
+ size.height
103
330
  end
104
331
 
105
- def keys
106
- attributes_nodesmap(&:name)
332
+ def height=(value)
333
+ size.height = value
107
334
  end
108
335
 
109
- def values
110
- attribute_nodes.map(&:value)
111
- end
336
+ # @!attribute id
337
+ # @return [String?] the ID of the element
338
+ def id
339
+ %x{
340
+ var id = #@native.id;
112
341
 
113
- def remove_attribute(name)
114
- `#@native.removeAttribute(name)`
342
+ if (id === "") {
343
+ return nil;
344
+ }
345
+ else {
346
+ return id;
347
+ }
348
+ }
115
349
  end
116
350
 
117
- def size(*inc)
118
- Size.new(self, *inc)
351
+ def id=(value)
352
+ `#@native.id = #{value.to_s}`
119
353
  end
120
354
 
121
- def height
122
- size.height
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
123
359
  end
124
360
 
125
- def height=(value)
126
- size.height = value
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
127
368
  end
128
369
 
129
- def width
130
- size.width
370
+ # @!attribute inner_html
371
+ # @return [String] the inner HTML of the element
372
+ def inner_html
373
+ `#@native.innerHTML`
131
374
  end
132
375
 
133
- def width=(value)
134
- size.width = value
376
+ def inner_html=(value)
377
+ `#@native.innerHTML = #{value}`
135
378
  end
136
379
 
137
- def position
138
- Position.new(self)
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}>"
139
392
  end
140
393
 
394
+ # @!attribute offset
395
+ # @return [Offset] the offset of the element
141
396
  def offset(*values)
142
397
  off = Offset.new(self)
143
398
 
@@ -152,116 +407,92 @@ class Element < Node
152
407
  offset.set(*value)
153
408
  end
154
409
 
155
- def scroll
156
- Scroll.new(self)
157
- end
158
-
159
- def inner_dom(&block)
160
- # FIXME: when block passing is fixed
161
- doc = document
162
- clear; Builder.new(doc, self, &block)
163
-
164
- self
165
- end
166
-
167
- def inner_dom=(node)
168
- clear; self << node
169
- end
170
-
171
- def /(*paths)
172
- paths.map { |path| xpath(path) }.flatten.uniq
173
- end
174
-
175
- def at(path)
176
- 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`
177
414
  end
178
415
 
179
- def at_css(*rules)
180
- rules.each {|rule|
181
- found = css(rule).first
182
-
183
- return found if found
184
- }
185
-
186
- nil
416
+ # @!attribute [r] position
417
+ # @return [Position] the position of the element
418
+ def position
419
+ @position ||= Position.new(self)
187
420
  end
188
421
 
189
- def at_xpath(*paths)
190
- paths.each {|path|
191
- found = xpath(path).first
192
-
193
- return found if found
194
- }
195
-
196
- nil
422
+ # @!attribute [r] scroll
423
+ # @return [Scroll] the scrolling for the element
424
+ def scroll
425
+ @scroll ||= Scroll.new(self)
197
426
  end
198
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]
199
433
  def search(*selectors)
200
- NodeSet.new document, selectors.map {|selector|
434
+ NodeSet.new selectors.map {|selector|
201
435
  xpath(selector).to_a.concat(css(selector).to_a)
202
436
  }.flatten.uniq
203
437
  end
204
438
 
205
- if Browser.supports? 'Query.css'
206
- def css(path)
207
- %x{
208
- try {
209
- var result = #@native.querySelectorAll(path);
210
-
211
- return #{NodeSet.new(document,
212
- Native::Array.new(`result`))};
213
- }
214
- catch(e) {
215
- return #{NodeSet.new(document)};
216
- }
217
- }
218
- end
219
- elsif Browser.loaded? 'Sizzle'
220
- def css(path)
221
- NodeSet.new(document, `Sizzle(#{path}, #@native)`)
222
- end
223
- else
224
- def css(selector)
225
- raise NotImplementedError, 'query by CSS selector unsupported'
226
- end
227
- end
439
+ alias set []=
228
440
 
229
- if Browser.supports?('Query.xpath') || Browser.loaded?('wicked-good-xpath')
230
- if Browser.loaded? 'wicked-good-xpath'
231
- `wgxpath.install()`
232
- end
441
+ alias set_attribute []=
233
442
 
234
- def xpath(path)
235
- %x{
236
- try {
237
- var result = (#@native.ownerDocument || #@native).evaluate(path,
238
- #@native, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
239
-
240
- return #{NodeSet.new(document,
241
- Native::Array.new(`result`, get: :snapshotItem, length: :snapshotLength))};
242
- }
243
- catch (e) {
244
- return #{NodeSet.new(document)};
245
- }
246
- }
247
- end
248
- else
249
- def xpath(path)
250
- raise NotImplementedError, 'query by XPath unsupported'
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"}})`)
251
454
  end
252
455
  end
253
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]
254
483
  def style(data = nil, &block)
255
484
  style = CSS::Declaration.new(`#@native.style`)
256
485
 
257
486
  return style unless data || block
258
487
 
259
- if data.is_a?(String)
488
+ if String === data
260
489
  style.replace(data)
261
- elsif data.is_a?(Enumerable)
490
+ elsif Hash === data
262
491
  style.assign(data)
263
492
  elsif block
264
493
  style.apply(&block)
494
+ else
495
+ raise ArgumentError, 'unknown data type'
265
496
  end
266
497
 
267
498
  self
@@ -276,113 +507,112 @@ class Element < Node
276
507
  CSS::Declaration.new(`#@native.currentStyle`)
277
508
  end
278
509
  else
510
+ # @!attribute [r] style!
511
+ # @return [CSS::Declaration] get the computed style for the element
279
512
  def style!
280
513
  raise NotImplementedError, 'computed style unsupported'
281
514
  end
282
515
  end
283
516
 
284
- def data(what)
285
- if Hash === what
286
- unless defined?(`#@native.$data`)
287
- `#@native.$data = {}`
288
- end
289
-
290
- what.each {|name, value|
291
- `#@native.$data[name] = value`
292
- }
293
- else
294
- return self["data-#{what}"] if self["data-#{what}"]
295
-
296
- return unless defined?(`#@native.$data`)
297
-
298
- %x{
299
- var value = #@native.$data[what];
300
-
301
- if (value === undefined) {
302
- return nil;
303
- }
304
- else {
305
- return value;
306
- }
307
- }
308
- end
517
+ # Remove an attribute from the element.
518
+ #
519
+ # @param name [String] the attribute name
520
+ def remove_attribute(name)
521
+ `#@native.removeAttribute(name)`
309
522
  end
310
523
 
311
- if Browser.supports? 'Element.matches'
312
- def matches?(selector)
313
- `#@native.matches(#{selector})`
314
- end
315
- elsif Browser.supports? 'Element.matches (Opera)'
316
- def matches?(selector)
317
- `#@native.oMatchesSelector(#{selector})`
318
- end
319
- elsif Browser.supports? 'Element.matches (Internet Explorer)'
320
- def matches?(selector)
321
- `#@native.msMatchesSelector(#{selector})`
322
- end
323
- elsif Browser.supports? 'Element.matches (Firefox)'
324
- def matches?(selector)
325
- `#@native.mozMatchesSelector(#{selector})`
326
- end
327
- elsif Browser.supports? 'Element.matches (Chrome)'
328
- def matches?(selector)
329
- `#@native.webkitMatchesSelector(#{selector})`
330
- end
331
- elsif Browser.loaded? 'Sizzle'
332
- def matches?(selector)
333
- `Sizzle.matchesSelector(#@native, #{selector})`
334
- end
335
- else
336
- def matches?(selector)
337
- raise NotImplementedError, 'selector matching unsupported'
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
531
+
532
+ if classes.empty?
533
+ `#@native.removeAttribute('class')`
534
+ else
535
+ `#@native.className = #{classes.join ' '}`
338
536
  end
339
- end
340
537
 
341
- # @abstract
342
- def window
343
- document.window
538
+ self
344
539
  end
345
540
 
346
- def inspect
347
- "#<DOM::Element: #{name}>"
541
+ # @!attribute [r] size
542
+ # @return [Size] the size of the element
543
+ def size(*inc)
544
+ Size.new(self, *inc)
348
545
  end
349
546
 
350
- class Attributes
351
- include Enumerable
352
-
353
- 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 }
354
554
 
355
- def initialize(element, options)
356
- @element = element
357
- @namespace = options[:namespace]
358
- end
555
+ add_class(*to_add)
556
+ remove_class(*to_remove)
557
+ end
359
558
 
360
- def each(&block)
361
- 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
362
564
 
363
- @element.attribute_nodes.each {|attr|
364
- yield attr.name, attr.value
365
- }
565
+ def width=(value)
566
+ size.width = value
567
+ end
366
568
 
367
- self
368
- end
569
+ # @!attribute [r] window
570
+ # @return [Window] the window for the element
571
+ def window
572
+ document.window
573
+ end
369
574
 
370
- def [](name)
371
- @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()`
372
578
  end
373
579
 
374
- def []=(name, value)
375
- @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[]
376
588
  end
377
-
378
- def merge!(hash)
379
- hash.each {|name, value|
380
- self[name] = value
381
- }
382
-
383
- 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'
384
597
  end
385
598
  end
386
599
  end
387
600
 
388
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'