Class: Dommy::Element

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods, EventTarget, Internal::ParentNode, Node
Defined in:
lib/dommy/element.rb

Direct Known Subclasses

HTMLElement, SVGElement

Constant Summary collapse

HTML_NAMESPACE =
"http://www.w3.org/1999/xhtml"
REFLECTED_TOKEN_LIST_HOSTS =

Element + namespace combinations for which a reflected DOMTokenList IDL attribute is defined; elsewhere the attribute does not exist (→ undefined).

{
  "relList" => {html: %w[a area link], svg: %w[a]},
  "htmlFor" => {html: %w[output]},
  "sandbox" => {html: %w[iframe]},
  "sizes" => {html: %w[link]}
}.freeze
SVG_NAMESPACE =
"http://www.w3.org/2000/svg"
SHADOW_HOST_TAGS =

Elements that may host a Shadow DOM tree per the HTML spec. Custom-element-style names (containing “-”) are also allowed.

%w[
article
aside
blockquote
body
div
footer
h1
h2
h3
h4
h5
h6
header
main
nav
p
section
span
    ]
.freeze
ELEMENT_NODE =

Node type / NodeFilter bitmask constants — DOM Level 3 says these are exposed on both the constructor and every instance. Defined at the bottom of the class so subclasses inherit them too.

1
ATTRIBUTE_NODE =
2
TEXT_NODE =
3
CDATA_SECTION_NODE =
4
PROCESSING_INSTRUCTION_NODE =
7
COMMENT_NODE =
8
DOCUMENT_NODE =
9
DOCUMENT_TYPE_NODE =
10
DOCUMENT_FRAGMENT_NODE =
11
DOCUMENT_POSITION_DISCONNECTED =
0x01
DOCUMENT_POSITION_PRECEDING =
0x02
DOCUMENT_POSITION_FOLLOWING =
0x04
DOCUMENT_POSITION_CONTAINS =
0x08
DOCUMENT_POSITION_CONTAINED_BY =
0x10
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC =
0x20

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Methods included from Internal::ParentNode

#append, #append_child, #prepend, #replace_children

Methods included from Node

#compare_document_position, #is_default_namespace, #is_equal_node, #is_same_node, #lookup_namespace_uri, #lookup_prefix

Methods included from EventTarget

#__internal_deliver_event__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, js_truthy?, #remove_event_listener

Constructor Details

#initialize(document, nokogiri_node) ⇒ Element

Returns a new instance of Element.



1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
# File 'lib/dommy/element.rb', line 1071

def initialize(document, nokogiri_node)
  @document = document
  @__node__ = nokogiri_node
  @class_list = ClassList.new(self)
  @style = StyleDeclaration.new(self)
  @dataset = DatasetMap.new(self)
  # `HTMLCollection` re-evaluates the child list on every
  # property access so callers that capture `el[:children]` once
  # see DOM mutations made between iterations — required by list
  # reconciliation patterns that rely on the spec's live
  # HTMLCollection semantics to detect already-positioned nodes.
  @live_children = HTMLCollection.new do
    @__node__.element_children.map { |n| @document.wrap_node(n) }.compact
  end
  # Live `childNodes` (all node types, not just elements), cached so
  # `el.childNodes === el.childNodes` holds like the spec's live NodeList.
  @live_child_nodes = LiveNodeList.new do
    @__node__.children.map { |n| @document.wrap_node(n) }.compact
  end
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



1067
1068
1069
# File 'lib/dommy/element.rb', line 1067

def document
  @document
end

Instance Method Details

#[](key) ⇒ Object

el` / `el = …` bracket shortcut for the JS-style property access pattern. Useful when porting browser-side code to CRuby tests.



1838
1839
1840
# File 'lib/dommy/element.rb', line 1838

def [](key)
  __js_get__(key.to_s)
end

#[]=(key, value) ⇒ Object



1842
1843
1844
# File 'lib/dommy/element.rb', line 1842

def []=(key, value)
  __js_set__(key.to_s, value)
end

#__dommy_backend_node__Object



1069
# File 'lib/dommy/element.rb', line 1069

def __dommy_backend_node__ = @__node__

#__internal_set_namespace__(namespace, prefix, local_name, qualified_name) ⇒ Object

Record the namespace/prefix/localName an element was created with via createElementNS, so the getters report them faithfully (Nokogiri can’t always round-trip a foreign-namespace prefix).



1139
1140
1141
1142
1143
1144
1145
# File 'lib/dommy/element.rb', line 1139

def __internal_set_namespace__(namespace, prefix, local_name, qualified_name)
  @__ns_uri = namespace
  @__ns_prefix = prefix
  @__ns_local = local_name
  @__ns_qname = qualified_name
  nil
end

#__internal_shadow_root__Object

Internal — gives access to the shadow root regardless of mode. Used by event composition / ‘composedPath()`.



1607
1608
1609
# File 'lib/dommy/element.rb', line 1607

def __internal_shadow_root__
  @__shadow_root
end

#__js_call__(method, args) ⇒ Object



2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
# File 'lib/dommy/element.rb', line 2214

def __js_call__(method, args)
  case method
  when "hasChildNodes"
    has_child_nodes?
  when "hasAttributes"
    has_attributes?
  when "getAttribute"
    get_attribute(args[0])
  when "setAttribute"
    set_attribute(args[0], args[1])
  when "hasAttribute"
    has_attribute?(args[0])
  when "removeAttribute"
    remove_attribute(args[0])
  when "getAttributeNS"
    get_attribute_ns(args[0], args[1])
  when "setAttributeNS"
    set_attribute_ns(args[0], args[1], args[2])
  when "hasAttributeNS"
    has_attribute_ns?(args[0], args[1])
  when "removeAttributeNS"
    remove_attribute_ns(args[0], args[1])
  when "getAttributeNodeNS"
    get_attribute_node_ns(args[0], args[1])
  when "setAttributeNodeNS"
    set_attribute_node(args[0])
  when "getAttributeNames"
    get_attribute_names
  when "closest"
    raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?

    closest(args[0])
  when "querySelector"
    query_selector(Internal.css_query_arg!(args))
  when "querySelectorAll"
    query_selector_all(Internal.css_query_arg!(args))
  when "getElementsByClassName"
    get_elements_by_class_name(args[0])
  when "getElementsByTagNameNS"
    get_elements_by_tag_name_ns(args[0], args[1])
  when "getElementsByTagName"
    get_elements_by_tag_name(args[0])
  when "getRootNode"
    get_root_node
  when "normalize"
    normalize
  when "insertAdjacentElement"
    insert_adjacent_element(args[0], args[1])
  when "insertAdjacentHTML"
    insert_adjacent_html(args[0], args[1])
  when "insertAdjacentText"
    insert_adjacent_text(args[0], args[1])
  when "toggleAttribute"
    toggle_attribute(args[0], args[1])
  when "matches"
    raise Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?

    matches?(args[0])
  when "isEqualNode"
    is_equal_node(args[0])
  when "isSameNode"
    is_same_node(args[0])
  when "compareDocumentPosition"
    compare_document_position(args[0])
  when "lookupNamespaceURI"
    lookup_namespace_uri(args[0])
  when "lookupPrefix"
    lookup_prefix(args[0])
  when "isDefaultNamespace"
    is_default_namespace(args[0])
  when "contains"
    contains?(args[0])
  when "toString"
    to_s
  when "getAttributeNode"
    get_attribute_node(args[0])
  when "setAttributeNode"
    set_attribute_node(args[0])
  when "removeAttributeNode"
    remove_attribute_node(args[0])
  when "focus"
    focus
  when "blur"
    blur
  when "attachShadow"
    attach_shadow(args[0])
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1], args[2])
  when "dispatchEvent"
    dispatch_event(args[0])
  when "appendChild"
    append_child(args[0])
  when "insertBefore"
    insert_before(args[0], args[1])
  when "removeChild"
    remove_child(args[0])
  when "replaceChild"
    replace_child(args[0], args[1])
  when "cloneNode"
    clone_node(args[0])
  when "append"
    append(*args)
  when "prepend"
    prepend(*args)
  when "replaceChildren"
    replace_children(*args)
  when "before"
    insert_adjacent(:before, args)
  when "after"
    insert_adjacent(:after, args)
  when "getInnerHTML", "getHTML"
    inner_html
  when "remove"
    remove
  when "replaceWith"
    replace_with(args)
  when "click"
    click
  when "getBoundingClientRect"
    get_bounding_client_rect
  when "getClientRects"
    get_client_rects
  when "scrollIntoView", "scroll", "scrollTo", "scrollBy"
    record_scroll(method, args)
  when "requestFullscreen"
    request_fullscreen
  when "showPopover"
    show_popover
  when "hidePopover"
    hide_popover
  when "togglePopover"
    toggle_popover
  else
    nil
  end
