dommy 0.6.0 → 0.7.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 +1 -1
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +1 -1
- data/lib/dommy/crypto.rb +9 -8
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +98 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +231 -50
- data/lib/dommy/event.rb +61 -25
- data/lib/dommy/event_source.rb +8 -8
- data/lib/dommy/fetch.rb +14 -6
- data/lib/dommy/file_reader.rb +3 -3
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +7 -4
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +110 -42
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +2 -2
- data/lib/dommy/location.rb +8 -4
- data/lib/dommy/media_query_list.rb +3 -3
- data/lib/dommy/message_channel.rb +9 -9
- data/lib/dommy/mutation_observer.rb +21 -11
- data/lib/dommy/navigator.rb +12 -12
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +3 -3
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance_observer.rb +2 -2
- data/lib/dommy/range.rb +2 -2
- data/lib/dommy/resize_observer.rb +2 -2
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +22 -22
- data/lib/dommy/text_codec.rb +4 -4
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +25 -8
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +13 -13
- data/lib/dommy/window.rb +14 -1
- data/lib/dommy/worker.rb +5 -5
- data/lib/dommy/xml_http_request.rb +19 -4
- data/lib/dommy.rb +12 -2
- metadata +12 -26
data/lib/dommy/html_elements.rb
CHANGED
|
@@ -154,6 +154,12 @@ module Dommy
|
|
|
154
154
|
# `<form>` — element collection, submit/reset, and a stubbed
|
|
155
155
|
# validation surface.
|
|
156
156
|
class HTMLFormElement < HTMLElement
|
|
157
|
+
# Own __js_call__ methods, on top of Element's.
|
|
158
|
+
JS_METHOD_NAMES = %w[submit reset requestSubmit checkValidity reportValidity].freeze
|
|
159
|
+
def __js_method_names__
|
|
160
|
+
super + JS_METHOD_NAMES
|
|
161
|
+
end
|
|
162
|
+
|
|
157
163
|
def name
|
|
158
164
|
reflected_string("name")
|
|
159
165
|
end
|
|
@@ -211,7 +217,7 @@ module Dommy
|
|
|
211
217
|
el = self
|
|
212
218
|
HTMLCollection.new do
|
|
213
219
|
el
|
|
214
|
-
.
|
|
220
|
+
.__dommy_backend_node__
|
|
215
221
|
.css("input, select, textarea, button, output, fieldset")
|
|
216
222
|
.map do |n|
|
|
217
223
|
el.document.wrap_node(n)
|
|
@@ -245,7 +251,7 @@ module Dommy
|
|
|
245
251
|
# `submitter` (if given) must be a button inside this form.
|
|
246
252
|
def request_submit(submitter = nil)
|
|
247
253
|
if submitter
|
|
248
|
-
unless submitter.respond_to?(:
|
|
254
|
+
unless submitter.respond_to?(:__dommy_backend_node__) && submitter.__dommy_backend_node__.ancestors.include?(@__node__)
|
|
249
255
|
raise DOMException::NotFoundError, "submitter is not a descendant of this form"
|
|
250
256
|
end
|
|
251
257
|
|
|
@@ -346,6 +352,15 @@ module Dommy
|
|
|
346
352
|
|
|
347
353
|
# `<input>` — covers the most-used form control surface.
|
|
348
354
|
class HTMLInputElement < HTMLElement
|
|
355
|
+
# Own __js_call__ methods, on top of Element's.
|
|
356
|
+
JS_METHOD_NAMES = %w[
|
|
357
|
+
select setSelectionRange setRangeText stepUp stepDown
|
|
358
|
+
checkValidity reportValidity setCustomValidity
|
|
359
|
+
].freeze
|
|
360
|
+
def __js_method_names__
|
|
361
|
+
super + JS_METHOD_NAMES
|
|
362
|
+
end
|
|
363
|
+
|
|
349
364
|
def type
|
|
350
365
|
raw = @__node__["type"].to_s
|
|
351
366
|
raw.empty? ? "text" : raw.downcase
|
|
@@ -422,14 +437,14 @@ module Dommy
|
|
|
422
437
|
end
|
|
423
438
|
|
|
424
439
|
# `files` — for `<input type="file">`. Browsers populate this via
|
|
425
|
-
# user interaction; in tests, code uses `
|
|
440
|
+
# user interaction; in tests, code uses `__driver_set_files__` to seed it.
|
|
426
441
|
def files
|
|
427
442
|
@__files ||= FileList.new
|
|
428
443
|
end
|
|
429
444
|
|
|
430
445
|
# Test-only seam: set the input's file list directly.
|
|
431
446
|
# Accepts an array (wrapped in a FileList) or a FileList itself.
|
|
432
|
-
def
|
|
447
|
+
def __driver_set_files__(files_input)
|
|
433
448
|
@__files = files_input.is_a?(FileList) ? files_input : FileList.new(Array(files_input))
|
|
434
449
|
end
|
|
435
450
|
|
|
@@ -1381,13 +1396,13 @@ module Dommy
|
|
|
1381
1396
|
def host_attr_value(name)
|
|
1382
1397
|
return "" unless @host
|
|
1383
1398
|
|
|
1384
|
-
@host.
|
|
1399
|
+
@host.__dommy_backend_node__[name].to_s
|
|
1385
1400
|
end
|
|
1386
1401
|
|
|
1387
1402
|
def host_attr_present?(name)
|
|
1388
1403
|
return false unless @host
|
|
1389
1404
|
|
|
1390
|
-
@host.
|
|
1405
|
+
@host.__dommy_backend_node__.key?(name.to_s)
|
|
1391
1406
|
end
|
|
1392
1407
|
|
|
1393
1408
|
def host_type
|
|
@@ -1482,7 +1497,7 @@ module Dommy
|
|
|
1482
1497
|
sel = closest("select")
|
|
1483
1498
|
return 0 unless sel
|
|
1484
1499
|
|
|
1485
|
-
sel.options.find_index { |o| o.
|
|
1500
|
+
sel.options.find_index { |o| o.__dommy_backend_node__ == @__node__ } || 0
|
|
1486
1501
|
end
|
|
1487
1502
|
|
|
1488
1503
|
def __js_get__(key)
|
|
@@ -1569,6 +1584,14 @@ module Dommy
|
|
|
1569
1584
|
|
|
1570
1585
|
# `<textarea>` — multi-line text input.
|
|
1571
1586
|
class HTMLTextAreaElement < HTMLElement
|
|
1587
|
+
# Own __js_call__ methods, on top of Element's.
|
|
1588
|
+
JS_METHOD_NAMES = %w[
|
|
1589
|
+
select setSelectionRange setRangeText checkValidity reportValidity setCustomValidity
|
|
1590
|
+
].freeze
|
|
1591
|
+
def __js_method_names__
|
|
1592
|
+
super + JS_METHOD_NAMES
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1572
1595
|
def value
|
|
1573
1596
|
@__node__["value"] || text_content
|
|
1574
1597
|
end
|
|
@@ -1865,7 +1888,7 @@ module Dommy
|
|
|
1865
1888
|
el = self
|
|
1866
1889
|
HTMLCollection.new do
|
|
1867
1890
|
el
|
|
1868
|
-
.
|
|
1891
|
+
.__dommy_backend_node__
|
|
1869
1892
|
.css("input, select, textarea, button, output, fieldset")
|
|
1870
1893
|
.map do |n|
|
|
1871
1894
|
el.document.wrap_node(n)
|
|
@@ -2031,6 +2054,12 @@ module Dommy
|
|
|
2031
2054
|
# `slot` attribute go to the unnamed default slot. If nothing is
|
|
2032
2055
|
# assigned, the slot's own children render as fallback content.
|
|
2033
2056
|
class HTMLSlotElement < HTMLElement
|
|
2057
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2058
|
+
JS_METHOD_NAMES = %w[assignedNodes assignedElements assign].freeze
|
|
2059
|
+
def __js_method_names__
|
|
2060
|
+
super + JS_METHOD_NAMES
|
|
2061
|
+
end
|
|
2062
|
+
|
|
2034
2063
|
def name
|
|
2035
2064
|
reflected_string("name")
|
|
2036
2065
|
end
|
|
@@ -2062,7 +2091,7 @@ module Dommy
|
|
|
2062
2091
|
# call and fire `slotchange` in both modes; named mode simply
|
|
2063
2092
|
# ignores the override.
|
|
2064
2093
|
def assign(*nodes)
|
|
2065
|
-
@__manual_assignment = nodes.flatten.select { |n| n.respond_to?(:
|
|
2094
|
+
@__manual_assignment = nodes.flatten.select { |n| n.respond_to?(:__dommy_backend_node__) }
|
|
2066
2095
|
dispatch_event(Event.new("slotchange", "bubbles" => true))
|
|
2067
2096
|
nil
|
|
2068
2097
|
end
|
|
@@ -2105,7 +2134,7 @@ module Dommy
|
|
|
2105
2134
|
private
|
|
2106
2135
|
|
|
2107
2136
|
def matching_light_nodes
|
|
2108
|
-
sr = @document.
|
|
2137
|
+
sr = @document.__internal_shadow_root_containing__(@__node__)
|
|
2109
2138
|
return [] unless sr
|
|
2110
2139
|
|
|
2111
2140
|
host = sr.host
|
|
@@ -2118,7 +2147,7 @@ module Dommy
|
|
|
2118
2147
|
end
|
|
2119
2148
|
|
|
2120
2149
|
host
|
|
2121
|
-
.
|
|
2150
|
+
.__dommy_backend_node__
|
|
2122
2151
|
.children
|
|
2123
2152
|
.map do |child|
|
|
2124
2153
|
wrapped = @document.wrap_node(child)
|
|
@@ -2139,6 +2168,12 @@ module Dommy
|
|
|
2139
2168
|
# `selectedIndex`, and dispatches change events. Minimal compared to
|
|
2140
2169
|
# happy-dom's full HTMLSelectElement, but covers common test cases.
|
|
2141
2170
|
class HTMLSelectElement < HTMLElement
|
|
2171
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2172
|
+
JS_METHOD_NAMES = %w[item add checkValidity reportValidity setCustomValidity].freeze
|
|
2173
|
+
def __js_method_names__
|
|
2174
|
+
super + JS_METHOD_NAMES
|
|
2175
|
+
end
|
|
2176
|
+
|
|
2142
2177
|
def name
|
|
2143
2178
|
reflected_string("name")
|
|
2144
2179
|
end
|
|
@@ -2165,7 +2200,7 @@ module Dommy
|
|
|
2165
2200
|
def options
|
|
2166
2201
|
el = self
|
|
2167
2202
|
HTMLOptionsCollection.new(self) do
|
|
2168
|
-
el.
|
|
2203
|
+
el.__dommy_backend_node__.css("option").map { |n| el.document.wrap_node(n) }.compact
|
|
2169
2204
|
end
|
|
2170
2205
|
end
|
|
2171
2206
|
|
|
@@ -2175,8 +2210,8 @@ module Dommy
|
|
|
2175
2210
|
def selected_options
|
|
2176
2211
|
el = self
|
|
2177
2212
|
HTMLCollection.new do
|
|
2178
|
-
opts = el.
|
|
2179
|
-
chosen = opts.select { |o| o.
|
|
2213
|
+
opts = el.__dommy_backend_node__.css("option").map { |n| el.document.wrap_node(n) }.compact
|
|
2214
|
+
chosen = opts.select { |o| o.__dommy_backend_node__.key?("selected") }
|
|
2180
2215
|
next chosen unless chosen.empty?
|
|
2181
2216
|
next [] if el.multiple
|
|
2182
2217
|
|
|
@@ -2196,7 +2231,7 @@ module Dommy
|
|
|
2196
2231
|
# not multiple, or -1 if multiple and none.
|
|
2197
2232
|
def selected_index
|
|
2198
2233
|
opts = options
|
|
2199
|
-
idx = opts.find_index { |o| o.
|
|
2234
|
+
idx = opts.find_index { |o| o.__dommy_backend_node__.key?("selected") }
|
|
2200
2235
|
return idx if idx
|
|
2201
2236
|
|
|
2202
2237
|
multiple ? -1 : (opts.empty? ? -1 : 0)
|
|
@@ -2207,7 +2242,7 @@ module Dommy
|
|
|
2207
2242
|
opts.each_with_index do |o, idx|
|
|
2208
2243
|
if idx == i.to_i
|
|
2209
2244
|
o.set_attribute("selected", "")
|
|
2210
|
-
elsif o.
|
|
2245
|
+
elsif o.__dommy_backend_node__.key?("selected")
|
|
2211
2246
|
o.remove_attribute("selected")
|
|
2212
2247
|
end
|
|
2213
2248
|
end
|
|
@@ -2216,15 +2251,15 @@ module Dommy
|
|
|
2216
2251
|
# `value` of the select = value of the selected option, or "".
|
|
2217
2252
|
def value
|
|
2218
2253
|
opts = options
|
|
2219
|
-
sel = opts.find { |o| o.
|
|
2220
|
-
sel ? (sel.
|
|
2254
|
+
sel = opts.find { |o| o.__dommy_backend_node__.key?("selected") } || opts.first
|
|
2255
|
+
sel ? (sel.__dommy_backend_node__["value"] || sel.text_content).to_s : ""
|
|
2221
2256
|
end
|
|
2222
2257
|
|
|
2223
2258
|
def value=(new_value)
|
|
2224
|
-
target = options.find { |o| (o.
|
|
2259
|
+
target = options.find { |o| (o.__dommy_backend_node__["value"] || o.text_content).to_s == new_value.to_s }
|
|
2225
2260
|
return unless target
|
|
2226
2261
|
|
|
2227
|
-
options.each { |o| o.remove_attribute("selected") if o.
|
|
2262
|
+
options.each { |o| o.remove_attribute("selected") if o.__dommy_backend_node__.key?("selected") }
|
|
2228
2263
|
target.set_attribute("selected", "")
|
|
2229
2264
|
end
|
|
2230
2265
|
|
|
@@ -2235,9 +2270,9 @@ module Dommy
|
|
|
2235
2270
|
|
|
2236
2271
|
# `select.add(option, before)` — appends or inserts before `before`.
|
|
2237
2272
|
def add(option, before = nil)
|
|
2238
|
-
return nil unless option.respond_to?(:
|
|
2273
|
+
return nil unless option.respond_to?(:__dommy_backend_node__)
|
|
2239
2274
|
|
|
2240
|
-
if before.respond_to?(:
|
|
2275
|
+
if before.respond_to?(:__dommy_backend_node__)
|
|
2241
2276
|
insert_before(option, before)
|
|
2242
2277
|
else
|
|
2243
2278
|
append_child(option)
|
|
@@ -2367,6 +2402,12 @@ module Dommy
|
|
|
2367
2402
|
# `close(returnValue?)`. Dommy has no modal stack, so showModal is
|
|
2368
2403
|
# functionally identical to show (no backdrop, no escape-to-close).
|
|
2369
2404
|
class HTMLDialogElement < HTMLElement
|
|
2405
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2406
|
+
JS_METHOD_NAMES = %w[show showModal close].freeze
|
|
2407
|
+
def __js_method_names__
|
|
2408
|
+
super + JS_METHOD_NAMES
|
|
2409
|
+
end
|
|
2410
|
+
|
|
2370
2411
|
def open
|
|
2371
2412
|
reflected_boolean("open")
|
|
2372
2413
|
end
|
|
@@ -2652,7 +2693,7 @@ module Dommy
|
|
|
2652
2693
|
row = closest("tr")
|
|
2653
2694
|
return -1 unless row
|
|
2654
2695
|
|
|
2655
|
-
row.cells.find_index { |c| c.
|
|
2696
|
+
row.cells.find_index { |c| c.__dommy_backend_node__ == @__node__ } || -1
|
|
2656
2697
|
end
|
|
2657
2698
|
|
|
2658
2699
|
def col_span
|
|
@@ -2738,11 +2779,17 @@ module Dommy
|
|
|
2738
2779
|
# `rowIndex` walks the enclosing table; `sectionRowIndex` walks
|
|
2739
2780
|
# the enclosing thead/tbody/tfoot.
|
|
2740
2781
|
class HTMLTableRowElement < HTMLElement
|
|
2782
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2783
|
+
JS_METHOD_NAMES = %w[insertCell deleteCell].freeze
|
|
2784
|
+
def __js_method_names__
|
|
2785
|
+
super + JS_METHOD_NAMES
|
|
2786
|
+
end
|
|
2787
|
+
|
|
2741
2788
|
def cells
|
|
2742
2789
|
el = self
|
|
2743
2790
|
HTMLCollection.new do
|
|
2744
2791
|
el
|
|
2745
|
-
.
|
|
2792
|
+
.__dommy_backend_node__
|
|
2746
2793
|
.element_children
|
|
2747
2794
|
.select { |n| %w[td th].include?(n.name) }
|
|
2748
2795
|
.map { |n| el.document.wrap_node(n) }
|
|
@@ -2754,7 +2801,7 @@ module Dommy
|
|
|
2754
2801
|
table = closest("table")
|
|
2755
2802
|
return -1 unless table
|
|
2756
2803
|
|
|
2757
|
-
table.rows.find_index { |r| r.
|
|
2804
|
+
table.rows.find_index { |r| r.__dommy_backend_node__ == @__node__ } || -1
|
|
2758
2805
|
end
|
|
2759
2806
|
|
|
2760
2807
|
def section_row_index
|
|
@@ -2812,11 +2859,17 @@ module Dommy
|
|
|
2812
2859
|
# `<thead>` / `<tbody>` / `<tfoot>` — share section-level row
|
|
2813
2860
|
# collection + insertRow / deleteRow.
|
|
2814
2861
|
class HTMLTableSectionElement < HTMLElement
|
|
2862
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2863
|
+
JS_METHOD_NAMES = %w[insertRow deleteRow].freeze
|
|
2864
|
+
def __js_method_names__
|
|
2865
|
+
super + JS_METHOD_NAMES
|
|
2866
|
+
end
|
|
2867
|
+
|
|
2815
2868
|
def rows
|
|
2816
2869
|
el = self
|
|
2817
2870
|
HTMLCollection.new do
|
|
2818
2871
|
el
|
|
2819
|
-
.
|
|
2872
|
+
.__dommy_backend_node__
|
|
2820
2873
|
.element_children
|
|
2821
2874
|
.select { |n| n.name == "tr" }
|
|
2822
2875
|
.map { |n| el.document.wrap_node(n) }
|
|
@@ -2866,16 +2919,25 @@ module Dommy
|
|
|
2866
2919
|
# tbody elements. `insertRow(-1)` appends to the last tbody (or
|
|
2867
2920
|
# creates one); `deleteRow` works against the merged `rows` list.
|
|
2868
2921
|
class HTMLTableElement < HTMLElement
|
|
2922
|
+
# Own __js_call__ methods, on top of Element's.
|
|
2923
|
+
JS_METHOD_NAMES = %w[
|
|
2924
|
+
insertRow deleteRow createCaption deleteCaption createTHead deleteTHead
|
|
2925
|
+
createTFoot deleteTFoot createTBody
|
|
2926
|
+
].freeze
|
|
2927
|
+
def __js_method_names__
|
|
2928
|
+
super + JS_METHOD_NAMES
|
|
2929
|
+
end
|
|
2930
|
+
|
|
2869
2931
|
def caption
|
|
2870
2932
|
@__node__.element_children.find { |n| n.name == "caption" }&.then { |n| @document.wrap_node(n) }
|
|
2871
2933
|
end
|
|
2872
2934
|
|
|
2873
2935
|
def caption=(new_caption)
|
|
2874
2936
|
delete_caption
|
|
2875
|
-
return unless new_caption.respond_to?(:
|
|
2937
|
+
return unless new_caption.respond_to?(:__dommy_backend_node__)
|
|
2876
2938
|
|
|
2877
2939
|
first = @__node__.children.first
|
|
2878
|
-
first ? first.add_previous_sibling(new_caption.
|
|
2940
|
+
first ? first.add_previous_sibling(new_caption.__dommy_backend_node__) : @__node__.add_child(new_caption.__dommy_backend_node__)
|
|
2879
2941
|
end
|
|
2880
2942
|
|
|
2881
2943
|
def t_head
|
|
@@ -2890,7 +2952,7 @@ module Dommy
|
|
|
2890
2952
|
el = self
|
|
2891
2953
|
HTMLCollection.new do
|
|
2892
2954
|
el
|
|
2893
|
-
.
|
|
2955
|
+
.__dommy_backend_node__
|
|
2894
2956
|
.element_children
|
|
2895
2957
|
.select { |n| n.name == "tbody" }
|
|
2896
2958
|
.map { |n| el.document.wrap_node(n) }
|
|
@@ -2902,10 +2964,10 @@ module Dommy
|
|
|
2902
2964
|
el = self
|
|
2903
2965
|
HTMLCollection.new do
|
|
2904
2966
|
ordered = []
|
|
2905
|
-
head = el.
|
|
2906
|
-
bodies = el.
|
|
2907
|
-
direct = el.
|
|
2908
|
-
foot = el.
|
|
2967
|
+
head = el.__dommy_backend_node__.element_children.find { |n| n.name == "thead" }
|
|
2968
|
+
bodies = el.__dommy_backend_node__.element_children.select { |n| n.name == "tbody" }
|
|
2969
|
+
direct = el.__dommy_backend_node__.element_children.select { |n| n.name == "tr" }
|
|
2970
|
+
foot = el.__dommy_backend_node__.element_children.find { |n| n.name == "tfoot" }
|
|
2909
2971
|
[head, *bodies, foot].compact.each do |sec|
|
|
2910
2972
|
sec.element_children.select { |n| n.name == "tr" }.each { |n| ordered << n }
|
|
2911
2973
|
end
|
|
@@ -2921,7 +2983,7 @@ module Dommy
|
|
|
2921
2983
|
|
|
2922
2984
|
cap = @document.create_element("caption")
|
|
2923
2985
|
first = @__node__.children.first
|
|
2924
|
-
first ? first.add_previous_sibling(cap.
|
|
2986
|
+
first ? first.add_previous_sibling(cap.__dommy_backend_node__) : @__node__.add_child(cap.__dommy_backend_node__)
|
|
2925
2987
|
cap
|
|
2926
2988
|
end
|
|
2927
2989
|
|
|
@@ -2938,10 +3000,10 @@ module Dommy
|
|
|
2938
3000
|
head = @document.create_element("thead")
|
|
2939
3001
|
cap = caption
|
|
2940
3002
|
if cap
|
|
2941
|
-
cap.
|
|
3003
|
+
cap.__dommy_backend_node__.add_next_sibling(head.__dommy_backend_node__)
|
|
2942
3004
|
else
|
|
2943
3005
|
first = @__node__.children.first
|
|
2944
|
-
first ? first.add_previous_sibling(head.
|
|
3006
|
+
first ? first.add_previous_sibling(head.__dommy_backend_node__) : @__node__.add_child(head.__dommy_backend_node__)
|
|
2945
3007
|
end
|
|
2946
3008
|
|
|
2947
3009
|
head
|
|
@@ -2957,7 +3019,7 @@ module Dommy
|
|
|
2957
3019
|
return existing if existing
|
|
2958
3020
|
|
|
2959
3021
|
foot = @document.create_element("tfoot")
|
|
2960
|
-
@__node__.add_child(foot.
|
|
3022
|
+
@__node__.add_child(foot.__dommy_backend_node__)
|
|
2961
3023
|
foot
|
|
2962
3024
|
end
|
|
2963
3025
|
|
|
@@ -2970,9 +3032,9 @@ module Dommy
|
|
|
2970
3032
|
tb = @document.create_element("tbody")
|
|
2971
3033
|
last_tbody = t_bodies.last
|
|
2972
3034
|
if last_tbody
|
|
2973
|
-
last_tbody.
|
|
3035
|
+
last_tbody.__dommy_backend_node__.add_next_sibling(tb.__dommy_backend_node__)
|
|
2974
3036
|
else
|
|
2975
|
-
@__node__.add_child(tb.
|
|
3037
|
+
@__node__.add_child(tb.__dommy_backend_node__)
|
|
2976
3038
|
end
|
|
2977
3039
|
|
|
2978
3040
|
tb
|
|
@@ -2995,10 +3057,10 @@ module Dommy
|
|
|
2995
3057
|
target_section.append_child(tr)
|
|
2996
3058
|
else
|
|
2997
3059
|
anchor = list[idx]
|
|
2998
|
-
section = anchor.
|
|
3060
|
+
section = anchor.__dommy_backend_node__.parent
|
|
2999
3061
|
if section
|
|
3000
|
-
anchor.
|
|
3001
|
-
@document.notify_child_list_mutation(target_node: section, added_nodes: [tr.
|
|
3062
|
+
anchor.__dommy_backend_node__.add_previous_sibling(tr.__dommy_backend_node__)
|
|
3063
|
+
@document.notify_child_list_mutation(target_node: section, added_nodes: [tr.__dommy_backend_node__], removed_nodes: [])
|
|
3002
3064
|
end
|
|
3003
3065
|
end
|
|
3004
3066
|
|
|
@@ -3066,6 +3128,12 @@ module Dommy
|
|
|
3066
3128
|
# absent in Dommy — getters return inert values, `play()` returns
|
|
3067
3129
|
# a resolved Promise, and `pause()` flips `paused` back to true.
|
|
3068
3130
|
class HTMLMediaElement < HTMLElement
|
|
3131
|
+
# Own __js_call__ methods, on top of Element's.
|
|
3132
|
+
JS_METHOD_NAMES = %w[play pause load canPlayType].freeze
|
|
3133
|
+
def __js_method_names__
|
|
3134
|
+
super + JS_METHOD_NAMES
|
|
3135
|
+
end
|
|
3136
|
+
|
|
3069
3137
|
NETWORK_EMPTY = 0
|
|
3070
3138
|
NETWORK_IDLE = 1
|
|
3071
3139
|
NETWORK_LOADING = 2
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# Custom Nokogiri pseudo-class handlers so CSS selectors like
|
|
6
|
+
# `:disabled` / `:enabled` / `:checked` work in query_selector(_all).
|
|
7
|
+
# Nokogiri calls the method named after the pseudo-class with the current
|
|
8
|
+
# node list and expects the filtered list back. Receives raw Nokogiri
|
|
9
|
+
# nodes (not Dommy wrappers).
|
|
10
|
+
class CSSPseudoHandlers < BasicObject
|
|
11
|
+
include ::Kernel
|
|
12
|
+
|
|
13
|
+
def disabled(list)
|
|
14
|
+
list.find_all { |node| node.has_attribute?("disabled") }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enabled(list)
|
|
18
|
+
list.find_all { |node| !node.has_attribute?("disabled") }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def checked(list)
|
|
22
|
+
list.find_all { |node| node.has_attribute?("checked") }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
CSS_PSEUDO_HANDLERS = CSSPseudoHandlers.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -57,7 +57,7 @@ module Dommy
|
|
|
57
57
|
#
|
|
58
58
|
# @param html [String]
|
|
59
59
|
def normalize_html(html)
|
|
60
|
-
|
|
60
|
+
Backend.fragment(html.to_s, owner_doc: nil).to_html.gsub(/\s+/, " ").strip
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Get the text_content of a scope, handling Document (which has
|
|
@@ -91,9 +91,9 @@ module Dommy
|
|
|
91
91
|
# ancestors (head/script/style/template), inline `display:none` /
|
|
92
92
|
# `visibility:hidden` on element or any ancestor.
|
|
93
93
|
def visible?(element)
|
|
94
|
-
return true unless element.respond_to?(:
|
|
94
|
+
return true unless element.respond_to?(:__dommy_backend_node__)
|
|
95
95
|
|
|
96
|
-
node = element.
|
|
96
|
+
node = element.__dommy_backend_node__
|
|
97
97
|
return false if node_invisible_self?(node)
|
|
98
98
|
|
|
99
99
|
NodeTraversal.each_ancestor(node) do |ancestor|
|
|
@@ -10,7 +10,7 @@ module Dommy
|
|
|
10
10
|
# Stops at Nokogiri::XML::Document (the root).
|
|
11
11
|
def self.each_ancestor(node)
|
|
12
12
|
current = node.respond_to?(:parent) ? node.parent : nil
|
|
13
|
-
while current && !current.is_a?(
|
|
13
|
+
while current && !current.is_a?(Backend.document_class)
|
|
14
14
|
yield current
|
|
15
15
|
current = current.respond_to?(:parent) ? current.parent : nil
|
|
16
16
|
end
|
|
@@ -33,15 +33,15 @@ module Dommy
|
|
|
33
33
|
raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
|
|
34
34
|
raise DOMException::InvalidCharacterError, "invalid element name: #{str.inspect}" unless str.match?(NAME_RE)
|
|
35
35
|
|
|
36
|
-
wrap_node(
|
|
36
|
+
wrap_node(Backend.create_element(str.downcase, @document.nokogiri_doc))
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def create_text_node(text)
|
|
40
|
-
wrap_node(
|
|
40
|
+
wrap_node(Backend.create_text(text.to_s, @document.nokogiri_doc))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def create_comment(text)
|
|
44
|
-
wrap_node(
|
|
44
|
+
wrap_node(Backend.create_comment(text.to_s, @document.nokogiri_doc))
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def create_document_fragment
|
|
@@ -69,8 +69,11 @@ module Dommy
|
|
|
69
69
|
raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
|
|
70
70
|
raise DOMException::InvalidCharacterError, "invalid qualified name: #{str.inspect}" unless str.match?(NAME_RE)
|
|
71
71
|
|
|
72
|
-
el =
|
|
73
|
-
|
|
72
|
+
el = Backend.create_element(str, @document.nokogiri_doc)
|
|
73
|
+
if namespace_uri && !namespace_uri.to_s.empty?
|
|
74
|
+
Backend.add_namespace_definition(el, nil, namespace_uri.to_s)
|
|
75
|
+
end
|
|
76
|
+
|
|
74
77
|
wrap(el)
|
|
75
78
|
end
|
|
76
79
|
|
|
@@ -79,13 +82,13 @@ module Dommy
|
|
|
79
82
|
def query_selector(selector)
|
|
80
83
|
return nil if selector.nil? || selector.to_s.empty?
|
|
81
84
|
|
|
82
|
-
wrap(@document.nokogiri_doc.at_css(selector.to_s))
|
|
85
|
+
wrap(@document.nokogiri_doc.at_css(selector.to_s, CSS_PSEUDO_HANDLERS))
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def query_selector_all(selector)
|
|
86
89
|
return NodeList.new if selector.nil? || selector.to_s.empty?
|
|
87
90
|
|
|
88
|
-
NodeList.new(@document.nokogiri_doc.css(selector.to_s).map { |node| wrap(node) }.compact)
|
|
91
|
+
NodeList.new(@document.nokogiri_doc.css(selector.to_s, CSS_PSEUDO_HANDLERS).map { |node| wrap(node) }.compact)
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def get_element_by_id(id)
|
|
@@ -131,6 +134,14 @@ module Dommy
|
|
|
131
134
|
@wrappers.delete(nokogiri_node.object_id)
|
|
132
135
|
end
|
|
133
136
|
|
|
137
|
+
# Register an externally-built wrapper. Used by
|
|
138
|
+
# Document#adopt_node when migrating a wrapper from another
|
|
139
|
+
# document so the existing Ruby object survives the move
|
|
140
|
+
# rather than being replaced by a freshly-built one.
|
|
141
|
+
def register(nokogiri_node, wrapper)
|
|
142
|
+
@wrappers[nokogiri_node.object_id] = wrapper
|
|
143
|
+
end
|
|
144
|
+
|
|
134
145
|
private
|
|
135
146
|
|
|
136
147
|
def wrap_node(node)
|
|
@@ -139,20 +150,20 @@ module Dommy
|
|
|
139
150
|
|
|
140
151
|
def build_wrapper_for(node)
|
|
141
152
|
case node
|
|
142
|
-
when
|
|
153
|
+
when Backend.element_class
|
|
143
154
|
build_element_wrapper(node)
|
|
144
|
-
when
|
|
155
|
+
when Backend.text_class
|
|
145
156
|
TextNode.new(@document, node)
|
|
146
|
-
when
|
|
157
|
+
when Backend.comment_class
|
|
147
158
|
CommentNode.new(@document, node)
|
|
148
|
-
when
|
|
159
|
+
when Backend.document_fragment_class
|
|
149
160
|
Fragment.new(@document, node)
|
|
150
161
|
end
|
|
151
162
|
end
|
|
152
163
|
|
|
153
164
|
def build_element_wrapper(node)
|
|
154
165
|
custom_klass = custom_element_class_for(node.name)
|
|
155
|
-
klass = custom_klass || Dommy.element_class_for(node.name, node
|
|
166
|
+
klass = custom_klass || Dommy.element_class_for(node.name, Backend.namespace_of(node)&.href)
|
|
156
167
|
instance = klass.new(@document, node)
|
|
157
168
|
|
|
158
169
|
@wrappers[node.object_id] = instance
|
|
@@ -19,16 +19,16 @@ module Dommy
|
|
|
19
19
|
# Parse HTML into a fragment and attach it as the template's content.
|
|
20
20
|
# Drops any pre-existing direct children of the template element.
|
|
21
21
|
def attach(template_element, html)
|
|
22
|
-
template_element.
|
|
22
|
+
template_element.__dommy_backend_node__.children.each(&:unlink)
|
|
23
23
|
fragment = @document.nokogiri_doc.fragment(html.to_s)
|
|
24
|
-
@fragments[template_element.
|
|
24
|
+
@fragments[template_element.__dommy_backend_node__.object_id] = fragment
|
|
25
25
|
fragment
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Get the wrapped Fragment for a template element, seeding from
|
|
29
29
|
# the template's current children if not previously migrated.
|
|
30
30
|
def fragment_for(template_element)
|
|
31
|
-
fragment = @fragments[template_element.
|
|
31
|
+
fragment = @fragments[template_element.__dommy_backend_node__.object_id]
|
|
32
32
|
fragment ||= seed(template_element)
|
|
33
33
|
@document.wrap_node(fragment)
|
|
34
34
|
end
|
|
@@ -40,7 +40,7 @@ module Dommy
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def inner_html_of(template_element)
|
|
43
|
-
fragment = @fragments[template_element.
|
|
43
|
+
fragment = @fragments[template_element.__dommy_backend_node__.object_id]
|
|
44
44
|
return "" unless fragment
|
|
45
45
|
|
|
46
46
|
fragment.children.map(&:to_html).join
|
|
@@ -79,8 +79,8 @@ module Dommy
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def seed(template_element)
|
|
82
|
-
migrate_one(template_element.
|
|
83
|
-
@fragments[template_element.
|
|
82
|
+
migrate_one(template_element.__dommy_backend_node__)
|
|
83
|
+
@fragments[template_element.__dommy_backend_node__.object_id]
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def migrate_one(template_node)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Dommy
|
|
4
4
|
# `IntersectionObserver` — stub for the viewport-intersection API.
|
|
5
5
|
# Dommy has no layout engine, so callbacks never fire automatically.
|
|
6
|
-
# Tests can drive callbacks explicitly via `
|
|
6
|
+
# Tests can drive callbacks explicitly via `__test_trigger__(entries)`.
|
|
7
7
|
#
|
|
8
8
|
# Spec: https://w3c.github.io/IntersectionObserver/
|
|
9
9
|
class IntersectionObserver
|
|
@@ -51,7 +51,7 @@ module Dommy
|
|
|
51
51
|
|
|
52
52
|
# Test seam: invoke the callback with a synthetic entries list.
|
|
53
53
|
# Each entry is whatever shape the test wants (usually a Hash).
|
|
54
|
-
def
|
|
54
|
+
def __test_trigger__(entries)
|
|
55
55
|
invoke_callback(entries)
|
|
56
56
|
end
|
|
57
57
|
|
data/lib/dommy/location.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "uri"
|
|
|
5
5
|
module Dommy
|
|
6
6
|
# `window.location` polyfill. The Window owns one Location and one
|
|
7
7
|
# History instance, and they share the same underlying state. Hash
|
|
8
|
-
# / pushState / replaceState all flow through `
|
|
8
|
+
# / pushState / replaceState all flow through `__internal_set_url__`.
|
|
9
9
|
class Location
|
|
10
10
|
def initialize(window, origin: "http://localhost", pathname: "/", search: "", hash: "")
|
|
11
11
|
@window = window
|
|
@@ -41,7 +41,7 @@ module Dommy
|
|
|
41
41
|
def __js_set__(key, value)
|
|
42
42
|
case key
|
|
43
43
|
when "href"
|
|
44
|
-
|
|
44
|
+
__internal_set_url__(value.to_s)
|
|
45
45
|
when "hash"
|
|
46
46
|
new_hash = value.to_s
|
|
47
47
|
new_hash = "##{new_hash}" unless new_hash.empty? || new_hash.start_with?("#")
|
|
@@ -68,7 +68,7 @@ module Dommy
|
|
|
68
68
|
def __js_call__(method, args)
|
|
69
69
|
case method
|
|
70
70
|
when "assign", "replace"
|
|
71
|
-
|
|
71
|
+
__internal_set_url__(args[0].to_s)
|
|
72
72
|
when "reload"
|
|
73
73
|
nil
|
|
74
74
|
when "toString"
|
|
@@ -83,12 +83,16 @@ module Dommy
|
|
|
83
83
|
# Internal — accepts an absolute or relative URL string and
|
|
84
84
|
# updates pathname / search / hash. Called by History pushState /
|
|
85
85
|
# replaceState and by `location.href = ...`.
|
|
86
|
-
def
|
|
86
|
+
def __internal_set_url__(raw)
|
|
87
87
|
previous_hash = @hash
|
|
88
88
|
if raw.start_with?("#")
|
|
89
89
|
@hash = raw
|
|
90
90
|
else
|
|
91
91
|
uri = URI.join(@origin + @pathname + @search + @hash, raw) rescue URI(raw)
|
|
92
|
+
# An absolute URL (carrying scheme + host) navigates to a new
|
|
93
|
+
# origin; a relative URL inherits the current origin from the
|
|
94
|
+
# join base, so rebuilding with the same parts is a no-op.
|
|
95
|
+
rebuild_origin(scheme: uri.scheme, host: uri.host, port: uri.port) if uri.scheme && uri.host
|
|
92
96
|
@pathname = uri.path.to_s == "" ? "/" : uri.path
|
|
93
97
|
@search = uri.query ? "?#{uri.query}" : ""
|
|
94
98
|
@hash = uri.fragment ? "##{uri.fragment}" : ""
|