dommy 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
data/lib/dommy/event.rb
CHANGED
|
@@ -11,11 +11,13 @@ module Dommy
|
|
|
11
11
|
return nil if type.nil? || cb.nil?
|
|
12
12
|
|
|
13
13
|
list = listeners_for(type.to_s)
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
14
|
+
entry = Listener.new(cb, options)
|
|
15
|
+
# Per spec, a listener is deduplicated by (type, callback, capture) — so
|
|
16
|
+
# the same function may be registered once as a capture and once as a
|
|
17
|
+
# bubble listener.
|
|
18
|
+
return nil if list.any? { |e| e.listener.equal?(cb) && e.capture? == entry.capture? }
|
|
17
19
|
|
|
18
|
-
list <<
|
|
20
|
+
list << entry
|
|
19
21
|
|
|
20
22
|
# `{ signal: AbortSignal }` — when the signal aborts, auto-
|
|
21
23
|
# remove the listener. Per spec, if the signal is already aborted
|
|
@@ -23,7 +25,7 @@ module Dommy
|
|
|
23
25
|
signal = options.is_a?(Hash) ? (options["signal"] || options[:signal]) : nil
|
|
24
26
|
if signal.respond_to?(:__js_get__)
|
|
25
27
|
if signal.__js_get__("aborted")
|
|
26
|
-
remove_event_listener(type, cb)
|
|
28
|
+
remove_event_listener(type, cb, options)
|
|
27
29
|
else
|
|
28
30
|
target = self
|
|
29
31
|
signal.__js_call__(
|
|
@@ -31,7 +33,7 @@ module Dommy
|
|
|
31
33
|
[
|
|
32
34
|
"abort",
|
|
33
35
|
proc {
|
|
34
|
-
target.remove_event_listener(type, cb)
|
|
36
|
+
target.remove_event_listener(type, cb, options)
|
|
35
37
|
}
|
|
36
38
|
]
|
|
37
39
|
)
|
|
@@ -41,10 +43,16 @@ module Dommy
|
|
|
41
43
|
nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def remove_event_listener(type, listener)
|
|
46
|
+
def remove_event_listener(type, listener, options = nil)
|
|
45
47
|
return nil if type.nil? || listener.nil?
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
# Per spec, a listener is identified by (type, callback, capture) — so
|
|
50
|
+
# removing must match the capture flag, not just the callback (a function
|
|
51
|
+
# registered as both a capture and a bubble listener is two listeners).
|
|
52
|
+
capture = EventTarget.capture_flag(options)
|
|
53
|
+
listeners_for(type.to_s).reject! do |entry|
|
|
54
|
+
entry.listener.equal?(listener) && entry.capture? == capture
|
|
55
|
+
end
|
|
48
56
|
nil
|
|
49
57
|
end
|
|
50
58
|
|
|
@@ -54,37 +62,89 @@ module Dommy
|
|
|
54
62
|
# Per spec, dispatchEvent must receive an Event instance.
|
|
55
63
|
raise TypeError, "dispatchEvent requires an Event, got #{event.class}" unless event.is_a?(Event)
|
|
56
64
|
|
|
57
|
-
event.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
event.__internal_prepare_for_dispatch__(self)
|
|
66
|
+
event.__internal_set_dispatch_flag__(true)
|
|
67
|
+
|
|
68
|
+
# The full propagation path: the target plus its ancestors (root last).
|
|
69
|
+
# Capturing always traverses the ancestors regardless of `bubbles`.
|
|
70
|
+
path = event.__js_get__("composed") ? composed_bubble_path(event) : event_bubble_path
|
|
71
|
+
event.__internal_record_path__(path) if event.respond_to?(:__internal_record_path__)
|
|
72
|
+
ancestors = path[1..] || []
|
|
73
|
+
|
|
74
|
+
catch(:stop_propagation) do
|
|
75
|
+
# Capturing phase: root → … → parent, capture listeners only.
|
|
76
|
+
event.__internal_set_event_phase__(Event::CAPTURING_PHASE)
|
|
77
|
+
ancestors.reverse_each do |node|
|
|
78
|
+
deliver_at(node, event, :capture)
|
|
79
|
+
end
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
event
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
# At the target: both capture and bubble listeners.
|
|
82
|
+
event.__internal_set_event_phase__(Event::AT_TARGET)
|
|
83
|
+
deliver_at(self, event, :both)
|
|
84
|
+
|
|
85
|
+
# Bubbling phase: parent → … → root, bubble listeners only (only when
|
|
86
|
+
# the event bubbles).
|
|
87
|
+
if event.bubbles?
|
|
88
|
+
event.__internal_set_event_phase__(Event::BUBBLING_PHASE)
|
|
89
|
+
ancestors.each do |node|
|
|
90
|
+
deliver_at(node, event, :bubble)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
69
93
|
end
|
|
70
94
|
|
|
95
|
+
# After dispatch, currentTarget reverts to null and eventPhase to NONE, and
|
|
96
|
+
# the propagation flags are unset so the event can be dispatched again.
|
|
97
|
+
event.__internal_set_current_target__(nil)
|
|
98
|
+
event.__internal_set_event_phase__(Event::NONE)
|
|
99
|
+
event.__internal_clear_propagation_flags__
|
|
100
|
+
event.__internal_set_dispatch_flag__(false)
|
|
101
|
+
|
|
71
102
|
!event.default_prevented?
|
|
72
103
|
end
|
|
73
104
|
|
|
74
|
-
|
|
105
|
+
# Deliver `event` to one node's listeners for the current phase, then honor
|
|
106
|
+
# stopPropagation (throws to end the whole walk after this node finishes).
|
|
107
|
+
def deliver_at(node, event, phase)
|
|
108
|
+
# Honor a stop-propagation flag set before reaching this node (including
|
|
109
|
+
# one set before dispatch began) — the spec checks it before invoking a
|
|
110
|
+
# node's listeners, not only after.
|
|
111
|
+
throw :stop_propagation if event.propagation_stopped?
|
|
112
|
+
|
|
113
|
+
event.__internal_set_current_target__(node)
|
|
114
|
+
node.__internal_deliver_event__(event, phase)
|
|
115
|
+
throw :stop_propagation if event.propagation_stopped?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `phase` is :capture (capture listeners), :bubble (non-capture), or :both
|
|
119
|
+
# (at the target). stopImmediatePropagation ends delivery within this node.
|
|
120
|
+
def __internal_deliver_event__(event, phase = :both)
|
|
75
121
|
listeners = listeners_for(event.type).dup
|
|
76
122
|
listeners.each do |entry|
|
|
77
|
-
|
|
123
|
+
next unless phase == :both || (phase == :capture ? entry.capture? : !entry.capture?)
|
|
124
|
+
|
|
125
|
+
# Spec: a `once` listener is removed BEFORE its callback runs, so a nested
|
|
126
|
+
# dispatch from within the callback can't invoke it a second time.
|
|
78
127
|
if entry.once?
|
|
79
|
-
listeners_for(event.type).reject!
|
|
128
|
+
listeners_for(event.type).reject! do |candidate|
|
|
129
|
+
candidate.listener.equal?(entry.listener) && candidate.capture? == entry.capture?
|
|
130
|
+
end
|
|
80
131
|
end
|
|
81
132
|
|
|
133
|
+
CallableInvoker.invoke_listener(entry.listener, event)
|
|
134
|
+
|
|
82
135
|
break if event.immediate_propagation_stopped?
|
|
83
136
|
end
|
|
84
137
|
|
|
85
138
|
nil
|
|
86
139
|
end
|
|
87
140
|
|
|
141
|
+
# The next target up the propagation path. The default (no parent) suits
|
|
142
|
+
# EventTargets that aren't tree nodes (AbortSignal, XHR, …); Element /
|
|
143
|
+
# Document / ShadowRoot override it to walk the node tree.
|
|
144
|
+
def __internal_event_parent__
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
88
148
|
private
|
|
89
149
|
|
|
90
150
|
Listener = Struct.new(:listener, :options) do
|
|
@@ -96,6 +156,37 @@ module Dommy
|
|
|
96
156
|
false
|
|
97
157
|
end
|
|
98
158
|
end
|
|
159
|
+
|
|
160
|
+
# useCapture: a boolean third argument, or `{capture: …}` in the options
|
|
161
|
+
# dictionary. A capture listener fires in the capturing phase; a non-capture
|
|
162
|
+
# listener in the bubbling phase (both at the target).
|
|
163
|
+
def capture?
|
|
164
|
+
EventTarget.capture_flag(options)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The capture flag for an addEventListener/removeEventListener options
|
|
169
|
+
# argument, using JS — not Ruby — truthiness: a boolean useCapture, or the
|
|
170
|
+
# `capture` member of an options dictionary, where 0 / "" / NaN / null /
|
|
171
|
+
# undefined are falsy (in Ruby 0 and "" are truthy, so a naive `!!` is wrong).
|
|
172
|
+
def self.capture_flag(options)
|
|
173
|
+
raw =
|
|
174
|
+
if options.is_a?(Hash)
|
|
175
|
+
options.key?("capture") ? options["capture"] : options[:capture]
|
|
176
|
+
else
|
|
177
|
+
options
|
|
178
|
+
end
|
|
179
|
+
js_truthy?(raw)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# JS ToBoolean: false for false/null/undefined, +0/-0, NaN, and "".
|
|
183
|
+
def self.js_truthy?(value)
|
|
184
|
+
return false if value.nil? || value == false
|
|
185
|
+
return false if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
|
|
186
|
+
return false if value.is_a?(Numeric) && (value.zero? || (value.respond_to?(:nan?) && value.nan?))
|
|
187
|
+
return false if value == ""
|
|
188
|
+
|
|
189
|
+
true
|
|
99
190
|
end
|
|
100
191
|
|
|
101
192
|
def listeners_for(type)
|
|
@@ -106,7 +197,7 @@ module Dommy
|
|
|
106
197
|
def event_bubble_path
|
|
107
198
|
path = [self]
|
|
108
199
|
current = self
|
|
109
|
-
while (current = current.
|
|
200
|
+
while (current = current.__send__(:__internal_event_parent__))
|
|
110
201
|
path << current
|
|
111
202
|
end
|
|
112
203
|
|
|
@@ -116,12 +207,12 @@ module Dommy
|
|
|
116
207
|
# Build the propagation path with optional shadow-boundary
|
|
117
208
|
# crossing. When the in-flight event has `composed: true`, the
|
|
118
209
|
# walk continues from a ShadowRoot to its host; otherwise it
|
|
119
|
-
# stops at the shadow boundary (nil from `
|
|
210
|
+
# stops at the shadow boundary (nil from `__internal_event_parent__`).
|
|
120
211
|
def composed_bubble_path(event)
|
|
121
212
|
path = [self]
|
|
122
213
|
current = self
|
|
123
214
|
loop do
|
|
124
|
-
nxt = current.
|
|
215
|
+
nxt = current.__send__(:__internal_event_parent__)
|
|
125
216
|
if nxt.nil? && event.respond_to?(:__js_get__) && event.__js_get__("composed")
|
|
126
217
|
# Try to cross a shadow boundary
|
|
127
218
|
if current.is_a?(ShadowRoot)
|
|
@@ -148,42 +239,27 @@ module Dommy
|
|
|
148
239
|
private
|
|
149
240
|
|
|
150
241
|
def enclosing_shadow_root_of(target)
|
|
151
|
-
return nil unless target.respond_to?(:
|
|
242
|
+
return nil unless target.respond_to?(:__dommy_backend_node__)
|
|
152
243
|
|
|
153
244
|
doc = target.instance_variable_get(:@document)
|
|
154
|
-
return nil unless doc && doc.respond_to?(:
|
|
245
|
+
return nil unless doc && doc.respond_to?(:__internal_shadow_root_containing__)
|
|
155
246
|
|
|
156
|
-
doc.
|
|
247
|
+
doc.__internal_shadow_root_containing__(target.__dommy_backend_node__)
|
|
157
248
|
end
|
|
158
249
|
|
|
159
|
-
public
|
|
160
|
-
|
|
161
|
-
def invoke_listener(listener, event)
|
|
162
|
-
# DOM spec: a listener can be (a) a function, or (b) an object
|
|
163
|
-
# with a `handleEvent` method. Both Ruby and JS-bridged callables
|
|
164
|
-
# are supported.
|
|
165
|
-
if listener.respond_to?(:handle_event)
|
|
166
|
-
listener.handle_event(event)
|
|
167
|
-
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
168
|
-
listener.call(event)
|
|
169
|
-
elsif listener.respond_to?(:__js_call__)
|
|
170
|
-
# Prefer handleEvent if the bridge object advertises it; fall
|
|
171
|
-
# back to call. We can't introspect on the JS side, so we just
|
|
172
|
-
# try call (the common case for JS.callback {}).
|
|
173
|
-
listener.__js_call__("call", [event])
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
250
|
end
|
|
177
251
|
|
|
178
252
|
class StandaloneEventTarget
|
|
179
253
|
include EventTarget
|
|
180
254
|
|
|
255
|
+
include Bridge::Methods
|
|
256
|
+
js_methods %w[addEventListener removeEventListener dispatchEvent]
|
|
181
257
|
def __js_call__(method, args)
|
|
182
258
|
case method
|
|
183
259
|
when "addEventListener"
|
|
184
260
|
add_event_listener(args[0], args[1], args[2])
|
|
185
261
|
when "removeEventListener"
|
|
186
|
-
remove_event_listener(args[0], args[1])
|
|
262
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
187
263
|
when "dispatchEvent"
|
|
188
264
|
dispatch_event(args[0])
|
|
189
265
|
else
|
|
@@ -191,12 +267,17 @@ module Dommy
|
|
|
191
267
|
end
|
|
192
268
|
end
|
|
193
269
|
|
|
194
|
-
def
|
|
270
|
+
def __internal_event_parent__
|
|
195
271
|
nil
|
|
196
272
|
end
|
|
197
273
|
end
|
|
198
274
|
|
|
199
275
|
class Event
|
|
276
|
+
NONE = 0
|
|
277
|
+
CAPTURING_PHASE = 1
|
|
278
|
+
AT_TARGET = 2
|
|
279
|
+
BUBBLING_PHASE = 3
|
|
280
|
+
|
|
200
281
|
def initialize(type, init = nil)
|
|
201
282
|
@type = type.to_s
|
|
202
283
|
@bubbles = !!read_init(init, "bubbles")
|
|
@@ -207,11 +288,20 @@ module Dommy
|
|
|
207
288
|
@immediate_propagation_stopped = false
|
|
208
289
|
@target = nil
|
|
209
290
|
@current_target = nil
|
|
291
|
+
@event_phase = NONE
|
|
210
292
|
@composed_path = []
|
|
211
293
|
# `timeStamp` is the high-resolution timestamp at construction
|
|
212
294
|
# in ms (browser uses performance.now). We use monotonic time
|
|
213
295
|
# for determinism across spec runs.
|
|
214
296
|
@time_stamp = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0)
|
|
297
|
+
@trusted = false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Mark this event as UA-generated (`isTrusted === true`). Used by the host
|
|
301
|
+
# for events it fires itself (e.g. AbortSignal's "abort").
|
|
302
|
+
def __internal_mark_trusted__
|
|
303
|
+
@trusted = true
|
|
304
|
+
self
|
|
215
305
|
end
|
|
216
306
|
|
|
217
307
|
attr_reader :type
|
|
@@ -232,14 +322,29 @@ module Dommy
|
|
|
232
322
|
@immediate_propagation_stopped
|
|
233
323
|
end
|
|
234
324
|
|
|
235
|
-
def
|
|
325
|
+
def __internal_prepare_for_dispatch__(target)
|
|
236
326
|
@target ||= target
|
|
237
327
|
end
|
|
238
328
|
|
|
239
|
-
|
|
329
|
+
# End-of-dispatch cleanup: the dispatch algorithm unsets the stop-propagation
|
|
330
|
+
# and stop-immediate-propagation flags (but NOT the canceled flag), so the
|
|
331
|
+
# same event object can be dispatched again. A stopPropagation() issued
|
|
332
|
+
# before the next dispatch is still honored — only the post-dispatch state is
|
|
333
|
+
# cleared here.
|
|
334
|
+
def __internal_clear_propagation_flags__
|
|
335
|
+
@propagation_stopped = false
|
|
336
|
+
@immediate_propagation_stopped = false
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def __internal_set_current_target__(target)
|
|
240
341
|
@current_target = target
|
|
241
342
|
end
|
|
242
343
|
|
|
344
|
+
def __internal_set_event_phase__(phase)
|
|
345
|
+
@event_phase = phase
|
|
346
|
+
end
|
|
347
|
+
|
|
243
348
|
def __js_get__(key)
|
|
244
349
|
case key
|
|
245
350
|
when "type"
|
|
@@ -252,7 +357,15 @@ module Dommy
|
|
|
252
357
|
@composed
|
|
253
358
|
when "defaultPrevented"
|
|
254
359
|
@default_prevented
|
|
255
|
-
when "
|
|
360
|
+
when "returnValue"
|
|
361
|
+
# Legacy alias: false once the default has been prevented, else true.
|
|
362
|
+
!@default_prevented
|
|
363
|
+
when "isTrusted"
|
|
364
|
+
# Script-created events are untrusted; a UA-fired event (e.g. an
|
|
365
|
+
# AbortSignal's "abort") is marked trusted via __internal_mark_trusted__.
|
|
366
|
+
@trusted == true
|
|
367
|
+
when "target", "srcElement"
|
|
368
|
+
# srcElement is a legacy alias of target — null (not undefined) when unset.
|
|
256
369
|
@target
|
|
257
370
|
when "currentTarget"
|
|
258
371
|
@current_target
|
|
@@ -262,6 +375,12 @@ module Dommy
|
|
|
262
375
|
@propagation_stopped
|
|
263
376
|
when "eventPhase"
|
|
264
377
|
event_phase
|
|
378
|
+
else
|
|
379
|
+
# An unknown property reads back as JS `undefined`, not `null` — e.g. a
|
|
380
|
+
# non-dictionary member passed to the constructor (`new Event("x", {sweet:
|
|
381
|
+
# 1}).sweet`) is not reflected on the event. Genuinely-null DOM attributes
|
|
382
|
+
# (target/currentTarget/…) are explicit cases above and still return nil.
|
|
383
|
+
Bridge::UNDEFINED
|
|
265
384
|
end
|
|
266
385
|
end
|
|
267
386
|
|
|
@@ -271,11 +390,19 @@ module Dommy
|
|
|
271
390
|
# Setting to truthy stops propagation; spec quirk that
|
|
272
391
|
# `cancelBubble = false` does NOT un-stop (browser observation).
|
|
273
392
|
@propagation_stopped = true if value
|
|
393
|
+
when "returnValue"
|
|
394
|
+
# Legacy alias: returnValue = false cancels the event (like
|
|
395
|
+
# preventDefault); a truthy value does not un-cancel.
|
|
396
|
+
@default_prevented = true if !value && @cancelable
|
|
397
|
+
else
|
|
398
|
+
return Bridge::UNHANDLED
|
|
274
399
|
end
|
|
275
400
|
|
|
276
401
|
nil
|
|
277
402
|
end
|
|
278
403
|
|
|
404
|
+
include Bridge::Methods
|
|
405
|
+
js_methods %w[preventDefault stopPropagation stopImmediatePropagation composedPath initEvent]
|
|
279
406
|
def __js_call__(method, args)
|
|
280
407
|
case method
|
|
281
408
|
when "preventDefault"
|
|
@@ -291,14 +418,26 @@ module Dommy
|
|
|
291
418
|
when "composedPath"
|
|
292
419
|
@composed_path.dup
|
|
293
420
|
when "initEvent"
|
|
421
|
+
# WebIDL: the `type` argument is mandatory.
|
|
422
|
+
raise Bridge::TypeError, "initEvent requires a type argument" if args.empty?
|
|
423
|
+
|
|
294
424
|
init_event(args[0], args[1], args[2])
|
|
295
425
|
end
|
|
296
426
|
end
|
|
297
427
|
|
|
428
|
+
# Set while the event is being dispatched, so initEvent() can short-circuit.
|
|
429
|
+
def __internal_set_dispatch_flag__(flag)
|
|
430
|
+
@dispatch_flag = flag
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
|
|
298
434
|
# Deprecated `Event#initEvent(type, bubbles, cancelable)` — older
|
|
299
435
|
# browsers used `document.createEvent("Event").initEvent(...)`.
|
|
300
436
|
# Resets internal flags as a side effect.
|
|
301
437
|
def init_event(type, bubbles = false, cancelable = false)
|
|
438
|
+
# Spec: initEvent is a no-op while the event is being dispatched.
|
|
439
|
+
return nil if @dispatch_flag
|
|
440
|
+
|
|
302
441
|
@type = type.to_s
|
|
303
442
|
@bubbles = !!bubbles
|
|
304
443
|
@cancelable = !!cancelable
|
|
@@ -314,7 +453,7 @@ module Dommy
|
|
|
314
453
|
# Per spec, `load` events do not propagate to the Window when
|
|
315
454
|
# composed paths are computed (resource-finished signal stays at
|
|
316
455
|
# the target).
|
|
317
|
-
def
|
|
456
|
+
def __internal_record_path__(targets)
|
|
318
457
|
@composed_path = if @type == "load"
|
|
319
458
|
targets.reject { |t| t.is_a?(Window) }
|
|
320
459
|
else
|
|
@@ -325,18 +464,9 @@ module Dommy
|
|
|
325
464
|
private
|
|
326
465
|
|
|
327
466
|
def event_phase
|
|
328
|
-
|
|
329
|
-
# implement capturing (phase 1) by design.
|
|
330
|
-
return 0 if @current_target.nil?
|
|
331
|
-
return 2 if @current_target.equal?(@target)
|
|
332
|
-
|
|
333
|
-
3
|
|
467
|
+
@event_phase
|
|
334
468
|
end
|
|
335
469
|
|
|
336
|
-
public
|
|
337
|
-
|
|
338
|
-
private
|
|
339
|
-
|
|
340
470
|
def read_init(init, key)
|
|
341
471
|
case init
|
|
342
472
|
when Hash
|
|
@@ -360,6 +490,45 @@ module Dommy
|
|
|
360
490
|
|
|
361
491
|
super
|
|
362
492
|
end
|
|
493
|
+
|
|
494
|
+
js_methods %w[initCustomEvent]
|
|
495
|
+
def __js_call__(method, args)
|
|
496
|
+
case method
|
|
497
|
+
when "initCustomEvent"
|
|
498
|
+
# Deprecated initCustomEvent(type, bubbles=false, cancelable=false,
|
|
499
|
+
# detail=null). Like initEvent, the type is mandatory and the whole call
|
|
500
|
+
# is a no-op while the event is being dispatched.
|
|
501
|
+
raise Bridge::TypeError, "initCustomEvent requires a type argument" if args.empty?
|
|
502
|
+
|
|
503
|
+
unless @dispatch_flag
|
|
504
|
+
init_event(args[0], args[1], args[2])
|
|
505
|
+
@detail = args[3]
|
|
506
|
+
end
|
|
507
|
+
nil
|
|
508
|
+
else
|
|
509
|
+
super
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# PopStateEvent — fired on history back/forward. Exposes the history entry's
|
|
515
|
+
# serialized state via `event.state` (the spec property; framework routers like
|
|
516
|
+
# Turbo read `event.state.turbo` to recognise their own navigations and restore
|
|
517
|
+
# the cached page). A plain `Event` subclass per spec — NOT a CustomEvent, so it
|
|
518
|
+
# has no `detail` and `instanceof CustomEvent` is false.
|
|
519
|
+
class PopStateEvent < Event
|
|
520
|
+
attr_reader :state
|
|
521
|
+
|
|
522
|
+
def initialize(type, init = nil)
|
|
523
|
+
super
|
|
524
|
+
@state = read_init(init, "state")
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def __js_get__(key)
|
|
528
|
+
return @state if key == "state"
|
|
529
|
+
|
|
530
|
+
super
|
|
531
|
+
end
|
|
363
532
|
end
|
|
364
533
|
|
|
365
534
|
class MouseEvent < Event
|
|
@@ -640,6 +809,8 @@ module Dommy
|
|
|
640
809
|
end
|
|
641
810
|
end
|
|
642
811
|
|
|
812
|
+
include Bridge::Methods
|
|
813
|
+
js_methods %w[item]
|
|
643
814
|
def __js_call__(method, args)
|
|
644
815
|
case method
|
|
645
816
|
when "item"
|
|
@@ -821,9 +992,9 @@ module Dommy
|
|
|
821
992
|
|
|
822
993
|
# Spec: `AbortSignal.abort(reason?)` returns a fresh, pre-aborted
|
|
823
994
|
# signal. Convenient for APIs that need an already-cancelled token.
|
|
824
|
-
def self.abort(reason
|
|
995
|
+
def self.abort(*reason)
|
|
825
996
|
signal = new
|
|
826
|
-
signal.
|
|
997
|
+
signal.__internal_mark_aborted__(*reason)
|
|
827
998
|
signal
|
|
828
999
|
end
|
|
829
1000
|
|
|
@@ -836,9 +1007,9 @@ module Dommy
|
|
|
836
1007
|
signal = new
|
|
837
1008
|
reason = DOMException::TimeoutError.new("operation timed out")
|
|
838
1009
|
if scheduler
|
|
839
|
-
scheduler.set_timeout(proc { signal.
|
|
1010
|
+
scheduler.set_timeout(proc { signal.__internal_mark_aborted__(reason) }, ms.to_i)
|
|
840
1011
|
else
|
|
841
|
-
signal.
|
|
1012
|
+
signal.__internal_schedule_thread_timeout__(ms.to_i, reason)
|
|
842
1013
|
end
|
|
843
1014
|
|
|
844
1015
|
signal
|
|
@@ -853,12 +1024,12 @@ module Dommy
|
|
|
853
1024
|
list = Array(signals).select { |s| s.is_a?(AbortSignal) }
|
|
854
1025
|
already = list.find(&:aborted?)
|
|
855
1026
|
if already
|
|
856
|
-
composite.
|
|
1027
|
+
composite.__internal_mark_aborted__(already.reason)
|
|
857
1028
|
return composite
|
|
858
1029
|
end
|
|
859
1030
|
|
|
860
1031
|
list.each do |sig|
|
|
861
|
-
sig.add_event_listener("abort", proc { composite.
|
|
1032
|
+
sig.add_event_listener("abort", proc { composite.__internal_mark_aborted__(sig.reason) })
|
|
862
1033
|
end
|
|
863
1034
|
|
|
864
1035
|
composite
|
|
@@ -871,11 +1042,11 @@ module Dommy
|
|
|
871
1042
|
|
|
872
1043
|
# Background-thread timeout used by `AbortSignal.timeout` when no
|
|
873
1044
|
# scheduler is provided. Kept package-private; tests can also
|
|
874
|
-
# drive the abort manually via `
|
|
875
|
-
def
|
|
1045
|
+
# drive the abort manually via `__internal_mark_aborted__`.
|
|
1046
|
+
def __internal_schedule_thread_timeout__(ms, reason)
|
|
876
1047
|
Thread.new do
|
|
877
1048
|
sleep(ms.to_f / 1000.0)
|
|
878
|
-
|
|
1049
|
+
__internal_mark_aborted__(reason)
|
|
879
1050
|
end
|
|
880
1051
|
|
|
881
1052
|
nil
|
|
@@ -893,8 +1064,12 @@ module Dommy
|
|
|
893
1064
|
# consumer code that polls before doing async work.
|
|
894
1065
|
def throw_if_aborted
|
|
895
1066
|
return unless @aborted
|
|
1067
|
+
# An Exception reason (the default AbortError, or an explicit DOMException)
|
|
1068
|
+
# is raised so the bridge tags it as a real JS error; any other reason — a
|
|
1069
|
+
# string, number, or opaque JSValue — is thrown verbatim, identity kept.
|
|
1070
|
+
raise @reason if @reason.is_a?(Exception)
|
|
896
1071
|
|
|
897
|
-
raise
|
|
1072
|
+
raise Bridge::ThrowValue.new(@reason)
|
|
898
1073
|
end
|
|
899
1074
|
|
|
900
1075
|
alias throwIfAborted throw_if_aborted
|
|
@@ -904,20 +1079,35 @@ module Dommy
|
|
|
904
1079
|
when "aborted"
|
|
905
1080
|
@aborted
|
|
906
1081
|
when "reason"
|
|
907
|
-
|
|
1082
|
+
# A non-aborted signal's reason is `undefined` (not null); once aborted
|
|
1083
|
+
# it is the abort reason (an explicit value or the default AbortError).
|
|
1084
|
+
@aborted ? @reason : Bridge::UNDEFINED
|
|
1085
|
+
when "onabort"
|
|
1086
|
+
@onabort_handler
|
|
908
1087
|
end
|
|
909
1088
|
end
|
|
910
1089
|
|
|
911
|
-
|
|
1090
|
+
# `signal.onabort = fn` is an event-handler IDL attribute: it registers a
|
|
1091
|
+
# single "abort" listener (replacing any previous one); null/undefined clears
|
|
1092
|
+
# it. (Setting it after the signal is already aborted never fires.)
|
|
1093
|
+
def __js_set__(key, value)
|
|
1094
|
+
return Bridge::UNHANDLED unless key == "onabort"
|
|
1095
|
+
|
|
1096
|
+
remove_event_listener("abort", @onabort_handler) if @onabort_handler
|
|
1097
|
+
cleared = value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
|
|
1098
|
+
@onabort_handler = cleared ? nil : value
|
|
1099
|
+
add_event_listener("abort", @onabort_handler) if @onabort_handler
|
|
912
1100
|
nil
|
|
913
1101
|
end
|
|
914
1102
|
|
|
1103
|
+
include Bridge::Methods
|
|
1104
|
+
js_methods %w[addEventListener removeEventListener dispatchEvent throwIfAborted]
|
|
915
1105
|
def __js_call__(method, args)
|
|
916
1106
|
case method
|
|
917
1107
|
when "addEventListener"
|
|
918
1108
|
add_event_listener(args[0], args[1], args[2])
|
|
919
1109
|
when "removeEventListener"
|
|
920
|
-
remove_event_listener(args[0], args[1])
|
|
1110
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
921
1111
|
when "dispatchEvent"
|
|
922
1112
|
dispatch_event(args[0])
|
|
923
1113
|
when "throwIfAborted"
|
|
@@ -925,12 +1115,18 @@ module Dommy
|
|
|
925
1115
|
end
|
|
926
1116
|
end
|
|
927
1117
|
|
|
928
|
-
|
|
1118
|
+
# Sentinel meaning "no reason argument was supplied" — distinct from an
|
|
1119
|
+
# explicit `null` reason (`abort(null)` keeps reason null, but `abort()` /
|
|
1120
|
+
# `abort(undefined)` default to a fresh AbortError).
|
|
1121
|
+
NO_REASON = Object.new
|
|
1122
|
+
|
|
1123
|
+
def __internal_mark_aborted__(reason = NO_REASON)
|
|
929
1124
|
return if @aborted
|
|
930
1125
|
|
|
931
1126
|
@aborted = true
|
|
932
|
-
|
|
933
|
-
|
|
1127
|
+
no_reason = reason.equal?(NO_REASON) || (defined?(Bridge::UNDEFINED) && reason.equal?(Bridge::UNDEFINED))
|
|
1128
|
+
@reason = no_reason ? DOMException::AbortError.new("signal is aborted without reason") : reason
|
|
1129
|
+
dispatch_event(Event.new("abort", "bubbles" => false, "cancelable" => false).__internal_mark_trusted__)
|
|
934
1130
|
end
|
|
935
1131
|
end
|
|
936
1132
|
|
|
@@ -946,13 +1142,17 @@ module Dommy
|
|
|
946
1142
|
end
|
|
947
1143
|
|
|
948
1144
|
def __js_set__(_key, _value)
|
|
949
|
-
|
|
1145
|
+
Bridge::UNHANDLED
|
|
950
1146
|
end
|
|
951
1147
|
|
|
1148
|
+
include Bridge::Methods
|
|
1149
|
+
js_methods %w[abort]
|
|
952
1150
|
def __js_call__(method, args)
|
|
953
1151
|
case method
|
|
954
1152
|
when "abort"
|
|
955
|
-
|
|
1153
|
+
# `abort()` (no arg) defaults the reason; `abort(reason)` — even
|
|
1154
|
+
# `abort(null)` — keeps the explicit reason.
|
|
1155
|
+
args.empty? ? @signal.__internal_mark_aborted__ : @signal.__internal_mark_aborted__(args[0])
|
|
956
1156
|
end
|
|
957
1157
|
end
|
|
958
1158
|
end
|