end

#__js_get__(key) ⇒ Object



1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
# File 'lib/dommy/element.rb', line 1846

def __js_get__(key)
  case key
  when "nodeType"
    1
  when "isConnected"
    is_connected?
  when
      "scrollTop",
      "scrollLeft",
      "scrollWidth",
      "scrollHeight",
      "clientWidth",
      "clientHeight",
      "clientTop",
      "clientLeft",
      "offsetWidth",
      "offsetHeight",
      "offsetTop",
      "offsetLeft"
    # No layout engine — zeroed values match what real browsers
    # report for hidden / pre-paint elements.
    0
  when "offsetParent"
    nil
  when "popover"
    get_attribute("popover")
  when "children"
    @live_children
  when "childNodes"
    @live_child_nodes
  when "firstChild"
    first_child
  when "lastChild"
    last_child
  when "childElementCount"
    child_element_count
  when "lastElementChild"
    last_element_child
  when "nextSibling"
    next_sibling
  when "previousSibling"
    previous_sibling
  when "nextElementSibling"
    next_element_sibling
  when "previousElementSibling"
    previous_element_sibling
  when "firstElementChild"
    first_element_child
  when "parentElement", "parent"
    wrap_parent(@__node__.parent)
  when "parentNode"
    # `parentNode` is broader than `parentElement` — includes
    # DocumentFragment / Document parents too. Reconcilers use
    # this to find the host before calling replaceChild.
    @__node__.parent && @document.wrap_node(@__node__.parent)
  when "textContent"
    @__node__.text
  when "innerHTML"
    inner_html
  when "outerHTML"
    outer_html
  when "tagName"
    tag_name
  when "prefix"
    element_prefix
  when "classList"
    @class_list
  when "relList"
    reflected_token_list("relList", "rel")
  when "htmlFor"
    reflected_token_list("htmlFor", "for")
  when "sandbox"
    reflected_token_list("sandbox", "sandbox")
  when "sizes"
    reflected_token_list("sizes", "sizes")
  when "style"
    @style
  when "dataset"
    @dataset
  when "content"
    template_content
  when "className"
    # DOM reflects the `class` attribute as the `className` string
    # property (space-separated tokens, "" when absent).
    @__node__["class"].to_s
  when "id"
    @__node__["id"].to_s
  when "hidden", "disabled", "checked", "readOnly", "multiple", "required"
    # Boolean reflected properties — true iff the matching HTML
    # attribute is present. Real DOM normalizes attribute names to
    # lowercase, mapped here too (e.g. `readOnly` ↔ `readonly`).
    @__node__.key?(reflected_attr_name(key))
  when "value"
    # For form elements `value` is a property that defaults to the
    # `value` attribute. We don't model the property/attribute
    # split here — both reads and writes go through the attribute.
    @__node__["value"].to_s
  when "href"
    anchor_href
  when "attributes"
    attributes
  when "namespaceURI"
    namespace_uri
  when "localName"
    local_name
  when "nodeName"
    tag_name
  when "slot"
    slot
  when "role"
    aria_get("role")
  when "accessKeyLabel"
    access_key_label
  when "baseURI"
    base_uri
  when "shadowRoot"
    shadow_root
  when "ownerDocument"
    @document
  else
    if (elements_attr = aria_elements_attr(key))
      # Plural ARIA element references (`ariaDescribedByElements` ↔
      # `aria-describedby`) — a list of Elements.
      aria_elements_get(elements_attr, key)
    elsif (element_attr = aria_element_attr(key))
      # ARIA element-reference IDL attribute (`ariaActiveDescendantElement`
      # ↔ `aria-activedescendant`) — resolves to an Element or null.
      aria_element_get(element_attr, key)
    elsif (content_attr = aria_content_attr(key))
      # ARIA / role reflected IDL attribute (`ariaLabel` ↔ `aria-label`,
      # `role` ↔ `role`) — a nullable DOMString (null when absent).
      aria_get(content_attr)
    elsif key.start_with?("on") && key.length > 2
      # `el.onXxx` event handler property — the registered callback or nil.
      @on_handlers&.[](event_name_from_on(key))
    end
  end
end

#__js_set__(key, value) ⇒ Object



2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
# File 'lib/dommy/element.rb', line 2143

def __js_set__(key, value)
  case key
  when "textContent"
    self.text_content = value
  when "innerHTML"
    self.inner_html = value
  when "outerHTML"
    # [CEReactions, LegacyNullToEmptyString] DOMString — null becomes "".
    self.outer_html = value.nil? ? "" : value.to_s
  when "hidden", "disabled", "checked", "readOnly", "multiple", "required"
    # Boolean reflected property — funnel through set_attribute /
    # remove_attribute so MutationObserver attribute records fire.
    name = reflected_attr_name(key)
    if value
      set_attribute(name, "")
    elsif @__node__.key?(name)
      remove_attribute(name)
    end

  when "className"
    set_attribute("class", value.to_s)
  when "classList"
    # WHATWG [PutForwards=value]: `el.classList = x` forwards to
    # `el.classList.value = x` (set the class attribute). Handling it here
    # (instead of letting the write fall through as unhandled) stops the JS
    # bridge from stashing a string expando that would shadow the classList
    # getter for the rest of the element's life.
    set_attribute("class", value.to_s)
  when "id"
    set_attribute("id", value.to_s)
  when "value"
    set_attribute("value", value.to_s)
  when "slot"
    set_attribute("slot", value.to_s)
  when "role"
    aria_set("role", value)
  else
    if (elements_attr = aria_elements_attr(key))
      # Plural ARIA element references setter (list of Elements).
      aria_elements_set(elements_attr, key, value)
    elsif (element_attr = aria_element_attr(key))
      # ARIA element-reference IDL attribute setter.
      aria_element_set(element_attr, key, value)
    elsif (content_attr = aria_content_attr(key))
      # ARIA / role reflected nullable DOMString (null/undefined → remove).
      aria_set(content_attr, value)
    elsif key.start_with?("on") && key.length > 2
      # `el.onXxx = fn` registers fn as a single named handler; nil removes.
      set_on_handler(event_name_from_on(key), value)
    else
      # Not a known DOM property — tell the JS host to keep it as a
      # JS-side expando (so object/instance fields keep their identity).
      Bridge::UNHANDLED
    end
  end
end

#__test_scroll_log__Object

Test inspector for scroll calls (no real layout to scroll).



2615
2616
2617
# File 'lib/dommy/element.rb', line 2615

def __test_scroll_log__
  @scroll_log ||= []
end

#access_key_labelObject

