opal-browser 0.2.0.beta1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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'