Module: Dommy::EventTarget

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

Instance Method Summary collapse

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).



172
173
174
175
176
177
178
179
180
# File 'lib/dommy/event.rb', line 172

def self.capture_flag(options)
  raw =
    if options.is_a?(Hash)
      options.key?("capture") ? options["capture"] : options[:capture]
    else
      options
    end
  js_truthy?(raw)
end

.js_truthy?(value) ⇒ Boolean

JS ToBoolean: false for false/null/undefined, +0/-0, NaN, and “”.

Returns:

  • (Boolean)


183
184
185
186
187
188
189
190
# File 'lib/dommy/event.rb', line 183

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?))
  return false if value == ""

  true
end

Instance Method Details

#__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.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/dommy/event.rb', line 120

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

    CallableInvoker.invoke_listener(entry.listener, event)

    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.



144
145
146
# File 'lib/dommy/event.rb', line 144

def __internal_event_parent__
  nil
end

#add_event_listener(type, listener = nil, options = nil, &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, options = nil, &block)
  cb = listener || block
  return nil if type.nil? || cb.nil?

  list = listeners_for(type.to_s)
  entry = Listener.new(cb, options)
  # 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 = options.is_a?(Hash) ? (options["signal"] || options[:signal]) : nil
  if signal.respond_to?(:__js_get__)
    if signal.__js_get__("aborted")
      remove_event_listener(type, cb, options)
    else
      target = self
      signal.__js_call__(
        "addEventListener",
        [
          "abort",
          proc {
            target.remove_event_listener(type, cb, options)
          }
        ]
      )
    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).



107
108
109
110
111
112
113
114
115
116
# File 'lib/dommy/event.rb', line 107

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

Raises:

  • (TypeError)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/dommy/event.rb', line 59

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

#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, options = 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(options)
  listeners_for(type.to_s).reject! do |entry|
    entry.listener.equal?(listener) && entry.capture? == capture
  end
  nil
end