‘accessKeyLabel` — the assigned access key’s platform label. The ‘accesskey` content attribute is a set of one-code-point candidates; a single valid candidate yields a (modifier-prefixed) label, anything else (empty, or multiple/multi-char tokens) yields the empty string. The exact modifier varies by platform — tests only assert non-empty vs empty.



2004
2005
2006
2007
2008
2009
# File 'lib/dommy/element.rb', line 2004

def access_key_label
  keys = @__node__["accesskey"].to_s.split(/[ \t\n\f\r]+/).reject(&:empty?)
  return "" unless keys.length == 1 && keys.first.length == 1

  "Alt+#{keys.first.upcase}"
end

#after(*args) ⇒ Object



1768
1769
1770
# File 'lib/dommy/element.rb', line 1768

def after(*args)
  insert_adjacent(:after, args)
end

#anchor_hrefObject

Anchor / area ‘href` IDL attribute reflects the attribute resolved against the document base URL (browser semantics). Routers rely on this to compare origins and detect external links.



1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
# File 'lib/dommy/element.rb', line 1988

def anchor_href
  raw = @__node__["href"]
  return "" if raw.nil?

  win = @document.default_view
  base = win&.location ? win.location.href : ""
  URI.join(base, raw.to_s).to_s
rescue URI::InvalidURIError, ArgumentError
  raw.to_s
end

#animate(keyframes, options = nil) ⇒ Object

Web Animations: start an animation on this element. Returns the new Animation. Dommy doesn’t interpolate; the animation simply transitions through the ‘playState` lifecycle, finishing via `scheduler.advance_time(duration)` or an explicit `animation.finish`.



2488
2489
2490
2491
2492
2493
2494
2495
# File 'lib/dommy/element.rb', line 2488

def animate(keyframes, options = nil)
  effect = KeyframeEffect.new(self, keyframes, options)
  animation = Animation.new(effect, nil, window: @document.default_view)
  @__animations ||= []
  @__animations << animation
  animation.play
  animation
end

#aria_content_attr(key) ⇒ Object

The content attribute a role/ARIA IDL attribute reflects, or nil for a non-ARIA key. ‘role` → “role”; `ariaXxx` → “aria-” + the rest, lowercased with humps removed (`ariaAutoComplete` → “aria-autocomplete”, `ariaColIndexText` → “aria-colindextext”).



2111
2112
2113
2114
2115
2116
2117
# File 'lib/dommy/element.rb', line 2111

def aria_content_attr(key)
  return "role" if key == "role"
  return nil unless key.is_a?(String) && key.length > 4 && key.start_with?("aria")
  return nil unless key[4] =~ /[A-Z]/

  "aria-#{key[4..].downcase}"
end

#aria_element_attr(key) ⇒ Object

The content attribute an ARIA element-reference IDL attribute reflects (‘ariaActiveDescendantElement` → “aria-activedescendant”, `ariaErrorMessageElement` → “aria-errormessage”), or nil. The IDL name is `aria<Xxx>Element`; the content attribute is “aria-” + <Xxx> lowercased.



2015
2016
2017
2018
2019
2020
# File 'lib/dommy/element.rb', line 2015

def aria_element_attr(key)
  return nil unless key.is_a?(String) && key.start_with?("aria") && key.end_with?("Element")
  return nil unless key.length > 11 && key[4] =~ /[A-Z]/

  "aria-#{key[4...-7].downcase}"
end

#aria_element_get(content_attr, key) ⇒ Object

Read an ARIA element reference: an explicitly-set Element wins; otherwise the content attribute is resolved as an IDREF (the element with that id in this element’s tree), or null.



2025
2026
2027
2028
2029
2030
2031
2032
2033
# File 'lib/dommy/element.rb', line 2025

def aria_element_get(content_attr, key)
  explicit = (@aria_element_refs ||= {})[key]
  return explicit if explicit

  idref = @__node__[content_attr].to_s
  return nil if idref.empty?

  aria_find_in_root(idref)
end

#aria_element_set(content_attr, key, value) ⇒ Object

Set an ARIA element reference: null/undefined clears it and removes the content attribute; an Element stores the explicit reference and sets the content attribute to the empty string (per the reflection spec).



2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
# File 'lib/dommy/element.rb', line 2038

def aria_element_set(content_attr, key, value)
  refs = (@aria_element_refs ||= {})
  if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
    refs.delete(key)
    remove_attribute(content_attr) if @__node__.key?(content_attr)
  else
    # set_attribute clears explicit refs via its aria-* hook, so store the
    # new reference afterward.
    set_attribute(content_attr, "")
    refs[key] = value
  end
  nil
end

#aria_elements_attr(key) ⇒ Object

