Module: Dommy::EventTarget
- Included in:
- AbortSignal, Animation, Attr, BatteryManager, BroadcastChannel, CharacterDataNode, Clipboard, CookieStore, Document, DocumentType, Element, EventSource, FileReader, Fragment, MediaQueryList, MessagePort, Notification, PermissionStatus, ShadowRoot, StandaloneEventTarget, WakeLockSentinel, WebSocket, Window, Worker, XMLHttpRequest, XMLHttpRequestUpload
- Defined in:
- lib/dommy/event.rb
Overview
Note: Callback and Constructor live in Dommy::Bridge::* —
they're bridge-adapter classes, not part of the public DOM
surface.
Defined Under Namespace
Classes: Listener
Class Method Summary collapse
-
.capture_flag(options) ⇒ Object
The capture flag for an addEventListener/removeEventListener options argument, using JS — not Ruby — truthiness: a boolean useCapture, or the
capturemember of an options dictionary, where 0 / "" / NaN / null / undefined are falsy (in Ruby 0 and "" are truthy, so a naive!!is wrong). -
.js_truthy?(value) ⇒ Boolean
JS ToBoolean: false for false/null/undefined, +0/-0, NaN, and "".
Instance Method Summary collapse
-
#__dommy_dump_event_failure__(event, listener, error) ⇒ Object
Diagnostic only (DOMMY_EVENT_DEBUG=
): when a listener throws, append the event type + error to a log file so an otherwise-unreproducible page failure can be traced to the handler that choked. -
#__internal_deliver_event__(event, phase = :both) ⇒ Object
phaseis :capture (capture listeners), :bubble (non-capture), or :both (at the target). -
#__internal_event_parent__ ⇒ Object
The next target up the propagation path.
-
#__internal_process_event_handler_return__(event, result) ⇒ Object
WHATWG event handler processing algorithm, step "process return value": the special error handler (onerror on a Window) cancels the event when the handler returns true; onbeforeunload has its own returnValue handling (left alone); any other handler cancels when it returns exactly false (
return falsefrom onclick/onsubmit). - #add_event_listener(type, listener = nil, options = nil, event_handler: false, &block) ⇒ Object
-
#deliver_at(node, event, phase) ⇒ Object
Deliver
eventto one node's listeners for the current phase, then honor stopPropagation (throws to end the whole walk after this node finishes). - #dispatch_event(event) ⇒ Object
-
#event_name_from_on(key) ⇒ Object
Event handler IDL attributes (
el.onclick = fn,window.onload = fn): one named handler registered as a listener, where assignment replaces any previous handler and a nil value removes it. -
#invoke_listener_isolated(listener, event, current_target = nil) ⇒ Object
Run one listener, isolating a throw so it can't escape the dispatch.
- #on_handler(event_name) ⇒ Object
- #remove_event_listener(type, listener, options = nil) ⇒ Object
- #set_on_handler(event_name, value) ⇒ Object
Class Method Details
.capture_flag(options) ⇒ Object
The capture flag for an addEventListener/removeEventListener options
argument, using JS — not Ruby — truthiness: a boolean useCapture, or the
capture member of an options dictionary, where 0 / "" / NaN / null /
undefined are falsy (in Ruby 0 and "" are truthy, so a naive !! is wrong).
263 264 265 266 267 268 269 270 271 |
# File 'lib/dommy/event.rb', line 263 def self.capture_flag() raw = if .is_a?(Hash) .key?("capture") ? ["capture"] : [:capture] else end js_truthy?(raw) end |
.js_truthy?(value) ⇒ Boolean
JS ToBoolean: false for false/null/undefined, +0/-0, NaN, and "".
274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/dommy/event.rb', line 274 def self.js_truthy?(value) return false if value.nil? || value == false return false if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED) return false if value.is_a?(Numeric) && (value.zero? || (value.respond_to?(:nan?) && value.nan?)) # The bridge marshals JS NaN as the symbol :NaN (it has no Ruby Float # equivalent that survives the round-trip), which ToBoolean treats as false. return false if value == :NaN return false if value == "" true end |
Instance Method Details
#__dommy_dump_event_failure__(event, listener, error) ⇒ Object
Diagnostic only (DOMMY_EVENT_DEBUG=
206 207 208 209 210 211 212 213 214 |
# File 'lib/dommy/event.rb', line 206 def __dommy_dump_event_failure__(event, listener, error) path = ENV["DOMMY_EVENT_DEBUG"] line = "=== event listener raised on #{event.type}: #{error.class}: #{error..to_s[0, 200]} " \ "(listener=#{listener.class})\n" # `::File` — inside `module Dommy`, bare `File` is Dommy's DOM File. ::File.write(path, line, mode: "a") rescue StandardError nil end |
#__internal_deliver_event__(event, phase = :both) ⇒ Object
phase is :capture (capture listeners), :bubble (non-capture), or :both
(at the target). stopImmediatePropagation ends delivery within this node.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/dommy/event.rb', line 143 def __internal_deliver_event__(event, phase = :both) listeners = listeners_for(event.type).dup listeners.each do |entry| next unless phase == :both || (phase == :capture ? entry.capture? : !entry.capture?) # Spec: a `once` listener is removed BEFORE its callback runs, so a nested # dispatch from within the callback can't invoke it a second time. if entry.once? listeners_for(event.type).reject! do |candidate| candidate.listener.equal?(entry.listener) && candidate.capture? == entry.capture? end end result = if entry.passive? event.__internal_run_passive__ { invoke_listener_isolated(entry.listener, event, self) } else invoke_listener_isolated(entry.listener, event, self) end # Event handler processing algorithm: a handler registered via onX has its # return value processed — onerror on a global cancels on `true`, every # other handler cancels on `false` (`onsubmit="return false"`). __internal_process_event_handler_return__(event, result) if entry.event_handler? break if event.immediate_propagation_stopped? end nil end |
#__internal_event_parent__ ⇒ Object
The next target up the propagation path. The default (no parent) suits EventTargets that aren't tree nodes (AbortSignal, XHR, …); Element / Document / ShadowRoot override it to walk the node tree.
219 220 221 |
# File 'lib/dommy/event.rb', line 219 def __internal_event_parent__ nil end |
#__internal_process_event_handler_return__(event, result) ⇒ Object
WHATWG event handler processing algorithm, step "process return value":
the special error handler (onerror on a Window) cancels the event when the
handler returns true; onbeforeunload has its own returnValue handling
(left alone); any other handler cancels when it returns exactly false
(return false from onclick/onsubmit). A non-cancelable event ignores it.
178 179 180 181 182 183 184 |
# File 'lib/dommy/event.rb', line 178 def __internal_process_event_handler_return__(event, result) if event.type == "error" && defined?(Dommy::Window) && is_a?(Dommy::Window) event.__js_call__("preventDefault", []) if result == true elsif event.type != "beforeunload" && result == false event.__js_call__("preventDefault", []) end end |
#add_event_listener(type, listener = nil, options = nil, event_handler: false, &block) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/dommy/event.rb', line 9 def add_event_listener(type, listener = nil, = nil, event_handler: false, &block) cb = listener || block return nil if type.nil? || cb.nil? list = listeners_for(type.to_s) entry = Listener.new(cb, , event_handler) # Per spec, a listener is deduplicated by (type, callback, capture) — so # the same function may be registered once as a capture and once as a # bubble listener. return nil if list.any? { |e| e.listener.equal?(cb) && e.capture? == entry.capture? } list << entry # `{ signal: AbortSignal }` — when the signal aborts, auto- # remove the listener. Per spec, if the signal is already aborted # the listener must not be registered at all. signal = .is_a?(Hash) ? (["signal"] || [:signal]) : nil if signal.respond_to?(:__js_get__) if signal.__js_get__("aborted") remove_event_listener(type, cb, ) else target = self signal.__js_call__( "addEventListener", [ "abort", proc { target.remove_event_listener(type, cb, ) } ] ) end end nil end |
#deliver_at(node, event, phase) ⇒ Object
Deliver event to one node's listeners for the current phase, then honor
stopPropagation (throws to end the whole walk after this node finishes).
130 131 132 133 134 135 136 137 138 139 |
# File 'lib/dommy/event.rb', line 130 def deliver_at(node, event, phase) # Honor a stop-propagation flag set before reaching this node (including # one set before dispatch began) — the spec checks it before invoking a # node's listeners, not only after. throw :stop_propagation if event.propagation_stopped? event.__internal_set_current_target__(node) node.__internal_deliver_event__(event, phase) throw :stop_propagation if event.propagation_stopped? end |
#dispatch_event(event) ⇒ Object
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/dommy/event.rb', line 82 def dispatch_event(event) return true if event.nil? # Per spec, dispatchEvent must receive an Event instance. raise TypeError, "dispatchEvent requires an Event, got #{event.class}" unless event.is_a?(Event) event.__internal_prepare_for_dispatch__(self) event.__internal_set_dispatch_flag__(true) # The full propagation path: the target plus its ancestors (root last). # Capturing always traverses the ancestors regardless of `bubbles`. path = event.__js_get__("composed") ? composed_bubble_path(event) : event_bubble_path event.__internal_record_path__(path) if event.respond_to?(:__internal_record_path__) ancestors = path[1..] || [] catch(:stop_propagation) do # Capturing phase: root → … → parent, capture listeners only. event.__internal_set_event_phase__(Event::CAPTURING_PHASE) ancestors.reverse_each do |node| deliver_at(node, event, :capture) end # At the target: both capture and bubble listeners. event.__internal_set_event_phase__(Event::AT_TARGET) deliver_at(self, event, :both) # Bubbling phase: parent → … → root, bubble listeners only (only when # the event bubbles). if event.bubbles? event.__internal_set_event_phase__(Event::BUBBLING_PHASE) ancestors.each do |node| deliver_at(node, event, :bubble) end end end # After dispatch, currentTarget reverts to null and eventPhase to NONE, and # the propagation flags are unset so the event can be dispatched again. event.__internal_set_current_target__(nil) event.__internal_set_event_phase__(Event::NONE) event.__internal_clear_propagation_flags__ event.__internal_set_dispatch_flag__(false) !event.default_prevented? end |
#event_name_from_on(key) ⇒ Object
Event handler IDL attributes (el.onclick = fn, window.onload = fn): one
named handler registered as a listener, where assignment replaces any
previous handler and a nil value removes it. Shared by Element and Window.
62 63 64 |
# File 'lib/dommy/event.rb', line 62 def event_name_from_on(key) key.to_s.sub(/\Aon/, "").downcase end |
#invoke_listener_isolated(listener, event, current_target = nil) ⇒ Object
Run one listener, isolating a throw so it can't escape the dispatch. WHATWG "inner invoke": if a listener's callback throws, the exception is reported (to the global error handler) and event dispatch continues with the remaining listeners — a broken handler must not derail the others or abort whatever Ruby drove the dispatch (a synthetic click from the host). The engine already swallows this for a JS-initiated dispatchEvent (HostBridge#invoke_callback, raising:false), but that has proven engine/Ruby-version-dependent — on some QuickJS/Ruby combinations a JS listener's throw surfaces as a Ruby exception here — so guard the host-initiated path too, mirroring MutationObserver#flush.
196 197 198 199 200 201 |
# File 'lib/dommy/event.rb', line 196 def invoke_listener_isolated(listener, event, current_target = nil) CallableInvoker.invoke_listener(listener, event, current_target) rescue StandardError => e __dommy_dump_event_failure__(event, listener, e) if ENV["DOMMY_EVENT_DEBUG"] nil end |
#on_handler(event_name) ⇒ Object
66 67 68 |
# File 'lib/dommy/event.rb', line 66 def on_handler(event_name) @on_handlers&.[](event_name) end |
#remove_event_listener(type, listener, options = nil) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/dommy/event.rb', line 46 def remove_event_listener(type, listener, = nil) return nil if type.nil? || listener.nil? # Per spec, a listener is identified by (type, callback, capture) — so # removing must match the capture flag, not just the callback (a function # registered as both a capture and a bubble listener is two listeners). capture = EventTarget.capture_flag() listeners_for(type.to_s).reject! do |entry| entry.listener.equal?(listener) && entry.capture? == capture end nil end |
#set_on_handler(event_name, value) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/dommy/event.rb', line 70 def set_on_handler(event_name, value) @on_handlers ||= {} previous = @on_handlers[event_name] remove_event_listener(event_name, previous) if previous if value add_event_listener(event_name, value, event_handler: true) @on_handlers[event_name] = value else @on_handlers.delete(event_name) end end |