dommy 0.14.0 → 0.16.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 +15 -1
- data/lib/dommy/attr.rb +56 -32
- data/lib/dommy/backend.rb +251 -208
- data/lib/dommy/blob.rb +24 -0
- data/lib/dommy/bridge/methods.rb +12 -2
- data/lib/dommy/bridge/values.rb +29 -2
- data/lib/dommy/bridge/wire_tags.rb +8 -4
- data/lib/dommy/browser.rb +328 -53
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +21 -28
- data/lib/dommy/class_list.rb +55 -23
- data/lib/dommy/cookie_jar.rb +408 -0
- data/lib/dommy/cookie_store.rb +104 -49
- data/lib/dommy/css.rb +37 -8
- data/lib/dommy/custom_elements.rb +360 -116
- data/lib/dommy/data_transfer.rb +22 -2
- data/lib/dommy/document.rb +1258 -346
- data/lib/dommy/dom_exception.rb +6 -0
- data/lib/dommy/dom_parser.rb +46 -14
- data/lib/dommy/dom_string_list.rb +36 -0
- data/lib/dommy/element.rb +408 -569
- data/lib/dommy/element_internals.rb +215 -0
- data/lib/dommy/encodings.rb +10 -0
- data/lib/dommy/event.rb +382 -148
- data/lib/dommy/event_source.rb +8 -34
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/file_reader.rb +5 -29
- data/lib/dommy/form_data.rb +53 -16
- data/lib/dommy/form_entry_list.rb +107 -138
- data/lib/dommy/fragment.rb +14 -16
- data/lib/dommy/history.rb +247 -90
- data/lib/dommy/html_canvas_element.rb +15 -35
- data/lib/dommy/html_collection.rb +76 -50
- data/lib/dommy/html_elements/base.rb +377 -71
- data/lib/dommy/html_elements/embedded.rb +350 -106
- data/lib/dommy/html_elements/forms/form.rb +424 -383
- data/lib/dommy/html_elements/forms/input.rb +351 -189
- data/lib/dommy/html_elements/forms/select.rb +97 -81
- data/lib/dommy/html_elements/interactive.rb +285 -72
- data/lib/dommy/html_elements/media.rb +73 -32
- data/lib/dommy/html_elements/metadata.rb +226 -78
- data/lib/dommy/html_elements/miscellaneous.rb +58 -14
- data/lib/dommy/html_elements/tables.rb +20 -42
- data/lib/dommy/html_elements.rb +28 -3
- data/lib/dommy/hyperlink_utils.rb +19 -8
- data/lib/dommy/interaction/drag_and_drop.rb +187 -0
- data/lib/dommy/interaction/event_synthesis.rb +182 -24
- data/lib/dommy/interaction/field_interactor.rb +18 -6
- data/lib/dommy/interaction/form_submission.rb +12 -6
- data/lib/dommy/interaction/key_sender.rb +102 -17
- data/lib/dommy/interaction.rb +1 -0
- data/lib/dommy/internal/accessibility_tree.rb +19 -7
- data/lib/dommy/internal/accessibility_visibility.rb +2 -2
- data/lib/dommy/internal/accessible_description.rb +3 -3
- data/lib/dommy/internal/accessible_name.rb +33 -45
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +27 -18
- data/lib/dommy/internal/aria_state.rb +7 -7
- data/lib/dommy/internal/autofill.rb +2 -2
- data/lib/dommy/internal/backend_prefilter.rb +21 -18
- data/lib/dommy/internal/ce_reactions.rb +285 -0
- data/lib/dommy/internal/ce_reactions_table.rb +296 -0
- data/lib/dommy/internal/child_list.rb +39 -0
- data/lib/dommy/internal/child_navigable.rb +155 -0
- data/lib/dommy/internal/child_node.rb +44 -41
- data/lib/dommy/internal/close_watcher_manager.rb +238 -0
- data/lib/dommy/internal/complex_match.rb +129 -0
- data/lib/dommy/internal/constraint_validation.rb +130 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
- data/lib/dommy/internal/css/cascade.rb +7 -7
- data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
- data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
- data/lib/dommy/internal/css/custom_properties.rb +63 -80
- data/lib/dommy/internal/css/parser.rb +53 -63
- data/lib/dommy/internal/css/renderability.rb +6 -0
- data/lib/dommy/internal/css/rule_index.rb +170 -108
- data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
- data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
- data/lib/dommy/internal/css/style_cache.rb +9 -6
- data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
- data/lib/dommy/internal/directionality.rb +2 -2
- data/lib/dommy/internal/document_generations.rb +91 -18
- data/lib/dommy/internal/document_interaction_state.rb +293 -9
- data/lib/dommy/internal/dom_matching.rb +7 -7
- data/lib/dommy/internal/element_aria.rb +65 -23
- data/lib/dommy/internal/element_css_inline_style.rb +26 -0
- data/lib/dommy/internal/element_editing.rb +41 -0
- data/lib/dommy/internal/element_nonce.rb +28 -0
- data/lib/dommy/internal/element_popover.rb +310 -0
- data/lib/dommy/internal/element_shadow.rb +107 -33
- data/lib/dommy/internal/element_state.rb +131 -26
- data/lib/dommy/internal/element_top_layer.rb +21 -51
- data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
- data/lib/dommy/internal/event_handler_tables.rb +241 -0
- data/lib/dommy/internal/event_handlers.rb +149 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/focusability.rb +267 -0
- data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
- data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
- data/lib/dommy/internal/html_serialization.rb +292 -0
- data/lib/dommy/internal/idl_reflection.rb +202 -0
- data/lib/dommy/internal/idl_reflection_table.rb +589 -0
- data/lib/dommy/internal/infra.rb +36 -0
- data/lib/dommy/internal/input_type.rb +90 -20
- data/lib/dommy/internal/insertion_point.rb +27 -3
- data/lib/dommy/internal/js_number.rb +60 -0
- data/lib/dommy/internal/light_dismiss.rb +178 -0
- data/lib/dommy/internal/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mime_type.rb +8 -0
- data/lib/dommy/internal/mutation_coordinator.rb +94 -60
- data/lib/dommy/internal/node_adopter.rb +165 -97
- data/lib/dommy/internal/node_factory.rb +111 -49
- data/lib/dommy/internal/node_identity.rb +1 -1
- data/lib/dommy/internal/node_traversal.rb +30 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/observer_options.rb +24 -14
- data/lib/dommy/internal/origin.rb +86 -0
- data/lib/dommy/internal/parent_node.rb +12 -10
- data/lib/dommy/internal/popover_invoker_element.rb +67 -0
- data/lib/dommy/internal/popover_stack.rb +120 -0
- data/lib/dommy/internal/post_insertion_steps.rb +146 -77
- data/lib/dommy/internal/reflected_attributes.rb +173 -43
- data/lib/dommy/internal/rendered_text/collector.rb +12 -9
- data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
- data/lib/dommy/internal/selector_arguments.rb +2 -16
- data/lib/dommy/internal/selector_index.rb +19 -10
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +91 -256
- data/lib/dommy/internal/selector_parser.rb +176 -100
- data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
- data/lib/dommy/internal/shadow_root_registry.rb +14 -6
- data/lib/dommy/internal/sibling_positions.rb +59 -0
- data/lib/dommy/internal/svg_uri_reference.rb +33 -0
- data/lib/dommy/internal/template_content_registry.rb +11 -5
- data/lib/dommy/internal/text_selection.rb +185 -32
- data/lib/dommy/internal/toggle_events.rb +6 -4
- data/lib/dommy/internal/unsafe_html.rb +107 -0
- data/lib/dommy/internal/url_parser.rb +84 -8
- data/lib/dommy/internal/user_activation.rb +192 -0
- data/lib/dommy/internal/web_idl.rb +160 -16
- data/lib/dommy/internal/window_constructors.rb +34 -14
- data/lib/dommy/internal/xml_serialization.rb +9 -45
- data/lib/dommy/js/constructor_resolver.rb +18 -0
- data/lib/dommy/js/custom_element_bridge.rb +57 -82
- data/lib/dommy/js/dom_interfaces.rb +51 -15
- data/lib/dommy/js/host_bridge.rb +189 -40
- data/lib/dommy/js/host_callbacks.rb +10 -0
- data/lib/dommy/js/host_runtime.js +1921 -416
- data/lib/dommy/js/marshaller.rb +13 -4
- data/lib/dommy/js/module_loader.rb +46 -5
- data/lib/dommy/js/module_preload.rb +194 -0
- data/lib/dommy/js/runtime.rb +9 -0
- data/lib/dommy/js/script_boot.rb +200 -114
- data/lib/dommy/js/serialized_record.rb +32 -0
- data/lib/dommy/js/webidl_event_handlers.js +45 -0
- data/lib/dommy/js/webidl_members.js +196 -0
- data/lib/dommy/js/webidl_signatures.js +2055 -0
- data/lib/dommy/js/webidl_tables.js +72 -101
- data/lib/dommy/js/window_wiring.rb +9 -5
- data/lib/dommy/location.rb +129 -76
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/media_query_list.rb +4 -13
- data/lib/dommy/message_channel.rb +137 -64
- data/lib/dommy/mutation_observer.rb +3 -7
- data/lib/dommy/navigation.rb +61 -11
- data/lib/dommy/navigator.rb +132 -10
- data/lib/dommy/node.rb +81 -44
- data/lib/dommy/notification.rb +10 -6
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +20 -26
- data/lib/dommy/range.rb +20 -47
- data/lib/dommy/resources.rb +38 -3
- data/lib/dommy/scheduler.rb +176 -48
- data/lib/dommy/shadow_root.rb +125 -25
- data/lib/dommy/storage.rb +135 -25
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/structured_data.rb +45 -0
- data/lib/dommy/style_declaration.rb +38 -6
- data/lib/dommy/svg_elements.rb +25 -11
- data/lib/dommy/text_codec.rb +2 -14
- data/lib/dommy/tree_walker.rb +12 -26
- data/lib/dommy/url.rb +12 -9
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +109 -107
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +10 -33
- data/lib/dommy/window.rb +817 -105
- data/lib/dommy/worker.rb +4 -28
- data/lib/dommy/xml_http_request.rb +32 -45
- data/lib/dommy.rb +43 -10
- metadata +52 -5
- data/lib/dommy/backend/makiri_adapter.rb +0 -408
- data/lib/dommy/internal/cookie_jar.rb +0 -29
|
@@ -9,14 +9,28 @@ module Dommy
|
|
|
9
9
|
# the ones it can actually invalidate, which is the whole point — a text
|
|
10
10
|
# edit inside a <p> must not throw away the rule index.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# Each is the sum of the document's own count, which its mutation paths
|
|
13
|
+
# move, and Makiri's count of the matching edits — tree_version for the
|
|
14
|
+
# child lists, attribute_version for the attributes — which moves however
|
|
15
|
+
# the backend was edited. Every term only grows, so the sum changes with
|
|
16
|
+
# any of them: an edit that reached Makiri without passing through the
|
|
17
|
+
# document's paths still retires what hangs on the counter. Makiri counts
|
|
18
|
+
# no character-data edit, so a text edit that flips nothing moves nothing
|
|
19
|
+
# here either; one that flips `:empty` is seen through the document's
|
|
20
|
+
# own path alone.
|
|
21
|
+
#
|
|
22
|
+
# Host contract: @backend_doc, @__internal_css_style_cache__,
|
|
23
|
+
# #__internal_style_sheet_elements__ and #quirks_mode?.
|
|
13
24
|
module DocumentGenerations
|
|
25
|
+
# The attributes SelectorIndex records an element under.
|
|
26
|
+
INDEXED_ATTRIBUTES = %w[id class].freeze
|
|
27
|
+
|
|
14
28
|
def style_generation
|
|
15
|
-
@style_generation || 0
|
|
29
|
+
(@style_generation || 0) + backend_tree_version
|
|
16
30
|
end
|
|
17
31
|
|
|
18
32
|
def dom_generation
|
|
19
|
-
@dom_generation || 0
|
|
33
|
+
(@dom_generation || 0) + backend_tree_version + backend_attribute_version
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
# Moves only on childList mutations — the coarsest epoch. Keys memos
|
|
@@ -24,23 +38,56 @@ module Dommy
|
|
|
24
38
|
# exist, in what order), like the document's <style>/<link> list: an
|
|
25
39
|
# attribute-triggered cascade rebuild can then skip re-walking for them.
|
|
26
40
|
def tree_generation
|
|
27
|
-
@tree_generation || 0
|
|
41
|
+
(@tree_generation || 0) + backend_tree_version
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# What the selector index (SelectorIndex) is built from: the element tree
|
|
45
|
+
# and each element's `id` and `class`. Nothing else it holds can go
|
|
46
|
+
# stale, so a value, focus or `data-*` change, which moves
|
|
47
|
+
# dom_generation, leaves it standing.
|
|
48
|
+
#
|
|
49
|
+
# An attribute edit the document saw and could name (see
|
|
50
|
+
# #__internal_note_attribute_mutation__) is accounted for: an `id` or
|
|
51
|
+
# `class` one moves the index's own counter, any other moves nothing.
|
|
52
|
+
# One Makiri counted that the document did not account for — edited
|
|
53
|
+
# around its mutation paths — moves the epoch, so the index is rebuilt
|
|
54
|
+
# rather than trusted.
|
|
55
|
+
def __internal_selector_index_generation__
|
|
56
|
+
version = backend_attribute_version
|
|
57
|
+
unless version == @__index_attribute_version
|
|
58
|
+
@__index_attribute_epoch = (@__index_attribute_epoch || 0) + 1
|
|
59
|
+
@__index_attribute_version = version
|
|
60
|
+
end
|
|
61
|
+
# The same frozen key while nothing moved: the matcher asks once per
|
|
62
|
+
# candidate, so this must not allocate.
|
|
63
|
+
tree = tree_generation
|
|
64
|
+
own = @__index_generation || 0
|
|
65
|
+
quirks = quirks_mode?
|
|
66
|
+
key = @__index_generation_key
|
|
67
|
+
unless key && key[0] == tree && key[1] == own && key[2] == @__index_attribute_epoch && key[3] == quirks
|
|
68
|
+
key = @__index_generation_key = [tree, own, @__index_attribute_epoch, quirks].freeze
|
|
69
|
+
end
|
|
70
|
+
key
|
|
28
71
|
end
|
|
29
72
|
|
|
30
73
|
def __internal_bump_style_generation__
|
|
31
|
-
@style_generation = style_generation + 1
|
|
74
|
+
@style_generation = (@style_generation || 0) + 1
|
|
75
|
+
# Whatever changed may have made the focused element unfocusable:
|
|
76
|
+
# the next rendering update's focus fixup checks.
|
|
77
|
+
__internal_schedule_rendering_update__ if @active_element
|
|
32
78
|
nil
|
|
33
79
|
end
|
|
34
80
|
|
|
35
81
|
def __internal_bump_dom_generation__
|
|
36
|
-
@dom_generation = dom_generation + 1
|
|
82
|
+
@dom_generation = (@dom_generation || 0) + 1
|
|
83
|
+
__internal_schedule_rendering_update__ if @active_element
|
|
37
84
|
nil
|
|
38
85
|
end
|
|
39
86
|
|
|
40
87
|
# A childList mutation: tree shape feeds both selector matching and the
|
|
41
88
|
# rule -> element index, so everything is suspect.
|
|
42
89
|
def __internal_note_tree_mutation__
|
|
43
|
-
@tree_generation = tree_generation + 1
|
|
90
|
+
@tree_generation = (@tree_generation || 0) + 1
|
|
44
91
|
__internal_bump_dom_generation__
|
|
45
92
|
__internal_bump_style_generation__
|
|
46
93
|
end
|
|
@@ -51,6 +98,7 @@ module Dommy
|
|
|
51
98
|
# <style>/<link>, whose media/disabled/rel gate whole sheets.
|
|
52
99
|
def __internal_note_attribute_mutation__(name, target_node)
|
|
53
100
|
__internal_bump_dom_generation__
|
|
101
|
+
__internal_note_index_attribute__(name)
|
|
54
102
|
__internal_bump_style_generation__ if __internal_style_affected_by_attribute__(name, target_node)
|
|
55
103
|
end
|
|
56
104
|
|
|
@@ -62,7 +110,7 @@ module Dommy
|
|
|
62
110
|
# Only a Text node's data participates in :empty; a comment/PI edit
|
|
63
111
|
# can't change any match. `target_node` is a backend node, so its data
|
|
64
112
|
# reads through the Nokogiri-compatible #content.
|
|
65
|
-
text = target_node.
|
|
113
|
+
text = target_node.node_type == 3
|
|
66
114
|
flipped = text && (old_value.to_s.empty? != target_node.content.to_s.empty?)
|
|
67
115
|
__internal_bump_dom_generation__ if flipped
|
|
68
116
|
if __internal_inside_style_element__(target_node) ||
|
|
@@ -95,7 +143,7 @@ module Dommy
|
|
|
95
143
|
end
|
|
96
144
|
|
|
97
145
|
def __internal_style_value_sensitive__
|
|
98
|
-
index = @
|
|
146
|
+
index = @__internal_css_style_cache__&.index
|
|
99
147
|
index ? index.value_sensitive? : true
|
|
100
148
|
end
|
|
101
149
|
|
|
@@ -109,7 +157,7 @@ module Dommy
|
|
|
109
157
|
return @__direction_sensitive[1] if @__direction_sensitive&.first == generation
|
|
110
158
|
|
|
111
159
|
sensitive = @backend_doc.css("[dir], bdi").any? do |node|
|
|
112
|
-
Directionality.text_dependent?(node.name, node
|
|
160
|
+
Directionality.text_dependent?(node.name, Backend.no_namespace_attribute_value(node, "dir"))
|
|
113
161
|
end
|
|
114
162
|
@__direction_sensitive = [generation, sensitive]
|
|
115
163
|
sensitive
|
|
@@ -122,21 +170,27 @@ module Dommy
|
|
|
122
170
|
def __internal_direction_sensitive_ancestor__(node)
|
|
123
171
|
current = node
|
|
124
172
|
while current
|
|
125
|
-
|
|
173
|
+
if current.element? &&
|
|
174
|
+
Directionality.text_dependent?(current.name, Backend.no_namespace_attribute_value(current, "dir"))
|
|
175
|
+
return true
|
|
176
|
+
end
|
|
126
177
|
|
|
127
|
-
current = current.
|
|
178
|
+
current = current.parent
|
|
128
179
|
end
|
|
129
180
|
false
|
|
130
181
|
end
|
|
131
182
|
|
|
132
183
|
def __internal_style_affected_by_attribute__(name, target_node)
|
|
133
|
-
owner = target_node.
|
|
184
|
+
owner = target_node.name.to_s.downcase
|
|
134
185
|
return true if owner == "style" || owner == "link"
|
|
135
186
|
# The `dir` attribute drives the computed `direction` (Directionality)
|
|
136
187
|
# as well as `:dir()`, neither of which is a plain attribute selector.
|
|
137
188
|
return true if name.to_s.casecmp?("dir")
|
|
189
|
+
# The UA rules for popovers and dialogs (UAStylesheet
|
|
190
|
+
# .top_layer_declarations) read these with no selector of their own.
|
|
191
|
+
return true if name.to_s.casecmp?("popover") || name.to_s.casecmp?("open")
|
|
138
192
|
|
|
139
|
-
index = @
|
|
193
|
+
index = @__internal_css_style_cache__&.index
|
|
140
194
|
# No RuleIndex yet: the bump is nearly free (at most it drops the
|
|
141
195
|
# author_css?/counters memos), so stay conservative.
|
|
142
196
|
return true unless index
|
|
@@ -145,7 +199,7 @@ module Dommy
|
|
|
145
199
|
end
|
|
146
200
|
|
|
147
201
|
def __internal_style_text_sensitive__
|
|
148
|
-
index = @
|
|
202
|
+
index = @__internal_css_style_cache__&.index
|
|
149
203
|
index ? index.text_sensitive? : true
|
|
150
204
|
end
|
|
151
205
|
|
|
@@ -156,14 +210,33 @@ module Dommy
|
|
|
156
210
|
# between childList mutations answers this without re-walking.
|
|
157
211
|
return false unless __internal_style_sheet_elements__.any? { |el| el.local_name.to_s.casecmp?("style") }
|
|
158
212
|
|
|
159
|
-
current = node.
|
|
213
|
+
current = node.parent
|
|
160
214
|
while current
|
|
161
|
-
return true if current.
|
|
215
|
+
return true if current.name.to_s.downcase == "style"
|
|
162
216
|
|
|
163
|
-
current = current.
|
|
217
|
+
current = current.parent
|
|
164
218
|
end
|
|
165
219
|
false
|
|
166
220
|
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
# The index-side accounting for one attribute edit, called after the
|
|
225
|
+
# backend took it. A non-indexed name is accounted only when it is the
|
|
226
|
+
# single backend edit since the last accounted one; anything else leaves
|
|
227
|
+
# the gap for #__internal_selector_index_generation__ to see.
|
|
228
|
+
def __internal_note_index_attribute__(name)
|
|
229
|
+
if INDEXED_ATTRIBUTES.any? { |indexed| name.casecmp?(indexed) }
|
|
230
|
+
@__index_generation = (@__index_generation || 0) + 1
|
|
231
|
+
else
|
|
232
|
+
version = backend_attribute_version
|
|
233
|
+
@__index_attribute_version = version if @__index_attribute_version == version - 1
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def backend_tree_version = @backend_doc ? @backend_doc.tree_version : 0
|
|
238
|
+
|
|
239
|
+
def backend_attribute_version = @backend_doc ? @backend_doc.attribute_version : 0
|
|
167
240
|
end
|
|
168
241
|
end
|
|
169
242
|
end
|
|
@@ -6,29 +6,230 @@ module Dommy
|
|
|
6
6
|
# They are not tree state, but :focus and :hover match on them, so a change
|
|
7
7
|
# has to tell the selector caches.
|
|
8
8
|
#
|
|
9
|
-
# Host contract: #body,
|
|
10
|
-
# focus or hover change owes the
|
|
9
|
+
# Host contract: #body, #document_element, #default_view, and
|
|
10
|
+
# DocumentGenerations for the epoch bump a focus or hover change owes the
|
|
11
|
+
# selector caches.
|
|
11
12
|
module DocumentInteractionState
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# DocumentOrShadowRoot's `activeElement` for the document: the focused
|
|
14
|
+
# element retargeted against the document — the outermost shadow host,
|
|
15
|
+
# for an element inside a shadow tree — or, with the viewport focused,
|
|
16
|
+
# the body element, else the document element.
|
|
14
17
|
def active_element
|
|
15
|
-
@active_element
|
|
18
|
+
focused = @active_element
|
|
19
|
+
if focused
|
|
20
|
+
candidate = Retargeting.retarget(focused, self)
|
|
21
|
+
return candidate if candidate.respond_to?(:get_root_node) && candidate.get_root_node.equal?(self)
|
|
22
|
+
end
|
|
23
|
+
body || document_element
|
|
16
24
|
end
|
|
17
25
|
|
|
26
|
+
# Designate `el` (nil for the viewport) as the focused area, firing
|
|
27
|
+
# nothing — what the focus fixup does, and what the focus update steps
|
|
28
|
+
# do between their blur and focus events.
|
|
18
29
|
def __internal_set_active_element__(el)
|
|
19
30
|
# Focus is selector-observable state (:focus / :focus-within rules), so
|
|
20
31
|
# a change invalidates cached query results and computed styles.
|
|
21
|
-
|
|
32
|
+
return if @active_element.equal?(el)
|
|
33
|
+
|
|
22
34
|
@active_element = el
|
|
35
|
+
__internal_note_selector_state_change__
|
|
23
36
|
end
|
|
24
37
|
|
|
25
|
-
# The explicitly focused element (nil when
|
|
26
|
-
#
|
|
27
|
-
# <body> per spec.
|
|
38
|
+
# The explicitly focused element (nil when the viewport has the focus) —
|
|
39
|
+
# the focused area's DOM anchor when it is an element.
|
|
28
40
|
def __internal_focused_element__
|
|
29
41
|
@active_element
|
|
30
42
|
end
|
|
31
43
|
|
|
44
|
+
# HTML's "focus update steps" from the focused area to `new_element`
|
|
45
|
+
# (nil, the viewport): blur and focusout at the element losing focus,
|
|
46
|
+
# then focus and focusin at the one gaining it, each a trusted
|
|
47
|
+
# FocusEvent whose relatedTarget is the other element.
|
|
48
|
+
#
|
|
49
|
+
# Whether the new focus is indicated (`:focus-visible`) is decided
|
|
50
|
+
# first, from how it moved (#__internal_with_focus_type__).
|
|
51
|
+
def __internal_focus_update__(new_element)
|
|
52
|
+
old = @active_element
|
|
53
|
+
type = @pending_focus_type || :script
|
|
54
|
+
explicit = @pending_focus_visible
|
|
55
|
+
# Focus moved by the handlers of the events below is script focus.
|
|
56
|
+
@pending_focus_type = nil
|
|
57
|
+
@pending_focus_visible = nil
|
|
58
|
+
return nil if old.equal?(new_element)
|
|
59
|
+
|
|
60
|
+
fire_focus_event(old, "blur", new_element) if old
|
|
61
|
+
fire_focus_event(old, "focusout", new_element) if old
|
|
62
|
+
if new_element
|
|
63
|
+
@focus_visible = focus_indicated?(new_element, type, explicit)
|
|
64
|
+
@last_focus_type = type unless type == :script
|
|
65
|
+
# Sequential navigation goes on from the newly focused element.
|
|
66
|
+
@__internal_sequential_focus_navigation_starting_point__ = new_element
|
|
67
|
+
else
|
|
68
|
+
@focus_visible = false
|
|
69
|
+
end
|
|
70
|
+
__internal_set_active_element__(new_element)
|
|
71
|
+
return nil unless new_element
|
|
72
|
+
|
|
73
|
+
fire_focus_event(new_element, "focus", old)
|
|
74
|
+
fire_focus_event(new_element, "focusin", old)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Run the block — focusing steps — as focus moved by `type`: :keyboard
|
|
79
|
+
# (sequential focus navigation), :mouse (a click) or :script, and with
|
|
80
|
+
# FocusOptions' `focusVisible` when the caller passed one.
|
|
81
|
+
def __internal_with_focus_type__(type, focus_visible: nil)
|
|
82
|
+
saved = [@pending_focus_type, @pending_focus_visible]
|
|
83
|
+
@pending_focus_type = type
|
|
84
|
+
@pending_focus_visible = focus_visible
|
|
85
|
+
yield
|
|
86
|
+
ensure
|
|
87
|
+
@pending_focus_type, @pending_focus_visible = saved
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether the user agent indicates the focus of the focused element:
|
|
91
|
+
# what `:focus-visible` matches on.
|
|
92
|
+
def __internal_focus_visible__? = @focus_visible && @active_element ? true : false
|
|
93
|
+
|
|
94
|
+
# `focus({focusVisible: true})` on the element already focused still
|
|
95
|
+
# indicates its focus.
|
|
96
|
+
def __internal_indicate_focus__
|
|
97
|
+
return nil if @focus_visible || @active_element.nil?
|
|
98
|
+
|
|
99
|
+
@focus_visible = true
|
|
100
|
+
__internal_note_selector_state_change__
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The input modality the focus heuristics remember: a key press makes
|
|
105
|
+
# later script focus visible, a pointer press undoes that.
|
|
106
|
+
def __internal_note_keyboard_input__
|
|
107
|
+
@had_keyboard_event = true
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def __internal_note_pointer_input__
|
|
112
|
+
@had_keyboard_event = false
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# HTML's removing steps, for the focus: when the focused element, or
|
|
117
|
+
# a shadow-including ancestor of it, leaves the document, the viewport
|
|
118
|
+
# becomes the focused area. No blur fires (the "focus fixup").
|
|
119
|
+
def __internal_focused_subtree_removed__(element)
|
|
120
|
+
focused = @active_element
|
|
121
|
+
return nil unless focused
|
|
122
|
+
return nil unless Retargeting.shadow_including_inclusive_ancestor?(element, focused)
|
|
123
|
+
|
|
124
|
+
__internal_set_active_element__(nil)
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The modal dialogs in the top layer, in the order they entered it.
|
|
129
|
+
# The last one blocks the document: everything outside it is inert.
|
|
130
|
+
def __internal_add_modal_dialog__(dialog)
|
|
131
|
+
(@modal_dialogs ||= []).reject! { |d| d.equal?(dialog) }
|
|
132
|
+
@modal_dialogs << dialog
|
|
133
|
+
__internal_note_selector_state_change__
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def __internal_remove_modal_dialog__(dialog)
|
|
138
|
+
return nil unless @modal_dialogs&.any? { |d| d.equal?(dialog) }
|
|
139
|
+
|
|
140
|
+
@modal_dialogs.reject! { |d| d.equal?(dialog) }
|
|
141
|
+
__internal_note_selector_state_change__
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# HTML "blocked by a modal dialog": the topmost dialog in the top layer.
|
|
146
|
+
def __internal_blocking_modal_dialog__ = @modal_dialogs&.last
|
|
147
|
+
|
|
148
|
+
# HTML's "open dialogs list": the open, connected dialogs, in the order
|
|
149
|
+
# their dialog setup steps ran (a copy).
|
|
150
|
+
def __internal_open_dialogs__ = (@open_dialogs ||= []).dup
|
|
151
|
+
|
|
152
|
+
def __internal_add_open_dialog__(dialog)
|
|
153
|
+
(@open_dialogs ||= []) << dialog unless @open_dialogs&.any? { |d| d.equal?(dialog) }
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def __internal_remove_open_dialog__(dialog)
|
|
158
|
+
@open_dialogs&.reject! { |d| d.equal?(dialog) }
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# HTML's "sequential focus navigation starting point": a node, or nil
|
|
163
|
+
# when unset (Internal::SequentialFocusNavigation).
|
|
164
|
+
attr_accessor :__internal_sequential_focus_navigation_starting_point__
|
|
165
|
+
|
|
166
|
+
# HTML's "popover pointerdown target" and "dialog pointerdown target",
|
|
167
|
+
# which light dismiss records on a press (Internal::LightDismiss).
|
|
168
|
+
attr_accessor :__internal_popover_pointerdown_target__, :__internal_dialog_pointerdown_target__
|
|
169
|
+
|
|
170
|
+
# HTML's steps for an element with an autofocus attribute inserted into
|
|
171
|
+
# a document: unless the top-level document has already processed its
|
|
172
|
+
# autofocus, the element joins (or moves to the end of) its autofocus
|
|
173
|
+
# candidates, which the next rendering update flushes.
|
|
174
|
+
def __internal_autofocus_inserted__(element)
|
|
175
|
+
return nil unless default_view.is_a?(Window)
|
|
176
|
+
|
|
177
|
+
top = __internal_top_document__
|
|
178
|
+
return nil if top.__internal_autofocus_processed__
|
|
179
|
+
|
|
180
|
+
top.__internal_add_autofocus_candidate__(element)
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def __internal_add_autofocus_candidate__(element)
|
|
185
|
+
candidates = (@autofocus_candidates ||= [])
|
|
186
|
+
candidates.reject! { |e| e.equal?(element) }
|
|
187
|
+
candidates << element
|
|
188
|
+
__internal_schedule_rendering_update__(:before)
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def __internal_autofocus_processed__ = @autofocus_processed ? true : false
|
|
193
|
+
|
|
194
|
+
# What the dialog and popover focusing steps do once they have focused
|
|
195
|
+
# something: the page's autofocus is settled.
|
|
196
|
+
def __internal_autofocus_done__
|
|
197
|
+
top = __internal_top_document__
|
|
198
|
+
top.instance_variable_set(:@autofocus_candidates, [])
|
|
199
|
+
top.instance_variable_set(:@autofocus_processed, true)
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The active document of this document's top-level traversable: up
|
|
204
|
+
# through the frames that host it.
|
|
205
|
+
def __internal_top_document__
|
|
206
|
+
document = self
|
|
207
|
+
seen = 0
|
|
208
|
+
while (view = document.default_view).respond_to?(:frame_element) && (frame = view.frame_element) &&
|
|
209
|
+
(seen += 1) < 64
|
|
210
|
+
document = frame.owner_document
|
|
211
|
+
end
|
|
212
|
+
document
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Ask for the next rendering update (Scheduler#request_rendering_update),
|
|
216
|
+
# once per phase: before the frame's rAF callbacks it flushes the
|
|
217
|
+
# autofocus candidates, after them it runs the focus fixup.
|
|
218
|
+
def __internal_schedule_rendering_update__(phase = :after)
|
|
219
|
+
pending = (@rendering_update_pending ||= {})
|
|
220
|
+
return nil if pending[phase]
|
|
221
|
+
|
|
222
|
+
view = default_view
|
|
223
|
+
return nil unless view.is_a?(Window) && view.document.equal?(self)
|
|
224
|
+
|
|
225
|
+
pending[phase] = true
|
|
226
|
+
view.scheduler.request_rendering_update(lambda do
|
|
227
|
+
pending.delete(phase)
|
|
228
|
+
phase == :before ? flush_autofocus_candidates : focus_fixup
|
|
229
|
+
end, phase: phase)
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
32
233
|
# The element the (virtual) pointer hovers — :hover matches it and its
|
|
33
234
|
# ancestors. Set from tests or capybara-dommy's Node#hover; nil clears.
|
|
34
235
|
def __internal_hovered_element__
|
|
@@ -42,6 +243,89 @@ module Dommy
|
|
|
42
243
|
__internal_note_selector_state_change__
|
|
43
244
|
nil
|
|
44
245
|
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
# Input types whose controls take keyboard input (they would bring up
|
|
250
|
+
# a virtual keyboard).
|
|
251
|
+
KEYBOARD_INPUT_TYPES = %w[text search url tel email password number date month week time
|
|
252
|
+
datetime-local].freeze
|
|
253
|
+
|
|
254
|
+
# Selectors' suggested heuristics for when to indicate focus: an
|
|
255
|
+
# explicit `focusVisible` wins; keyboard focus is indicated; a click
|
|
256
|
+
# indicates it only on an element that takes keyboard input; script
|
|
257
|
+
# focus is indicated unless the last focus came from a pointer with no
|
|
258
|
+
# key pressed since — or the element takes keyboard input.
|
|
259
|
+
def focus_indicated?(element, type, explicit)
|
|
260
|
+
return explicit unless explicit.nil?
|
|
261
|
+
|
|
262
|
+
case type
|
|
263
|
+
when :keyboard then true
|
|
264
|
+
when :mouse then keyboard_input?(element)
|
|
265
|
+
else @last_focus_type != :mouse || @had_keyboard_event || keyboard_input?(element)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def keyboard_input?(element)
|
|
270
|
+
return true if Focusability.editing_host?(element)
|
|
271
|
+
return false unless element.namespace_uri == Namespaces::HTML
|
|
272
|
+
|
|
273
|
+
case element.local_name
|
|
274
|
+
when "textarea" then true
|
|
275
|
+
when "input" then KEYBOARD_INPUT_TYPES.include?(element.type.to_s)
|
|
276
|
+
else false
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# HTML's focus fixup (in "update the rendering"): a focused element
|
|
281
|
+
# that stopped being a focusable area (disabled, hidden, made inert)
|
|
282
|
+
# gives the focus to the viewport, firing blur.
|
|
283
|
+
def focus_fixup
|
|
284
|
+
focused = @active_element
|
|
285
|
+
__internal_focus_update__(nil) if focused && !Focusability.focusable_area?(focused)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# HTML "flush autofocus candidates", for a top-level document: the
|
|
289
|
+
# first candidate that is (or delegates to) a focusable area takes the
|
|
290
|
+
# focus — unless something is focused already or the document has a
|
|
291
|
+
# target element, which settles the matter without focusing anything.
|
|
292
|
+
def flush_autofocus_candidates
|
|
293
|
+
return if @autofocus_processed
|
|
294
|
+
|
|
295
|
+
candidates = @autofocus_candidates
|
|
296
|
+
return if candidates.nil? || candidates.empty?
|
|
297
|
+
|
|
298
|
+
if @active_element || Internal.target_id(self)
|
|
299
|
+
candidates.clear
|
|
300
|
+
@autofocus_processed = true
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
until candidates.empty?
|
|
305
|
+
element = candidates.shift
|
|
306
|
+
doc = element.owner_document
|
|
307
|
+
next unless element.is_connected? && doc.__internal_top_document__.equal?(self)
|
|
308
|
+
next if Internal.target_id(doc)
|
|
309
|
+
|
|
310
|
+
target = Focusability.focusable_area?(element) ? element : Focusability.focusable_area_for(element)
|
|
311
|
+
next if target.nil?
|
|
312
|
+
|
|
313
|
+
candidates.clear
|
|
314
|
+
@autofocus_processed = true
|
|
315
|
+
target == :viewport ? __internal_focus_update__(nil) : Focusability.run_focusing_steps(target)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# HTML "fire a focus event": a FocusEvent with the related target, the
|
|
320
|
+
# window as its view, and the composed flag; focusin and focusout
|
|
321
|
+
# (UI Events) bubble.
|
|
322
|
+
def fire_focus_event(target, type, related)
|
|
323
|
+
bubbles = type == "focusin" || type == "focusout"
|
|
324
|
+
view = default_view
|
|
325
|
+
event = FocusEvent.new(type, "bubbles" => bubbles, "composed" => true, "relatedTarget" => related,
|
|
326
|
+
"view" => view.is_a?(Window) ? view : nil)
|
|
327
|
+
target.dispatch_event(event.__internal_mark_trusted__)
|
|
328
|
+
end
|
|
45
329
|
end
|
|
46
330
|
end
|
|
47
331
|
end
|
|
@@ -91,9 +91,9 @@ module Dommy
|
|
|
91
91
|
# detected through the computed style as well. No layout: geometry-
|
|
92
92
|
# dependent invisibility stays out of scope.
|
|
93
93
|
def visible?(element)
|
|
94
|
-
|
|
94
|
+
node = element.__dommy_backend_node__ if element.is_a?(Node)
|
|
95
|
+
return true unless node
|
|
95
96
|
|
|
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|
|
|
@@ -170,12 +170,12 @@ module Dommy
|
|
|
170
170
|
# @api private (kept module-level only because visible? calls them)
|
|
171
171
|
|
|
172
172
|
def node_invisible_self?(node)
|
|
173
|
-
return false unless node.
|
|
173
|
+
return false unless node.element?
|
|
174
174
|
|
|
175
|
-
return true if node
|
|
176
|
-
return true if node.
|
|
175
|
+
return true if Backend.no_namespace_attribute_value(node, "hidden")
|
|
176
|
+
return true if node.name == "input" && Backend.no_namespace_attribute_value(node, "type") == "hidden"
|
|
177
177
|
|
|
178
|
-
style = node
|
|
178
|
+
style = Backend.no_namespace_attribute_value(node, "style").to_s
|
|
179
179
|
style.match?(/display\s*:\s*none/i) ||
|
|
180
180
|
style.match?(/visibility\s*:\s*hidden/i) ||
|
|
181
181
|
# Inline zero opacity hides too — the same number grammar as the
|
|
@@ -189,7 +189,7 @@ module Dommy
|
|
|
189
189
|
NON_RENDERED_TAGS = %w[head script style template noscript].freeze
|
|
190
190
|
|
|
191
191
|
def non_rendering_tag?(node)
|
|
192
|
-
|
|
192
|
+
NON_RENDERED_TAGS.include?(node.name)
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Visible text of a node's subtree: like `text_content`, but excluding
|