The content attribute a plural ARIA element-references IDL attribute reflects (‘ariaDescribedByElements` → “aria-describedby”, `ariaLabelledByElements` → “aria-labelledby”), or nil. The IDL name is `aria<Xxx>Elements`; the content attribute is “aria-” + <Xxx> lowercased.



2056
2057
2058
2059
2060
2061
# File 'lib/dommy/element.rb', line 2056

def aria_elements_attr(key)
  return nil unless key.is_a?(String) && key.start_with?("aria") && key.end_with?("Elements")
  return nil unless key.length > 12 && key[4] =~ /[A-Z]/

  "aria-#{key[4...-8].downcase}"
end

#aria_elements_get(content_attr, key) ⇒ Object

Read a plural ARIA element references value (a list of Elements): the explicitly-set array wins; otherwise the content attribute is split as a space-separated IDREF list and each resolved (missing ids dropped).



2066
2067
2068
2069
2070
2071
2072
2073
# File 'lib/dommy/element.rb', line 2066

def aria_elements_get(content_attr, key)
  explicit = (@aria_elements_refs ||= {})[key]
  return explicit.dup if explicit

  @__node__[content_attr].to_s.split(/[ \t\n\f\r]+/).reject(&:empty?).filter_map do |id|
    aria_find_in_root(id)
  end
end

#aria_elements_set(content_attr, key, value) ⇒ Object

Set a plural ARIA element references value: null/undefined clears it and removes the content attribute; an array of Elements is stored and the content attribute is set to the empty string.



2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
# File 'lib/dommy/element.rb', line 2088

def aria_elements_set(content_attr, key, value)
  refs = (@aria_elements_refs ||= {})
  if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
    refs.delete(key)
    remove_attribute(content_attr) if @__node__.key?(content_attr)
  else
    set_attribute(content_attr, "")
    refs[key] = Array(value)
  end
  nil
end

#aria_find_in_root(id) ⇒ Object

Resolve an ARIA IDREF within this element’s tree ROOT (its topmost ancestor) rather than the document — so references keep working when the subtree is disconnected from the document.



2078
2079
2080
2081
2082
2083
# File 'lib/dommy/element.rb', line 2078

def aria_find_in_root(id)
  root = @__node__
  root = root.parent while root.parent && !root.parent.is_a?(Backend.document_class)
  node = ([root] + root.css("*").to_a).find { |n| n["id"].to_s == id }
  node && @document.wrap_node(node)
end

#aria_get(content_attr) ⇒ Object

Read a reflected nullable DOMString: the content attribute value, or nil (→ JS null) when the attribute is absent.



2121
2122
2123
# File 'lib/dommy/element.rb', line 2121

def aria_get(content_attr)
  @__node__.key?(content_attr) ? @__node__[content_attr].to_s : nil
end

#aria_set(content_attr, value) ⇒ Object

Write a reflected nullable DOMString: null / undefined removes the content attribute; any other value is ToString-coerced and set.



2127
2128
2129
2130
2131
2132
2133
2134
# File 'lib/dommy/element.rb', line 2127

def aria_set(content_attr, value)
  if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
    remove_attribute(content_attr) if @__node__.key?(content_attr)
  else
    set_attribute(content_attr, value.to_s)
  end
  nil
end

#at_xpath(expression) ⇒ Object

XPath queries scoped to this element, returning wrapped nodes.



2540
2541
2542
2543
# File 'lib/dommy/element.rb', line 2540

def at_xpath(expression)
  node = @__node__.at_xpath(expression)
  node && @document.wrap_node(node)
end

#attach_shadow(options = nil) ⇒ Object

‘el.attachShadow({ mode: “open” | “closed” })` — creates and attaches a ShadowRoot. The shadow tree lives in its own Nokogiri fragment and is invisible to the outer querySelector / children chain. Per spec:

- the `mode` field is REQUIRED in the init dict
- only certain host element types are valid (see SHADOW_HOST_TAGS)
- re-attaching to an element that already has a shadow throws


1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
# File 'lib/dommy/element.rb', line 1572

def attach_shadow(options = nil)
  tag = @__node__.name.downcase
  unless SHADOW_HOST_TAGS.include?(tag) || tag.include?("-")
    raise DOMException::NotSupportedError, "<#{tag}> cannot host a shadow root"
  end

  raise DOMException::InvalidStateError, "Shadow root already attached" if @__shadow_root

  opts = options.is_a?(Hash) ? options : {}
  mode_raw = opts.key?("mode") ? opts["mode"] : opts[:mode]
  raise TypeError, "attachShadow init dictionary requires 'mode'" if mode_raw.nil?

  mode = mode_raw.to_s
  raise DOMException::SyntaxError, "mode must be 'open' or 'closed'" unless %w[open closed].include?(mode)

  @__shadow_root = ShadowRoot.new(
    self,
    mode: mode,
    delegates_focus: opts["delegatesFocus"] || opts[:delegatesFocus] || false,
    slot_assignment: opts["slotAssignment"] || opts[:slotAssignment] || "named"
  )
  @__shadow_root
end

#attributesObject

NamedNodeMap of attributes. Lazily allocated and re-used so ‘el.attributes === el.attributes` and `attr.ownerElement === el`.



1437
1438
1439
# File 'lib/dommy/element.rb', line 1437

def attributes
  @attributes ||= NamedNodeMap.new(self)
end

#base_uriObject

‘Node.baseURI` — resolves against the document’s base URL, which in turn honors the first ‘<base href>` element (see `Document#base_uri`).



1490
1491
1492
# File 'lib/dommy/element.rb', line 1490

def base_uri
  @document.base_uri
end

#before(*args) ⇒ Object

ChildNode mixin — before / after / replaceWith with mixed args.



1764
1765
1766
# File 'lib/dommy/element.rb', line 1764

def before(*args)
  insert_adjacent(:before, args)
end

#blurObject



1536
1537
1538
1539
# File 'lib/dommy/element.rb', line 1536

def blur
  @document.__internal_set_active_element__(nil)
  nil
end

#child_element_countObject



1246
1247
1248
# File 'lib/dommy/element.rb', line 1246

def child_element_count
  @__node__.element_children.size
end

#child_nodesObject



1250
1251
1252
# File 'lib/dommy/element.rb', line 1250

def child_nodes
  NodeList.new(@__node__.children.map { |n| @document.wrap_node(n) }.compact)
end

#childrenObject



1216
1217
1218
# File 'lib/dommy/element.rb', line 1216

def children
  @live_children
end

#class_listObject



1177
1178
1179
# File 'lib/dommy/element.rb', line 1177

def class_list
  @class_list
end

#class_nameObject



1169
1170
1171
# File 'lib/dommy/element.rb', line 1169

def class_name
  @__node__["class"].to_s
end

#class_name=(value) ⇒ Object



1173
1174
1175
# File 'lib/dommy/element.rb', line 1173

def class_name=(value)
  set_attribute("class", value.to_s)
end

#clear_aria_element_ref_for(content_attr) ⇒ Object

Drop any explicit ARIA element reference (singular or plural) whose content attribute was just set directly (so the IDL getter re-resolves the IDREF).



2102
2103
2104
2105
# File 'lib/dommy/element.rb', line 2102

def clear_aria_element_ref_for(content_attr)
  @aria_element_refs&.delete_if { |key, _| aria_element_attr(key) == content_attr }
  @aria_elements_refs&.delete_if { |key, _| aria_elements_attr(key) == content_attr }
end

#clickObject



1787
1788
1789
# File 'lib/dommy/element.rb', line 1787

def click
  dispatch_event(MouseEvent.new("click", "bubbles" => true, "cancelable" => true, "button" => 0))
end

#clone_node(deep_arg) ⇒ Object



2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
# File 'lib/dommy/element.rb', line 2601

def clone_node(deep_arg)
  # Copy the node in place via libxml's deep dup, NOT by re-parsing to_html as
  # a fragment: the HTML fragment parser unwraps `<body>` / `<head>` /
  # `<html>`, so cloning a body produced its children, not a body element
  # (which broke Turbo's snapshot cache — it clones the body and restores it
  # via documentElement.replaceChild on back/forward). dup(1) preserves the
  # element's namespace and attributes (createElement would lose the
  # namespace); for a shallow clone we keep that node but drop its subtree.
  copy = @__node__.dup(1)
  copy.children.each(&:unlink) unless deep_arg
  @document.wrap_node(copy)
end

#closest(selector) ⇒ Object



2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
# File 'lib/dommy/element.rb', line 2444

def closest(selector)
  return nil if selector.nil?
  Internal.validate_selector!(selector)

  # Elements matching the selector (scoped to this element, so `:scope`
  # resolves here), then return the nearest inclusive ancestor among them.
  handler = Internal.scoped_pseudo_handlers(@__node__)
  safe = Internal.backend_safe_selector(selector.to_s)
  matched = with_selector_errors(selector) do
    @document.nokogiri_doc.css(safe, handler).map(&:pointer_id)
  end

  node = @__node__
  while node&.element?
    return @document.wrap_node(node) if matched.include?(node.pointer_id)

    node = node.parent
  end

  nil
end

#contains?(other) ⇒ Boolean

‘el.contains(other)` — true if `other` is `el` itself or any descendant. Per spec, returns false for null/non-Node.

Returns:

  • (Boolean)


1330
1331
1332
1333
1334
1335
1336
1337
# File 'lib/dommy/element.rb', line 1330

def contains?(other)
  return false unless other.respond_to?(:__dommy_backend_node__)

  other_node = other.__dommy_backend_node__
  return true if other_node == @__node__

  Internal::NodeTraversal.ancestor_of?(@__node__, other_node)
end

#datasetObject



1212
1213
1214
# File 'lib/dommy/element.rb', line 1212

def dataset
  @dataset
end

#element_prefixObject



1157
1158
1159
# File 'lib/dommy/element.rb', line 1157

def element_prefix
  @__ns_prefix
end

#equal_node?(other) ⇒ Boolean

Structural equality — same nodeType, same tagName, same attribute set, and recursively-equal children. Used by linkedom test suite and standard DOM Node.isEqualNode.

Returns:

  • (Boolean)


1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
# File 'lib/dommy/element.rb', line 1744

def equal_node?(other)
  return false unless other.is_a?(Element)
  return false unless @__node__.name == other.__dommy_backend_node__.name
  return false unless attribute_signature == other.send(:attribute_signature)
  return false unless @__node__.children.size == other.__dommy_backend_node__.children.size

  @__node__.children.zip(other.__dommy_backend_node__.children).all? do |a, b|
    wa = @document.wrap_node(a)
    wb = @document.wrap_node(b)
    wa.respond_to?(:equal_node?) ? wa.equal_node?(wb) : a.content == b.content
  end
end

#first_childObject



1238
1239
1240
# File 'lib/dommy/element.rb', line 1238

def first_child
  @document.wrap_node(@__node__.children.first)
end

#first_element_childObject



1230
1231
1232
# File 'lib/dommy/element.rb', line 1230

def first_element_child
  @document.wrap_node(@__node__.element_children.first)
end

#focusObject

‘focus()` / `blur()` — Dommy has no layout / real focus, but tests rely on `document.activeElement` updating. Track the most recently focused element on the document.



1531
1532
1533
1534
# File 'lib/dommy/element.rb', line 1531

def focus
  @document.__internal_set_active_element__(self)
  nil
end

#get_animations(_options = nil) ⇒ Object Also known as: getAnimations



2497
2498
2499
# File 'lib/dommy/element.rb', line 2497

def get_animations(_options = nil)
  (@__animations ||= []).dup
end

#get_attribute(name) ⇒ Object



2353
2354
2355
2356
2357
# File 'lib/dommy/element.rb', line 2353

def get_attribute(name)
  return nil if name.nil?

  @__node__[normalize_attr_key(name)]
end

#get_attribute_namesObject



1791
1792
1793
# File 'lib/dommy/element.rb', line 1791

def get_attribute_names
  Backend.attribute_nodes(@__node__).map(&:name)
end

#get_attribute_node(name) ⇒ Object



1441
1442
1443
# File 'lib/dommy/element.rb', line 1441

def get_attribute_node(name)
  attributes.get_named_item(name)
end

#get_attribute_node_ns(namespace, local_name) ⇒ Object



2440
2441
2442
# File 'lib/dommy/element.rb', line 2440

def get_attribute_node_ns(namespace, local_name)
  attributes.get_named_item_ns(namespace, local_name)
end

#get_attribute_ns(namespace, local_name) ⇒ Object

—– Namespaced attributes (DOM *AttributeNS) —–



2403
2404
2405
2406
2407
2408
# File 'lib/dommy/element.rb', line 2403

def get_attribute_ns(namespace, local_name)
  return nil if local_name.nil?

  ns = namespace.to_s
  Backend.get_attribute_ns(@__node__, ns.empty? ? nil : ns, local_name.to_s)
end

#get_bounding_client_rectObject

No layout engine — geometry getters return zeroed rects.



1796
1797
1798
# File 'lib/dommy/element.rb', line 1796

def get_bounding_client_rect
  DOMRect.new
end

#get_client_rectsObject



1800
1801
1802
# File 'lib/dommy/element.rb', line 1800

def get_client_rects
  []
end

#get_elements_by_class_name(name) ⇒ Object



1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
# File 'lib/dommy/element.rb', line 1408

def get_elements_by_class_name(name)
  tokens = name.to_s.split(/\s+/).reject(&:empty?)
  root = @__node__
  doc = @document
  HTMLCollection.new do
    next [] if tokens.empty?

    selector = tokens.map { |t| ".#{t}" }.join("")
    root.css(selector).map { |n| doc.wrap_node(n) }.compact
  end
end

#get_elements_by_tag_name(name) ⇒ Object



1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
# File 'lib/dommy/element.rb', line 1420

def get_elements_by_tag_name(name)
  n = name.to_s.downcase
  root = @__node__
  doc = @document
  if n == "*"
    HTMLCollection.new { root.css("*").map { |x| doc.wrap_node(x) }.compact }
  else
    HTMLCollection.new { root.css(n).map { |x| doc.wrap_node(x) }.compact }
  end
end

#get_elements_by_tag_name_ns(namespace, local_name) ⇒ Object



1431
1432
1433
# File 'lib/dommy/element.rb', line 1431

def get_elements_by_tag_name_ns(namespace, local_name)
  HTMLCollection.elements_by_tag_name_ns(@__node__, @document, namespace, local_name)
end

#get_html(_options = nil) ⇒ Object



1783
1784
1785
# File 'lib/dommy/element.rb', line 1783

def get_html(_options = nil)
  inner_html
end

#get_inner_html(_options = nil) ⇒ Object

‘getInnerHTML()` — happy-dom alias for the `innerHTML` getter. Real browsers add a `{ includeShadowRoots }` option which we ignore (no Shadow DOM in Dommy).



1779
1780
1781
# File 'lib/dommy/element.rb', line 1779

def get_inner_html(_options = nil)
  inner_html
end

#has_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


2377
2378
2379
2380
2381
# File 'lib/dommy/element.rb', line 2377

def has_attribute?(name)
  return false if name.nil?

  @__node__.key?(normalize_attr_key(name))
end

#has_attribute_ns?(namespace, local_name) ⇒ Boolean

Returns:

  • (Boolean)


2410
2411
2412
2413
2414
2415
# File 'lib/dommy/element.rb', line 2410

def has_attribute_ns?(namespace, local_name)
  return false if local_name.nil?

  ns = namespace.to_s
  Backend.has_attribute_ns?(@__node__, ns.empty? ? nil : ns, local_name.to_s)
end

#has_attributes?Boolean

Returns:

  • (Boolean)


1266
1267
1268
# File 'lib/dommy/element.rb', line 1266

def has_attributes?
  Backend.attribute_nodes(@__node__).any?
end

#has_child_nodes?Boolean

Returns:

  • (Boolean)


1262
1263
1264
# File 'lib/dommy/element.rb', line 1262

def has_child_nodes?
  @__node__.children.any?
end

#hide_popoverObject



1816
1817
1818
1819
# File 'lib/dommy/element.rb', line 1816

def hide_popover
  toggle_popover_state(false)
  nil
end

#idObject



1161
1162
1163
# File 'lib/dommy/element.rb', line 1161

def id
  @__node__["id"].to_s
end

#id=(value) ⇒ Object



1165
1166
1167
# File 'lib/dommy/element.rb', line 1165

def id=(value)
  set_attribute("id", value.to_s)
end

#inner_htmlObject



1113
1114
1115
1116
1117
1118
1119
# File 'lib/dommy/element.rb', line 1113

def inner_html
  if @__node__.name == "template"
    @document.template_content_inner_html(self)
  else
    @__node__.inner_html
  end
end

#inner_html=(value) ⇒ Object



1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
# File 'lib/dommy/element.rb', line 1121

def inner_html=(value)
  removed = @__node__.children.to_a
  if @__node__.name == "template"
    # `<template>` content is invisible to outer selectors in real DOM (it
    # lives in a separate DocumentFragment exposed via `[:content]`).
    @document.attach_template_content(self, value.to_s)
  else
    @__node__.inner_html = value.to_s
    @document.migrate_template_descendants(@__node__)
  end
  notify_child_list(added: @__node__.children.to_a, removed: removed)
end

#insert_adjacent_element(position, element) ⇒ Object

‘el.insertAdjacentElement(position, element)` — DOM spec positions: “beforebegin”, “afterbegin”, “beforeend”, “afterend”. Returns the inserted element or nil if position has no anchor (root cases).



1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
# File 'lib/dommy/element.rb', line 1614

def insert_adjacent_element(position, element)
  return nil unless element.respond_to?(:__dommy_backend_node__)

  case position.to_s
  when "beforebegin"
    return nil unless @__node__.parent

    node = detach_for_insert(element)
    @__node__.add_previous_sibling(node)
    notify_child_list(added: [node], target: @__node__.parent)
  when "afterbegin"
    node = detach_for_insert(element)
    first = @__node__.children.first
    first ? first.add_previous_sibling(node) : @__node__.add_child(node)
    notify_child_list(added: [node])
  when "beforeend"
    node = detach_for_insert(element)
    @__node__.add_child(node)
    notify_child_list(added: [node])
  when "afterend"
    return nil unless @__node__.parent

    node = detach_for_insert(element)
    @__node__.add_next_sibling(node)
    notify_child_list(added: [node], target: @__node__.parent)
  else
    return nil
  end

  element
end

#insert_adjacent_html(position, html) ⇒ Object



1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
# File 'lib/dommy/element.rb', line 1646

def insert_adjacent_html(position, html)
  # Position is ASCII case-insensitive ("beforeBegin" == "beforebegin").
  pos = position.to_s.downcase
  unless %w[beforebegin afterbegin beforeend afterend].include?(pos)
    raise DOMException::SyntaxError, "The value provided ('#{position}') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'."
  end

  fragment = Parser.fragment(html.to_s, owner_doc: @__node__.document)
  nodes = fragment.children.to_a
  # `add_previous_sibling` inserts immediately before the anchor, so a forward
  # walk preserves document order; `add_next_sibling` inserts immediately
  # after, so afterend walks in reverse to keep order.
  case pos
  when "beforebegin"
    parent = insertion_parent!
    nodes.each { |n| @__node__.add_previous_sibling(n) }
    notify_child_list(added: nodes, target: parent)
  when "afterbegin"
    first = @__node__.children.first
    if first
      nodes.each { |n| first.add_previous_sibling(n) }
    else
      nodes.each { |n| @__node__.add_child(n) }
    end

    notify_child_list(added: nodes)
  when "beforeend"
    nodes.each { |n| @__node__.add_child(n) }
    notify_child_list(added: nodes)
  when "afterend"
    parent = insertion_parent!
    nodes.reverse_each { |n| @__node__.add_next_sibling(n) }
    notify_child_list(added: nodes, target: parent)
  end

  nil
end

#insert_adjacent_text(position, text) ⇒ Object



1697
1698
1699
1700
1701
# File 'lib/dommy/element.rb', line 1697

def insert_adjacent_text(position, text)
  return nil if text.to_s.empty?

  insert_adjacent_element(position, @document.create_text_node(text.to_s))
end

#insert_before(child, reference) ⇒ Object



2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
# File 'lib/dommy/element.rb', line 2554

def insert_before(child, reference)
  check_hierarchy!(child)
  nodes = detach_dom_nodes(child)
  if reference.nil?
    append_dom_nodes(nodes)
  else
    ref_node = unwrap_dom_node(reference)
    if ref_node&.parent != @__node__
      # Per spec this should be a NotFoundError, but the legacy
      # behaviour of `appendChild` when reference is foreign is a
      # silent append. Preserve that for compatibility.
      append_dom_nodes(nodes)
    else
      nodes.reverse_each { |node| ref_node.add_previous_sibling(node) }
    end
  end

  notify_child_list(added: nodes)
  child
end

#insertion_parent!Object

The parent that a beforebegin/afterend insertion targets. Per the spec, if the element has no parent, or its parent is the Document, there is nowhere to insert a sibling — throw NoModificationAllowedError.



1687
1688
1689
1690
1691
1692
1693
1694
1695
# File 'lib/dommy/element.rb', line 1687

def insertion_parent!
  parent = @__node__.parent
  is_document = parent && ((parent.respond_to?(:document?) && parent.document?) || parent.name == "document")
  if parent.nil? || is_document
    raise DOMException::NoModificationAllowedError, "The element has no parent."
  end

  parent
end

#is_connected?Boolean Also known as: connected?

Walks parents up to the Document (or false when the chain dead-ends). Crosses ShadowRoot boundaries: a node inside an open or closed shadow tree is connected iff its host is.

Returns:

  • (Boolean)


1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
# File 'lib/dommy/element.rb', line 1501

def is_connected?
  current = @__node__
  seen = {}
  loop do
    # Guard against unexpected cycles in malformed trees.
    return false if seen[current.object_id]

    seen[current.object_id] = true

    parent = current.respond_to?(:parent) ? current.parent : nil
    return false unless parent
    return true if parent.is_a?(Backend.document_class)

    sr = @document.__internal_shadow_root_for_fragment__(parent)
    if sr
      host = sr.host
      return false unless host

      current = host.__dommy_backend_node__
    else
      current = parent
    end
  end
end

#last_childObject



1242
1243
1244
# File 'lib/dommy/element.rb', line 1242

def last_child
  @document.wrap_node(@__node__.children.last)
end

#last_element_childObject



1234
1235
1236
# File 'lib/dommy/element.rb', line 1234

def last_element_child
  @document.wrap_node(@__node__.element_children.last)
end

#live_child_nodesObject

Live NodeList over this element’s children. Reflects later mutations on every access.



1256
1257
1258
1259
1260
# File 'lib/dommy/element.rb', line 1256

def live_child_nodes
  @live_child_nodes ||= LiveNodeList.new do
    @__node__.children.map { |n| @document.wrap_node(n) }.compact
  end
end

#local_nameObject



1463
1464
1465
1466
1467
# File 'lib/dommy/element.rb', line 1463

def local_name
  return @__ns_local if @__ns_qname

  @__node__.name.downcase
end

#matches?(selector) ⇒ Boolean

Returns:

  • (Boolean)


1399
1400
1401
1402
1403
1404
1405
1406
# File 'lib/dommy/element.rb', line 1399

def matches?(selector)
  return false if selector.nil?
  Internal.validate_selector!(selector)

  # `:scope` pseudo — match against this element itself.
  sel = Internal.backend_safe_selector(selector.to_s).gsub(":scope", "*:nth-last-child(n)")
  matches_selector?(@__node__, sel)
end

#namespace_uriObject

HTML namespace constants — most HTML elements live in xhtml ns.



1456
1457
1458
1459
1460
1461
# File 'lib/dommy/element.rb', line 1456

def namespace_uri
  return @__ns_uri if @__ns_qname

  ns = Backend.namespace_of(@__node__)
  ns ? ns.href : HTML_NAMESPACE
end

#next_element_siblingObject



1278
1279
1280
1281
1282
# File 'lib/dommy/element.rb', line 1278

def next_element_sibling
  node = @__node__.next
  node = node.next while node && !node.element?
  node && @document.wrap_node(node)
end

#next_siblingObject



1270
1271
1272
# File 'lib/dommy/element.rb', line 1270

def next_sibling
  @__node__.next && @document.wrap_node(@__node__.next)
end

#normalizeObject

Merge adjacent text node siblings and drop empty text nodes.



1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
# File 'lib/dommy/element.rb', line 1368

def normalize
  @__node__.traverse do |node|
    next unless node.text?
    next if node.parent.nil?

    if node.content == "" && node.parent
      node.unlink
    elsif node.next && node.next.text?
      node.content = node.content + node.next.content
      node.next.unlink
    end
  end

  nil
end

#on(type, &block) ⇒ Object

Ruby block-style listener (in addition to the (type, callable, options) form inherited from EventTarget). Returns the resolved listener so callers can pass it back to remove_event_listener.



1830
1831
1832
1833
# File 'lib/dommy/element.rb', line 1830

def on(type, &block)
  add_event_listener(type, block)
  block
end

#outer_htmlObject

Outer HTML — serializes this element and its subtree. Setter replaces this element in its parent with the parsed fragment.



1292
1293
1294
# File 'lib/dommy/element.rb', line 1292

def outer_html
  @__node__.to_html
end

#outer_html=(html) ⇒ Object

Per WHATWG DOM Parsing:

- parent is null (detached element) → return silently
- parent is the Document (`<html>` element) → throw
  NoModificationAllowedError (can't replace the document
  element via this API)
- otherwise, parse `html` as a fragment in the parent's
  context and replace this element with the parsed nodes


1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
# File 'lib/dommy/element.rb', line 1303

def outer_html=(html)
  parent = @__node__.parent
  return unless parent

  if parent.is_a?(Backend.document_class)
    raise(
      DOMException::NoModificationAllowedError,
      "outerHTML setter not allowed on the document element"
    )
  end

  fragment = Parser.fragment(html.to_s, owner_doc: @__node__.document)
  anchor = @__node__.next_sibling
  removed = @__node__
  new_nodes = fragment.children.to_a
  @__node__.unlink
  if anchor
    new_nodes.reverse_each { |n| anchor.add_previous_sibling(n) }
  else
    new_nodes.each { |n| parent.add_child(n) }
  end

  notify_child_list(added: new_nodes, removed: [removed], target: parent)
end

#owner_documentObject



1494
1495
1496
# File 'lib/dommy/element.rb', line 1494

def owner_document
  @document
end

#parent_elementObject Also known as: parent



1220
1221
1222
# File 'lib/dommy/element.rb', line 1220

def parent_element
  @document.wrap_node(@__node__.parent) if @__node__.parent&.element?
end

#parent_nodeObject



1226
1227
1228
# File 'lib/dommy/element.rb', line 1226

def parent_node
  @__node__.parent && @document.wrap_node(@__node__.parent)
end

#pathObject

The XPath string locating this element in its document.



2550
2551
2552
# File 'lib/dommy/element.rb', line 2550

def path
  @__node__.path
end

#previous_element_siblingObject



1284
1285
1286
1287
1288
# File 'lib/dommy/element.rb', line 1284

def previous_element_sibling
  node = @__node__.previous
  node = node.previous while node && !node.element?
  node && @document.wrap_node(node)
end

#previous_siblingObject



1274
1275
1276
# File 'lib/dommy/element.rb', line 1274

def previous_sibling
  @__node__.previous && @document.wrap_node(@__node__.previous)
end

#query_selector(selector) ⇒ Object



2503
2504
2505
2506
2507
2508
2509
2510
# File 'lib/dommy/element.rb', line 2503

def query_selector(selector)
  return nil if selector.nil?
  # The empty string is not a valid selector (an explicit DOMString "" is a
  # SyntaxError; `null` coerces to "null" and is handled above as nil).
  Internal.validate_selector!(selector)

  @document.wrap_node(scoped_query(selector.to_s).first)
end

#query_selector_all(selector) ⇒ Object



2512
2513
2514
2515
2516
2517
# File 'lib/dommy/element.rb', line 2512

def query_selector_all(selector)
  return NodeList.new if selector.nil?
  Internal.validate_selector!(selector)

  NodeList.new(scoped_query(selector.to_s).map { |node| @document.wrap_node(node) }.compact)
end

#reflected_attr_name(key) ⇒ Object

Map a JS boolean property name to its underlying HTML attribute. HTML attribute names are lowercase; the DOM property may be camelCase (‘readOnly` → `readonly`).



2139
2140
2141
# File 'lib/dommy/element.rb', line 2139

def reflected_attr_name(key)
  {"readOnly" => "readonly"}.fetch(key, key)
end

#reflected_token_list(prop, attribute) ⇒ Object

A reflected DOMTokenList for ‘prop` backed by content attribute `attribute`, cached for identity (`el.relList === el.relList`). Returns the UNDEFINED sentinel (→ JS `undefined`) when the attribute is not defined on this element in its namespace.



1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
# File 'lib/dommy/element.rb', line 1196

def reflected_token_list(prop, attribute)
  hosts = REFLECTED_TOKEN_LIST_HOSTS[prop]
  ns = namespace_uri
  ln = local_name
  applicable =
    (ns == HTML_NAMESPACE && hosts[:html].include?(ln)) ||
    (ns == SVG_NAMESPACE && Array(hosts[:svg]).include?(ln))
  return Bridge::UNDEFINED unless applicable

  (@reflected_token_lists ||= {})[prop] ||= ClassList.new(self, attribute)
end

#removeObject



1757
1758
1759
1760
# File 'lib/dommy/element.rb', line 1757

def remove
  @document.remove_node_with_notify(@__node__)
  nil
end

#remove_attribute(name) ⇒ Object



2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
# File 'lib/dommy/element.rb', line 2383

def remove_attribute(name)
  return nil if name.nil?

  key = normalize_attr_key(name)
  return nil unless @__node__.key?(key)

  old = @__node__[key]
  # Detach the cached Attr (caching its value) *before* the backend drop,
  # so a held reference keeps the value it had when removed.
  @attributes&.__internal_evict__(nil, key)
  @__node__.remove_attribute(key)
  # Removing an `aria-*` IDREF attribute also clears any explicitly-set
  # element reference (the IDL getter then returns null).
  clear_aria_element_ref_for(key) if key.start_with?("aria-")
  @document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
  nil
end

#remove_attribute_node(attr) ⇒ Object



1449
1450
1451
1452
1453
# File 'lib/dommy/element.rb', line 1449

def remove_attribute_node(attr)
  return nil unless attr.respond_to?(:name)

  attributes.remove_named_item(attr.name)
end

#remove_attribute_ns(namespace, local_name) ⇒ Object



2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
# File 'lib/dommy/element.rb', line 2425

def remove_attribute_ns(namespace, local_name)
  return nil if local_name.nil?

  ns = namespace.to_s
  ns = nil if ns.empty?
  local = local_name.to_s
  old = Backend.get_attribute_ns(@__node__, ns, local)
  @attributes&.__internal_evict__(ns, local)
  Backend.remove_attribute_ns(@__node__, ns, local)
  if old
    @document.notify_attribute_mutation(target_node: @__node__, attribute_name: local, old_value: old, namespace: ns)
  end
  nil
end

#remove_child(child) ⇒ Object



2575
2576
2577
2578
2579
2580
2581
2582
2583
# File 'lib/dommy/element.rb', line 2575

def remove_child(child)
  node = unwrap_dom_node(child)
  unless node&.parent == @__node__
    raise DOMException::NotFoundError, "node is not a child of this element"
  end

  @document.remove_node_with_notify(node)
  child
end

#replace_child(new_child, old_child) ⇒ Object

‘node.replaceChild(newChild, oldChild)` — required for in-place item updates in list reconcilers. Inserts newChild where oldChild was, then unlinks oldChild. Notifies MutationObserver of both changes in one record so observers see the swap atomically.



2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
# File 'lib/dommy/element.rb', line 2590

def replace_child(new_child, old_child)
  old_node = unwrap_dom_node(old_child)
  return nil unless old_node&.parent == @__node__

  new_nodes = detach_dom_nodes(new_child)
  new_nodes.reverse_each { |node| old_node.add_previous_sibling(node) }
  old_node.unlink
  notify_child_list(added: new_nodes, removed: [old_node])
  old_child
end

#replace_with_nodes(*args) ⇒ Object



1772
1773
1774
# File 'lib/dommy/element.rb', line 1772

def replace_with_nodes(*args)
  replace_with(args)
end

#request_fullscreenObject



1804
1805
1806
1807
# File 'lib/dommy/element.rb', line 1804

def request_fullscreen
  @document.__internal_set_fullscreen_element__(self)
  PromiseValue.resolve(@document.default_view, nil)
end

#roleObject



1479
1480
1481
# File 'lib/dommy/element.rb', line 1479

def role
  @__node__["role"].to_s
end

#role=(value) ⇒ Object



1483
1484
1485
# File 'lib/dommy/element.rb', line 1483

def role=(value)
  set_attribute("role", value.to_s)
end

#root_nodeObject Also known as: get_root_node

‘el.getRootNode()` — returns the topmost ancestor (document, ShadowRoot, fragment, or self if detached). If the element lives inside a shadow tree, returns that ShadowRoot. Otherwise walks until we hit the Nokogiri Document (then returns the Document).



1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
# File 'lib/dommy/element.rb', line 1343

def root_node
  sr = @document.__internal_shadow_root_containing__(@__node__)
  return sr if sr

  current = @__node__
  attached = false
  loop do
    parent = current.respond_to?(:parent) ? current.parent : nil
    break unless parent
    if parent.is_a?(Backend.document_class)
      attached = true
      break
    end

    current = parent
  end

  return @document if attached

  @document.wrap_node(current) || @document
end

#same_node?(other) ⇒ Boolean

‘Node.isSameNode(other)` — strict reference identity. The DOM spec deprecates this in favor of `===`, but linkedom-style tests still call it.

Returns:

  • (Boolean)


1737
1738
1739
# File 'lib/dommy/element.rb', line 1737

def same_node?(other)
  equal?(other)
end

#scoped_query(sel) ⇒ Object

Run a CSS query rooted at this element. A ‘:scope` selector must resolve to this element, but Nokogiri scopes `el.css` to descendants (`.//`), which excludes the element itself — so for `:scope` queries we evaluate against the whole document (where this element IS reachable) and restrict the results to this element’s own subtree.



2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
# File 'lib/dommy/element.rb', line 2524

def scoped_query(sel)
  sel = Internal.backend_safe_selector(sel)
  handler = Internal.scoped_pseudo_handlers(@__node__)
  with_selector_errors(sel) do
    if sel.include?(":scope")
      self_id = @__node__.pointer_id
      @document.nokogiri_doc.css(sel, handler).select do |n|
        n.ancestors.any? { |a| a.pointer_id == self_id }
      end
    else
      @__node__.css(sel, handler)
    end
  end
end

#set_attribute(name, value) ⇒ Object



2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
# File 'lib/dommy/element.rb', line 2359

def set_attribute(name, value)
  return nil if name.nil?

  # WHATWG: a qualifiedName not matching the Name production throws.
  # The WPT corpus exercises only the empty string here (other shapes
  # like "0"/":"/"invalid^Name" are deliberately treated as valid).
  raise DOMException::InvalidCharacterError, "empty attribute name" if name.to_s.empty?

  key = normalize_attr_key(name)
  old = @__node__[key]
  @__node__[key] = value.to_s
  # A direct write to an `aria-*` IDREF attribute drops any explicitly-set
  # element reference, so the IDL getter re-resolves the new IDREF.
  clear_aria_element_ref_for(key) if key.start_with?("aria-")
  @document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
  nil
end

#set_attribute_node(attr) ⇒ Object



1445
1446
1447
# File 'lib/dommy/element.rb', line 1445

def set_attribute_node(attr)
  attributes.set_named_item(attr)
end

#set_attribute_ns(namespace, qualified_name, value) ⇒ Object



2417
2418
2419
2420
2421
2422
2423
# File 'lib/dommy/element.rb', line 2417

def set_attribute_ns(namespace, qualified_name, value)
  ns, prefix, local = Internal::Namespaces.validate_and_extract(namespace, qualified_name)
  old = Backend.get_attribute_ns(@__node__, ns, local)
  Backend.set_attribute_ns(@__node__, ns, prefix, local, qualified_name.to_s, value.to_s)
  @document.notify_attribute_mutation(target_node: @__node__, attribute_name: local, old_value: old, namespace: ns)
  nil
end

#shadow_rootObject

‘el.shadowRoot` — returns the attached ShadowRoot only when mode is “open”; closed shadows are hidden from external code.



1598
1599
1600
1601
1602
1603
# File 'lib/dommy/element.rb', line 1598

def shadow_root
  return nil unless @__shadow_root
  return nil if @__shadow_root.mode == "closed"

  @__shadow_root
end

#show_popoverObject

Popover API — show / hide / toggle fire beforetoggle + toggle events (no real visual change). Return values mirror the IDL.



1811
1812
1813
1814
# File 'lib/dommy/element.rb', line 1811

def show_popover
  toggle_popover_state(true)
  nil
end

#slotObject

‘slot` and `role` are simple reflected string attributes —added as named accessors for happy-dom test parity.



1471
1472
1473
# File 'lib/dommy/element.rb', line 1471

def slot
  @__node__["slot"].to_s
end

#slot=(value) ⇒ Object



1475
1476
1477
# File 'lib/dommy/element.rb', line 1475

def slot=(value)
  set_attribute("slot", value.to_s)
end

#styleObject



1208
1209
1210
# File 'lib/dommy/element.rb', line 1208

def style
  @style
end

#tag_nameObject

tagName is the qualified name, ASCII-upper-cased only for an HTML-namespace element whose node document is an HTML document. An XHTML element (HTML namespace, but in an XML document) and any non-HTML-namespace element keep their case.



1151
1152
1153
1154
1155
# File 'lib/dommy/element.rb', line 1151

def tag_name
  qname = @__ns_qname || @__node__.name
  html_ns = @__ns_qname ? @__ns_uri == HTML_NAMESPACE : true
  html_ns && @document.html_document? ? qname.upcase(:ascii) : qname
end

#text_contentObject

—– Public Ruby API (snake_case) —–

Mirrors HTMLElement DOM properties / methods in idiomatic Ruby form. The bridge protocol (‘js_get` / `js_call`) routes camelCase JS names through these same accessors, so any fix here is visible in both views.



1099
1100
1101
# File 'lib/dommy/element.rb', line 1099

def text_content
  @__node__.text
end

#text_content=(value) ⇒ Object



1103
1104
1105
1106
1107
1108
1109
1110
1111
# File 'lib/dommy/element.rb', line 1103

def text_content=(value)
  # `node.content =` removes all existing children and (if value is
  # non-empty) appends a single text node. Capture before/after to feed
  # MutationObserver.
  removed = @__node__.children.to_a
  @__node__.content = value.to_s
  added = @__node__.children.to_a
  notify_child_list(added: added, removed: removed)
end

#to_sObject

Convenience alias matching the DOM idiom ‘String(el)` → outerHTML.



1704
1705
1706
# File 'lib/dommy/element.rb', line 1704

def to_s
  outer_html
end

#toggle_attribute(name, force = nil) ⇒ Object



1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
# File 'lib/dommy/element.rb', line 1384

def toggle_attribute(name, force = nil)
  raise DOMException::InvalidCharacterError, "empty attribute name" if name.to_s.empty?

  key = name.to_s.downcase
  present = @__node__.key?(key)
  desired = force.nil? ? !present : !!force
  if desired
    set_attribute(key, "") unless present
    true
  else
    remove_attribute(key) if present
    false
  end
end

#toggle_popoverObject



1821
1822
1823
1824
1825
# File 'lib/dommy/element.rb', line 1821

def toggle_popover
  new_state = !@__popover_open__
  toggle_popover_state(new_state)
  new_state
end

#with_selector_errors(selector) ⇒ Object

Map Nokogiri’s selector errors to spec behavior:

  • a CSS parse error (“unexpected … after …”) means the selector is syntactically invalid → SyntaxError (querySelector/closest must throw);

  • an “Unregistered function” means a valid pseudo Nokogiri compiled but can’t evaluate (‘:hover`, `:invalid`, …) → degrade to matching nothing.



2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
# File 'lib/dommy/element.rb', line 2471

def with_selector_errors(selector)
  yield
rescue ::StandardError => e
  return [] if e.message.include?("Unregistered function")

  if (defined?(::Nokogiri::CSS::SyntaxError) && e.is_a?(::Nokogiri::CSS::SyntaxError)) || e.message.include?("unexpected")
    raise DOMException::SyntaxError, "'#{selector}' is not a valid selector."
  end

  raise
end

#xpath(expression) ⇒ Object



2545
2546
2547
# File 'lib/dommy/element.rb', line 2545

def xpath(expression)
  @__node__.xpath(expression).map { |node| @document.wrap_node(node) }
end