dommy 0.6.0 → 0.8.0
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.
- checksums.yaml +4 -4
- data/README.md +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
data/lib/dommy/element.rb
CHANGED
|
@@ -8,8 +8,11 @@ module Dommy
|
|
|
8
8
|
class Fragment
|
|
9
9
|
include EventTarget
|
|
10
10
|
include Node
|
|
11
|
+
include Internal::ParentNode
|
|
11
12
|
|
|
12
|
-
attr_reader :
|
|
13
|
+
attr_reader :document
|
|
14
|
+
|
|
15
|
+
def __dommy_backend_node__ = @__node__
|
|
13
16
|
|
|
14
17
|
def initialize(document, nokogiri_node)
|
|
15
18
|
@document = document
|
|
@@ -26,8 +29,12 @@ module Dommy
|
|
|
26
29
|
@__node__.element_children.size
|
|
27
30
|
end
|
|
28
31
|
|
|
32
|
+
# Live, cached childNodes so `fragment.childNodes === fragment.childNodes` and
|
|
33
|
+
# later mutations are reflected (WHATWG live NodeList).
|
|
29
34
|
def child_nodes
|
|
30
|
-
|
|
35
|
+
@live_child_nodes ||= LiveNodeList.new do
|
|
36
|
+
@__node__.children.map { |n| @document.wrap_node(n) }.compact
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def first_child
|
|
@@ -50,23 +57,18 @@ module Dommy
|
|
|
50
57
|
@__node__.text
|
|
51
58
|
end
|
|
52
59
|
|
|
53
|
-
def append_child(child)
|
|
54
|
-
nodes = detach_dom_nodes(child)
|
|
55
|
-
nodes.each { |n| @__node__.add_child(n) }
|
|
56
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
57
|
-
child
|
|
58
|
-
end
|
|
59
|
-
|
|
60
60
|
def query_selector(selector)
|
|
61
|
-
return nil if selector.nil?
|
|
61
|
+
return nil if selector.nil?
|
|
62
|
+
Internal.validate_selector!(selector)
|
|
62
63
|
|
|
63
|
-
@document.wrap_node(@__node__.at_css(selector.to_s))
|
|
64
|
+
@document.wrap_node(@__node__.at_css(Internal.backend_safe_selector(selector.to_s)))
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
def query_selector_all(selector)
|
|
67
|
-
return NodeList.new if selector.nil?
|
|
68
|
+
return NodeList.new if selector.nil?
|
|
69
|
+
Internal.validate_selector!(selector)
|
|
68
70
|
|
|
69
|
-
NodeList.new(@__node__.css(selector.to_s).map { |n| @document.wrap_node(n) }.compact)
|
|
71
|
+
NodeList.new(@__node__.css(Internal.backend_safe_selector(selector.to_s)).map { |n| @document.wrap_node(n) }.compact)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def get_element_by_id(id)
|
|
@@ -79,6 +81,8 @@ module Dommy
|
|
|
79
81
|
case key
|
|
80
82
|
when "nodeType"
|
|
81
83
|
11
|
|
84
|
+
when "nodeName"
|
|
85
|
+
"#document-fragment"
|
|
82
86
|
when "children"
|
|
83
87
|
element_children
|
|
84
88
|
when "childNodes"
|
|
@@ -95,23 +99,69 @@ module Dommy
|
|
|
95
99
|
last_element_child
|
|
96
100
|
when "textContent"
|
|
97
101
|
@__node__.text
|
|
102
|
+
when "ownerDocument"
|
|
103
|
+
@document
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
|
|
107
|
+
include Bridge::Methods
|
|
108
|
+
js_methods %w[cloneNode querySelector querySelectorAll getElementById appendChild isEqualNode hasChildNodes
|
|
109
|
+
append prepend replaceChildren removeChild insertBefore replaceChild
|
|
110
|
+
isSameNode getRootNode contains normalize compareDocumentPosition
|
|
111
|
+
lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
112
|
+
addEventListener removeEventListener dispatchEvent]
|
|
101
113
|
def __js_call__(method, args)
|
|
102
114
|
case method
|
|
115
|
+
when "hasChildNodes"
|
|
116
|
+
@__node__.children.any?
|
|
117
|
+
when "compareDocumentPosition"
|
|
118
|
+
compare_document_position(args[0])
|
|
119
|
+
when "lookupNamespaceURI"
|
|
120
|
+
lookup_namespace_uri(args[0])
|
|
121
|
+
when "lookupPrefix"
|
|
122
|
+
lookup_prefix(args[0])
|
|
123
|
+
when "isDefaultNamespace"
|
|
124
|
+
is_default_namespace(args[0])
|
|
103
125
|
when "cloneNode"
|
|
104
126
|
deep = args.empty? ? false : !!args[0]
|
|
105
127
|
deep ? @document.wrap_node(Parser.fragment(@__node__.to_html, owner_doc: @document.nokogiri_doc)) : @document
|
|
106
128
|
.wrap_node(Parser.fragment("", owner_doc: @document.nokogiri_doc))
|
|
107
129
|
when "querySelector"
|
|
108
|
-
query_selector(args
|
|
130
|
+
query_selector(Internal.css_query_arg!(args))
|
|
109
131
|
when "querySelectorAll"
|
|
110
|
-
query_selector_all(args
|
|
132
|
+
query_selector_all(Internal.css_query_arg!(args))
|
|
111
133
|
when "getElementById"
|
|
112
134
|
get_element_by_id(args[0])
|
|
113
135
|
when "appendChild"
|
|
114
136
|
append_child(args[0])
|
|
137
|
+
when "append"
|
|
138
|
+
append(*args)
|
|
139
|
+
when "prepend"
|
|
140
|
+
prepend(*args)
|
|
141
|
+
when "replaceChildren"
|
|
142
|
+
replace_children(*args)
|
|
143
|
+
when "removeChild"
|
|
144
|
+
remove_child(args[0])
|
|
145
|
+
when "insertBefore"
|
|
146
|
+
insert_before(args[0], args[1])
|
|
147
|
+
when "replaceChild"
|
|
148
|
+
replace_child(args[0], args[1])
|
|
149
|
+
when "isEqualNode"
|
|
150
|
+
is_equal_node(args[0])
|
|
151
|
+
when "isSameNode"
|
|
152
|
+
is_same_node(args[0])
|
|
153
|
+
when "getRootNode"
|
|
154
|
+
get_root_node(args[0])
|
|
155
|
+
when "contains"
|
|
156
|
+
contains?(args[0])
|
|
157
|
+
when "normalize"
|
|
158
|
+
normalize
|
|
159
|
+
when "addEventListener"
|
|
160
|
+
add_event_listener(args[0], args[1], args[2])
|
|
161
|
+
when "removeEventListener"
|
|
162
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
163
|
+
when "dispatchEvent"
|
|
164
|
+
dispatch_event(args[0])
|
|
115
165
|
else
|
|
116
166
|
nil
|
|
117
167
|
end
|
|
@@ -123,21 +173,50 @@ module Dommy
|
|
|
123
173
|
nodes
|
|
124
174
|
end
|
|
125
175
|
|
|
126
|
-
|
|
176
|
+
# Node mutation on the fragment's children (ParentNode covers append/prepend/
|
|
177
|
+
# replaceChildren; these are the remaining Node methods).
|
|
178
|
+
def remove_child(node)
|
|
179
|
+
bn = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil
|
|
180
|
+
raise DOMException::NotFoundError, "node is not a child of this fragment" unless bn && bn.parent == @__node__
|
|
127
181
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
[@document.create_text_node(value).__node__]
|
|
132
|
-
else
|
|
133
|
-
node = value.respond_to?(:__node__) ? value.__node__ : nil
|
|
134
|
-
return [] unless node
|
|
182
|
+
bn.unlink
|
|
183
|
+
node
|
|
184
|
+
end
|
|
135
185
|
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
def insert_before(node, ref)
|
|
187
|
+
nodes = detach_dom_nodes(node)
|
|
188
|
+
ref_bn = ref.respond_to?(:__dommy_backend_node__) ? ref.__dommy_backend_node__ : nil
|
|
189
|
+
if ref_bn && ref_bn.parent == @__node__
|
|
190
|
+
nodes.each { |n| ref_bn.add_previous_sibling(n) }
|
|
191
|
+
else
|
|
192
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
138
193
|
end
|
|
194
|
+
node
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def replace_child(new_child, old_child)
|
|
198
|
+
old_bn = old_child.respond_to?(:__dommy_backend_node__) ? old_child.__dommy_backend_node__ : nil
|
|
199
|
+
raise DOMException::NotFoundError, "node is not a child of this fragment" unless old_bn && old_bn.parent == @__node__
|
|
200
|
+
|
|
201
|
+
detach_dom_nodes(new_child).each { |n| old_bn.add_previous_sibling(n) }
|
|
202
|
+
old_bn.unlink
|
|
203
|
+
old_child
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def contains?(other)
|
|
207
|
+
return false unless other.respond_to?(:__dommy_backend_node__)
|
|
208
|
+
|
|
209
|
+
on = other.__dommy_backend_node__
|
|
210
|
+
on == @__node__ || on.ancestors.include?(@__node__)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def normalize
|
|
214
|
+
@__node__.children.each { |n| n.unlink if n.text? && n.content.empty? }
|
|
215
|
+
nil
|
|
139
216
|
end
|
|
140
217
|
|
|
218
|
+
private
|
|
219
|
+
|
|
141
220
|
def element_children
|
|
142
221
|
@__node__.element_children.each_with_object([]) do |node, out|
|
|
143
222
|
wrapped = @document.wrap_node(node)
|
|
@@ -147,7 +226,7 @@ module Dommy
|
|
|
147
226
|
|
|
148
227
|
# Fragments aren't part of the bubble chain; nil terminates
|
|
149
228
|
# bubbling at the boundary (shadow root, detached fragment, etc.).
|
|
150
|
-
def
|
|
229
|
+
def __internal_event_parent__
|
|
151
230
|
nil
|
|
152
231
|
end
|
|
153
232
|
end
|
|
@@ -156,8 +235,29 @@ module Dommy
|
|
|
156
235
|
# nodeValue / textContent API and `remove` / `cloneNode` semantics.
|
|
157
236
|
class CharacterDataNode
|
|
158
237
|
include Node
|
|
238
|
+
include EventTarget
|
|
239
|
+
|
|
240
|
+
def __dommy_backend_node__ = @__node__
|
|
241
|
+
|
|
242
|
+
# EventTarget needs a parent for event propagation; a character-data node
|
|
243
|
+
# bubbles to its parent element.
|
|
244
|
+
def __internal_event_parent__
|
|
245
|
+
@__node__.parent && @document.wrap_node(@__node__.parent)
|
|
246
|
+
end
|
|
159
247
|
|
|
160
|
-
|
|
248
|
+
# Text.splitText / CharacterData split: break the node at `offset`, keeping
|
|
249
|
+
# [0, offset) here and returning a new sibling node with the remainder.
|
|
250
|
+
def split_text(offset)
|
|
251
|
+
off = offset.to_i
|
|
252
|
+
full = @__node__.content
|
|
253
|
+
raise DOMException::IndexSizeError, "offset #{off} is out of bounds" if off.negative? || off > full.length
|
|
254
|
+
|
|
255
|
+
rest = full[off..] || ""
|
|
256
|
+
write_data(full[0, off])
|
|
257
|
+
new_node = @document.create_text_node(rest)
|
|
258
|
+
@__node__.add_next_sibling(new_node.__dommy_backend_node__) if @__node__.parent
|
|
259
|
+
new_node
|
|
260
|
+
end
|
|
161
261
|
|
|
162
262
|
def initialize(document, nokogiri_node)
|
|
163
263
|
@document = document
|
|
@@ -191,7 +291,7 @@ module Dommy
|
|
|
191
291
|
end
|
|
192
292
|
|
|
193
293
|
def remove
|
|
194
|
-
@__node__
|
|
294
|
+
@document.remove_node_with_notify(@__node__)
|
|
195
295
|
nil
|
|
196
296
|
end
|
|
197
297
|
|
|
@@ -215,22 +315,83 @@ module Dommy
|
|
|
215
315
|
__js_set__(key.to_s, value)
|
|
216
316
|
end
|
|
217
317
|
|
|
318
|
+
# WHATWG nodeName for character-data nodes is a per-type constant
|
|
319
|
+
# ("#text" / "#comment" / "#cdata-section"), not the element name.
|
|
320
|
+
def node_name
|
|
321
|
+
case node_type
|
|
322
|
+
when 3 then "#text"
|
|
323
|
+
when 4 then "#cdata-section"
|
|
324
|
+
when 8 then "#comment"
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# CharacterData length / mutation methods. Offsets and counts are UTF-16 code
|
|
329
|
+
# units per spec; for BMP text (the common case) Ruby char indices match.
|
|
330
|
+
# Each mutating op routes through write_data, which fires the characterData
|
|
331
|
+
# MutationObserver record.
|
|
332
|
+
|
|
333
|
+
def length
|
|
334
|
+
@__node__.content.length
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def substring_data(offset, count)
|
|
338
|
+
s = @__node__.content
|
|
339
|
+
raise DOMException::IndexSizeError, "offset out of bounds" if offset.to_i.negative? || offset.to_i > s.length
|
|
340
|
+
|
|
341
|
+
s[offset.to_i, [count.to_i, 0].max].to_s
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def append_data(value)
|
|
345
|
+
write_data(@__node__.content + value.to_s)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def insert_data(offset, value)
|
|
349
|
+
replace_data(offset, 0, value)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def delete_data(offset, count)
|
|
353
|
+
replace_data(offset, count, "")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def replace_data(offset, count, value)
|
|
357
|
+
s = @__node__.content
|
|
358
|
+
o = offset.to_i
|
|
359
|
+
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > s.length
|
|
360
|
+
|
|
361
|
+
c = [[count.to_i, 0].max, s.length - o].min
|
|
362
|
+
write_data(s[0, o].to_s + value.to_s + s[(o + c)..].to_s)
|
|
363
|
+
end
|
|
364
|
+
|
|
218
365
|
def __js_get__(key)
|
|
219
366
|
case key
|
|
220
367
|
when "nodeType"
|
|
221
368
|
node_type
|
|
369
|
+
when "nodeName"
|
|
370
|
+
node_name
|
|
222
371
|
when "textContent"
|
|
223
372
|
@__node__.content
|
|
224
373
|
when "data"
|
|
225
374
|
@__node__.content
|
|
226
375
|
when "nodeValue"
|
|
227
376
|
@__node__.content
|
|
377
|
+
when "length"
|
|
378
|
+
length
|
|
228
379
|
when "parentNode"
|
|
229
380
|
parent_node
|
|
381
|
+
when "ownerDocument"
|
|
382
|
+
@document
|
|
230
383
|
when "nextSibling"
|
|
231
384
|
next_sibling
|
|
232
385
|
when "previousSibling"
|
|
233
386
|
previous_sibling
|
|
387
|
+
when "childNodes"
|
|
388
|
+
# CharacterData is a leaf node: childNodes is always an empty (but
|
|
389
|
+
# present and iterable) NodeList, and firstChild/lastChild are null.
|
|
390
|
+
# DOM-walking code (e.g. idiomorph's morphChildren) iterates
|
|
391
|
+
# `node.childNodes` on every node, so a missing one crashes it.
|
|
392
|
+
NodeList.new
|
|
393
|
+
when "firstChild", "lastChild"
|
|
394
|
+
nil
|
|
234
395
|
end
|
|
235
396
|
end
|
|
236
397
|
|
|
@@ -243,16 +404,151 @@ module Dommy
|
|
|
243
404
|
nil
|
|
244
405
|
end
|
|
245
406
|
|
|
246
|
-
|
|
407
|
+
include Bridge::Methods
|
|
408
|
+
js_methods %w[remove before after replaceWith isEqualNode hasChildNodes
|
|
409
|
+
appendData insertData deleteData replaceData substringData contains
|
|
410
|
+
isSameNode getRootNode normalize splitText compareDocumentPosition
|
|
411
|
+
lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
412
|
+
appendChild insertBefore removeChild replaceChild
|
|
413
|
+
addEventListener removeEventListener dispatchEvent]
|
|
414
|
+
def __js_call__(method, args)
|
|
247
415
|
case method
|
|
416
|
+
when "hasChildNodes"
|
|
417
|
+
false
|
|
418
|
+
when "contains"
|
|
419
|
+
# A leaf node contains only itself (no descendants).
|
|
420
|
+
args[0].respond_to?(:__dommy_backend_node__) &&
|
|
421
|
+
args[0].__dommy_backend_node__ == @__node__
|
|
422
|
+
when "appendChild", "insertBefore"
|
|
423
|
+
# CharacterData is a leaf — it cannot be a parent.
|
|
424
|
+
raise DOMException::HierarchyRequestError, "this node type does not support children"
|
|
425
|
+
when "removeChild", "replaceChild"
|
|
426
|
+
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
427
|
+
when "compareDocumentPosition"
|
|
428
|
+
compare_document_position(args[0])
|
|
429
|
+
when "isSameNode"
|
|
430
|
+
is_same_node(args[0])
|
|
431
|
+
when "getRootNode"
|
|
432
|
+
get_root_node(args[0])
|
|
433
|
+
when "lookupNamespaceURI"
|
|
434
|
+
lookup_namespace_uri(args[0])
|
|
435
|
+
when "lookupPrefix"
|
|
436
|
+
lookup_prefix(args[0])
|
|
437
|
+
when "isDefaultNamespace"
|
|
438
|
+
is_default_namespace(args[0])
|
|
439
|
+
when "normalize"
|
|
440
|
+
nil # a leaf has no child text runs to merge
|
|
441
|
+
when "splitText"
|
|
442
|
+
split_text(args[0])
|
|
443
|
+
when "addEventListener"
|
|
444
|
+
add_event_listener(args[0], args[1], args[2])
|
|
445
|
+
when "removeEventListener"
|
|
446
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
447
|
+
when "dispatchEvent"
|
|
448
|
+
dispatch_event(args[0])
|
|
449
|
+
when "appendData"
|
|
450
|
+
append_data(args[0])
|
|
451
|
+
when "insertData"
|
|
452
|
+
insert_data(args[0], args[1])
|
|
453
|
+
when "deleteData"
|
|
454
|
+
delete_data(args[0], args[1])
|
|
455
|
+
when "replaceData"
|
|
456
|
+
replace_data(args[0], args[1], args[2])
|
|
457
|
+
when "substringData"
|
|
458
|
+
substring_data(args[0], args[1])
|
|
248
459
|
when "remove"
|
|
249
|
-
|
|
250
|
-
|
|
460
|
+
remove
|
|
461
|
+
when "before"
|
|
462
|
+
before(*args)
|
|
463
|
+
when "after"
|
|
464
|
+
after(*args)
|
|
465
|
+
when "replaceWith"
|
|
466
|
+
replace_with(*args)
|
|
467
|
+
when "isEqualNode"
|
|
468
|
+
is_equal_node(args[0])
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# ChildNode mixin: WHATWG DOM defines `before`, `after`,
|
|
473
|
+
# `replaceWith` on all child nodes, including Text and Comment.
|
|
474
|
+
# Implementations operate on the Nokogiri layer and notify the
|
|
475
|
+
# MutationObserver with the underlying nodes (mirroring
|
|
476
|
+
# Element#remove_child / replace_child).
|
|
477
|
+
|
|
478
|
+
def before(*args)
|
|
479
|
+
parent = @__node__.parent
|
|
480
|
+
return nil unless parent
|
|
481
|
+
|
|
482
|
+
added = args.map { |arg| coerce_node(arg) }.compact
|
|
483
|
+
added.reverse_each { |node| @__node__.add_previous_sibling(node) }
|
|
484
|
+
notify_child_list_added(parent, added)
|
|
485
|
+
nil
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def after(*args)
|
|
489
|
+
parent = @__node__.parent
|
|
490
|
+
return nil unless parent
|
|
491
|
+
|
|
492
|
+
added = args.map { |arg| coerce_node(arg) }.compact
|
|
493
|
+
anchor = @__node__.next_sibling
|
|
494
|
+
if anchor
|
|
495
|
+
added.reverse_each { |node| anchor.add_previous_sibling(node) }
|
|
496
|
+
else
|
|
497
|
+
added.each { |node| parent.add_child(node) }
|
|
498
|
+
end
|
|
499
|
+
notify_child_list_added(parent, added)
|
|
500
|
+
nil
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def replace_with(*args)
|
|
504
|
+
parent = @__node__.parent
|
|
505
|
+
return nil unless parent
|
|
506
|
+
|
|
507
|
+
added = args.map { |arg| coerce_node(arg) }.compact
|
|
508
|
+
removed = @__node__
|
|
509
|
+
anchor = @__node__.next_sibling
|
|
510
|
+
@__node__.unlink
|
|
511
|
+
if anchor
|
|
512
|
+
added.reverse_each { |node| anchor.add_previous_sibling(node) }
|
|
513
|
+
else
|
|
514
|
+
added.each { |node| parent.add_child(node) }
|
|
251
515
|
end
|
|
516
|
+
@document.notify_child_list_mutation(
|
|
517
|
+
target_node: parent,
|
|
518
|
+
added_nodes: added,
|
|
519
|
+
removed_nodes: [removed]
|
|
520
|
+
)
|
|
521
|
+
nil
|
|
252
522
|
end
|
|
253
523
|
|
|
254
524
|
private
|
|
255
525
|
|
|
526
|
+
# Coerce a `before` / `after` / `replaceWith` argument into a raw
|
|
527
|
+
# Nokogiri node, ready to be linked into a parent. Strings become
|
|
528
|
+
# fresh text nodes; existing nodes are detached from their current
|
|
529
|
+
# parent first (matching Element#detach_dom_nodes minus the
|
|
530
|
+
# Fragment branch which is rarely needed off a text/comment node).
|
|
531
|
+
def coerce_node(arg)
|
|
532
|
+
case arg
|
|
533
|
+
when String
|
|
534
|
+
@document.create_text_node(arg).__dommy_backend_node__
|
|
535
|
+
else
|
|
536
|
+
node = arg.respond_to?(:__dommy_backend_node__) ? arg.__dommy_backend_node__ : nil
|
|
537
|
+
node.unlink if node && node.parent
|
|
538
|
+
node
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def notify_child_list_added(parent, added)
|
|
543
|
+
return if added.empty?
|
|
544
|
+
|
|
545
|
+
@document.notify_child_list_mutation(
|
|
546
|
+
target_node: parent,
|
|
547
|
+
added_nodes: added,
|
|
548
|
+
removed_nodes: []
|
|
549
|
+
)
|
|
550
|
+
end
|
|
551
|
+
|
|
256
552
|
def write_data(value)
|
|
257
553
|
old = @__node__.content
|
|
258
554
|
@__node__.content = value.to_s
|
|
@@ -265,6 +561,8 @@ module Dommy
|
|
|
265
561
|
3
|
|
266
562
|
end
|
|
267
563
|
|
|
564
|
+
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
565
|
+
js_methods %w[cloneNode]
|
|
268
566
|
def __js_call__(method, args)
|
|
269
567
|
case method
|
|
270
568
|
when "cloneNode"
|
|
@@ -275,11 +573,21 @@ module Dommy
|
|
|
275
573
|
end
|
|
276
574
|
end
|
|
277
575
|
|
|
576
|
+
# CDATASection — a Text subtype (nodeType 4). CharacterData methods and the
|
|
577
|
+
# "#cdata-section" nodeName come from CharacterDataNode via node_type.
|
|
578
|
+
class CDATASectionNode < TextNode
|
|
579
|
+
def node_type
|
|
580
|
+
4
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
278
584
|
class CommentNode < CharacterDataNode
|
|
279
585
|
def node_type
|
|
280
586
|
8
|
|
281
587
|
end
|
|
282
588
|
|
|
589
|
+
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
590
|
+
js_methods %w[cloneNode]
|
|
283
591
|
def __js_call__(method, args)
|
|
284
592
|
case method
|
|
285
593
|
when "cloneNode"
|
|
@@ -296,8 +604,11 @@ module Dommy
|
|
|
296
604
|
class ClassList
|
|
297
605
|
include Enumerable
|
|
298
606
|
|
|
299
|
-
|
|
607
|
+
# `attribute` is the content attribute this token list reflects ("class" for
|
|
608
|
+
# `classList`, "rel" for `relList`, "sandbox", "sizes", "for", …).
|
|
609
|
+
def initialize(element, attribute = "class")
|
|
300
610
|
@element = element
|
|
611
|
+
@attribute = attribute
|
|
301
612
|
end
|
|
302
613
|
|
|
303
614
|
def length
|
|
@@ -307,15 +618,18 @@ module Dommy
|
|
|
307
618
|
alias size length
|
|
308
619
|
|
|
309
620
|
def item(index)
|
|
310
|
-
|
|
621
|
+
i = index.to_i
|
|
622
|
+
return nil if i.negative?
|
|
623
|
+
|
|
624
|
+
class_tokens[i]
|
|
311
625
|
end
|
|
312
626
|
|
|
313
627
|
def value
|
|
314
|
-
@element.
|
|
628
|
+
@element.__dommy_backend_node__[@attribute].to_s
|
|
315
629
|
end
|
|
316
630
|
|
|
317
631
|
def value=(new_value)
|
|
318
|
-
@element.set_attribute(
|
|
632
|
+
@element.set_attribute(@attribute, new_value.to_s)
|
|
319
633
|
end
|
|
320
634
|
|
|
321
635
|
# Spec: contains() does NOT validate (no SyntaxError on empty).
|
|
@@ -334,14 +648,22 @@ module Dommy
|
|
|
334
648
|
end
|
|
335
649
|
|
|
336
650
|
def replace(old_token, new_token)
|
|
337
|
-
|
|
338
|
-
|
|
651
|
+
# Spec order: both tokens' empty checks (SyntaxError) precede both
|
|
652
|
+
# whitespace checks (InvalidCharacterError) — so replace(" ", "") is a
|
|
653
|
+
# SyntaxError (the empty newToken), not an InvalidCharacterError.
|
|
654
|
+
old_s = stringify_token(old_token)
|
|
655
|
+
new_s = stringify_token(new_token)
|
|
656
|
+
raise DOMException::SyntaxError, "token is empty" if old_s.empty? || new_s.empty?
|
|
657
|
+
if old_s.match?(/[ \t\n\f\r]/) || new_s.match?(/[ \t\n\f\r]/)
|
|
658
|
+
raise DOMException::InvalidCharacterError, "token contains whitespace"
|
|
659
|
+
end
|
|
660
|
+
|
|
339
661
|
tokens = class_tokens
|
|
340
662
|
idx = tokens.index(old_s)
|
|
341
663
|
return false unless idx
|
|
342
664
|
|
|
343
665
|
tokens[idx] = new_s
|
|
344
|
-
@element.set_attribute(
|
|
666
|
+
@element.set_attribute(@attribute, tokens.uniq.join(" "))
|
|
345
667
|
true
|
|
346
668
|
end
|
|
347
669
|
|
|
@@ -368,8 +690,14 @@ module Dommy
|
|
|
368
690
|
when "value"
|
|
369
691
|
value
|
|
370
692
|
else
|
|
371
|
-
|
|
372
|
-
|
|
693
|
+
# Indexed getter: `classList[i]` is an undefined-returning indexed
|
|
694
|
+
# property — out-of-range or negative indices yield JS `undefined`
|
|
695
|
+
# (unlike `item(i)`, which returns null). Returning Ruby nil here would
|
|
696
|
+
# marshal as JS null, so use the UNDEFINED sentinel.
|
|
697
|
+
if key.is_a?(Integer) || key.to_s.match?(/\A-?\d+\z/)
|
|
698
|
+
i = key.to_i
|
|
699
|
+
token = i.negative? ? nil : class_tokens[i]
|
|
700
|
+
token.nil? ? Bridge::UNDEFINED : token
|
|
373
701
|
end
|
|
374
702
|
end
|
|
375
703
|
end
|
|
@@ -383,22 +711,30 @@ module Dommy
|
|
|
383
711
|
nil
|
|
384
712
|
end
|
|
385
713
|
|
|
714
|
+
include Bridge::Methods
|
|
715
|
+
# NOTE: `supports` is intentionally absent — for the class attribute's token
|
|
716
|
+
# list it must throw a TypeError, which `list.supports(...)` (not a function)
|
|
717
|
+
# already does.
|
|
718
|
+
js_methods %w[add remove contains toggle replace item toString]
|
|
386
719
|
def __js_call__(method, args)
|
|
387
720
|
case method
|
|
388
721
|
when "add"
|
|
389
722
|
update_tokens { |tokens| tokens | normalize_tokens(args) }
|
|
390
|
-
|
|
723
|
+
Bridge::UNDEFINED
|
|
391
724
|
when "remove"
|
|
392
725
|
update_tokens { |tokens| tokens - normalize_tokens(args) }
|
|
393
|
-
|
|
726
|
+
Bridge::UNDEFINED
|
|
394
727
|
when "contains"
|
|
395
|
-
|
|
728
|
+
# contains() does not validate; null coerces to the string "null".
|
|
729
|
+
class_tokens.include?(stringify_token(args[0]))
|
|
396
730
|
when "toggle"
|
|
397
731
|
toggle(args[0], args[1])
|
|
398
732
|
when "replace"
|
|
399
733
|
replace(args[0], args[1])
|
|
400
734
|
when "item"
|
|
401
735
|
item(args[0])
|
|
736
|
+
when "toString"
|
|
737
|
+
value
|
|
402
738
|
else
|
|
403
739
|
nil
|
|
404
740
|
end
|
|
@@ -409,52 +745,63 @@ module Dommy
|
|
|
409
745
|
def toggle(token, force)
|
|
410
746
|
name = validate_token(token)
|
|
411
747
|
present = class_tokens.include?(name)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
748
|
+
force_given = !(force.nil? || force.equal?(Bridge::UNDEFINED))
|
|
749
|
+
|
|
750
|
+
# Spec: toggle runs the update steps only when it actually adds or removes.
|
|
751
|
+
# With an explicit force that already matches the current state it's a
|
|
752
|
+
# no-op — the attribute is left byte-for-byte untouched (no re-serialize).
|
|
753
|
+
if force_given
|
|
754
|
+
want = !!force
|
|
755
|
+
return want if want == present
|
|
417
756
|
|
|
418
|
-
|
|
419
|
-
|
|
757
|
+
update_tokens { |tokens| want ? tokens | [name] : tokens - [name] }
|
|
758
|
+
return want
|
|
420
759
|
end
|
|
421
760
|
|
|
761
|
+
desired = !present
|
|
762
|
+
update_tokens { |tokens| desired ? tokens | [name] : tokens - [name] }
|
|
422
763
|
desired
|
|
423
764
|
end
|
|
424
765
|
|
|
766
|
+
# USVString coercion of a token argument: JS `null` becomes the string
|
|
767
|
+
# "null" (so `add(null)` adds the token "null"), not the empty string.
|
|
768
|
+
def stringify_token(token)
|
|
769
|
+
token.nil? ? "null" : token.to_s
|
|
770
|
+
end
|
|
771
|
+
|
|
425
772
|
# Spec: any empty-string argument throws SyntaxError; any token
|
|
426
773
|
# containing ASCII whitespace throws InvalidCharacterError. Applies
|
|
427
774
|
# to add / remove / replace / toggle.
|
|
428
775
|
def normalize_tokens(args)
|
|
429
|
-
args.map
|
|
430
|
-
s = t.to_s
|
|
431
|
-
raise DOMException::SyntaxError, "token is empty" if s.empty?
|
|
432
|
-
raise DOMException::InvalidCharacterError, "token contains whitespace: #{s.inspect}" if s.match?(/\s/)
|
|
433
|
-
|
|
434
|
-
s
|
|
435
|
-
end
|
|
776
|
+
args.map { |t| validate_token(t) }
|
|
436
777
|
end
|
|
437
778
|
|
|
438
779
|
def validate_token(token)
|
|
439
|
-
s = token
|
|
780
|
+
s = stringify_token(token)
|
|
440
781
|
raise DOMException::SyntaxError, "token is empty" if s.empty?
|
|
441
782
|
raise DOMException::InvalidCharacterError, "token contains whitespace: #{s.inspect}" if s.match?(/\s/)
|
|
442
783
|
|
|
443
784
|
s
|
|
444
785
|
end
|
|
445
786
|
|
|
787
|
+
# The DOMTokenList token set: the class attribute parsed as an *ordered set*
|
|
788
|
+
# (whitespace-split, duplicates removed preserving first-seen order). length,
|
|
789
|
+
# item, iteration, and contains all operate on this set; `value`/`toString`
|
|
790
|
+
# return the raw attribute. ASCII whitespace per the spec is space/tab/LF/FF/CR.
|
|
446
791
|
def class_tokens
|
|
447
|
-
raw = @element.
|
|
448
|
-
raw.split(
|
|
792
|
+
raw = @element.__dommy_backend_node__[@attribute].to_s
|
|
793
|
+
raw.split(/[ \t\n\f\r]+/).reject(&:empty?).uniq
|
|
449
794
|
end
|
|
450
795
|
|
|
796
|
+
# DOMTokenList "update steps": serialize the (deduplicated) token set back to
|
|
797
|
+
# the class attribute. add/remove/replace always run this, so duplicates
|
|
798
|
+
# collapse and whitespace normalizes even on a no-op token. The one carve-out
|
|
799
|
+
# (per spec) is an empty set with no existing attribute — don't create one.
|
|
451
800
|
def update_tokens
|
|
452
801
|
tokens = yield(class_tokens)
|
|
453
|
-
if tokens.empty?
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
@element.set_attribute("class", tokens.join(" "))
|
|
457
|
-
end
|
|
802
|
+
return if tokens.empty? && !@element.__dommy_backend_node__.key?(@attribute)
|
|
803
|
+
|
|
804
|
+
@element.set_attribute(@attribute, tokens.join(" "))
|
|
458
805
|
end
|
|
459
806
|
end
|
|
460
807
|
|
|
@@ -467,7 +814,7 @@ module Dommy
|
|
|
467
814
|
end
|
|
468
815
|
|
|
469
816
|
def __js_get__(key)
|
|
470
|
-
@element.
|
|
817
|
+
@element.__dommy_backend_node__[attr_name(key)]
|
|
471
818
|
end
|
|
472
819
|
|
|
473
820
|
def __js_set__(key, value)
|
|
@@ -475,10 +822,28 @@ module Dommy
|
|
|
475
822
|
nil
|
|
476
823
|
end
|
|
477
824
|
|
|
825
|
+
# Named deleter (`delete el.dataset.foo`): removes the data-* attribute.
|
|
826
|
+
def __js_delete__(key)
|
|
827
|
+
@element.remove_attribute(attr_name(key))
|
|
828
|
+
true
|
|
829
|
+
end
|
|
830
|
+
|
|
478
831
|
def __js_call__(_method, _args)
|
|
479
832
|
nil
|
|
480
833
|
end
|
|
481
834
|
|
|
835
|
+
# WebIDL "supported property names" for DOMStringMap: each `data-*`
|
|
836
|
+
# attribute's name with the `data-` prefix stripped and `-x` sequences
|
|
837
|
+
# camel-cased (`data-date-of-birth` → `dateOfBirth`, `data-` → ``).
|
|
838
|
+
def __js_named_props__
|
|
839
|
+
Backend.attribute_nodes(@element.__dommy_backend_node__).filter_map do |a|
|
|
840
|
+
name = Backend.attribute_ns_info(a)[:qualified_name]
|
|
841
|
+
next unless name.start_with?("data-")
|
|
842
|
+
|
|
843
|
+
name.sub(/\Adata-/, "").gsub(/-([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
|
|
482
847
|
private
|
|
483
848
|
|
|
484
849
|
def attr_name(key)
|
|
@@ -629,6 +994,8 @@ module Dommy
|
|
|
629
994
|
nil
|
|
630
995
|
end
|
|
631
996
|
|
|
997
|
+
include Bridge::Methods
|
|
998
|
+
js_methods %w[setProperty removeProperty getPropertyValue item]
|
|
632
999
|
def __js_call__(method, args)
|
|
633
1000
|
case method
|
|
634
1001
|
when "setProperty"
|
|
@@ -674,7 +1041,7 @@ module Dommy
|
|
|
674
1041
|
end
|
|
675
1042
|
|
|
676
1043
|
def properties
|
|
677
|
-
raw = @element.
|
|
1044
|
+
raw = @element.__dommy_backend_node__["style"].to_s
|
|
678
1045
|
raw.split(";").each_with_object({}) do |entry, out|
|
|
679
1046
|
key, value = entry.split(":", 2)
|
|
680
1047
|
next unless key && value
|
|
@@ -685,7 +1052,7 @@ module Dommy
|
|
|
685
1052
|
|
|
686
1053
|
def write_properties(props)
|
|
687
1054
|
if props.empty?
|
|
688
|
-
@element.remove_attribute("style") if @element.
|
|
1055
|
+
@element.remove_attribute("style") if @element.__dommy_backend_node__.key?("style")
|
|
689
1056
|
else
|
|
690
1057
|
@element.set_attribute("style", props.map { |k, v| "#{k}:#{v}" }.join(";"))
|
|
691
1058
|
end
|
|
@@ -695,8 +1062,11 @@ module Dommy
|
|
|
695
1062
|
class Element
|
|
696
1063
|
include EventTarget
|
|
697
1064
|
include Node
|
|
1065
|
+
include Internal::ParentNode
|
|
698
1066
|
|
|
699
|
-
attr_reader :
|
|
1067
|
+
attr_reader :document
|
|
1068
|
+
|
|
1069
|
+
def __dommy_backend_node__ = @__node__
|
|
700
1070
|
|
|
701
1071
|
def initialize(document, nokogiri_node)
|
|
702
1072
|
@document = document
|
|
@@ -712,6 +1082,11 @@ module Dommy
|
|
|
712
1082
|
@live_children = HTMLCollection.new do
|
|
713
1083
|
@__node__.element_children.map { |n| @document.wrap_node(n) }.compact
|
|
714
1084
|
end
|
|
1085
|
+
# Live `childNodes` (all node types, not just elements), cached so
|
|
1086
|
+
# `el.childNodes === el.childNodes` holds like the spec's live NodeList.
|
|
1087
|
+
@live_child_nodes = LiveNodeList.new do
|
|
1088
|
+
@__node__.children.map { |n| @document.wrap_node(n) }.compact
|
|
1089
|
+
end
|
|
715
1090
|
end
|
|
716
1091
|
|
|
717
1092
|
# ----- Public Ruby API (snake_case) -----
|
|
@@ -726,19 +1101,61 @@ module Dommy
|
|
|
726
1101
|
end
|
|
727
1102
|
|
|
728
1103
|
def text_content=(value)
|
|
729
|
-
|
|
1104
|
+
# `node.content =` removes all existing children and (if value is
|
|
1105
|
+
# non-empty) appends a single text node. Capture before/after to feed
|
|
1106
|
+
# MutationObserver.
|
|
1107
|
+
removed = @__node__.children.to_a
|
|
1108
|
+
@__node__.content = value.to_s
|
|
1109
|
+
added = @__node__.children.to_a
|
|
1110
|
+
notify_child_list(added: added, removed: removed)
|
|
730
1111
|
end
|
|
731
1112
|
|
|
732
1113
|
def inner_html
|
|
733
|
-
|
|
1114
|
+
if @__node__.name == "template"
|
|
1115
|
+
@document.template_content_inner_html(self)
|
|
1116
|
+
else
|
|
1117
|
+
@__node__.inner_html
|
|
1118
|
+
end
|
|
734
1119
|
end
|
|
735
1120
|
|
|
736
1121
|
def inner_html=(value)
|
|
737
|
-
|
|
1122
|
+
removed = @__node__.children.to_a
|
|
1123
|
+
if @__node__.name == "template"
|
|
1124
|
+
# `<template>` content is invisible to outer selectors in real DOM (it
|
|
1125
|
+
# lives in a separate DocumentFragment exposed via `[:content]`).
|
|
1126
|
+
@document.attach_template_content(self, value.to_s)
|
|
1127
|
+
else
|
|
1128
|
+
@__node__.inner_html = value.to_s
|
|
1129
|
+
@document.migrate_template_descendants(@__node__)
|
|
1130
|
+
end
|
|
1131
|
+
notify_child_list(added: @__node__.children.to_a, removed: removed)
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
|
|
1135
|
+
|
|
1136
|
+
# Record the namespace/prefix/localName an element was created with via
|
|
1137
|
+
# createElementNS, so the getters report them faithfully (Nokogiri can't
|
|
1138
|
+
# always round-trip a foreign-namespace prefix).
|
|
1139
|
+
def __internal_set_namespace__(namespace, prefix, local_name, qualified_name)
|
|
1140
|
+
@__ns_uri = namespace
|
|
1141
|
+
@__ns_prefix = prefix
|
|
1142
|
+
@__ns_local = local_name
|
|
1143
|
+
@__ns_qname = qualified_name
|
|
1144
|
+
nil
|
|
738
1145
|
end
|
|
739
1146
|
|
|
1147
|
+
# tagName is the qualified name, ASCII-upper-cased only for an HTML-namespace
|
|
1148
|
+
# element whose node document is an HTML document. An XHTML element (HTML
|
|
1149
|
+
# namespace, but in an XML document) and any non-HTML-namespace element keep
|
|
1150
|
+
# their case.
|
|
740
1151
|
def tag_name
|
|
741
|
-
@__node__.name
|
|
1152
|
+
qname = @__ns_qname || @__node__.name
|
|
1153
|
+
html_ns = @__ns_qname ? @__ns_uri == HTML_NAMESPACE : true
|
|
1154
|
+
html_ns && @document.html_document? ? qname.upcase(:ascii) : qname
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
def element_prefix
|
|
1158
|
+
@__ns_prefix
|
|
742
1159
|
end
|
|
743
1160
|
|
|
744
1161
|
def id
|
|
@@ -761,6 +1178,33 @@ module Dommy
|
|
|
761
1178
|
@class_list
|
|
762
1179
|
end
|
|
763
1180
|
|
|
1181
|
+
# Element + namespace combinations for which a reflected DOMTokenList IDL
|
|
1182
|
+
# attribute is defined; elsewhere the attribute does not exist (→ undefined).
|
|
1183
|
+
REFLECTED_TOKEN_LIST_HOSTS = {
|
|
1184
|
+
"relList" => {html: %w[a area link], svg: %w[a]},
|
|
1185
|
+
"htmlFor" => {html: %w[output]},
|
|
1186
|
+
"sandbox" => {html: %w[iframe]},
|
|
1187
|
+
"sizes" => {html: %w[link]}
|
|
1188
|
+
}.freeze
|
|
1189
|
+
|
|
1190
|
+
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
1191
|
+
|
|
1192
|
+
# A reflected DOMTokenList for `prop` backed by content attribute
|
|
1193
|
+
# `attribute`, cached for identity (`el.relList === el.relList`). Returns the
|
|
1194
|
+
# UNDEFINED sentinel (→ JS `undefined`) when the attribute is not defined on
|
|
1195
|
+
# this element in its namespace.
|
|
1196
|
+
def reflected_token_list(prop, attribute)
|
|
1197
|
+
hosts = REFLECTED_TOKEN_LIST_HOSTS[prop]
|
|
1198
|
+
ns = namespace_uri
|
|
1199
|
+
ln = local_name
|
|
1200
|
+
applicable =
|
|
1201
|
+
(ns == HTML_NAMESPACE && hosts[:html].include?(ln)) ||
|
|
1202
|
+
(ns == SVG_NAMESPACE && Array(hosts[:svg]).include?(ln))
|
|
1203
|
+
return Bridge::UNDEFINED unless applicable
|
|
1204
|
+
|
|
1205
|
+
(@reflected_token_lists ||= {})[prop] ||= ClassList.new(self, attribute)
|
|
1206
|
+
end
|
|
1207
|
+
|
|
764
1208
|
def style
|
|
765
1209
|
@style
|
|
766
1210
|
end
|
|
@@ -820,7 +1264,7 @@ module Dommy
|
|
|
820
1264
|
end
|
|
821
1265
|
|
|
822
1266
|
def has_attributes?
|
|
823
|
-
@__node__.
|
|
1267
|
+
Backend.attribute_nodes(@__node__).any?
|
|
824
1268
|
end
|
|
825
1269
|
|
|
826
1270
|
def next_sibling
|
|
@@ -860,7 +1304,7 @@ module Dommy
|
|
|
860
1304
|
parent = @__node__.parent
|
|
861
1305
|
return unless parent
|
|
862
1306
|
|
|
863
|
-
if parent.is_a?(
|
|
1307
|
+
if parent.is_a?(Backend.document_class)
|
|
864
1308
|
raise(
|
|
865
1309
|
DOMException::NoModificationAllowedError,
|
|
866
1310
|
"outerHTML setter not allowed on the document element"
|
|
@@ -878,15 +1322,15 @@ module Dommy
|
|
|
878
1322
|
new_nodes.each { |n| parent.add_child(n) }
|
|
879
1323
|
end
|
|
880
1324
|
|
|
881
|
-
|
|
1325
|
+
notify_child_list(added: new_nodes, removed: [removed], target: parent)
|
|
882
1326
|
end
|
|
883
1327
|
|
|
884
1328
|
# `el.contains(other)` — true if `other` is `el` itself or any
|
|
885
1329
|
# descendant. Per spec, returns false for null/non-Node.
|
|
886
1330
|
def contains?(other)
|
|
887
|
-
return false unless other.respond_to?(:
|
|
1331
|
+
return false unless other.respond_to?(:__dommy_backend_node__)
|
|
888
1332
|
|
|
889
|
-
other_node = other.
|
|
1333
|
+
other_node = other.__dommy_backend_node__
|
|
890
1334
|
return true if other_node == @__node__
|
|
891
1335
|
|
|
892
1336
|
Internal::NodeTraversal.ancestor_of?(@__node__, other_node)
|
|
@@ -897,7 +1341,7 @@ module Dommy
|
|
|
897
1341
|
# inside a shadow tree, returns that ShadowRoot. Otherwise walks
|
|
898
1342
|
# until we hit the Nokogiri Document (then returns the Document).
|
|
899
1343
|
def root_node
|
|
900
|
-
sr = @document.
|
|
1344
|
+
sr = @document.__internal_shadow_root_containing__(@__node__)
|
|
901
1345
|
return sr if sr
|
|
902
1346
|
|
|
903
1347
|
current = @__node__
|
|
@@ -905,7 +1349,7 @@ module Dommy
|
|
|
905
1349
|
loop do
|
|
906
1350
|
parent = current.respond_to?(:parent) ? current.parent : nil
|
|
907
1351
|
break unless parent
|
|
908
|
-
if parent.is_a?(
|
|
1352
|
+
if parent.is_a?(Backend.document_class)
|
|
909
1353
|
attached = true
|
|
910
1354
|
break
|
|
911
1355
|
end
|
|
@@ -938,6 +1382,8 @@ module Dommy
|
|
|
938
1382
|
end
|
|
939
1383
|
|
|
940
1384
|
def toggle_attribute(name, force = nil)
|
|
1385
|
+
raise DOMException::InvalidCharacterError, "empty attribute name" if name.to_s.empty?
|
|
1386
|
+
|
|
941
1387
|
key = name.to_s.downcase
|
|
942
1388
|
present = @__node__.key?(key)
|
|
943
1389
|
desired = force.nil? ? !present : !!force
|
|
@@ -951,10 +1397,11 @@ module Dommy
|
|
|
951
1397
|
end
|
|
952
1398
|
|
|
953
1399
|
def matches?(selector)
|
|
954
|
-
return false if selector.nil?
|
|
1400
|
+
return false if selector.nil?
|
|
1401
|
+
Internal.validate_selector!(selector)
|
|
955
1402
|
|
|
956
1403
|
# `:scope` pseudo — match against this element itself.
|
|
957
|
-
sel = selector.to_s.gsub(":scope", "*:nth-last-child(n)")
|
|
1404
|
+
sel = Internal.backend_safe_selector(selector.to_s).gsub(":scope", "*:nth-last-child(n)")
|
|
958
1405
|
matches_selector?(@__node__, sel)
|
|
959
1406
|
end
|
|
960
1407
|
|
|
@@ -981,6 +1428,10 @@ module Dommy
|
|
|
981
1428
|
end
|
|
982
1429
|
end
|
|
983
1430
|
|
|
1431
|
+
def get_elements_by_tag_name_ns(namespace, local_name)
|
|
1432
|
+
HTMLCollection.elements_by_tag_name_ns(@__node__, @document, namespace, local_name)
|
|
1433
|
+
end
|
|
1434
|
+
|
|
984
1435
|
# NamedNodeMap of attributes. Lazily allocated and re-used so
|
|
985
1436
|
# `el.attributes === el.attributes` and `attr.ownerElement === el`.
|
|
986
1437
|
def attributes
|
|
@@ -1003,11 +1454,15 @@ module Dommy
|
|
|
1003
1454
|
|
|
1004
1455
|
# HTML namespace constants — most HTML elements live in xhtml ns.
|
|
1005
1456
|
def namespace_uri
|
|
1006
|
-
|
|
1007
|
-
|
|
1457
|
+
return @__ns_uri if @__ns_qname
|
|
1458
|
+
|
|
1459
|
+
ns = Backend.namespace_of(@__node__)
|
|
1460
|
+
ns ? ns.href : HTML_NAMESPACE
|
|
1008
1461
|
end
|
|
1009
1462
|
|
|
1010
1463
|
def local_name
|
|
1464
|
+
return @__ns_local if @__ns_qname
|
|
1465
|
+
|
|
1011
1466
|
@__node__.name.downcase
|
|
1012
1467
|
end
|
|
1013
1468
|
|
|
@@ -1054,14 +1509,14 @@ module Dommy
|
|
|
1054
1509
|
|
|
1055
1510
|
parent = current.respond_to?(:parent) ? current.parent : nil
|
|
1056
1511
|
return false unless parent
|
|
1057
|
-
return true if parent.is_a?(
|
|
1512
|
+
return true if parent.is_a?(Backend.document_class)
|
|
1058
1513
|
|
|
1059
|
-
sr = @document.
|
|
1514
|
+
sr = @document.__internal_shadow_root_for_fragment__(parent)
|
|
1060
1515
|
if sr
|
|
1061
1516
|
host = sr.host
|
|
1062
1517
|
return false unless host
|
|
1063
1518
|
|
|
1064
|
-
current = host.
|
|
1519
|
+
current = host.__dommy_backend_node__
|
|
1065
1520
|
else
|
|
1066
1521
|
current = parent
|
|
1067
1522
|
end
|
|
@@ -1074,12 +1529,12 @@ module Dommy
|
|
|
1074
1529
|
# tests rely on `document.activeElement` updating. Track the most
|
|
1075
1530
|
# recently focused element on the document.
|
|
1076
1531
|
def focus
|
|
1077
|
-
@document.
|
|
1532
|
+
@document.__internal_set_active_element__(self)
|
|
1078
1533
|
nil
|
|
1079
1534
|
end
|
|
1080
1535
|
|
|
1081
1536
|
def blur
|
|
1082
|
-
@document.
|
|
1537
|
+
@document.__internal_set_active_element__(nil)
|
|
1083
1538
|
nil
|
|
1084
1539
|
end
|
|
1085
1540
|
|
|
@@ -1149,7 +1604,7 @@ module Dommy
|
|
|
1149
1604
|
|
|
1150
1605
|
# Internal — gives access to the shadow root regardless of mode.
|
|
1151
1606
|
# Used by event composition / `composedPath()`.
|
|
1152
|
-
def
|
|
1607
|
+
def __internal_shadow_root__
|
|
1153
1608
|
@__shadow_root
|
|
1154
1609
|
end
|
|
1155
1610
|
|
|
@@ -1157,7 +1612,7 @@ module Dommy
|
|
|
1157
1612
|
# "beforebegin", "afterbegin", "beforeend", "afterend". Returns the
|
|
1158
1613
|
# inserted element or nil if position has no anchor (root cases).
|
|
1159
1614
|
def insert_adjacent_element(position, element)
|
|
1160
|
-
return nil unless element.respond_to?(:
|
|
1615
|
+
return nil unless element.respond_to?(:__dommy_backend_node__)
|
|
1161
1616
|
|
|
1162
1617
|
case position.to_s
|
|
1163
1618
|
when "beforebegin"
|
|
@@ -1165,22 +1620,22 @@ module Dommy
|
|
|
1165
1620
|
|
|
1166
1621
|
node = detach_for_insert(element)
|
|
1167
1622
|
@__node__.add_previous_sibling(node)
|
|
1168
|
-
|
|
1623
|
+
notify_child_list(added: [node], target: @__node__.parent)
|
|
1169
1624
|
when "afterbegin"
|
|
1170
1625
|
node = detach_for_insert(element)
|
|
1171
1626
|
first = @__node__.children.first
|
|
1172
1627
|
first ? first.add_previous_sibling(node) : @__node__.add_child(node)
|
|
1173
|
-
|
|
1628
|
+
notify_child_list(added: [node])
|
|
1174
1629
|
when "beforeend"
|
|
1175
1630
|
node = detach_for_insert(element)
|
|
1176
1631
|
@__node__.add_child(node)
|
|
1177
|
-
|
|
1632
|
+
notify_child_list(added: [node])
|
|
1178
1633
|
when "afterend"
|
|
1179
1634
|
return nil unless @__node__.parent
|
|
1180
1635
|
|
|
1181
1636
|
node = detach_for_insert(element)
|
|
1182
1637
|
@__node__.add_next_sibling(node)
|
|
1183
|
-
|
|
1638
|
+
notify_child_list(added: [node], target: @__node__.parent)
|
|
1184
1639
|
else
|
|
1185
1640
|
return nil
|
|
1186
1641
|
end
|
|
@@ -1189,36 +1644,56 @@ module Dommy
|
|
|
1189
1644
|
end
|
|
1190
1645
|
|
|
1191
1646
|
def insert_adjacent_html(position, html)
|
|
1647
|
+
# Position is ASCII case-insensitive ("beforeBegin" == "beforebegin").
|
|
1648
|
+
pos = position.to_s.downcase
|
|
1649
|
+
unless %w[beforebegin afterbegin beforeend afterend].include?(pos)
|
|
1650
|
+
raise DOMException::SyntaxError, "The value provided ('#{position}') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'."
|
|
1651
|
+
end
|
|
1652
|
+
|
|
1192
1653
|
fragment = Parser.fragment(html.to_s, owner_doc: @__node__.document)
|
|
1193
1654
|
nodes = fragment.children.to_a
|
|
1194
|
-
|
|
1655
|
+
# `add_previous_sibling` inserts immediately before the anchor, so a forward
|
|
1656
|
+
# walk preserves document order; `add_next_sibling` inserts immediately
|
|
1657
|
+
# after, so afterend walks in reverse to keep order.
|
|
1658
|
+
case pos
|
|
1195
1659
|
when "beforebegin"
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
nodes
|
|
1199
|
-
@document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: nodes, removed_nodes: [])
|
|
1660
|
+
parent = insertion_parent!
|
|
1661
|
+
nodes.each { |n| @__node__.add_previous_sibling(n) }
|
|
1662
|
+
notify_child_list(added: nodes, target: parent)
|
|
1200
1663
|
when "afterbegin"
|
|
1201
1664
|
first = @__node__.children.first
|
|
1202
1665
|
if first
|
|
1203
|
-
nodes.
|
|
1666
|
+
nodes.each { |n| first.add_previous_sibling(n) }
|
|
1204
1667
|
else
|
|
1205
1668
|
nodes.each { |n| @__node__.add_child(n) }
|
|
1206
1669
|
end
|
|
1207
1670
|
|
|
1208
|
-
|
|
1671
|
+
notify_child_list(added: nodes)
|
|
1209
1672
|
when "beforeend"
|
|
1210
1673
|
nodes.each { |n| @__node__.add_child(n) }
|
|
1211
|
-
|
|
1674
|
+
notify_child_list(added: nodes)
|
|
1212
1675
|
when "afterend"
|
|
1213
|
-
|
|
1214
|
-
|
|
1676
|
+
parent = insertion_parent!
|
|
1215
1677
|
nodes.reverse_each { |n| @__node__.add_next_sibling(n) }
|
|
1216
|
-
|
|
1678
|
+
notify_child_list(added: nodes, target: parent)
|
|
1217
1679
|
end
|
|
1218
1680
|
|
|
1219
1681
|
nil
|
|
1220
1682
|
end
|
|
1221
1683
|
|
|
1684
|
+
# The parent that a beforebegin/afterend insertion targets. Per the spec, if
|
|
1685
|
+
# the element has no parent, or its parent is the Document, there is nowhere
|
|
1686
|
+
# to insert a sibling — throw NoModificationAllowedError.
|
|
1687
|
+
def insertion_parent!
|
|
1688
|
+
parent = @__node__.parent
|
|
1689
|
+
is_document = parent && ((parent.respond_to?(:document?) && parent.document?) || parent.name == "document")
|
|
1690
|
+
if parent.nil? || is_document
|
|
1691
|
+
raise DOMException::NoModificationAllowedError, "The element has no parent."
|
|
1692
|
+
end
|
|
1693
|
+
|
|
1694
|
+
parent
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1222
1697
|
def insert_adjacent_text(position, text)
|
|
1223
1698
|
return nil if text.to_s.empty?
|
|
1224
1699
|
|
|
@@ -1254,48 +1729,7 @@ module Dommy
|
|
|
1254
1729
|
# CONTAINS/CONTAINED_BY bitmask for ancestor/descendant pairs, or
|
|
1255
1730
|
# PRECEDING/FOLLOWING for siblings (and DISCONNECTED for unrelated
|
|
1256
1731
|
# nodes).
|
|
1257
|
-
|
|
1258
|
-
return 0 if equal?(other)
|
|
1259
|
-
return DOCUMENT_POSITION_DISCONNECTED unless other.respond_to?(:__node__)
|
|
1260
|
-
|
|
1261
|
-
self_node = @__node__
|
|
1262
|
-
other_node = other.__node__
|
|
1263
|
-
|
|
1264
|
-
self_ancestors = ancestor_chain(self_node)
|
|
1265
|
-
other_ancestors = ancestor_chain(other_node)
|
|
1266
|
-
|
|
1267
|
-
common = nil
|
|
1268
|
-
self_ancestors.each do |a|
|
|
1269
|
-
if other_ancestors.include?(a)
|
|
1270
|
-
common = a
|
|
1271
|
-
break
|
|
1272
|
-
end
|
|
1273
|
-
end
|
|
1274
|
-
|
|
1275
|
-
unless common
|
|
1276
|
-
return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING
|
|
1277
|
-
end
|
|
1278
|
-
|
|
1279
|
-
if common == self_node
|
|
1280
|
-
return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING
|
|
1281
|
-
elsif common == other_node
|
|
1282
|
-
return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING
|
|
1283
|
-
end
|
|
1284
|
-
|
|
1285
|
-
# Sibling-of-some-level case: compare the two branch points
|
|
1286
|
-
# under the common ancestor.
|
|
1287
|
-
self_branch = branch_under(common, self_ancestors)
|
|
1288
|
-
other_branch = branch_under(common, other_ancestors)
|
|
1289
|
-
common.children.each do |child|
|
|
1290
|
-
if child == self_branch
|
|
1291
|
-
return DOCUMENT_POSITION_FOLLOWING
|
|
1292
|
-
elsif child == other_branch
|
|
1293
|
-
return DOCUMENT_POSITION_PRECEDING
|
|
1294
|
-
end
|
|
1295
|
-
end
|
|
1296
|
-
|
|
1297
|
-
DOCUMENT_POSITION_DISCONNECTED
|
|
1298
|
-
end
|
|
1732
|
+
# compareDocumentPosition is provided generically by the Node module.
|
|
1299
1733
|
|
|
1300
1734
|
# `Node.isSameNode(other)` — strict reference identity. The DOM
|
|
1301
1735
|
# spec deprecates this in favor of `===`, but linkedom-style
|
|
@@ -1309,62 +1743,19 @@ module Dommy
|
|
|
1309
1743
|
# suite and standard DOM Node.isEqualNode.
|
|
1310
1744
|
def equal_node?(other)
|
|
1311
1745
|
return false unless other.is_a?(Element)
|
|
1312
|
-
return false unless @__node__.name == other.
|
|
1746
|
+
return false unless @__node__.name == other.__dommy_backend_node__.name
|
|
1313
1747
|
return false unless attribute_signature == other.send(:attribute_signature)
|
|
1314
|
-
return false unless @__node__.children.size == other.
|
|
1748
|
+
return false unless @__node__.children.size == other.__dommy_backend_node__.children.size
|
|
1315
1749
|
|
|
1316
|
-
@__node__.children.zip(other.
|
|
1750
|
+
@__node__.children.zip(other.__dommy_backend_node__.children).all? do |a, b|
|
|
1317
1751
|
wa = @document.wrap_node(a)
|
|
1318
1752
|
wb = @document.wrap_node(b)
|
|
1319
1753
|
wa.respond_to?(:equal_node?) ? wa.equal_node?(wb) : a.content == b.content
|
|
1320
1754
|
end
|
|
1321
1755
|
end
|
|
1322
1756
|
|
|
1323
|
-
private
|
|
1324
|
-
|
|
1325
|
-
def ancestor_chain(node)
|
|
1326
|
-
chain = [node]
|
|
1327
|
-
Internal::NodeTraversal.each_ancestor(node) { |n| chain << n }
|
|
1328
|
-
chain
|
|
1329
|
-
end
|
|
1330
|
-
|
|
1331
|
-
def branch_under(common, chain)
|
|
1332
|
-
# Walk back along `chain` to find the entry whose parent is `common`.
|
|
1333
|
-
chain.each_with_index do |node, i|
|
|
1334
|
-
return node if i.zero? && node == common
|
|
1335
|
-
return node if node.respond_to?(:parent) && node.parent == common
|
|
1336
|
-
end
|
|
1337
|
-
|
|
1338
|
-
nil
|
|
1339
|
-
end
|
|
1340
|
-
|
|
1341
|
-
def attribute_signature
|
|
1342
|
-
@__node__.attribute_nodes.map { |a| [a.name, a.value] }.sort
|
|
1343
|
-
end
|
|
1344
|
-
|
|
1345
|
-
public
|
|
1346
|
-
|
|
1347
1757
|
def remove
|
|
1348
|
-
|
|
1349
|
-
end
|
|
1350
|
-
|
|
1351
|
-
# ParentNode mixin methods — append / prepend / replaceChildren
|
|
1352
|
-
# take a mix of Node and String args (strings become text nodes).
|
|
1353
|
-
|
|
1354
|
-
def append(*args)
|
|
1355
|
-
append_nodes(args)
|
|
1356
|
-
end
|
|
1357
|
-
|
|
1358
|
-
def prepend(*args)
|
|
1359
|
-
prepend_nodes(args)
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
|
-
def replace_children(*args)
|
|
1363
|
-
removed = @__node__.children.to_a
|
|
1364
|
-
removed.each(&:unlink)
|
|
1365
|
-
nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
|
|
1366
|
-
nodes.each { |n| @__node__.add_child(n) }
|
|
1367
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: removed)
|
|
1758
|
+
@document.remove_node_with_notify(@__node__)
|
|
1368
1759
|
nil
|
|
1369
1760
|
end
|
|
1370
1761
|
|
|
@@ -1394,25 +1785,61 @@ module Dommy
|
|
|
1394
1785
|
end
|
|
1395
1786
|
|
|
1396
1787
|
def click
|
|
1397
|
-
|
|
1788
|
+
dispatch_event(MouseEvent.new("click", "bubbles" => true, "cancelable" => true, "button" => 0))
|
|
1398
1789
|
end
|
|
1399
1790
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
# listener so callers can pass it back to remove_event_listener.
|
|
1403
|
-
def on(type, &block)
|
|
1404
|
-
add_event_listener(type, block)
|
|
1405
|
-
block
|
|
1791
|
+
def get_attribute_names
|
|
1792
|
+
Backend.attribute_nodes(@__node__).map(&:name)
|
|
1406
1793
|
end
|
|
1407
1794
|
|
|
1408
|
-
#
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
def [](key)
|
|
1412
|
-
__js_get__(key.to_s)
|
|
1795
|
+
# No layout engine — geometry getters return zeroed rects.
|
|
1796
|
+
def get_bounding_client_rect
|
|
1797
|
+
DOMRect.new
|
|
1413
1798
|
end
|
|
1414
1799
|
|
|
1415
|
-
def
|
|
1800
|
+
def get_client_rects
|
|
1801
|
+
[]
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
def request_fullscreen
|
|
1805
|
+
@document.__internal_set_fullscreen_element__(self)
|
|
1806
|
+
PromiseValue.resolve(@document.default_view, nil)
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
# Popover API — show / hide / toggle fire beforetoggle + toggle events
|
|
1810
|
+
# (no real visual change). Return values mirror the IDL.
|
|
1811
|
+
def show_popover
|
|
1812
|
+
toggle_popover_state(true)
|
|
1813
|
+
nil
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
def hide_popover
|
|
1817
|
+
toggle_popover_state(false)
|
|
1818
|
+
nil
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def toggle_popover
|
|
1822
|
+
new_state = !@__popover_open__
|
|
1823
|
+
toggle_popover_state(new_state)
|
|
1824
|
+
new_state
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
# Ruby block-style listener (in addition to the (type, callable,
|
|
1828
|
+
# options) form inherited from EventTarget). Returns the resolved
|
|
1829
|
+
# listener so callers can pass it back to remove_event_listener.
|
|
1830
|
+
def on(type, &block)
|
|
1831
|
+
add_event_listener(type, block)
|
|
1832
|
+
block
|
|
1833
|
+
end
|
|
1834
|
+
|
|
1835
|
+
# `el[:foo]` / `el[:foo] = ...` bracket shortcut for the JS-style
|
|
1836
|
+
# property access pattern. Useful when porting browser-side code
|
|
1837
|
+
# to CRuby tests.
|
|
1838
|
+
def [](key)
|
|
1839
|
+
__js_get__(key.to_s)
|
|
1840
|
+
end
|
|
1841
|
+
|
|
1842
|
+
def []=(key, value)
|
|
1416
1843
|
__js_set__(key.to_s, value)
|
|
1417
1844
|
end
|
|
1418
1845
|
|
|
@@ -1444,8 +1871,26 @@ module Dommy
|
|
|
1444
1871
|
get_attribute("popover")
|
|
1445
1872
|
when "children"
|
|
1446
1873
|
@live_children
|
|
1874
|
+
when "childNodes"
|
|
1875
|
+
@live_child_nodes
|
|
1876
|
+
when "firstChild"
|
|
1877
|
+
first_child
|
|
1878
|
+
when "lastChild"
|
|
1879
|
+
last_child
|
|
1880
|
+
when "childElementCount"
|
|
1881
|
+
child_element_count
|
|
1882
|
+
when "lastElementChild"
|
|
1883
|
+
last_element_child
|
|
1884
|
+
when "nextSibling"
|
|
1885
|
+
next_sibling
|
|
1886
|
+
when "previousSibling"
|
|
1887
|
+
previous_sibling
|
|
1888
|
+
when "nextElementSibling"
|
|
1889
|
+
next_element_sibling
|
|
1890
|
+
when "previousElementSibling"
|
|
1891
|
+
previous_element_sibling
|
|
1447
1892
|
when "firstElementChild"
|
|
1448
|
-
|
|
1893
|
+
first_element_child
|
|
1449
1894
|
when "parentElement", "parent"
|
|
1450
1895
|
wrap_parent(@__node__.parent)
|
|
1451
1896
|
when "parentNode"
|
|
@@ -1456,16 +1901,23 @@ module Dommy
|
|
|
1456
1901
|
when "textContent"
|
|
1457
1902
|
@__node__.text
|
|
1458
1903
|
when "innerHTML"
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
@__node__.inner_html
|
|
1463
|
-
end
|
|
1464
|
-
|
|
1904
|
+
inner_html
|
|
1905
|
+
when "outerHTML"
|
|
1906
|
+
outer_html
|
|
1465
1907
|
when "tagName"
|
|
1466
|
-
|
|
1908
|
+
tag_name
|
|
1909
|
+
when "prefix"
|
|
1910
|
+
element_prefix
|
|
1467
1911
|
when "classList"
|
|
1468
1912
|
@class_list
|
|
1913
|
+
when "relList"
|
|
1914
|
+
reflected_token_list("relList", "rel")
|
|
1915
|
+
when "htmlFor"
|
|
1916
|
+
reflected_token_list("htmlFor", "for")
|
|
1917
|
+
when "sandbox"
|
|
1918
|
+
reflected_token_list("sandbox", "sandbox")
|
|
1919
|
+
when "sizes"
|
|
1920
|
+
reflected_token_list("sizes", "sizes")
|
|
1469
1921
|
when "style"
|
|
1470
1922
|
@style
|
|
1471
1923
|
when "dataset"
|
|
@@ -1497,11 +1949,13 @@ module Dommy
|
|
|
1497
1949
|
when "localName"
|
|
1498
1950
|
local_name
|
|
1499
1951
|
when "nodeName"
|
|
1500
|
-
|
|
1952
|
+
tag_name
|
|
1501
1953
|
when "slot"
|
|
1502
1954
|
slot
|
|
1503
1955
|
when "role"
|
|
1504
|
-
role
|
|
1956
|
+
aria_get("role")
|
|
1957
|
+
when "accessKeyLabel"
|
|
1958
|
+
access_key_label
|
|
1505
1959
|
when "baseURI"
|
|
1506
1960
|
base_uri
|
|
1507
1961
|
when "shadowRoot"
|
|
@@ -1509,9 +1963,20 @@ module Dommy
|
|
|
1509
1963
|
when "ownerDocument"
|
|
1510
1964
|
@document
|
|
1511
1965
|
else
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1966
|
+
if (elements_attr = aria_elements_attr(key))
|
|
1967
|
+
# Plural ARIA element references (`ariaDescribedByElements` ↔
|
|
1968
|
+
# `aria-describedby`) — a list of Elements.
|
|
1969
|
+
aria_elements_get(elements_attr, key)
|
|
1970
|
+
elsif (element_attr = aria_element_attr(key))
|
|
1971
|
+
# ARIA element-reference IDL attribute (`ariaActiveDescendantElement`
|
|
1972
|
+
# ↔ `aria-activedescendant`) — resolves to an Element or null.
|
|
1973
|
+
aria_element_get(element_attr, key)
|
|
1974
|
+
elsif (content_attr = aria_content_attr(key))
|
|
1975
|
+
# ARIA / role reflected IDL attribute (`ariaLabel` ↔ `aria-label`,
|
|
1976
|
+
# `role` ↔ `role`) — a nullable DOMString (null when absent).
|
|
1977
|
+
aria_get(content_attr)
|
|
1978
|
+
elsif key.start_with?("on") && key.length > 2
|
|
1979
|
+
# `el.onXxx` event handler property — the registered callback or nil.
|
|
1515
1980
|
@on_handlers&.[](event_name_from_on(key))
|
|
1516
1981
|
end
|
|
1517
1982
|
end
|
|
@@ -1531,6 +1996,143 @@ module Dommy
|
|
|
1531
1996
|
raw.to_s
|
|
1532
1997
|
end
|
|
1533
1998
|
|
|
1999
|
+
# `accessKeyLabel` — the assigned access key's platform label. The
|
|
2000
|
+
# `accesskey` content attribute is a set of one-code-point candidates; a
|
|
2001
|
+
# single valid candidate yields a (modifier-prefixed) label, anything else
|
|
2002
|
+
# (empty, or multiple/multi-char tokens) yields the empty string. The exact
|
|
2003
|
+
# modifier varies by platform — tests only assert non-empty vs empty.
|
|
2004
|
+
def access_key_label
|
|
2005
|
+
keys = @__node__["accesskey"].to_s.split(/[ \t\n\f\r]+/).reject(&:empty?)
|
|
2006
|
+
return "" unless keys.length == 1 && keys.first.length == 1
|
|
2007
|
+
|
|
2008
|
+
"Alt+#{keys.first.upcase}"
|
|
2009
|
+
end
|
|
2010
|
+
|
|
2011
|
+
# The content attribute an ARIA element-reference IDL attribute reflects
|
|
2012
|
+
# (`ariaActiveDescendantElement` → "aria-activedescendant",
|
|
2013
|
+
# `ariaErrorMessageElement` → "aria-errormessage"), or nil. The IDL name is
|
|
2014
|
+
# `aria<Xxx>Element`; the content attribute is "aria-" + <Xxx> lowercased.
|
|
2015
|
+
def aria_element_attr(key)
|
|
2016
|
+
return nil unless key.is_a?(String) && key.start_with?("aria") && key.end_with?("Element")
|
|
2017
|
+
return nil unless key.length > 11 && key[4] =~ /[A-Z]/
|
|
2018
|
+
|
|
2019
|
+
"aria-#{key[4...-7].downcase}"
|
|
2020
|
+
end
|
|
2021
|
+
|
|
2022
|
+
# Read an ARIA element reference: an explicitly-set Element wins; otherwise
|
|
2023
|
+
# the content attribute is resolved as an IDREF (the element with that id in
|
|
2024
|
+
# this element's tree), or null.
|
|
2025
|
+
def aria_element_get(content_attr, key)
|
|
2026
|
+
explicit = (@aria_element_refs ||= {})[key]
|
|
2027
|
+
return explicit if explicit
|
|
2028
|
+
|
|
2029
|
+
idref = @__node__[content_attr].to_s
|
|
2030
|
+
return nil if idref.empty?
|
|
2031
|
+
|
|
2032
|
+
aria_find_in_root(idref)
|
|
2033
|
+
end
|
|
2034
|
+
|
|
2035
|
+
# Set an ARIA element reference: null/undefined clears it and removes the
|
|
2036
|
+
# content attribute; an Element stores the explicit reference and sets the
|
|
2037
|
+
# content attribute to the empty string (per the reflection spec).
|
|
2038
|
+
def aria_element_set(content_attr, key, value)
|
|
2039
|
+
refs = (@aria_element_refs ||= {})
|
|
2040
|
+
if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
|
|
2041
|
+
refs.delete(key)
|
|
2042
|
+
remove_attribute(content_attr) if @__node__.key?(content_attr)
|
|
2043
|
+
else
|
|
2044
|
+
# set_attribute clears explicit refs via its aria-* hook, so store the
|
|
2045
|
+
# new reference afterward.
|
|
2046
|
+
set_attribute(content_attr, "")
|
|
2047
|
+
refs[key] = value
|
|
2048
|
+
end
|
|
2049
|
+
nil
|
|
2050
|
+
end
|
|
2051
|
+
|
|
2052
|
+
# The content attribute a plural ARIA element-references IDL attribute
|
|
2053
|
+
# reflects (`ariaDescribedByElements` → "aria-describedby",
|
|
2054
|
+
# `ariaLabelledByElements` → "aria-labelledby"), or nil. The IDL name is
|
|
2055
|
+
# `aria<Xxx>Elements`; the content attribute is "aria-" + <Xxx> lowercased.
|
|
2056
|
+
def aria_elements_attr(key)
|
|
2057
|
+
return nil unless key.is_a?(String) && key.start_with?("aria") && key.end_with?("Elements")
|
|
2058
|
+
return nil unless key.length > 12 && key[4] =~ /[A-Z]/
|
|
2059
|
+
|
|
2060
|
+
"aria-#{key[4...-8].downcase}"
|
|
2061
|
+
end
|
|
2062
|
+
|
|
2063
|
+
# Read a plural ARIA element references value (a list of Elements): the
|
|
2064
|
+
# explicitly-set array wins; otherwise the content attribute is split as a
|
|
2065
|
+
# space-separated IDREF list and each resolved (missing ids dropped).
|
|
2066
|
+
def aria_elements_get(content_attr, key)
|
|
2067
|
+
explicit = (@aria_elements_refs ||= {})[key]
|
|
2068
|
+
return explicit.dup if explicit
|
|
2069
|
+
|
|
2070
|
+
@__node__[content_attr].to_s.split(/[ \t\n\f\r]+/).reject(&:empty?).filter_map do |id|
|
|
2071
|
+
aria_find_in_root(id)
|
|
2072
|
+
end
|
|
2073
|
+
end
|
|
2074
|
+
|
|
2075
|
+
# Resolve an ARIA IDREF within this element's tree ROOT (its topmost
|
|
2076
|
+
# ancestor) rather than the document — so references keep working when the
|
|
2077
|
+
# subtree is disconnected from the document.
|
|
2078
|
+
def aria_find_in_root(id)
|
|
2079
|
+
root = @__node__
|
|
2080
|
+
root = root.parent while root.parent && !root.parent.is_a?(Backend.document_class)
|
|
2081
|
+
node = ([root] + root.css("*").to_a).find { |n| n["id"].to_s == id }
|
|
2082
|
+
node && @document.wrap_node(node)
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
# Set a plural ARIA element references value: null/undefined clears it and
|
|
2086
|
+
# removes the content attribute; an array of Elements is stored and the
|
|
2087
|
+
# content attribute is set to the empty string.
|
|
2088
|
+
def aria_elements_set(content_attr, key, value)
|
|
2089
|
+
refs = (@aria_elements_refs ||= {})
|
|
2090
|
+
if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
|
|
2091
|
+
refs.delete(key)
|
|
2092
|
+
remove_attribute(content_attr) if @__node__.key?(content_attr)
|
|
2093
|
+
else
|
|
2094
|
+
set_attribute(content_attr, "")
|
|
2095
|
+
refs[key] = Array(value)
|
|
2096
|
+
end
|
|
2097
|
+
nil
|
|
2098
|
+
end
|
|
2099
|
+
|
|
2100
|
+
# Drop any explicit ARIA element reference (singular or plural) whose content
|
|
2101
|
+
# attribute was just set directly (so the IDL getter re-resolves the IDREF).
|
|
2102
|
+
def clear_aria_element_ref_for(content_attr)
|
|
2103
|
+
@aria_element_refs&.delete_if { |key, _| aria_element_attr(key) == content_attr }
|
|
2104
|
+
@aria_elements_refs&.delete_if { |key, _| aria_elements_attr(key) == content_attr }
|
|
2105
|
+
end
|
|
2106
|
+
|
|
2107
|
+
# The content attribute a role/ARIA IDL attribute reflects, or nil for a
|
|
2108
|
+
# non-ARIA key. `role` → "role"; `ariaXxx` → "aria-" + the rest, lowercased
|
|
2109
|
+
# with humps removed (`ariaAutoComplete` → "aria-autocomplete",
|
|
2110
|
+
# `ariaColIndexText` → "aria-colindextext").
|
|
2111
|
+
def aria_content_attr(key)
|
|
2112
|
+
return "role" if key == "role"
|
|
2113
|
+
return nil unless key.is_a?(String) && key.length > 4 && key.start_with?("aria")
|
|
2114
|
+
return nil unless key[4] =~ /[A-Z]/
|
|
2115
|
+
|
|
2116
|
+
"aria-#{key[4..].downcase}"
|
|
2117
|
+
end
|
|
2118
|
+
|
|
2119
|
+
# Read a reflected nullable DOMString: the content attribute value, or nil
|
|
2120
|
+
# (→ JS null) when the attribute is absent.
|
|
2121
|
+
def aria_get(content_attr)
|
|
2122
|
+
@__node__.key?(content_attr) ? @__node__[content_attr].to_s : nil
|
|
2123
|
+
end
|
|
2124
|
+
|
|
2125
|
+
# Write a reflected nullable DOMString: null / undefined removes the content
|
|
2126
|
+
# attribute; any other value is ToString-coerced and set.
|
|
2127
|
+
def aria_set(content_attr, value)
|
|
2128
|
+
if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
|
|
2129
|
+
remove_attribute(content_attr) if @__node__.key?(content_attr)
|
|
2130
|
+
else
|
|
2131
|
+
set_attribute(content_attr, value.to_s)
|
|
2132
|
+
end
|
|
2133
|
+
nil
|
|
2134
|
+
end
|
|
2135
|
+
|
|
1534
2136
|
# Map a JS boolean property name to its underlying HTML attribute.
|
|
1535
2137
|
# HTML attribute names are lowercase; the DOM property may be
|
|
1536
2138
|
# camelCase (`readOnly` → `readonly`).
|
|
@@ -1541,25 +2143,12 @@ module Dommy
|
|
|
1541
2143
|
def __js_set__(key, value)
|
|
1542
2144
|
case key
|
|
1543
2145
|
when "textContent"
|
|
1544
|
-
|
|
2146
|
+
self.text_content = value
|
|
1545
2147
|
when "innerHTML"
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
# via `[:content]`). Mirror that here so child placeholders
|
|
1551
|
-
# inside the template don't pollute outer queries.
|
|
1552
|
-
@document.attach_template_content(self, value.to_s)
|
|
1553
|
-
else
|
|
1554
|
-
@__node__.inner_html = value.to_s
|
|
1555
|
-
@document.migrate_template_descendants(@__node__)
|
|
1556
|
-
end
|
|
1557
|
-
|
|
1558
|
-
@document.notify_child_list_mutation(
|
|
1559
|
-
target_node: @__node__,
|
|
1560
|
-
added_nodes: @__node__.children.to_a,
|
|
1561
|
-
removed_nodes: removed
|
|
1562
|
-
)
|
|
2148
|
+
self.inner_html = value
|
|
2149
|
+
when "outerHTML"
|
|
2150
|
+
# [CEReactions, LegacyNullToEmptyString] DOMString — null becomes "".
|
|
2151
|
+
self.outer_html = value.nil? ? "" : value.to_s
|
|
1563
2152
|
when "hidden", "disabled", "checked", "readOnly", "multiple", "required"
|
|
1564
2153
|
# Boolean reflected property — funnel through set_attribute /
|
|
1565
2154
|
# remove_attribute so MutationObserver attribute records fire.
|
|
@@ -1572,6 +2161,13 @@ module Dommy
|
|
|
1572
2161
|
|
|
1573
2162
|
when "className"
|
|
1574
2163
|
set_attribute("class", value.to_s)
|
|
2164
|
+
when "classList"
|
|
2165
|
+
# WHATWG [PutForwards=value]: `el.classList = x` forwards to
|
|
2166
|
+
# `el.classList.value = x` (set the class attribute). Handling it here
|
|
2167
|
+
# (instead of letting the write fall through as unhandled) stops the JS
|
|
2168
|
+
# bridge from stashing a string expando that would shadow the classList
|
|
2169
|
+
# getter for the rest of the element's life.
|
|
2170
|
+
set_attribute("class", value.to_s)
|
|
1575
2171
|
when "id"
|
|
1576
2172
|
set_attribute("id", value.to_s)
|
|
1577
2173
|
when "value"
|
|
@@ -1579,40 +2175,48 @@ module Dommy
|
|
|
1579
2175
|
when "slot"
|
|
1580
2176
|
set_attribute("slot", value.to_s)
|
|
1581
2177
|
when "role"
|
|
1582
|
-
|
|
2178
|
+
aria_set("role", value)
|
|
1583
2179
|
else
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2180
|
+
if (elements_attr = aria_elements_attr(key))
|
|
2181
|
+
# Plural ARIA element references setter (list of Elements).
|
|
2182
|
+
aria_elements_set(elements_attr, key, value)
|
|
2183
|
+
elsif (element_attr = aria_element_attr(key))
|
|
2184
|
+
# ARIA element-reference IDL attribute setter.
|
|
2185
|
+
aria_element_set(element_attr, key, value)
|
|
2186
|
+
elsif (content_attr = aria_content_attr(key))
|
|
2187
|
+
# ARIA / role reflected nullable DOMString (null/undefined → remove).
|
|
2188
|
+
aria_set(content_attr, value)
|
|
2189
|
+
elsif key.start_with?("on") && key.length > 2
|
|
2190
|
+
# `el.onXxx = fn` registers fn as a single named handler; nil removes.
|
|
1587
2191
|
set_on_handler(event_name_from_on(key), value)
|
|
1588
2192
|
else
|
|
1589
|
-
|
|
2193
|
+
# Not a known DOM property — tell the JS host to keep it as a
|
|
2194
|
+
# JS-side expando (so object/instance fields keep their identity).
|
|
2195
|
+
Bridge::UNHANDLED
|
|
1590
2196
|
end
|
|
1591
2197
|
end
|
|
1592
2198
|
end
|
|
1593
2199
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
@on_handlers.delete(event_name)
|
|
1609
|
-
end
|
|
1610
|
-
end
|
|
1611
|
-
|
|
1612
|
-
public
|
|
1613
|
-
|
|
2200
|
+
include Bridge::Methods
|
|
2201
|
+
js_methods %w[
|
|
2202
|
+
getAttribute setAttribute hasAttribute removeAttribute getAttributeNames closest
|
|
2203
|
+
getAttributeNS setAttributeNS hasAttributeNS removeAttributeNS getAttributeNodeNS setAttributeNodeNS
|
|
2204
|
+
querySelector querySelectorAll getElementsByClassName getElementsByTagName getElementsByTagNameNS
|
|
2205
|
+
insertAdjacentElement insertAdjacentHTML insertAdjacentText toggleAttribute matches
|
|
2206
|
+
toString getAttributeNode setAttributeNode removeAttributeNode focus blur attachShadow
|
|
2207
|
+
addEventListener removeEventListener dispatchEvent appendChild insertBefore removeChild
|
|
2208
|
+
replaceChild cloneNode append prepend replaceChildren before after getInnerHTML getHTML
|
|
2209
|
+
remove replaceWith click getBoundingClientRect getClientRects scrollIntoView scroll
|
|
2210
|
+
scrollTo scrollBy requestFullscreen showPopover hidePopover togglePopover isEqualNode
|
|
2211
|
+
hasChildNodes hasAttributes getRootNode normalize contains
|
|
2212
|
+
compareDocumentPosition isSameNode lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
2213
|
+
]
|
|
1614
2214
|
def __js_call__(method, args)
|
|
1615
2215
|
case method
|
|
2216
|
+
when "hasChildNodes"
|
|
2217
|
+
has_child_nodes?
|
|
2218
|
+
when "hasAttributes"
|
|
2219
|
+
has_attributes?
|
|
1616
2220
|
when "getAttribute"
|
|
1617
2221
|
get_attribute(args[0])
|
|
1618
2222
|
when "setAttribute"
|
|
@@ -1621,18 +2225,38 @@ module Dommy
|
|
|
1621
2225
|
has_attribute?(args[0])
|
|
1622
2226
|
when "removeAttribute"
|
|
1623
2227
|
remove_attribute(args[0])
|
|
2228
|
+
when "getAttributeNS"
|
|
2229
|
+
get_attribute_ns(args[0], args[1])
|
|
2230
|
+
when "setAttributeNS"
|
|
2231
|
+
set_attribute_ns(args[0], args[1], args[2])
|
|
2232
|
+
when "hasAttributeNS"
|
|
2233
|
+
has_attribute_ns?(args[0], args[1])
|
|
2234
|
+
when "removeAttributeNS"
|
|
2235
|
+
remove_attribute_ns(args[0], args[1])
|
|
2236
|
+
when "getAttributeNodeNS"
|
|
2237
|
+
get_attribute_node_ns(args[0], args[1])
|
|
2238
|
+
when "setAttributeNodeNS"
|
|
2239
|
+
set_attribute_node(args[0])
|
|
1624
2240
|
when "getAttributeNames"
|
|
1625
|
-
|
|
2241
|
+
get_attribute_names
|
|
1626
2242
|
when "closest"
|
|
2243
|
+
raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?
|
|
2244
|
+
|
|
1627
2245
|
closest(args[0])
|
|
1628
2246
|
when "querySelector"
|
|
1629
|
-
query_selector(args
|
|
2247
|
+
query_selector(Internal.css_query_arg!(args))
|
|
1630
2248
|
when "querySelectorAll"
|
|
1631
|
-
query_selector_all(args
|
|
2249
|
+
query_selector_all(Internal.css_query_arg!(args))
|
|
1632
2250
|
when "getElementsByClassName"
|
|
1633
2251
|
get_elements_by_class_name(args[0])
|
|
2252
|
+
when "getElementsByTagNameNS"
|
|
2253
|
+
get_elements_by_tag_name_ns(args[0], args[1])
|
|
1634
2254
|
when "getElementsByTagName"
|
|
1635
2255
|
get_elements_by_tag_name(args[0])
|
|
2256
|
+
when "getRootNode"
|
|
2257
|
+
get_root_node
|
|
2258
|
+
when "normalize"
|
|
2259
|
+
normalize
|
|
1636
2260
|
when "insertAdjacentElement"
|
|
1637
2261
|
insert_adjacent_element(args[0], args[1])
|
|
1638
2262
|
when "insertAdjacentHTML"
|
|
@@ -1642,7 +2266,23 @@ module Dommy
|
|
|
1642
2266
|
when "toggleAttribute"
|
|
1643
2267
|
toggle_attribute(args[0], args[1])
|
|
1644
2268
|
when "matches"
|
|
2269
|
+
raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?
|
|
2270
|
+
|
|
1645
2271
|
matches?(args[0])
|
|
2272
|
+
when "isEqualNode"
|
|
2273
|
+
is_equal_node(args[0])
|
|
2274
|
+
when "isSameNode"
|
|
2275
|
+
is_same_node(args[0])
|
|
2276
|
+
when "compareDocumentPosition"
|
|
2277
|
+
compare_document_position(args[0])
|
|
2278
|
+
when "lookupNamespaceURI"
|
|
2279
|
+
lookup_namespace_uri(args[0])
|
|
2280
|
+
when "lookupPrefix"
|
|
2281
|
+
lookup_prefix(args[0])
|
|
2282
|
+
when "isDefaultNamespace"
|
|
2283
|
+
is_default_namespace(args[0])
|
|
2284
|
+
when "contains"
|
|
2285
|
+
contains?(args[0])
|
|
1646
2286
|
when "toString"
|
|
1647
2287
|
to_s
|
|
1648
2288
|
when "getAttributeNode"
|
|
@@ -1660,7 +2300,7 @@ module Dommy
|
|
|
1660
2300
|
when "addEventListener"
|
|
1661
2301
|
add_event_listener(args[0], args[1], args[2])
|
|
1662
2302
|
when "removeEventListener"
|
|
1663
|
-
remove_event_listener(args[0], args[1])
|
|
2303
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
1664
2304
|
when "dispatchEvent"
|
|
1665
2305
|
dispatch_event(args[0])
|
|
1666
2306
|
when "appendChild"
|
|
@@ -1674,9 +2314,9 @@ module Dommy
|
|
|
1674
2314
|
when "cloneNode"
|
|
1675
2315
|
clone_node(args[0])
|
|
1676
2316
|
when "append"
|
|
1677
|
-
|
|
2317
|
+
append(*args)
|
|
1678
2318
|
when "prepend"
|
|
1679
|
-
|
|
2319
|
+
prepend(*args)
|
|
1680
2320
|
when "replaceChildren"
|
|
1681
2321
|
replace_children(*args)
|
|
1682
2322
|
when "before"
|
|
@@ -1686,91 +2326,30 @@ module Dommy
|
|
|
1686
2326
|
when "getInnerHTML", "getHTML"
|
|
1687
2327
|
inner_html
|
|
1688
2328
|
when "remove"
|
|
1689
|
-
|
|
1690
|
-
@__node__.unlink
|
|
1691
|
-
@document.notify_child_list_mutation(target_node: parent, added_nodes: [], removed_nodes: [@__node__]) if parent
|
|
1692
|
-
nil
|
|
2329
|
+
remove
|
|
1693
2330
|
when "replaceWith"
|
|
1694
2331
|
replace_with(args)
|
|
1695
2332
|
when "click"
|
|
1696
|
-
|
|
2333
|
+
click
|
|
1697
2334
|
when "getBoundingClientRect"
|
|
1698
|
-
|
|
2335
|
+
get_bounding_client_rect
|
|
1699
2336
|
when "getClientRects"
|
|
1700
|
-
|
|
2337
|
+
get_client_rects
|
|
1701
2338
|
when "scrollIntoView", "scroll", "scrollTo", "scrollBy"
|
|
1702
|
-
|
|
1703
|
-
@__scroll_log__ ||= []
|
|
1704
|
-
@__scroll_log__ << [method, args]
|
|
1705
|
-
nil
|
|
2339
|
+
record_scroll(method, args)
|
|
1706
2340
|
when "requestFullscreen"
|
|
1707
|
-
|
|
1708
|
-
PromiseValue.resolve(@document.default_view, nil)
|
|
2341
|
+
request_fullscreen
|
|
1709
2342
|
when "showPopover"
|
|
1710
|
-
|
|
1711
|
-
nil
|
|
2343
|
+
show_popover
|
|
1712
2344
|
when "hidePopover"
|
|
1713
|
-
|
|
1714
|
-
nil
|
|
2345
|
+
hide_popover
|
|
1715
2346
|
when "togglePopover"
|
|
1716
|
-
|
|
1717
|
-
toggle_popover_state(new_state)
|
|
1718
|
-
new_state
|
|
2347
|
+
toggle_popover
|
|
1719
2348
|
else
|
|
1720
2349
|
nil
|
|
1721
2350
|
end
|
|
1722
2351
|
end
|
|
1723
2352
|
|
|
1724
|
-
private
|
|
1725
|
-
|
|
1726
|
-
def normalize_attr_key(name)
|
|
1727
|
-
s = name.to_s
|
|
1728
|
-
case_sensitive_attribute_names? ? s : s.downcase
|
|
1729
|
-
end
|
|
1730
|
-
|
|
1731
|
-
def element_children
|
|
1732
|
-
@__node__.element_children.each_with_object([]) do |node, out|
|
|
1733
|
-
wrapped = @document.wrap_node(node)
|
|
1734
|
-
out << wrapped if wrapped
|
|
1735
|
-
end
|
|
1736
|
-
end
|
|
1737
|
-
|
|
1738
|
-
def wrap_parent(node)
|
|
1739
|
-
@document.wrap_node(node)
|
|
1740
|
-
end
|
|
1741
|
-
|
|
1742
|
-
def __event_parent__
|
|
1743
|
-
parent_node = @__node__.parent
|
|
1744
|
-
# If our Nokogiri parent is a shadow tree's backing fragment,
|
|
1745
|
-
# the bubble path's next stop is the ShadowRoot itself — not
|
|
1746
|
-
# the bare Fragment wrapper. The ShadowRoot's __event_parent__
|
|
1747
|
-
# will return nil (composed events route to host explicitly).
|
|
1748
|
-
if parent_node.is_a?(Nokogiri::XML::DocumentFragment)
|
|
1749
|
-
sr = @document.__shadow_root_for_fragment__(parent_node)
|
|
1750
|
-
return sr if sr
|
|
1751
|
-
end
|
|
1752
|
-
|
|
1753
|
-
parent = wrap_parent(parent_node)
|
|
1754
|
-
parent || @document
|
|
1755
|
-
end
|
|
1756
|
-
|
|
1757
|
-
def template_content
|
|
1758
|
-
return nil unless @__node__.name == "template"
|
|
1759
|
-
|
|
1760
|
-
@document.template_content_fragment(self)
|
|
1761
|
-
end
|
|
1762
|
-
|
|
1763
|
-
# Attribute name handling depends on the element's namespace:
|
|
1764
|
-
# - HTML: case-insensitive (browser DOM stores everything lowercase).
|
|
1765
|
-
# - SVG / other XML: case-sensitive (`viewBox` ≠ `viewbox`).
|
|
1766
|
-
# Subclasses with a known namespace override `case_sensitive_attribute_names?`
|
|
1767
|
-
# to flip the behavior. Generic Element nodes inspect the namespace
|
|
1768
|
-
# URI directly.
|
|
1769
|
-
def case_sensitive_attribute_names?
|
|
1770
|
-
ns = namespace_uri
|
|
1771
|
-
!ns.nil? && ns != "http://www.w3.org/1999/xhtml"
|
|
1772
|
-
end
|
|
1773
|
-
|
|
1774
2353
|
def get_attribute(name)
|
|
1775
2354
|
return nil if name.nil?
|
|
1776
2355
|
|
|
@@ -1780,9 +2359,17 @@ module Dommy
|
|
|
1780
2359
|
def set_attribute(name, value)
|
|
1781
2360
|
return nil if name.nil?
|
|
1782
2361
|
|
|
2362
|
+
# WHATWG: a qualifiedName not matching the Name production throws.
|
|
2363
|
+
# The WPT corpus exercises only the empty string here (other shapes
|
|
2364
|
+
# like "0"/":"/"invalid^Name" are deliberately treated as valid).
|
|
2365
|
+
raise DOMException::InvalidCharacterError, "empty attribute name" if name.to_s.empty?
|
|
2366
|
+
|
|
1783
2367
|
key = normalize_attr_key(name)
|
|
1784
2368
|
old = @__node__[key]
|
|
1785
2369
|
@__node__[key] = value.to_s
|
|
2370
|
+
# A direct write to an `aria-*` IDREF attribute drops any explicitly-set
|
|
2371
|
+
# element reference, so the IDL getter re-resolves the new IDREF.
|
|
2372
|
+
clear_aria_element_ref_for(key) if key.start_with?("aria-")
|
|
1786
2373
|
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
|
|
1787
2374
|
nil
|
|
1788
2375
|
end
|
|
@@ -1800,17 +2387,75 @@ module Dommy
|
|
|
1800
2387
|
return nil unless @__node__.key?(key)
|
|
1801
2388
|
|
|
1802
2389
|
old = @__node__[key]
|
|
2390
|
+
# Detach the cached Attr (caching its value) *before* the backend drop,
|
|
2391
|
+
# so a held reference keeps the value it had when removed.
|
|
2392
|
+
@attributes&.__internal_evict__(nil, key)
|
|
1803
2393
|
@__node__.remove_attribute(key)
|
|
2394
|
+
# Removing an `aria-*` IDREF attribute also clears any explicitly-set
|
|
2395
|
+
# element reference (the IDL getter then returns null).
|
|
2396
|
+
clear_aria_element_ref_for(key) if key.start_with?("aria-")
|
|
1804
2397
|
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
|
|
1805
2398
|
nil
|
|
1806
2399
|
end
|
|
1807
2400
|
|
|
2401
|
+
# ----- Namespaced attributes (DOM *AttributeNS) -----
|
|
2402
|
+
|
|
2403
|
+
def get_attribute_ns(namespace, local_name)
|
|
2404
|
+
return nil if local_name.nil?
|
|
2405
|
+
|
|
2406
|
+
ns = namespace.to_s
|
|
2407
|
+
Backend.get_attribute_ns(@__node__, ns.empty? ? nil : ns, local_name.to_s)
|
|
2408
|
+
end
|
|
2409
|
+
|
|
2410
|
+
def has_attribute_ns?(namespace, local_name)
|
|
2411
|
+
return false if local_name.nil?
|
|
2412
|
+
|
|
2413
|
+
ns = namespace.to_s
|
|
2414
|
+
Backend.has_attribute_ns?(@__node__, ns.empty? ? nil : ns, local_name.to_s)
|
|
2415
|
+
end
|
|
2416
|
+
|
|
2417
|
+
def set_attribute_ns(namespace, qualified_name, value)
|
|
2418
|
+
ns, prefix, local = Internal::Namespaces.validate_and_extract(namespace, qualified_name)
|
|
2419
|
+
old = Backend.get_attribute_ns(@__node__, ns, local)
|
|
2420
|
+
Backend.set_attribute_ns(@__node__, ns, prefix, local, qualified_name.to_s, value.to_s)
|
|
2421
|
+
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: local, old_value: old, namespace: ns)
|
|
2422
|
+
nil
|
|
2423
|
+
end
|
|
2424
|
+
|
|
2425
|
+
def remove_attribute_ns(namespace, local_name)
|
|
2426
|
+
return nil if local_name.nil?
|
|
2427
|
+
|
|
2428
|
+
ns = namespace.to_s
|
|
2429
|
+
ns = nil if ns.empty?
|
|
2430
|
+
local = local_name.to_s
|
|
2431
|
+
old = Backend.get_attribute_ns(@__node__, ns, local)
|
|
2432
|
+
@attributes&.__internal_evict__(ns, local)
|
|
2433
|
+
Backend.remove_attribute_ns(@__node__, ns, local)
|
|
2434
|
+
if old
|
|
2435
|
+
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: local, old_value: old, namespace: ns)
|
|
2436
|
+
end
|
|
2437
|
+
nil
|
|
2438
|
+
end
|
|
2439
|
+
|
|
2440
|
+
def get_attribute_node_ns(namespace, local_name)
|
|
2441
|
+
attributes.get_named_item_ns(namespace, local_name)
|
|
2442
|
+
end
|
|
2443
|
+
|
|
1808
2444
|
def closest(selector)
|
|
1809
|
-
return nil if selector.nil?
|
|
2445
|
+
return nil if selector.nil?
|
|
2446
|
+
Internal.validate_selector!(selector)
|
|
2447
|
+
|
|
2448
|
+
# Elements matching the selector (scoped to this element, so `:scope`
|
|
2449
|
+
# resolves here), then return the nearest inclusive ancestor among them.
|
|
2450
|
+
handler = Internal.scoped_pseudo_handlers(@__node__)
|
|
2451
|
+
safe = Internal.backend_safe_selector(selector.to_s)
|
|
2452
|
+
matched = with_selector_errors(selector) do
|
|
2453
|
+
@document.nokogiri_doc.css(safe, handler).map(&:pointer_id)
|
|
2454
|
+
end
|
|
1810
2455
|
|
|
1811
2456
|
node = @__node__
|
|
1812
2457
|
while node&.element?
|
|
1813
|
-
return @document.wrap_node(node) if
|
|
2458
|
+
return @document.wrap_node(node) if matched.include?(node.pointer_id)
|
|
1814
2459
|
|
|
1815
2460
|
node = node.parent
|
|
1816
2461
|
end
|
|
@@ -1818,6 +2463,23 @@ module Dommy
|
|
|
1818
2463
|
nil
|
|
1819
2464
|
end
|
|
1820
2465
|
|
|
2466
|
+
# Map Nokogiri's selector errors to spec behavior:
|
|
2467
|
+
# - a CSS *parse* error ("unexpected … after …") means the selector is
|
|
2468
|
+
# syntactically invalid → SyntaxError (querySelector/closest must throw);
|
|
2469
|
+
# - an "Unregistered function" means a valid pseudo Nokogiri compiled but
|
|
2470
|
+
# can't evaluate (`:hover`, `:invalid`, …) → degrade to matching nothing.
|
|
2471
|
+
def with_selector_errors(selector)
|
|
2472
|
+
yield
|
|
2473
|
+
rescue ::StandardError => e
|
|
2474
|
+
return [] if e.message.include?("Unregistered function")
|
|
2475
|
+
|
|
2476
|
+
if (defined?(::Nokogiri::CSS::SyntaxError) && e.is_a?(::Nokogiri::CSS::SyntaxError)) || e.message.include?("unexpected")
|
|
2477
|
+
raise DOMException::SyntaxError, "'#{selector}' is not a valid selector."
|
|
2478
|
+
end
|
|
2479
|
+
|
|
2480
|
+
raise
|
|
2481
|
+
end
|
|
2482
|
+
|
|
1821
2483
|
# Web Animations: start an animation on this element.
|
|
1822
2484
|
# Returns the new Animation. Dommy doesn't interpolate; the
|
|
1823
2485
|
# animation simply transitions through the `playState` lifecycle,
|
|
@@ -1839,23 +2501,54 @@ module Dommy
|
|
|
1839
2501
|
alias getAnimations get_animations
|
|
1840
2502
|
|
|
1841
2503
|
def query_selector(selector)
|
|
1842
|
-
return nil if selector.nil?
|
|
2504
|
+
return nil if selector.nil?
|
|
2505
|
+
# The empty string is not a valid selector (an explicit DOMString "" is a
|
|
2506
|
+
# SyntaxError; `null` coerces to "null" and is handled above as nil).
|
|
2507
|
+
Internal.validate_selector!(selector)
|
|
1843
2508
|
|
|
1844
|
-
@document.wrap_node(
|
|
2509
|
+
@document.wrap_node(scoped_query(selector.to_s).first)
|
|
1845
2510
|
end
|
|
1846
2511
|
|
|
1847
2512
|
def query_selector_all(selector)
|
|
1848
|
-
return NodeList.new if selector.nil?
|
|
2513
|
+
return NodeList.new if selector.nil?
|
|
2514
|
+
Internal.validate_selector!(selector)
|
|
2515
|
+
|
|
2516
|
+
NodeList.new(scoped_query(selector.to_s).map { |node| @document.wrap_node(node) }.compact)
|
|
2517
|
+
end
|
|
2518
|
+
|
|
2519
|
+
# Run a CSS query rooted at this element. A `:scope` selector must resolve to
|
|
2520
|
+
# this element, but Nokogiri scopes `el.css` to descendants (`.//`), which
|
|
2521
|
+
# excludes the element itself — so for `:scope` queries we evaluate against
|
|
2522
|
+
# the whole document (where this element IS reachable) and restrict the
|
|
2523
|
+
# results to this element's own subtree.
|
|
2524
|
+
def scoped_query(sel)
|
|
2525
|
+
sel = Internal.backend_safe_selector(sel)
|
|
2526
|
+
handler = Internal.scoped_pseudo_handlers(@__node__)
|
|
2527
|
+
with_selector_errors(sel) do
|
|
2528
|
+
if sel.include?(":scope")
|
|
2529
|
+
self_id = @__node__.pointer_id
|
|
2530
|
+
@document.nokogiri_doc.css(sel, handler).select do |n|
|
|
2531
|
+
n.ancestors.any? { |a| a.pointer_id == self_id }
|
|
2532
|
+
end
|
|
2533
|
+
else
|
|
2534
|
+
@__node__.css(sel, handler)
|
|
2535
|
+
end
|
|
2536
|
+
end
|
|
2537
|
+
end
|
|
2538
|
+
|
|
2539
|
+
# XPath queries scoped to this element, returning wrapped nodes.
|
|
2540
|
+
def at_xpath(expression)
|
|
2541
|
+
node = @__node__.at_xpath(expression)
|
|
2542
|
+
node && @document.wrap_node(node)
|
|
2543
|
+
end
|
|
1849
2544
|
|
|
1850
|
-
|
|
2545
|
+
def xpath(expression)
|
|
2546
|
+
@__node__.xpath(expression).map { |node| @document.wrap_node(node) }
|
|
1851
2547
|
end
|
|
1852
2548
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
append_dom_nodes(nodes)
|
|
1857
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
1858
|
-
child
|
|
2549
|
+
# The XPath string locating this element in its document.
|
|
2550
|
+
def path
|
|
2551
|
+
@__node__.path
|
|
1859
2552
|
end
|
|
1860
2553
|
|
|
1861
2554
|
def insert_before(child, reference)
|
|
@@ -1875,7 +2568,7 @@ module Dommy
|
|
|
1875
2568
|
end
|
|
1876
2569
|
end
|
|
1877
2570
|
|
|
1878
|
-
|
|
2571
|
+
notify_child_list(added: nodes)
|
|
1879
2572
|
child
|
|
1880
2573
|
end
|
|
1881
2574
|
|
|
@@ -1885,8 +2578,7 @@ module Dommy
|
|
|
1885
2578
|
raise DOMException::NotFoundError, "node is not a child of this element"
|
|
1886
2579
|
end
|
|
1887
2580
|
|
|
1888
|
-
node
|
|
1889
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: [], removed_nodes: [node])
|
|
2581
|
+
@document.remove_node_with_notify(node)
|
|
1890
2582
|
child
|
|
1891
2583
|
end
|
|
1892
2584
|
|
|
@@ -1902,50 +2594,102 @@ module Dommy
|
|
|
1902
2594
|
new_nodes = detach_dom_nodes(new_child)
|
|
1903
2595
|
new_nodes.reverse_each { |node| old_node.add_previous_sibling(node) }
|
|
1904
2596
|
old_node.unlink
|
|
1905
|
-
|
|
1906
|
-
target_node: @__node__,
|
|
1907
|
-
added_nodes: new_nodes,
|
|
1908
|
-
removed_nodes: [old_node]
|
|
1909
|
-
)
|
|
2597
|
+
notify_child_list(added: new_nodes, removed: [old_node])
|
|
1910
2598
|
old_child
|
|
1911
2599
|
end
|
|
1912
2600
|
|
|
1913
2601
|
def clone_node(deep_arg)
|
|
1914
|
-
deep
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2602
|
+
# Copy the node in place via libxml's deep dup, NOT by re-parsing to_html as
|
|
2603
|
+
# a fragment: the HTML fragment parser unwraps `<body>` / `<head>` /
|
|
2604
|
+
# `<html>`, so cloning a body produced its children, not a body element
|
|
2605
|
+
# (which broke Turbo's snapshot cache — it clones the body and restores it
|
|
2606
|
+
# via documentElement.replaceChild on back/forward). dup(1) preserves the
|
|
2607
|
+
# element's namespace and attributes (createElement would lose the
|
|
2608
|
+
# namespace); for a shallow clone we keep that node but drop its subtree.
|
|
2609
|
+
copy = @__node__.dup(1)
|
|
2610
|
+
copy.children.each(&:unlink) unless deep_arg
|
|
2611
|
+
@document.wrap_node(copy)
|
|
2612
|
+
end
|
|
2613
|
+
|
|
2614
|
+
# Test inspector for scroll calls (no real layout to scroll).
|
|
2615
|
+
def __test_scroll_log__
|
|
2616
|
+
@scroll_log ||= []
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
# ---- Internal helpers (single private section) ----
|
|
2620
|
+
private
|
|
2621
|
+
|
|
2622
|
+
def attribute_signature
|
|
2623
|
+
Backend.attribute_nodes(@__node__).map { |a| [a.name, a.value] }.sort
|
|
2624
|
+
end
|
|
2625
|
+
|
|
2626
|
+
# on* event-handler property helpers.
|
|
2627
|
+
def event_name_from_on(key)
|
|
2628
|
+
key.to_s.sub(/\Aon/, "").downcase
|
|
2629
|
+
end
|
|
2630
|
+
|
|
2631
|
+
def set_on_handler(event_name, value)
|
|
2632
|
+
@on_handlers ||= {}
|
|
2633
|
+
previous = @on_handlers[event_name]
|
|
2634
|
+
remove_event_listener(event_name, previous) if previous
|
|
2635
|
+
if value
|
|
2636
|
+
add_event_listener(event_name, value)
|
|
2637
|
+
@on_handlers[event_name] = value
|
|
1919
2638
|
else
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2639
|
+
@on_handlers.delete(event_name)
|
|
2640
|
+
end
|
|
2641
|
+
end
|
|
2642
|
+
|
|
2643
|
+
# Attribute-key / child-wrapping / event-parent helpers.
|
|
2644
|
+
def normalize_attr_key(name)
|
|
2645
|
+
s = name.to_s
|
|
2646
|
+
case_sensitive_attribute_names? ? s : s.downcase
|
|
2647
|
+
end
|
|
1924
2648
|
|
|
1925
|
-
|
|
2649
|
+
def element_children
|
|
2650
|
+
@__node__.element_children.each_with_object([]) do |node, out|
|
|
2651
|
+
wrapped = @document.wrap_node(node)
|
|
2652
|
+
out << wrapped if wrapped
|
|
1926
2653
|
end
|
|
1927
2654
|
end
|
|
1928
2655
|
|
|
1929
|
-
def
|
|
1930
|
-
|
|
1931
|
-
append_dom_nodes(nodes)
|
|
1932
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
1933
|
-
nil
|
|
2656
|
+
def wrap_parent(node)
|
|
2657
|
+
@document.wrap_node(node)
|
|
1934
2658
|
end
|
|
1935
2659
|
|
|
1936
|
-
def
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2660
|
+
def __internal_event_parent__
|
|
2661
|
+
parent_node = @__node__.parent
|
|
2662
|
+
# If our Nokogiri parent is a shadow tree's backing fragment,
|
|
2663
|
+
# the bubble path's next stop is the ShadowRoot itself — not
|
|
2664
|
+
# the bare Fragment wrapper. The ShadowRoot's __internal_event_parent__
|
|
2665
|
+
# will return nil (composed events route to host explicitly).
|
|
2666
|
+
if parent_node.is_a?(Backend.document_fragment_class)
|
|
2667
|
+
sr = @document.__internal_shadow_root_for_fragment__(parent_node)
|
|
2668
|
+
return sr if sr
|
|
1943
2669
|
end
|
|
1944
2670
|
|
|
1945
|
-
|
|
1946
|
-
|
|
2671
|
+
parent = wrap_parent(parent_node)
|
|
2672
|
+
parent || @document
|
|
2673
|
+
end
|
|
2674
|
+
|
|
2675
|
+
def template_content
|
|
2676
|
+
return nil unless @__node__.name == "template"
|
|
2677
|
+
|
|
2678
|
+
@document.template_content_fragment(self)
|
|
2679
|
+
end
|
|
2680
|
+
|
|
2681
|
+
# Attribute name handling depends on the element's namespace:
|
|
2682
|
+
# - HTML: case-insensitive (browser DOM stores everything lowercase).
|
|
2683
|
+
# - SVG / other XML: case-sensitive (`viewBox` ≠ `viewbox`).
|
|
2684
|
+
# Subclasses with a known namespace override `case_sensitive_attribute_names?`
|
|
2685
|
+
# to flip the behavior. Generic Element nodes inspect the namespace
|
|
2686
|
+
# URI directly.
|
|
2687
|
+
def case_sensitive_attribute_names?
|
|
2688
|
+
ns = namespace_uri
|
|
2689
|
+
!ns.nil? && ns != "http://www.w3.org/1999/xhtml"
|
|
1947
2690
|
end
|
|
1948
2691
|
|
|
2692
|
+
# Insertion / scroll / popover helpers.
|
|
1949
2693
|
def insert_adjacent(side, args)
|
|
1950
2694
|
parent = @__node__.parent
|
|
1951
2695
|
return nil unless parent
|
|
@@ -1963,7 +2707,7 @@ module Dommy
|
|
|
1963
2707
|
end
|
|
1964
2708
|
end
|
|
1965
2709
|
|
|
1966
|
-
|
|
2710
|
+
notify_child_list(added: nodes, target: parent)
|
|
1967
2711
|
nil
|
|
1968
2712
|
end
|
|
1969
2713
|
|
|
@@ -1981,7 +2725,7 @@ module Dommy
|
|
|
1981
2725
|
nodes.each { |node| parent.add_child(node) }
|
|
1982
2726
|
end
|
|
1983
2727
|
|
|
1984
|
-
|
|
2728
|
+
notify_child_list(added: nodes, removed: [removed], target: parent)
|
|
1985
2729
|
nil
|
|
1986
2730
|
end
|
|
1987
2731
|
|
|
@@ -1989,14 +2733,20 @@ module Dommy
|
|
|
1989
2733
|
nodes.each { |node| @__node__.add_child(node) }
|
|
1990
2734
|
end
|
|
1991
2735
|
|
|
2736
|
+
# ParentNode hook: Element enforces the no-cycle hierarchy check that
|
|
2737
|
+
# Fragment / ShadowRoot skip.
|
|
2738
|
+
def check_insertion!(child)
|
|
2739
|
+
check_hierarchy!(child)
|
|
2740
|
+
end
|
|
2741
|
+
|
|
1992
2742
|
# Raise HierarchyRequestError when the proposed insertion would
|
|
1993
2743
|
# produce a cycle (inserting an ancestor as a descendant of
|
|
1994
2744
|
# itself). Strings and Fragments are always safe.
|
|
1995
2745
|
def check_hierarchy!(child)
|
|
1996
|
-
return unless child.respond_to?(:
|
|
2746
|
+
return unless child.respond_to?(:__dommy_backend_node__)
|
|
1997
2747
|
|
|
1998
|
-
node = child.
|
|
1999
|
-
return unless node.is_a?(
|
|
2748
|
+
node = child.__dommy_backend_node__
|
|
2749
|
+
return unless node.is_a?(Backend.node_class)
|
|
2000
2750
|
|
|
2001
2751
|
if node == @__node__ || @__node__.ancestors.any? { |a| a == node }
|
|
2002
2752
|
raise(
|
|
@@ -2010,27 +2760,8 @@ module Dommy
|
|
|
2010
2760
|
detach_dom_nodes(value).first
|
|
2011
2761
|
end
|
|
2012
2762
|
|
|
2013
|
-
def detach_dom_nodes(value)
|
|
2014
|
-
case value
|
|
2015
|
-
when Element, TextNode, CommentNode
|
|
2016
|
-
node = value.__node__
|
|
2017
|
-
node.unlink if node.parent
|
|
2018
|
-
[node]
|
|
2019
|
-
when Fragment
|
|
2020
|
-
value.extract_children
|
|
2021
|
-
when String
|
|
2022
|
-
[@document.create_text_node(value).__node__]
|
|
2023
|
-
else
|
|
2024
|
-
node = unwrap_dom_node(value)
|
|
2025
|
-
return [] unless node
|
|
2026
|
-
|
|
2027
|
-
node.unlink if node.parent
|
|
2028
|
-
[node]
|
|
2029
|
-
end
|
|
2030
|
-
end
|
|
2031
|
-
|
|
2032
2763
|
def unwrap_dom_node(value)
|
|
2033
|
-
return value.
|
|
2764
|
+
return value.__dommy_backend_node__ if value.respond_to?(:__dommy_backend_node__)
|
|
2034
2765
|
|
|
2035
2766
|
nil
|
|
2036
2767
|
end
|
|
@@ -2043,6 +2774,13 @@ module Dommy
|
|
|
2043
2774
|
end
|
|
2044
2775
|
end
|
|
2045
2776
|
|
|
2777
|
+
# No real layout — record the scroll request so tests can assert it.
|
|
2778
|
+
def record_scroll(name, args)
|
|
2779
|
+
@scroll_log ||= []
|
|
2780
|
+
@scroll_log << [name, args]
|
|
2781
|
+
nil
|
|
2782
|
+
end
|
|
2783
|
+
|
|
2046
2784
|
# Popover state — modern HTML pattern. `show`/`hide`/`toggle`
|
|
2047
2785
|
# fire `beforetoggle` and `toggle` events (no real visual change).
|
|
2048
2786
|
def toggle_popover_state(open)
|
|
@@ -2064,34 +2802,5 @@ module Dommy
|
|
|
2064
2802
|
)
|
|
2065
2803
|
)
|
|
2066
2804
|
end
|
|
2067
|
-
|
|
2068
|
-
# Test inspector for scroll calls (no real layout to scroll).
|
|
2069
|
-
def __scroll_log__
|
|
2070
|
-
@__scroll_log__ ||= []
|
|
2071
|
-
end
|
|
2072
|
-
|
|
2073
|
-
public :__scroll_log__
|
|
2074
|
-
|
|
2075
|
-
# Re-expose snake_case methods that the JS bridge dispatch routes
|
|
2076
|
-
# to. Defined as private originally so internal helpers (element_children,
|
|
2077
|
-
# detach_dom_nodes, etc.) stay encapsulated; CRuby users call these
|
|
2078
|
-
# as the public Ruby API.
|
|
2079
|
-
public(
|
|
2080
|
-
:get_attribute,
|
|
2081
|
-
:set_attribute,
|
|
2082
|
-
:has_attribute?,
|
|
2083
|
-
:remove_attribute,
|
|
2084
|
-
:append_child,
|
|
2085
|
-
:insert_before,
|
|
2086
|
-
:remove_child,
|
|
2087
|
-
:replace_child,
|
|
2088
|
-
:clone_node,
|
|
2089
|
-
:query_selector,
|
|
2090
|
-
:query_selector_all,
|
|
2091
|
-
:closest,
|
|
2092
|
-
:animate,
|
|
2093
|
-
:get_animations,
|
|
2094
|
-
:getAnimations
|
|
2095
|
-
)
|
|
2096
2805
|
end
|
|
2097
2806
|
end
|