dommy 0.7.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/lib/dommy/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- 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/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- 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_wrapper_cache.rb +62 -27
- 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/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
data/lib/dommy/element.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Dommy
|
|
|
8
8
|
class Fragment
|
|
9
9
|
include EventTarget
|
|
10
10
|
include Node
|
|
11
|
+
include Internal::ParentNode
|
|
11
12
|
|
|
12
13
|
attr_reader :document
|
|
13
14
|
|
|
@@ -28,8 +29,12 @@ module Dommy
|
|
|
28
29
|
@__node__.element_children.size
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Live, cached childNodes so `fragment.childNodes === fragment.childNodes` and
|
|
33
|
+
# later mutations are reflected (WHATWG live NodeList).
|
|
31
34
|
def child_nodes
|
|
32
|
-
|
|
35
|
+
@live_child_nodes ||= LiveNodeList.new do
|
|
36
|
+
@__node__.children.map { |n| @document.wrap_node(n) }.compact
|
|
37
|
+
end
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def first_child
|
|
@@ -52,23 +57,18 @@ module Dommy
|
|
|
52
57
|
@__node__.text
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
def append_child(child)
|
|
56
|
-
nodes = detach_dom_nodes(child)
|
|
57
|
-
nodes.each { |n| @__node__.add_child(n) }
|
|
58
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
59
|
-
child
|
|
60
|
-
end
|
|
61
|
-
|
|
62
60
|
def query_selector(selector)
|
|
63
|
-
return nil if selector.nil?
|
|
61
|
+
return nil if selector.nil?
|
|
62
|
+
Internal.validate_selector!(selector)
|
|
64
63
|
|
|
65
|
-
@document.wrap_node(@__node__.at_css(selector.to_s))
|
|
64
|
+
@document.wrap_node(@__node__.at_css(Internal.backend_safe_selector(selector.to_s)))
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
def query_selector_all(selector)
|
|
69
|
-
return NodeList.new if selector.nil?
|
|
68
|
+
return NodeList.new if selector.nil?
|
|
69
|
+
Internal.validate_selector!(selector)
|
|
70
70
|
|
|
71
|
-
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)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def get_element_by_id(id)
|
|
@@ -81,6 +81,8 @@ module Dommy
|
|
|
81
81
|
case key
|
|
82
82
|
when "nodeType"
|
|
83
83
|
11
|
|
84
|
+
when "nodeName"
|
|
85
|
+
"#document-fragment"
|
|
84
86
|
when "children"
|
|
85
87
|
element_children
|
|
86
88
|
when "childNodes"
|
|
@@ -97,29 +99,69 @@ module Dommy
|
|
|
97
99
|
last_element_child
|
|
98
100
|
when "textContent"
|
|
99
101
|
@__node__.text
|
|
102
|
+
when "ownerDocument"
|
|
103
|
+
@document
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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]
|
|
109
113
|
def __js_call__(method, args)
|
|
110
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])
|
|
111
125
|
when "cloneNode"
|
|
112
126
|
deep = args.empty? ? false : !!args[0]
|
|
113
127
|
deep ? @document.wrap_node(Parser.fragment(@__node__.to_html, owner_doc: @document.nokogiri_doc)) : @document
|
|
114
128
|
.wrap_node(Parser.fragment("", owner_doc: @document.nokogiri_doc))
|
|
115
129
|
when "querySelector"
|
|
116
|
-
query_selector(args
|
|
130
|
+
query_selector(Internal.css_query_arg!(args))
|
|
117
131
|
when "querySelectorAll"
|
|
118
|
-
query_selector_all(args
|
|
132
|
+
query_selector_all(Internal.css_query_arg!(args))
|
|
119
133
|
when "getElementById"
|
|
120
134
|
get_element_by_id(args[0])
|
|
121
135
|
when "appendChild"
|
|
122
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])
|
|
123
165
|
else
|
|
124
166
|
nil
|
|
125
167
|
end
|
|
@@ -131,21 +173,50 @@ module Dommy
|
|
|
131
173
|
nodes
|
|
132
174
|
end
|
|
133
175
|
|
|
134
|
-
|
|
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__
|
|
135
181
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
[@document.create_text_node(value).__dommy_backend_node__]
|
|
140
|
-
else
|
|
141
|
-
node = value.respond_to?(:__dommy_backend_node__) ? value.__dommy_backend_node__ : nil
|
|
142
|
-
return [] unless node
|
|
182
|
+
bn.unlink
|
|
183
|
+
node
|
|
184
|
+
end
|
|
143
185
|
|
|
144
|
-
|
|
145
|
-
|
|
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) }
|
|
146
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
|
|
147
216
|
end
|
|
148
217
|
|
|
218
|
+
private
|
|
219
|
+
|
|
149
220
|
def element_children
|
|
150
221
|
@__node__.element_children.each_with_object([]) do |node, out|
|
|
151
222
|
wrapped = @document.wrap_node(node)
|
|
@@ -164,9 +235,30 @@ module Dommy
|
|
|
164
235
|
# nodeValue / textContent API and `remove` / `cloneNode` semantics.
|
|
165
236
|
class CharacterDataNode
|
|
166
237
|
include Node
|
|
238
|
+
include EventTarget
|
|
167
239
|
|
|
168
240
|
def __dommy_backend_node__ = @__node__
|
|
169
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
|
|
247
|
+
|
|
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
|
|
261
|
+
|
|
170
262
|
def initialize(document, nokogiri_node)
|
|
171
263
|
@document = document
|
|
172
264
|
@__node__ = nokogiri_node
|
|
@@ -199,19 +291,7 @@ module Dommy
|
|
|
199
291
|
end
|
|
200
292
|
|
|
201
293
|
def remove
|
|
202
|
-
|
|
203
|
-
removed = @__node__
|
|
204
|
-
@__node__.unlink
|
|
205
|
-
# Mirror Element#remove_child: notify with the Nokogiri::Node
|
|
206
|
-
# (not the Dommy wrapper) so MutationCoordinator's wrap_node
|
|
207
|
-
# cache keys consistently.
|
|
208
|
-
if parent
|
|
209
|
-
@document.notify_child_list_mutation(
|
|
210
|
-
target_node: parent,
|
|
211
|
-
added_nodes: [],
|
|
212
|
-
removed_nodes: [removed]
|
|
213
|
-
)
|
|
214
|
-
end
|
|
294
|
+
@document.remove_node_with_notify(@__node__)
|
|
215
295
|
nil
|
|
216
296
|
end
|
|
217
297
|
|
|
@@ -235,22 +315,83 @@ module Dommy
|
|
|
235
315
|
__js_set__(key.to_s, value)
|
|
236
316
|
end
|
|
237
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
|
+
|
|
238
365
|
def __js_get__(key)
|
|
239
366
|
case key
|
|
240
367
|
when "nodeType"
|
|
241
368
|
node_type
|
|
369
|
+
when "nodeName"
|
|
370
|
+
node_name
|
|
242
371
|
when "textContent"
|
|
243
372
|
@__node__.content
|
|
244
373
|
when "data"
|
|
245
374
|
@__node__.content
|
|
246
375
|
when "nodeValue"
|
|
247
376
|
@__node__.content
|
|
377
|
+
when "length"
|
|
378
|
+
length
|
|
248
379
|
when "parentNode"
|
|
249
380
|
parent_node
|
|
381
|
+
when "ownerDocument"
|
|
382
|
+
@document
|
|
250
383
|
when "nextSibling"
|
|
251
384
|
next_sibling
|
|
252
385
|
when "previousSibling"
|
|
253
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
|
|
254
395
|
end
|
|
255
396
|
end
|
|
256
397
|
|
|
@@ -263,14 +404,58 @@ module Dommy
|
|
|
263
404
|
nil
|
|
264
405
|
end
|
|
265
406
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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]
|
|
272
414
|
def __js_call__(method, args)
|
|
273
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])
|
|
274
459
|
when "remove"
|
|
275
460
|
remove
|
|
276
461
|
when "before"
|
|
@@ -279,6 +464,8 @@ module Dommy
|
|
|
279
464
|
after(*args)
|
|
280
465
|
when "replaceWith"
|
|
281
466
|
replace_with(*args)
|
|
467
|
+
when "isEqualNode"
|
|
468
|
+
is_equal_node(args[0])
|
|
282
469
|
end
|
|
283
470
|
end
|
|
284
471
|
|
|
@@ -375,11 +562,7 @@ module Dommy
|
|
|
375
562
|
end
|
|
376
563
|
|
|
377
564
|
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
378
|
-
|
|
379
|
-
def __js_method_names__
|
|
380
|
-
super + JS_METHOD_NAMES
|
|
381
|
-
end
|
|
382
|
-
|
|
565
|
+
js_methods %w[cloneNode]
|
|
383
566
|
def __js_call__(method, args)
|
|
384
567
|
case method
|
|
385
568
|
when "cloneNode"
|
|
@@ -390,17 +573,21 @@ module Dommy
|
|
|
390
573
|
end
|
|
391
574
|
end
|
|
392
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
|
+
|
|
393
584
|
class CommentNode < CharacterDataNode
|
|
394
585
|
def node_type
|
|
395
586
|
8
|
|
396
587
|
end
|
|
397
588
|
|
|
398
589
|
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
399
|
-
|
|
400
|
-
def __js_method_names__
|
|
401
|
-
super + JS_METHOD_NAMES
|
|
402
|
-
end
|
|
403
|
-
|
|
590
|
+
js_methods %w[cloneNode]
|
|
404
591
|
def __js_call__(method, args)
|
|
405
592
|
case method
|
|
406
593
|
when "cloneNode"
|
|
@@ -417,8 +604,11 @@ module Dommy
|
|
|
417
604
|
class ClassList
|
|
418
605
|
include Enumerable
|
|
419
606
|
|
|
420
|
-
|
|
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")
|
|
421
610
|
@element = element
|
|
611
|
+
@attribute = attribute
|
|
422
612
|
end
|
|
423
613
|
|
|
424
614
|
def length
|
|
@@ -428,15 +618,18 @@ module Dommy
|
|
|
428
618
|
alias size length
|
|
429
619
|
|
|
430
620
|
def item(index)
|
|
431
|
-
|
|
621
|
+
i = index.to_i
|
|
622
|
+
return nil if i.negative?
|
|
623
|
+
|
|
624
|
+
class_tokens[i]
|
|
432
625
|
end
|
|
433
626
|
|
|
434
627
|
def value
|
|
435
|
-
@element.__dommy_backend_node__[
|
|
628
|
+
@element.__dommy_backend_node__[@attribute].to_s
|
|
436
629
|
end
|
|
437
630
|
|
|
438
631
|
def value=(new_value)
|
|
439
|
-
@element.set_attribute(
|
|
632
|
+
@element.set_attribute(@attribute, new_value.to_s)
|
|
440
633
|
end
|
|
441
634
|
|
|
442
635
|
# Spec: contains() does NOT validate (no SyntaxError on empty).
|
|
@@ -455,14 +648,22 @@ module Dommy
|
|
|
455
648
|
end
|
|
456
649
|
|
|
457
650
|
def replace(old_token, new_token)
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
|
|
460
661
|
tokens = class_tokens
|
|
461
662
|
idx = tokens.index(old_s)
|
|
462
663
|
return false unless idx
|
|
463
664
|
|
|
464
665
|
tokens[idx] = new_s
|
|
465
|
-
@element.set_attribute(
|
|
666
|
+
@element.set_attribute(@attribute, tokens.uniq.join(" "))
|
|
466
667
|
true
|
|
467
668
|
end
|
|
468
669
|
|
|
@@ -489,8 +690,14 @@ module Dommy
|
|
|
489
690
|
when "value"
|
|
490
691
|
value
|
|
491
692
|
else
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
494
701
|
end
|
|
495
702
|
end
|
|
496
703
|
end
|
|
@@ -504,28 +711,30 @@ module Dommy
|
|
|
504
711
|
nil
|
|
505
712
|
end
|
|
506
713
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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]
|
|
513
719
|
def __js_call__(method, args)
|
|
514
720
|
case method
|
|
515
721
|
when "add"
|
|
516
722
|
update_tokens { |tokens| tokens | normalize_tokens(args) }
|
|
517
|
-
|
|
723
|
+
Bridge::UNDEFINED
|
|
518
724
|
when "remove"
|
|
519
725
|
update_tokens { |tokens| tokens - normalize_tokens(args) }
|
|
520
|
-
|
|
726
|
+
Bridge::UNDEFINED
|
|
521
727
|
when "contains"
|
|
522
|
-
|
|
728
|
+
# contains() does not validate; null coerces to the string "null".
|
|
729
|
+
class_tokens.include?(stringify_token(args[0]))
|
|
523
730
|
when "toggle"
|
|
524
731
|
toggle(args[0], args[1])
|
|
525
732
|
when "replace"
|
|
526
733
|
replace(args[0], args[1])
|
|
527
734
|
when "item"
|
|
528
735
|
item(args[0])
|
|
736
|
+
when "toString"
|
|
737
|
+
value
|
|
529
738
|
else
|
|
530
739
|
nil
|
|
531
740
|
end
|
|
@@ -536,52 +745,63 @@ module Dommy
|
|
|
536
745
|
def toggle(token, force)
|
|
537
746
|
name = validate_token(token)
|
|
538
747
|
present = class_tokens.include?(name)
|
|
539
|
-
|
|
540
|
-
desired = !present
|
|
541
|
-
else
|
|
542
|
-
desired = !!force
|
|
543
|
-
end
|
|
748
|
+
force_given = !(force.nil? || force.equal?(Bridge::UNDEFINED))
|
|
544
749
|
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
756
|
+
|
|
757
|
+
update_tokens { |tokens| want ? tokens | [name] : tokens - [name] }
|
|
758
|
+
return want
|
|
547
759
|
end
|
|
548
760
|
|
|
761
|
+
desired = !present
|
|
762
|
+
update_tokens { |tokens| desired ? tokens | [name] : tokens - [name] }
|
|
549
763
|
desired
|
|
550
764
|
end
|
|
551
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
|
+
|
|
552
772
|
# Spec: any empty-string argument throws SyntaxError; any token
|
|
553
773
|
# containing ASCII whitespace throws InvalidCharacterError. Applies
|
|
554
774
|
# to add / remove / replace / toggle.
|
|
555
775
|
def normalize_tokens(args)
|
|
556
|
-
args.map
|
|
557
|
-
s = t.to_s
|
|
558
|
-
raise DOMException::SyntaxError, "token is empty" if s.empty?
|
|
559
|
-
raise DOMException::InvalidCharacterError, "token contains whitespace: #{s.inspect}" if s.match?(/\s/)
|
|
560
|
-
|
|
561
|
-
s
|
|
562
|
-
end
|
|
776
|
+
args.map { |t| validate_token(t) }
|
|
563
777
|
end
|
|
564
778
|
|
|
565
779
|
def validate_token(token)
|
|
566
|
-
s = token
|
|
780
|
+
s = stringify_token(token)
|
|
567
781
|
raise DOMException::SyntaxError, "token is empty" if s.empty?
|
|
568
782
|
raise DOMException::InvalidCharacterError, "token contains whitespace: #{s.inspect}" if s.match?(/\s/)
|
|
569
783
|
|
|
570
784
|
s
|
|
571
785
|
end
|
|
572
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.
|
|
573
791
|
def class_tokens
|
|
574
|
-
raw = @element.__dommy_backend_node__[
|
|
575
|
-
raw.split(
|
|
792
|
+
raw = @element.__dommy_backend_node__[@attribute].to_s
|
|
793
|
+
raw.split(/[ \t\n\f\r]+/).reject(&:empty?).uniq
|
|
576
794
|
end
|
|
577
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.
|
|
578
800
|
def update_tokens
|
|
579
801
|
tokens = yield(class_tokens)
|
|
580
|
-
if tokens.empty?
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
@element.set_attribute("class", tokens.join(" "))
|
|
584
|
-
end
|
|
802
|
+
return if tokens.empty? && !@element.__dommy_backend_node__.key?(@attribute)
|
|
803
|
+
|
|
804
|
+
@element.set_attribute(@attribute, tokens.join(" "))
|
|
585
805
|
end
|
|
586
806
|
end
|
|
587
807
|
|
|
@@ -602,10 +822,28 @@ module Dommy
|
|
|
602
822
|
nil
|
|
603
823
|
end
|
|
604
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
|
+
|
|
605
831
|
def __js_call__(_method, _args)
|
|
606
832
|
nil
|
|
607
833
|
end
|
|
608
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
|
+
|
|
609
847
|
private
|
|
610
848
|
|
|
611
849
|
def attr_name(key)
|
|
@@ -756,12 +994,8 @@ module Dommy
|
|
|
756
994
|
nil
|
|
757
995
|
end
|
|
758
996
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
def __js_method_names__
|
|
762
|
-
JS_METHOD_NAMES
|
|
763
|
-
end
|
|
764
|
-
|
|
997
|
+
include Bridge::Methods
|
|
998
|
+
js_methods %w[setProperty removeProperty getPropertyValue item]
|
|
765
999
|
def __js_call__(method, args)
|
|
766
1000
|
case method
|
|
767
1001
|
when "setProperty"
|
|
@@ -828,6 +1062,7 @@ module Dommy
|
|
|
828
1062
|
class Element
|
|
829
1063
|
include EventTarget
|
|
830
1064
|
include Node
|
|
1065
|
+
include Internal::ParentNode
|
|
831
1066
|
|
|
832
1067
|
attr_reader :document
|
|
833
1068
|
|
|
@@ -847,6 +1082,11 @@ module Dommy
|
|
|
847
1082
|
@live_children = HTMLCollection.new do
|
|
848
1083
|
@__node__.element_children.map { |n| @document.wrap_node(n) }.compact
|
|
849
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
|
|
850
1090
|
end
|
|
851
1091
|
|
|
852
1092
|
# ----- Public Ruby API (snake_case) -----
|
|
@@ -861,19 +1101,61 @@ module Dommy
|
|
|
861
1101
|
end
|
|
862
1102
|
|
|
863
1103
|
def text_content=(value)
|
|
864
|
-
|
|
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)
|
|
865
1111
|
end
|
|
866
1112
|
|
|
867
1113
|
def inner_html
|
|
868
|
-
|
|
1114
|
+
if @__node__.name == "template"
|
|
1115
|
+
@document.template_content_inner_html(self)
|
|
1116
|
+
else
|
|
1117
|
+
@__node__.inner_html
|
|
1118
|
+
end
|
|
869
1119
|
end
|
|
870
1120
|
|
|
871
1121
|
def inner_html=(value)
|
|
872
|
-
|
|
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
|
|
873
1145
|
end
|
|
874
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.
|
|
875
1151
|
def tag_name
|
|
876
|
-
@__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
|
|
877
1159
|
end
|
|
878
1160
|
|
|
879
1161
|
def id
|
|
@@ -896,6 +1178,33 @@ module Dommy
|
|
|
896
1178
|
@class_list
|
|
897
1179
|
end
|
|
898
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
|
+
|
|
899
1208
|
def style
|
|
900
1209
|
@style
|
|
901
1210
|
end
|
|
@@ -955,7 +1264,7 @@ module Dommy
|
|
|
955
1264
|
end
|
|
956
1265
|
|
|
957
1266
|
def has_attributes?
|
|
958
|
-
@__node__.
|
|
1267
|
+
Backend.attribute_nodes(@__node__).any?
|
|
959
1268
|
end
|
|
960
1269
|
|
|
961
1270
|
def next_sibling
|
|
@@ -1013,7 +1322,7 @@ module Dommy
|
|
|
1013
1322
|
new_nodes.each { |n| parent.add_child(n) }
|
|
1014
1323
|
end
|
|
1015
1324
|
|
|
1016
|
-
|
|
1325
|
+
notify_child_list(added: new_nodes, removed: [removed], target: parent)
|
|
1017
1326
|
end
|
|
1018
1327
|
|
|
1019
1328
|
# `el.contains(other)` — true if `other` is `el` itself or any
|
|
@@ -1073,6 +1382,8 @@ module Dommy
|
|
|
1073
1382
|
end
|
|
1074
1383
|
|
|
1075
1384
|
def toggle_attribute(name, force = nil)
|
|
1385
|
+
raise DOMException::InvalidCharacterError, "empty attribute name" if name.to_s.empty?
|
|
1386
|
+
|
|
1076
1387
|
key = name.to_s.downcase
|
|
1077
1388
|
present = @__node__.key?(key)
|
|
1078
1389
|
desired = force.nil? ? !present : !!force
|
|
@@ -1086,10 +1397,11 @@ module Dommy
|
|
|
1086
1397
|
end
|
|
1087
1398
|
|
|
1088
1399
|
def matches?(selector)
|
|
1089
|
-
return false if selector.nil?
|
|
1400
|
+
return false if selector.nil?
|
|
1401
|
+
Internal.validate_selector!(selector)
|
|
1090
1402
|
|
|
1091
1403
|
# `:scope` pseudo — match against this element itself.
|
|
1092
|
-
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)")
|
|
1093
1405
|
matches_selector?(@__node__, sel)
|
|
1094
1406
|
end
|
|
1095
1407
|
|
|
@@ -1116,6 +1428,10 @@ module Dommy
|
|
|
1116
1428
|
end
|
|
1117
1429
|
end
|
|
1118
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
|
+
|
|
1119
1435
|
# NamedNodeMap of attributes. Lazily allocated and re-used so
|
|
1120
1436
|
# `el.attributes === el.attributes` and `attr.ownerElement === el`.
|
|
1121
1437
|
def attributes
|
|
@@ -1138,11 +1454,15 @@ module Dommy
|
|
|
1138
1454
|
|
|
1139
1455
|
# HTML namespace constants — most HTML elements live in xhtml ns.
|
|
1140
1456
|
def namespace_uri
|
|
1141
|
-
|
|
1142
|
-
|
|
1457
|
+
return @__ns_uri if @__ns_qname
|
|
1458
|
+
|
|
1459
|
+
ns = Backend.namespace_of(@__node__)
|
|
1460
|
+
ns ? ns.href : HTML_NAMESPACE
|
|
1143
1461
|
end
|
|
1144
1462
|
|
|
1145
1463
|
def local_name
|
|
1464
|
+
return @__ns_local if @__ns_qname
|
|
1465
|
+
|
|
1146
1466
|
@__node__.name.downcase
|
|
1147
1467
|
end
|
|
1148
1468
|
|
|
@@ -1300,22 +1620,22 @@ module Dommy
|
|
|
1300
1620
|
|
|
1301
1621
|
node = detach_for_insert(element)
|
|
1302
1622
|
@__node__.add_previous_sibling(node)
|
|
1303
|
-
|
|
1623
|
+
notify_child_list(added: [node], target: @__node__.parent)
|
|
1304
1624
|
when "afterbegin"
|
|
1305
1625
|
node = detach_for_insert(element)
|
|
1306
1626
|
first = @__node__.children.first
|
|
1307
1627
|
first ? first.add_previous_sibling(node) : @__node__.add_child(node)
|
|
1308
|
-
|
|
1628
|
+
notify_child_list(added: [node])
|
|
1309
1629
|
when "beforeend"
|
|
1310
1630
|
node = detach_for_insert(element)
|
|
1311
1631
|
@__node__.add_child(node)
|
|
1312
|
-
|
|
1632
|
+
notify_child_list(added: [node])
|
|
1313
1633
|
when "afterend"
|
|
1314
1634
|
return nil unless @__node__.parent
|
|
1315
1635
|
|
|
1316
1636
|
node = detach_for_insert(element)
|
|
1317
1637
|
@__node__.add_next_sibling(node)
|
|
1318
|
-
|
|
1638
|
+
notify_child_list(added: [node], target: @__node__.parent)
|
|
1319
1639
|
else
|
|
1320
1640
|
return nil
|
|
1321
1641
|
end
|
|
@@ -1324,36 +1644,56 @@ module Dommy
|
|
|
1324
1644
|
end
|
|
1325
1645
|
|
|
1326
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
|
+
|
|
1327
1653
|
fragment = Parser.fragment(html.to_s, owner_doc: @__node__.document)
|
|
1328
1654
|
nodes = fragment.children.to_a
|
|
1329
|
-
|
|
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
|
|
1330
1659
|
when "beforebegin"
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
nodes
|
|
1334
|
-
@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)
|
|
1335
1663
|
when "afterbegin"
|
|
1336
1664
|
first = @__node__.children.first
|
|
1337
1665
|
if first
|
|
1338
|
-
nodes.
|
|
1666
|
+
nodes.each { |n| first.add_previous_sibling(n) }
|
|
1339
1667
|
else
|
|
1340
1668
|
nodes.each { |n| @__node__.add_child(n) }
|
|
1341
1669
|
end
|
|
1342
1670
|
|
|
1343
|
-
|
|
1671
|
+
notify_child_list(added: nodes)
|
|
1344
1672
|
when "beforeend"
|
|
1345
1673
|
nodes.each { |n| @__node__.add_child(n) }
|
|
1346
|
-
|
|
1674
|
+
notify_child_list(added: nodes)
|
|
1347
1675
|
when "afterend"
|
|
1348
|
-
|
|
1349
|
-
|
|
1676
|
+
parent = insertion_parent!
|
|
1350
1677
|
nodes.reverse_each { |n| @__node__.add_next_sibling(n) }
|
|
1351
|
-
|
|
1678
|
+
notify_child_list(added: nodes, target: parent)
|
|
1352
1679
|
end
|
|
1353
1680
|
|
|
1354
1681
|
nil
|
|
1355
1682
|
end
|
|
1356
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
|
+
|
|
1357
1697
|
def insert_adjacent_text(position, text)
|
|
1358
1698
|
return nil if text.to_s.empty?
|
|
1359
1699
|
|
|
@@ -1389,48 +1729,7 @@ module Dommy
|
|
|
1389
1729
|
# CONTAINS/CONTAINED_BY bitmask for ancestor/descendant pairs, or
|
|
1390
1730
|
# PRECEDING/FOLLOWING for siblings (and DISCONNECTED for unrelated
|
|
1391
1731
|
# nodes).
|
|
1392
|
-
|
|
1393
|
-
return 0 if equal?(other)
|
|
1394
|
-
return DOCUMENT_POSITION_DISCONNECTED unless other.respond_to?(:__dommy_backend_node__)
|
|
1395
|
-
|
|
1396
|
-
self_node = @__node__
|
|
1397
|
-
other_node = other.__dommy_backend_node__
|
|
1398
|
-
|
|
1399
|
-
self_ancestors = ancestor_chain(self_node)
|
|
1400
|
-
other_ancestors = ancestor_chain(other_node)
|
|
1401
|
-
|
|
1402
|
-
common = nil
|
|
1403
|
-
self_ancestors.each do |a|
|
|
1404
|
-
if other_ancestors.include?(a)
|
|
1405
|
-
common = a
|
|
1406
|
-
break
|
|
1407
|
-
end
|
|
1408
|
-
end
|
|
1409
|
-
|
|
1410
|
-
unless common
|
|
1411
|
-
return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING
|
|
1412
|
-
end
|
|
1413
|
-
|
|
1414
|
-
if common == self_node
|
|
1415
|
-
return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING
|
|
1416
|
-
elsif common == other_node
|
|
1417
|
-
return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING
|
|
1418
|
-
end
|
|
1419
|
-
|
|
1420
|
-
# Sibling-of-some-level case: compare the two branch points
|
|
1421
|
-
# under the common ancestor.
|
|
1422
|
-
self_branch = branch_under(common, self_ancestors)
|
|
1423
|
-
other_branch = branch_under(common, other_ancestors)
|
|
1424
|
-
common.children.each do |child|
|
|
1425
|
-
if child == self_branch
|
|
1426
|
-
return DOCUMENT_POSITION_FOLLOWING
|
|
1427
|
-
elsif child == other_branch
|
|
1428
|
-
return DOCUMENT_POSITION_PRECEDING
|
|
1429
|
-
end
|
|
1430
|
-
end
|
|
1431
|
-
|
|
1432
|
-
DOCUMENT_POSITION_DISCONNECTED
|
|
1433
|
-
end
|
|
1732
|
+
# compareDocumentPosition is provided generically by the Node module.
|
|
1434
1733
|
|
|
1435
1734
|
# `Node.isSameNode(other)` — strict reference identity. The DOM
|
|
1436
1735
|
# spec deprecates this in favor of `===`, but linkedom-style
|
|
@@ -1455,51 +1754,8 @@ module Dommy
|
|
|
1455
1754
|
end
|
|
1456
1755
|
end
|
|
1457
1756
|
|
|
1458
|
-
private
|
|
1459
|
-
|
|
1460
|
-
def ancestor_chain(node)
|
|
1461
|
-
chain = [node]
|
|
1462
|
-
Internal::NodeTraversal.each_ancestor(node) { |n| chain << n }
|
|
1463
|
-
chain
|
|
1464
|
-
end
|
|
1465
|
-
|
|
1466
|
-
def branch_under(common, chain)
|
|
1467
|
-
# Walk back along `chain` to find the entry whose parent is `common`.
|
|
1468
|
-
chain.each_with_index do |node, i|
|
|
1469
|
-
return node if i.zero? && node == common
|
|
1470
|
-
return node if node.respond_to?(:parent) && node.parent == common
|
|
1471
|
-
end
|
|
1472
|
-
|
|
1473
|
-
nil
|
|
1474
|
-
end
|
|
1475
|
-
|
|
1476
|
-
def attribute_signature
|
|
1477
|
-
@__node__.attribute_nodes.map { |a| [a.name, a.value] }.sort
|
|
1478
|
-
end
|
|
1479
|
-
|
|
1480
|
-
public
|
|
1481
|
-
|
|
1482
1757
|
def remove
|
|
1483
|
-
|
|
1484
|
-
end
|
|
1485
|
-
|
|
1486
|
-
# ParentNode mixin methods — append / prepend / replaceChildren
|
|
1487
|
-
# take a mix of Node and String args (strings become text nodes).
|
|
1488
|
-
|
|
1489
|
-
def append(*args)
|
|
1490
|
-
append_nodes(args)
|
|
1491
|
-
end
|
|
1492
|
-
|
|
1493
|
-
def prepend(*args)
|
|
1494
|
-
prepend_nodes(args)
|
|
1495
|
-
end
|
|
1496
|
-
|
|
1497
|
-
def replace_children(*args)
|
|
1498
|
-
removed = @__node__.children.to_a
|
|
1499
|
-
removed.each(&:unlink)
|
|
1500
|
-
nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
|
|
1501
|
-
nodes.each { |n| @__node__.add_child(n) }
|
|
1502
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: removed)
|
|
1758
|
+
@document.remove_node_with_notify(@__node__)
|
|
1503
1759
|
nil
|
|
1504
1760
|
end
|
|
1505
1761
|
|
|
@@ -1529,7 +1785,43 @@ module Dommy
|
|
|
1529
1785
|
end
|
|
1530
1786
|
|
|
1531
1787
|
def click
|
|
1532
|
-
|
|
1788
|
+
dispatch_event(MouseEvent.new("click", "bubbles" => true, "cancelable" => true, "button" => 0))
|
|
1789
|
+
end
|
|
1790
|
+
|
|
1791
|
+
def get_attribute_names
|
|
1792
|
+
Backend.attribute_nodes(@__node__).map(&:name)
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
# No layout engine — geometry getters return zeroed rects.
|
|
1796
|
+
def get_bounding_client_rect
|
|
1797
|
+
DOMRect.new
|
|
1798
|
+
end
|
|
1799
|
+
|
|
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
|
|
1533
1825
|
end
|
|
1534
1826
|
|
|
1535
1827
|
# Ruby block-style listener (in addition to the (type, callable,
|
|
@@ -1579,8 +1871,26 @@ module Dommy
|
|
|
1579
1871
|
get_attribute("popover")
|
|
1580
1872
|
when "children"
|
|
1581
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
|
|
1582
1892
|
when "firstElementChild"
|
|
1583
|
-
|
|
1893
|
+
first_element_child
|
|
1584
1894
|
when "parentElement", "parent"
|
|
1585
1895
|
wrap_parent(@__node__.parent)
|
|
1586
1896
|
when "parentNode"
|
|
@@ -1591,16 +1901,23 @@ module Dommy
|
|
|
1591
1901
|
when "textContent"
|
|
1592
1902
|
@__node__.text
|
|
1593
1903
|
when "innerHTML"
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
@__node__.inner_html
|
|
1598
|
-
end
|
|
1599
|
-
|
|
1904
|
+
inner_html
|
|
1905
|
+
when "outerHTML"
|
|
1906
|
+
outer_html
|
|
1600
1907
|
when "tagName"
|
|
1601
|
-
|
|
1908
|
+
tag_name
|
|
1909
|
+
when "prefix"
|
|
1910
|
+
element_prefix
|
|
1602
1911
|
when "classList"
|
|
1603
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")
|
|
1604
1921
|
when "style"
|
|
1605
1922
|
@style
|
|
1606
1923
|
when "dataset"
|
|
@@ -1632,11 +1949,13 @@ module Dommy
|
|
|
1632
1949
|
when "localName"
|
|
1633
1950
|
local_name
|
|
1634
1951
|
when "nodeName"
|
|
1635
|
-
|
|
1952
|
+
tag_name
|
|
1636
1953
|
when "slot"
|
|
1637
1954
|
slot
|
|
1638
1955
|
when "role"
|
|
1639
|
-
role
|
|
1956
|
+
aria_get("role")
|
|
1957
|
+
when "accessKeyLabel"
|
|
1958
|
+
access_key_label
|
|
1640
1959
|
when "baseURI"
|
|
1641
1960
|
base_uri
|
|
1642
1961
|
when "shadowRoot"
|
|
@@ -1644,9 +1963,20 @@ module Dommy
|
|
|
1644
1963
|
when "ownerDocument"
|
|
1645
1964
|
@document
|
|
1646
1965
|
else
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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.
|
|
1650
1980
|
@on_handlers&.[](event_name_from_on(key))
|
|
1651
1981
|
end
|
|
1652
1982
|
end
|
|
@@ -1666,6 +1996,143 @@ module Dommy
|
|
|
1666
1996
|
raw.to_s
|
|
1667
1997
|
end
|
|
1668
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
|
+
|
|
1669
2136
|
# Map a JS boolean property name to its underlying HTML attribute.
|
|
1670
2137
|
# HTML attribute names are lowercase; the DOM property may be
|
|
1671
2138
|
# camelCase (`readOnly` → `readonly`).
|
|
@@ -1676,38 +2143,12 @@ module Dommy
|
|
|
1676
2143
|
def __js_set__(key, value)
|
|
1677
2144
|
case key
|
|
1678
2145
|
when "textContent"
|
|
1679
|
-
|
|
1680
|
-
# value is non-empty) appends a single text node. Capture
|
|
1681
|
-
# before/after to feed MutationObserver — mirrors the
|
|
1682
|
-
# innerHTML branch below.
|
|
1683
|
-
removed = @__node__.children.to_a
|
|
1684
|
-
@__node__.content = value.to_s
|
|
1685
|
-
added = @__node__.children.to_a
|
|
1686
|
-
if removed.any? || added.any?
|
|
1687
|
-
@document.notify_child_list_mutation(
|
|
1688
|
-
target_node: @__node__,
|
|
1689
|
-
added_nodes: added,
|
|
1690
|
-
removed_nodes: removed
|
|
1691
|
-
)
|
|
1692
|
-
end
|
|
2146
|
+
self.text_content = value
|
|
1693
2147
|
when "innerHTML"
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
# via `[:content]`). Mirror that here so child placeholders
|
|
1699
|
-
# inside the template don't pollute outer queries.
|
|
1700
|
-
@document.attach_template_content(self, value.to_s)
|
|
1701
|
-
else
|
|
1702
|
-
@__node__.inner_html = value.to_s
|
|
1703
|
-
@document.migrate_template_descendants(@__node__)
|
|
1704
|
-
end
|
|
1705
|
-
|
|
1706
|
-
@document.notify_child_list_mutation(
|
|
1707
|
-
target_node: @__node__,
|
|
1708
|
-
added_nodes: @__node__.children.to_a,
|
|
1709
|
-
removed_nodes: removed
|
|
1710
|
-
)
|
|
2148
|
+
self.inner_html = value
|
|
2149
|
+
when "outerHTML"
|
|
2150
|
+
# [CEReactions, LegacyNullToEmptyString] DOMString — null becomes "".
|
|
2151
|
+
self.outer_html = value.nil? ? "" : value.to_s
|
|
1711
2152
|
when "hidden", "disabled", "checked", "readOnly", "multiple", "required"
|
|
1712
2153
|
# Boolean reflected property — funnel through set_attribute /
|
|
1713
2154
|
# remove_attribute so MutationObserver attribute records fire.
|
|
@@ -1720,6 +2161,13 @@ module Dommy
|
|
|
1720
2161
|
|
|
1721
2162
|
when "className"
|
|
1722
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)
|
|
1723
2171
|
when "id"
|
|
1724
2172
|
set_attribute("id", value.to_s)
|
|
1725
2173
|
when "value"
|
|
@@ -1727,55 +2175,48 @@ module Dommy
|
|
|
1727
2175
|
when "slot"
|
|
1728
2176
|
set_attribute("slot", value.to_s)
|
|
1729
2177
|
when "role"
|
|
1730
|
-
|
|
2178
|
+
aria_set("role", value)
|
|
1731
2179
|
else
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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.
|
|
1735
2191
|
set_on_handler(event_name_from_on(key), value)
|
|
1736
2192
|
else
|
|
1737
|
-
|
|
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
|
|
1738
2196
|
end
|
|
1739
2197
|
end
|
|
1740
2198
|
end
|
|
1741
2199
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
def event_name_from_on(key)
|
|
1745
|
-
key.to_s.sub(/\Aon/, "").downcase
|
|
1746
|
-
end
|
|
1747
|
-
|
|
1748
|
-
def set_on_handler(event_name, value)
|
|
1749
|
-
@on_handlers ||= {}
|
|
1750
|
-
previous = @on_handlers[event_name]
|
|
1751
|
-
remove_event_listener(event_name, previous) if previous
|
|
1752
|
-
if value
|
|
1753
|
-
add_event_listener(event_name, value)
|
|
1754
|
-
@on_handlers[event_name] = value
|
|
1755
|
-
else
|
|
1756
|
-
@on_handlers.delete(event_name)
|
|
1757
|
-
end
|
|
1758
|
-
end
|
|
1759
|
-
|
|
1760
|
-
public
|
|
1761
|
-
|
|
1762
|
-
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
1763
|
-
JS_METHOD_NAMES = %w[
|
|
2200
|
+
include Bridge::Methods
|
|
2201
|
+
js_methods %w[
|
|
1764
2202
|
getAttribute setAttribute hasAttribute removeAttribute getAttributeNames closest
|
|
1765
|
-
|
|
2203
|
+
getAttributeNS setAttributeNS hasAttributeNS removeAttributeNS getAttributeNodeNS setAttributeNodeNS
|
|
2204
|
+
querySelector querySelectorAll getElementsByClassName getElementsByTagName getElementsByTagNameNS
|
|
1766
2205
|
insertAdjacentElement insertAdjacentHTML insertAdjacentText toggleAttribute matches
|
|
1767
2206
|
toString getAttributeNode setAttributeNode removeAttributeNode focus blur attachShadow
|
|
1768
2207
|
addEventListener removeEventListener dispatchEvent appendChild insertBefore removeChild
|
|
1769
2208
|
replaceChild cloneNode append prepend replaceChildren before after getInnerHTML getHTML
|
|
1770
2209
|
remove replaceWith click getBoundingClientRect getClientRects scrollIntoView scroll
|
|
1771
|
-
scrollTo scrollBy requestFullscreen showPopover hidePopover togglePopover
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
end
|
|
1776
|
-
|
|
2210
|
+
scrollTo scrollBy requestFullscreen showPopover hidePopover togglePopover isEqualNode
|
|
2211
|
+
hasChildNodes hasAttributes getRootNode normalize contains
|
|
2212
|
+
compareDocumentPosition isSameNode lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
2213
|
+
]
|
|
1777
2214
|
def __js_call__(method, args)
|
|
1778
2215
|
case method
|
|
2216
|
+
when "hasChildNodes"
|
|
2217
|
+
has_child_nodes?
|
|
2218
|
+
when "hasAttributes"
|
|
2219
|
+
has_attributes?
|
|
1779
2220
|
when "getAttribute"
|
|
1780
2221
|
get_attribute(args[0])
|
|
1781
2222
|
when "setAttribute"
|
|
@@ -1784,18 +2225,38 @@ module Dommy
|
|
|
1784
2225
|
has_attribute?(args[0])
|
|
1785
2226
|
when "removeAttribute"
|
|
1786
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])
|
|
1787
2240
|
when "getAttributeNames"
|
|
1788
|
-
|
|
2241
|
+
get_attribute_names
|
|
1789
2242
|
when "closest"
|
|
2243
|
+
raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?
|
|
2244
|
+
|
|
1790
2245
|
closest(args[0])
|
|
1791
2246
|
when "querySelector"
|
|
1792
|
-
query_selector(args
|
|
2247
|
+
query_selector(Internal.css_query_arg!(args))
|
|
1793
2248
|
when "querySelectorAll"
|
|
1794
|
-
query_selector_all(args
|
|
2249
|
+
query_selector_all(Internal.css_query_arg!(args))
|
|
1795
2250
|
when "getElementsByClassName"
|
|
1796
2251
|
get_elements_by_class_name(args[0])
|
|
2252
|
+
when "getElementsByTagNameNS"
|
|
2253
|
+
get_elements_by_tag_name_ns(args[0], args[1])
|
|
1797
2254
|
when "getElementsByTagName"
|
|
1798
2255
|
get_elements_by_tag_name(args[0])
|
|
2256
|
+
when "getRootNode"
|
|
2257
|
+
get_root_node
|
|
2258
|
+
when "normalize"
|
|
2259
|
+
normalize
|
|
1799
2260
|
when "insertAdjacentElement"
|
|
1800
2261
|
insert_adjacent_element(args[0], args[1])
|
|
1801
2262
|
when "insertAdjacentHTML"
|
|
@@ -1805,7 +2266,23 @@ module Dommy
|
|
|
1805
2266
|
when "toggleAttribute"
|
|
1806
2267
|
toggle_attribute(args[0], args[1])
|
|
1807
2268
|
when "matches"
|
|
2269
|
+
raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?
|
|
2270
|
+
|
|
1808
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])
|
|
1809
2286
|
when "toString"
|
|
1810
2287
|
to_s
|
|
1811
2288
|
when "getAttributeNode"
|
|
@@ -1823,7 +2300,7 @@ module Dommy
|
|
|
1823
2300
|
when "addEventListener"
|
|
1824
2301
|
add_event_listener(args[0], args[1], args[2])
|
|
1825
2302
|
when "removeEventListener"
|
|
1826
|
-
remove_event_listener(args[0], args[1])
|
|
2303
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
1827
2304
|
when "dispatchEvent"
|
|
1828
2305
|
dispatch_event(args[0])
|
|
1829
2306
|
when "appendChild"
|
|
@@ -1837,9 +2314,9 @@ module Dommy
|
|
|
1837
2314
|
when "cloneNode"
|
|
1838
2315
|
clone_node(args[0])
|
|
1839
2316
|
when "append"
|
|
1840
|
-
|
|
2317
|
+
append(*args)
|
|
1841
2318
|
when "prepend"
|
|
1842
|
-
|
|
2319
|
+
prepend(*args)
|
|
1843
2320
|
when "replaceChildren"
|
|
1844
2321
|
replace_children(*args)
|
|
1845
2322
|
when "before"
|
|
@@ -1849,91 +2326,30 @@ module Dommy
|
|
|
1849
2326
|
when "getInnerHTML", "getHTML"
|
|
1850
2327
|
inner_html
|
|
1851
2328
|
when "remove"
|
|
1852
|
-
|
|
1853
|
-
@__node__.unlink
|
|
1854
|
-
@document.notify_child_list_mutation(target_node: parent, added_nodes: [], removed_nodes: [@__node__]) if parent
|
|
1855
|
-
nil
|
|
2329
|
+
remove
|
|
1856
2330
|
when "replaceWith"
|
|
1857
2331
|
replace_with(args)
|
|
1858
2332
|
when "click"
|
|
1859
|
-
|
|
2333
|
+
click
|
|
1860
2334
|
when "getBoundingClientRect"
|
|
1861
|
-
|
|
2335
|
+
get_bounding_client_rect
|
|
1862
2336
|
when "getClientRects"
|
|
1863
|
-
|
|
2337
|
+
get_client_rects
|
|
1864
2338
|
when "scrollIntoView", "scroll", "scrollTo", "scrollBy"
|
|
1865
|
-
|
|
1866
|
-
@scroll_log ||= []
|
|
1867
|
-
@scroll_log << [method, args]
|
|
1868
|
-
nil
|
|
2339
|
+
record_scroll(method, args)
|
|
1869
2340
|
when "requestFullscreen"
|
|
1870
|
-
|
|
1871
|
-
PromiseValue.resolve(@document.default_view, nil)
|
|
2341
|
+
request_fullscreen
|
|
1872
2342
|
when "showPopover"
|
|
1873
|
-
|
|
1874
|
-
nil
|
|
2343
|
+
show_popover
|
|
1875
2344
|
when "hidePopover"
|
|
1876
|
-
|
|
1877
|
-
nil
|
|
2345
|
+
hide_popover
|
|
1878
2346
|
when "togglePopover"
|
|
1879
|
-
|
|
1880
|
-
toggle_popover_state(new_state)
|
|
1881
|
-
new_state
|
|
2347
|
+
toggle_popover
|
|
1882
2348
|
else
|
|
1883
2349
|
nil
|
|
1884
2350
|
end
|
|
1885
2351
|
end
|
|
1886
2352
|
|
|
1887
|
-
private
|
|
1888
|
-
|
|
1889
|
-
def normalize_attr_key(name)
|
|
1890
|
-
s = name.to_s
|
|
1891
|
-
case_sensitive_attribute_names? ? s : s.downcase
|
|
1892
|
-
end
|
|
1893
|
-
|
|
1894
|
-
def element_children
|
|
1895
|
-
@__node__.element_children.each_with_object([]) do |node, out|
|
|
1896
|
-
wrapped = @document.wrap_node(node)
|
|
1897
|
-
out << wrapped if wrapped
|
|
1898
|
-
end
|
|
1899
|
-
end
|
|
1900
|
-
|
|
1901
|
-
def wrap_parent(node)
|
|
1902
|
-
@document.wrap_node(node)
|
|
1903
|
-
end
|
|
1904
|
-
|
|
1905
|
-
def __internal_event_parent__
|
|
1906
|
-
parent_node = @__node__.parent
|
|
1907
|
-
# If our Nokogiri parent is a shadow tree's backing fragment,
|
|
1908
|
-
# the bubble path's next stop is the ShadowRoot itself — not
|
|
1909
|
-
# the bare Fragment wrapper. The ShadowRoot's __internal_event_parent__
|
|
1910
|
-
# will return nil (composed events route to host explicitly).
|
|
1911
|
-
if parent_node.is_a?(Backend.document_fragment_class)
|
|
1912
|
-
sr = @document.__internal_shadow_root_for_fragment__(parent_node)
|
|
1913
|
-
return sr if sr
|
|
1914
|
-
end
|
|
1915
|
-
|
|
1916
|
-
parent = wrap_parent(parent_node)
|
|
1917
|
-
parent || @document
|
|
1918
|
-
end
|
|
1919
|
-
|
|
1920
|
-
def template_content
|
|
1921
|
-
return nil unless @__node__.name == "template"
|
|
1922
|
-
|
|
1923
|
-
@document.template_content_fragment(self)
|
|
1924
|
-
end
|
|
1925
|
-
|
|
1926
|
-
# Attribute name handling depends on the element's namespace:
|
|
1927
|
-
# - HTML: case-insensitive (browser DOM stores everything lowercase).
|
|
1928
|
-
# - SVG / other XML: case-sensitive (`viewBox` ≠ `viewbox`).
|
|
1929
|
-
# Subclasses with a known namespace override `case_sensitive_attribute_names?`
|
|
1930
|
-
# to flip the behavior. Generic Element nodes inspect the namespace
|
|
1931
|
-
# URI directly.
|
|
1932
|
-
def case_sensitive_attribute_names?
|
|
1933
|
-
ns = namespace_uri
|
|
1934
|
-
!ns.nil? && ns != "http://www.w3.org/1999/xhtml"
|
|
1935
|
-
end
|
|
1936
|
-
|
|
1937
2353
|
def get_attribute(name)
|
|
1938
2354
|
return nil if name.nil?
|
|
1939
2355
|
|
|
@@ -1943,9 +2359,17 @@ module Dommy
|
|
|
1943
2359
|
def set_attribute(name, value)
|
|
1944
2360
|
return nil if name.nil?
|
|
1945
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
|
+
|
|
1946
2367
|
key = normalize_attr_key(name)
|
|
1947
2368
|
old = @__node__[key]
|
|
1948
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-")
|
|
1949
2373
|
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
|
|
1950
2374
|
nil
|
|
1951
2375
|
end
|
|
@@ -1963,17 +2387,75 @@ module Dommy
|
|
|
1963
2387
|
return nil unless @__node__.key?(key)
|
|
1964
2388
|
|
|
1965
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)
|
|
1966
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-")
|
|
1967
2397
|
@document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
|
|
1968
2398
|
nil
|
|
1969
2399
|
end
|
|
1970
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
|
+
|
|
1971
2444
|
def closest(selector)
|
|
1972
|
-
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
|
|
1973
2455
|
|
|
1974
2456
|
node = @__node__
|
|
1975
2457
|
while node&.element?
|
|
1976
|
-
return @document.wrap_node(node) if
|
|
2458
|
+
return @document.wrap_node(node) if matched.include?(node.pointer_id)
|
|
1977
2459
|
|
|
1978
2460
|
node = node.parent
|
|
1979
2461
|
end
|
|
@@ -1981,6 +2463,23 @@ module Dommy
|
|
|
1981
2463
|
nil
|
|
1982
2464
|
end
|
|
1983
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
|
+
|
|
1984
2483
|
# Web Animations: start an animation on this element.
|
|
1985
2484
|
# Returns the new Animation. Dommy doesn't interpolate; the
|
|
1986
2485
|
# animation simply transitions through the `playState` lifecycle,
|
|
@@ -2002,15 +2501,39 @@ module Dommy
|
|
|
2002
2501
|
alias getAnimations get_animations
|
|
2003
2502
|
|
|
2004
2503
|
def query_selector(selector)
|
|
2005
|
-
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)
|
|
2006
2508
|
|
|
2007
|
-
@document.wrap_node(
|
|
2509
|
+
@document.wrap_node(scoped_query(selector.to_s).first)
|
|
2008
2510
|
end
|
|
2009
2511
|
|
|
2010
2512
|
def query_selector_all(selector)
|
|
2011
|
-
return NodeList.new if selector.nil?
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
|
2014
2537
|
end
|
|
2015
2538
|
|
|
2016
2539
|
# XPath queries scoped to this element, returning wrapped nodes.
|
|
@@ -2028,14 +2551,6 @@ module Dommy
|
|
|
2028
2551
|
@__node__.path
|
|
2029
2552
|
end
|
|
2030
2553
|
|
|
2031
|
-
def append_child(child)
|
|
2032
|
-
check_hierarchy!(child)
|
|
2033
|
-
nodes = detach_dom_nodes(child)
|
|
2034
|
-
append_dom_nodes(nodes)
|
|
2035
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
2036
|
-
child
|
|
2037
|
-
end
|
|
2038
|
-
|
|
2039
2554
|
def insert_before(child, reference)
|
|
2040
2555
|
check_hierarchy!(child)
|
|
2041
2556
|
nodes = detach_dom_nodes(child)
|
|
@@ -2053,7 +2568,7 @@ module Dommy
|
|
|
2053
2568
|
end
|
|
2054
2569
|
end
|
|
2055
2570
|
|
|
2056
|
-
|
|
2571
|
+
notify_child_list(added: nodes)
|
|
2057
2572
|
child
|
|
2058
2573
|
end
|
|
2059
2574
|
|
|
@@ -2063,8 +2578,7 @@ module Dommy
|
|
|
2063
2578
|
raise DOMException::NotFoundError, "node is not a child of this element"
|
|
2064
2579
|
end
|
|
2065
2580
|
|
|
2066
|
-
node
|
|
2067
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: [], removed_nodes: [node])
|
|
2581
|
+
@document.remove_node_with_notify(node)
|
|
2068
2582
|
child
|
|
2069
2583
|
end
|
|
2070
2584
|
|
|
@@ -2080,50 +2594,102 @@ module Dommy
|
|
|
2080
2594
|
new_nodes = detach_dom_nodes(new_child)
|
|
2081
2595
|
new_nodes.reverse_each { |node| old_node.add_previous_sibling(node) }
|
|
2082
2596
|
old_node.unlink
|
|
2083
|
-
|
|
2084
|
-
target_node: @__node__,
|
|
2085
|
-
added_nodes: new_nodes,
|
|
2086
|
-
removed_nodes: [old_node]
|
|
2087
|
-
)
|
|
2597
|
+
notify_child_list(added: new_nodes, removed: [old_node])
|
|
2088
2598
|
old_child
|
|
2089
2599
|
end
|
|
2090
2600
|
|
|
2091
2601
|
def clone_node(deep_arg)
|
|
2092
|
-
deep
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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
|
|
2097
2638
|
else
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
end
|
|
2639
|
+
@on_handlers.delete(event_name)
|
|
2640
|
+
end
|
|
2641
|
+
end
|
|
2102
2642
|
|
|
2103
|
-
|
|
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
|
|
2648
|
+
|
|
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
|
|
2104
2653
|
end
|
|
2105
2654
|
end
|
|
2106
2655
|
|
|
2107
|
-
def
|
|
2108
|
-
|
|
2109
|
-
append_dom_nodes(nodes)
|
|
2110
|
-
@document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
|
|
2111
|
-
nil
|
|
2656
|
+
def wrap_parent(node)
|
|
2657
|
+
@document.wrap_node(node)
|
|
2112
2658
|
end
|
|
2113
2659
|
|
|
2114
|
-
def
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
|
2121
2669
|
end
|
|
2122
2670
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2671
|
+
parent = wrap_parent(parent_node)
|
|
2672
|
+
parent || @document
|
|
2125
2673
|
end
|
|
2126
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"
|
|
2690
|
+
end
|
|
2691
|
+
|
|
2692
|
+
# Insertion / scroll / popover helpers.
|
|
2127
2693
|
def insert_adjacent(side, args)
|
|
2128
2694
|
parent = @__node__.parent
|
|
2129
2695
|
return nil unless parent
|
|
@@ -2141,7 +2707,7 @@ module Dommy
|
|
|
2141
2707
|
end
|
|
2142
2708
|
end
|
|
2143
2709
|
|
|
2144
|
-
|
|
2710
|
+
notify_child_list(added: nodes, target: parent)
|
|
2145
2711
|
nil
|
|
2146
2712
|
end
|
|
2147
2713
|
|
|
@@ -2159,7 +2725,7 @@ module Dommy
|
|
|
2159
2725
|
nodes.each { |node| parent.add_child(node) }
|
|
2160
2726
|
end
|
|
2161
2727
|
|
|
2162
|
-
|
|
2728
|
+
notify_child_list(added: nodes, removed: [removed], target: parent)
|
|
2163
2729
|
nil
|
|
2164
2730
|
end
|
|
2165
2731
|
|
|
@@ -2167,6 +2733,12 @@ module Dommy
|
|
|
2167
2733
|
nodes.each { |node| @__node__.add_child(node) }
|
|
2168
2734
|
end
|
|
2169
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
|
+
|
|
2170
2742
|
# Raise HierarchyRequestError when the proposed insertion would
|
|
2171
2743
|
# produce a cycle (inserting an ancestor as a descendant of
|
|
2172
2744
|
# itself). Strings and Fragments are always safe.
|
|
@@ -2188,25 +2760,6 @@ module Dommy
|
|
|
2188
2760
|
detach_dom_nodes(value).first
|
|
2189
2761
|
end
|
|
2190
2762
|
|
|
2191
|
-
def detach_dom_nodes(value)
|
|
2192
|
-
case value
|
|
2193
|
-
when Element, TextNode, CommentNode
|
|
2194
|
-
node = value.__dommy_backend_node__
|
|
2195
|
-
node.unlink if node.parent
|
|
2196
|
-
[node]
|
|
2197
|
-
when Fragment
|
|
2198
|
-
value.extract_children
|
|
2199
|
-
when String
|
|
2200
|
-
[@document.create_text_node(value).__dommy_backend_node__]
|
|
2201
|
-
else
|
|
2202
|
-
node = unwrap_dom_node(value)
|
|
2203
|
-
return [] unless node
|
|
2204
|
-
|
|
2205
|
-
node.unlink if node.parent
|
|
2206
|
-
[node]
|
|
2207
|
-
end
|
|
2208
|
-
end
|
|
2209
|
-
|
|
2210
2763
|
def unwrap_dom_node(value)
|
|
2211
2764
|
return value.__dommy_backend_node__ if value.respond_to?(:__dommy_backend_node__)
|
|
2212
2765
|
|
|
@@ -2221,6 +2774,13 @@ module Dommy
|
|
|
2221
2774
|
end
|
|
2222
2775
|
end
|
|
2223
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
|
+
|
|
2224
2784
|
# Popover state — modern HTML pattern. `show`/`hide`/`toggle`
|
|
2225
2785
|
# fire `beforetoggle` and `toggle` events (no real visual change).
|
|
2226
2786
|
def toggle_popover_state(open)
|
|
@@ -2242,37 +2802,5 @@ module Dommy
|
|
|
2242
2802
|
)
|
|
2243
2803
|
)
|
|
2244
2804
|
end
|
|
2245
|
-
|
|
2246
|
-
# Test inspector for scroll calls (no real layout to scroll).
|
|
2247
|
-
def __test_scroll_log__
|
|
2248
|
-
@scroll_log ||= []
|
|
2249
|
-
end
|
|
2250
|
-
|
|
2251
|
-
public :__test_scroll_log__
|
|
2252
|
-
|
|
2253
|
-
# Re-expose snake_case methods that the JS bridge dispatch routes
|
|
2254
|
-
# to. Defined as private originally so internal helpers (element_children,
|
|
2255
|
-
# detach_dom_nodes, etc.) stay encapsulated; CRuby users call these
|
|
2256
|
-
# as the public Ruby API.
|
|
2257
|
-
public(
|
|
2258
|
-
:get_attribute,
|
|
2259
|
-
:set_attribute,
|
|
2260
|
-
:has_attribute?,
|
|
2261
|
-
:remove_attribute,
|
|
2262
|
-
:append_child,
|
|
2263
|
-
:insert_before,
|
|
2264
|
-
:remove_child,
|
|
2265
|
-
:replace_child,
|
|
2266
|
-
:clone_node,
|
|
2267
|
-
:query_selector,
|
|
2268
|
-
:query_selector_all,
|
|
2269
|
-
:at_xpath,
|
|
2270
|
-
:xpath,
|
|
2271
|
-
:path,
|
|
2272
|
-
:closest,
|
|
2273
|
-
:animate,
|
|
2274
|
-
:get_animations,
|
|
2275
|
-
:getAnimations
|
|
2276
|
-
)
|
|
2277
2805
|
end
|
|
2278
2806
|
end
|