dommy 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dommy/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
data/lib/dommy/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
|
|
|
@@ -55,42 +63,88 @@ module Dommy
|
|
|
55
63
|
raise TypeError, "dispatchEvent requires an Event, got #{event.class}" unless event.is_a?(Event)
|
|
56
64
|
|
|
57
65
|
event.__internal_prepare_for_dispatch__(self)
|
|
58
|
-
|
|
59
|
-
event.__js_get__("composed") ? composed_bubble_path(event) : event_bubble_path
|
|
60
|
-
else
|
|
61
|
-
[self]
|
|
62
|
-
end
|
|
66
|
+
event.__internal_set_dispatch_flag__(true)
|
|
63
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
|
|
64
71
|
event.__internal_record_path__(path) if event.respond_to?(:__internal_record_path__)
|
|
65
|
-
path
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
80
|
+
|
|
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
|
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
# eventPhase from currentTarget, so clearing it here covers
|
|
74
|
-
# both spec requirements.
|
|
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.
|
|
75
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)
|
|
76
101
|
|
|
77
102
|
!event.default_prevented?
|
|
78
103
|
end
|
|
79
104
|
|
|
80
|
-
|
|
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)
|
|
81
121
|
listeners = listeners_for(event.type).dup
|
|
82
122
|
listeners.each do |entry|
|
|
83
|
-
|
|
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.
|
|
84
127
|
if entry.once?
|
|
85
|
-
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
|
|
86
131
|
end
|
|
87
132
|
|
|
133
|
+
CallableInvoker.invoke_listener(entry.listener, event)
|
|
134
|
+
|
|
88
135
|
break if event.immediate_propagation_stopped?
|
|
89
136
|
end
|
|
90
137
|
|
|
91
138
|
nil
|
|
92
139
|
end
|
|
93
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
|
+
|
|
94
148
|
private
|
|
95
149
|
|
|
96
150
|
Listener = Struct.new(:listener, :options) do
|
|
@@ -102,6 +156,37 @@ module Dommy
|
|
|
102
156
|
false
|
|
103
157
|
end
|
|
104
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
|
|
105
190
|
end
|
|
106
191
|
|
|
107
192
|
def listeners_for(type)
|
|
@@ -112,7 +197,7 @@ module Dommy
|
|
|
112
197
|
def event_bubble_path
|
|
113
198
|
path = [self]
|
|
114
199
|
current = self
|
|
115
|
-
while (current = current.
|
|
200
|
+
while (current = current.__send__(:__internal_event_parent__))
|
|
116
201
|
path << current
|
|
117
202
|
end
|
|
118
203
|
|
|
@@ -127,7 +212,7 @@ module Dommy
|
|
|
127
212
|
path = [self]
|
|
128
213
|
current = self
|
|
129
214
|
loop do
|
|
130
|
-
nxt = current.
|
|
215
|
+
nxt = current.__send__(:__internal_event_parent__)
|
|
131
216
|
if nxt.nil? && event.respond_to?(:__js_get__) && event.__js_get__("composed")
|
|
132
217
|
# Try to cross a shadow boundary
|
|
133
218
|
if current.is_a?(ShadowRoot)
|
|
@@ -162,40 +247,19 @@ module Dommy
|
|
|
162
247
|
doc.__internal_shadow_root_containing__(target.__dommy_backend_node__)
|
|
163
248
|
end
|
|
164
249
|
|
|
165
|
-
public
|
|
166
|
-
|
|
167
|
-
def invoke_listener(listener, event)
|
|
168
|
-
# DOM spec: a listener can be (a) a function, or (b) an object
|
|
169
|
-
# with a `handleEvent` method. Both Ruby and JS-bridged callables
|
|
170
|
-
# are supported.
|
|
171
|
-
if listener.respond_to?(:handle_event)
|
|
172
|
-
listener.handle_event(event)
|
|
173
|
-
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
174
|
-
listener.call(event)
|
|
175
|
-
elsif listener.respond_to?(:__js_call__)
|
|
176
|
-
# Prefer handleEvent if the bridge object advertises it; fall
|
|
177
|
-
# back to call. We can't introspect on the JS side, so we just
|
|
178
|
-
# try call (the common case for JS.callback {}).
|
|
179
|
-
listener.__js_call__("call", [event])
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
250
|
end
|
|
183
251
|
|
|
184
252
|
class StandaloneEventTarget
|
|
185
253
|
include EventTarget
|
|
186
254
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def __js_method_names__
|
|
190
|
-
JS_METHOD_NAMES
|
|
191
|
-
end
|
|
192
|
-
|
|
255
|
+
include Bridge::Methods
|
|
256
|
+
js_methods %w[addEventListener removeEventListener dispatchEvent]
|
|
193
257
|
def __js_call__(method, args)
|
|
194
258
|
case method
|
|
195
259
|
when "addEventListener"
|
|
196
260
|
add_event_listener(args[0], args[1], args[2])
|
|
197
261
|
when "removeEventListener"
|
|
198
|
-
remove_event_listener(args[0], args[1])
|
|
262
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
199
263
|
when "dispatchEvent"
|
|
200
264
|
dispatch_event(args[0])
|
|
201
265
|
else
|
|
@@ -209,6 +273,11 @@ module Dommy
|
|
|
209
273
|
end
|
|
210
274
|
|
|
211
275
|
class Event
|
|
276
|
+
NONE = 0
|
|
277
|
+
CAPTURING_PHASE = 1
|
|
278
|
+
AT_TARGET = 2
|
|
279
|
+
BUBBLING_PHASE = 3
|
|
280
|
+
|
|
212
281
|
def initialize(type, init = nil)
|
|
213
282
|
@type = type.to_s
|
|
214
283
|
@bubbles = !!read_init(init, "bubbles")
|
|
@@ -219,11 +288,20 @@ module Dommy
|
|
|
219
288
|
@immediate_propagation_stopped = false
|
|
220
289
|
@target = nil
|
|
221
290
|
@current_target = nil
|
|
291
|
+
@event_phase = NONE
|
|
222
292
|
@composed_path = []
|
|
223
293
|
# `timeStamp` is the high-resolution timestamp at construction
|
|
224
294
|
# in ms (browser uses performance.now). We use monotonic time
|
|
225
295
|
# for determinism across spec runs.
|
|
226
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
|
|
227
305
|
end
|
|
228
306
|
|
|
229
307
|
attr_reader :type
|
|
@@ -248,10 +326,25 @@ module Dommy
|
|
|
248
326
|
@target ||= target
|
|
249
327
|
end
|
|
250
328
|
|
|
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
|
+
|
|
251
340
|
def __internal_set_current_target__(target)
|
|
252
341
|
@current_target = target
|
|
253
342
|
end
|
|
254
343
|
|
|
344
|
+
def __internal_set_event_phase__(phase)
|
|
345
|
+
@event_phase = phase
|
|
346
|
+
end
|
|
347
|
+
|
|
255
348
|
def __js_get__(key)
|
|
256
349
|
case key
|
|
257
350
|
when "type"
|
|
@@ -264,7 +357,15 @@ module Dommy
|
|
|
264
357
|
@composed
|
|
265
358
|
when "defaultPrevented"
|
|
266
359
|
@default_prevented
|
|
267
|
-
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.
|
|
268
369
|
@target
|
|
269
370
|
when "currentTarget"
|
|
270
371
|
@current_target
|
|
@@ -274,6 +375,12 @@ module Dommy
|
|
|
274
375
|
@propagation_stopped
|
|
275
376
|
when "eventPhase"
|
|
276
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
|
|
277
384
|
end
|
|
278
385
|
end
|
|
279
386
|
|
|
@@ -283,17 +390,19 @@ module Dommy
|
|
|
283
390
|
# Setting to truthy stops propagation; spec quirk that
|
|
284
391
|
# `cancelBubble = false` does NOT un-stop (browser observation).
|
|
285
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
|
|
286
399
|
end
|
|
287
400
|
|
|
288
401
|
nil
|
|
289
402
|
end
|
|
290
403
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def __js_method_names__
|
|
294
|
-
JS_METHOD_NAMES
|
|
295
|
-
end
|
|
296
|
-
|
|
404
|
+
include Bridge::Methods
|
|
405
|
+
js_methods %w[preventDefault stopPropagation stopImmediatePropagation composedPath initEvent]
|
|
297
406
|
def __js_call__(method, args)
|
|
298
407
|
case method
|
|
299
408
|
when "preventDefault"
|
|
@@ -309,14 +418,26 @@ module Dommy
|
|
|
309
418
|
when "composedPath"
|
|
310
419
|
@composed_path.dup
|
|
311
420
|
when "initEvent"
|
|
421
|
+
# WebIDL: the `type` argument is mandatory.
|
|
422
|
+
raise Bridge::TypeError, "initEvent requires a type argument" if args.empty?
|
|
423
|
+
|
|
312
424
|
init_event(args[0], args[1], args[2])
|
|
313
425
|
end
|
|
314
426
|
end
|
|
315
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
|
+
|
|
316
434
|
# Deprecated `Event#initEvent(type, bubbles, cancelable)` — older
|
|
317
435
|
# browsers used `document.createEvent("Event").initEvent(...)`.
|
|
318
436
|
# Resets internal flags as a side effect.
|
|
319
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
|
+
|
|
320
441
|
@type = type.to_s
|
|
321
442
|
@bubbles = !!bubbles
|
|
322
443
|
@cancelable = !!cancelable
|
|
@@ -343,18 +464,9 @@ module Dommy
|
|
|
343
464
|
private
|
|
344
465
|
|
|
345
466
|
def event_phase
|
|
346
|
-
|
|
347
|
-
# implement capturing (phase 1) by design.
|
|
348
|
-
return 0 if @current_target.nil?
|
|
349
|
-
return 2 if @current_target.equal?(@target)
|
|
350
|
-
|
|
351
|
-
3
|
|
467
|
+
@event_phase
|
|
352
468
|
end
|
|
353
469
|
|
|
354
|
-
public
|
|
355
|
-
|
|
356
|
-
private
|
|
357
|
-
|
|
358
470
|
def read_init(init, key)
|
|
359
471
|
case init
|
|
360
472
|
when Hash
|
|
@@ -378,6 +490,45 @@ module Dommy
|
|
|
378
490
|
|
|
379
491
|
super
|
|
380
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
|
|
381
532
|
end
|
|
382
533
|
|
|
383
534
|
class MouseEvent < Event
|
|
@@ -658,12 +809,8 @@ module Dommy
|
|
|
658
809
|
end
|
|
659
810
|
end
|
|
660
811
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
def __js_method_names__
|
|
664
|
-
JS_METHOD_NAMES
|
|
665
|
-
end
|
|
666
|
-
|
|
812
|
+
include Bridge::Methods
|
|
813
|
+
js_methods %w[item]
|
|
667
814
|
def __js_call__(method, args)
|
|
668
815
|
case method
|
|
669
816
|
when "item"
|
|
@@ -845,9 +992,9 @@ module Dommy
|
|
|
845
992
|
|
|
846
993
|
# Spec: `AbortSignal.abort(reason?)` returns a fresh, pre-aborted
|
|
847
994
|
# signal. Convenient for APIs that need an already-cancelled token.
|
|
848
|
-
def self.abort(reason
|
|
995
|
+
def self.abort(*reason)
|
|
849
996
|
signal = new
|
|
850
|
-
signal.__internal_mark_aborted__(reason)
|
|
997
|
+
signal.__internal_mark_aborted__(*reason)
|
|
851
998
|
signal
|
|
852
999
|
end
|
|
853
1000
|
|
|
@@ -917,8 +1064,12 @@ module Dommy
|
|
|
917
1064
|
# consumer code that polls before doing async work.
|
|
918
1065
|
def throw_if_aborted
|
|
919
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)
|
|
920
1071
|
|
|
921
|
-
raise
|
|
1072
|
+
raise Bridge::ThrowValue.new(@reason)
|
|
922
1073
|
end
|
|
923
1074
|
|
|
924
1075
|
alias throwIfAborted throw_if_aborted
|
|
@@ -928,26 +1079,35 @@ module Dommy
|
|
|
928
1079
|
when "aborted"
|
|
929
1080
|
@aborted
|
|
930
1081
|
when "reason"
|
|
931
|
-
|
|
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
|
|
932
1087
|
end
|
|
933
1088
|
end
|
|
934
1089
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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"
|
|
938
1095
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
1100
|
+
nil
|
|
943
1101
|
end
|
|
944
1102
|
|
|
1103
|
+
include Bridge::Methods
|
|
1104
|
+
js_methods %w[addEventListener removeEventListener dispatchEvent throwIfAborted]
|
|
945
1105
|
def __js_call__(method, args)
|
|
946
1106
|
case method
|
|
947
1107
|
when "addEventListener"
|
|
948
1108
|
add_event_listener(args[0], args[1], args[2])
|
|
949
1109
|
when "removeEventListener"
|
|
950
|
-
remove_event_listener(args[0], args[1])
|
|
1110
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
951
1111
|
when "dispatchEvent"
|
|
952
1112
|
dispatch_event(args[0])
|
|
953
1113
|
when "throwIfAborted"
|
|
@@ -955,12 +1115,18 @@ module Dommy
|
|
|
955
1115
|
end
|
|
956
1116
|
end
|
|
957
1117
|
|
|
958
|
-
|
|
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)
|
|
959
1124
|
return if @aborted
|
|
960
1125
|
|
|
961
1126
|
@aborted = true
|
|
962
|
-
|
|
963
|
-
|
|
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__)
|
|
964
1130
|
end
|
|
965
1131
|
end
|
|
966
1132
|
|
|
@@ -976,19 +1142,17 @@ module Dommy
|
|
|
976
1142
|
end
|
|
977
1143
|
|
|
978
1144
|
def __js_set__(_key, _value)
|
|
979
|
-
|
|
980
|
-
end
|
|
981
|
-
|
|
982
|
-
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
983
|
-
JS_METHOD_NAMES = %w[abort].freeze
|
|
984
|
-
def __js_method_names__
|
|
985
|
-
JS_METHOD_NAMES
|
|
1145
|
+
Bridge::UNHANDLED
|
|
986
1146
|
end
|
|
987
1147
|
|
|
1148
|
+
include Bridge::Methods
|
|
1149
|
+
js_methods %w[abort]
|
|
988
1150
|
def __js_call__(method, args)
|
|
989
1151
|
case method
|
|
990
1152
|
when "abort"
|
|
991
|
-
|
|
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])
|
|
992
1156
|
end
|
|
993
1157
|
end
|
|
994
1158
|
end
|
data/lib/dommy/event_source.rb
CHANGED
|
@@ -84,10 +84,14 @@ module Dommy
|
|
|
84
84
|
|
|
85
85
|
def __js_set__(key, value)
|
|
86
86
|
event = inline_event_for(key)
|
|
87
|
-
|
|
87
|
+
return Bridge::UNHANDLED unless event
|
|
88
|
+
|
|
89
|
+
set_inline_handler(event, value)
|
|
88
90
|
nil
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
include Bridge::Methods
|
|
94
|
+
js_methods %w[close addEventListener removeEventListener dispatchEvent]
|
|
91
95
|
def __js_call__(method, args)
|
|
92
96
|
case method
|
|
93
97
|
when "close"
|
|
@@ -95,7 +99,7 @@ module Dommy
|
|
|
95
99
|
when "addEventListener"
|
|
96
100
|
add_event_listener(args[0], args[1], args[2])
|
|
97
101
|
when "removeEventListener"
|
|
98
|
-
remove_event_listener(args[0], args[1])
|
|
102
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
99
103
|
when "dispatchEvent"
|
|
100
104
|
dispatch_event(args[0])
|
|
101
105
|
end
|