Class: Dommy::Element

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

Direct Known Subclasses

HTMLElement, SVGElement

Constant Summary collapse

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
JS_METHOD_NAMES =

Methods routed through js_call (keep in sync with its when-arms).

%w[
  getAttribute setAttribute hasAttribute removeAttribute getAttributeNames closest
  querySelector querySelectorAll getElementsByClassName getElementsByTagName
  insertAdjacentElement insertAdjacentHTML insertAdjacentText toggleAttribute matches
  toString getAttributeNode setAttributeNode removeAttributeNode focus blur attachShadow
  addEventListener removeEventListener dispatchEvent appendChild insertBefore removeChild
  replaceChild cloneNode append prepend replaceChildren before after getInnerHTML getHTML
  remove replaceWith click getBoundingClientRect getClientRects scrollIntoView scroll
  scrollTo scrollBy requestFullscreen showPopover hidePopover togglePopover
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EventTarget

#__internal_deliver_event__, #add_event_listener, #dispatch_event, #invoke_listener, #remove_event_listener

Constructor Details

#initialize(document, nokogiri_node) ⇒ Element

Returns a new instance of Element.



836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'lib/dommy/element.rb', line 836

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
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



832
833
834
# File 'lib/dommy/element.rb', line 832

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.



1546
1547
1548
# File 'lib/dommy/element.rb', line 1546

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

#[]=(key, value) ⇒ Object



1550
1551
1552
# File 'lib/dommy/element.rb', line 1550

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

#__dommy_backend_node__Object



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

def __dommy_backend_node__ = @__node__

#__internal_shadow_root__Object

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



1287
1288
1289
# File 'lib/dommy/element.rb', line 1287

def __internal_shadow_root__
  @__shadow_root
end

#__js_call__(method, args) ⇒ Object



1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
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
# File 'lib/dommy/element.rb', line 1777

def __js_call__(method, args)
  case method
  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 "getAttributeNames"
    @__node__.attribute_nodes.map(&:name)
  when "closest"
    closest(args[0])
  when "querySelector"
    query_selector(args[0])
  when "querySelectorAll"
    query_selector_all(args[0])
  when "getElementsByClassName"
    get_elements_by_class_name(args[0])
  when "getElementsByTagName"
    get_elements_by_tag_name(args[0])
  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"
    matches?(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])
  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_nodes(args)
  when "prepend"
    prepend_nodes(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"
    parent = @__node__.parent
    @__node__.unlink
    @document.notify_child_list_mutation(target_node: parent, added_nodes: [], removed_nodes: [@__node__]) if parent
    nil
  when "replaceWith"
    replace_with(args)
  when "click"
    dispatch_event(MouseEvent.new("click", "bubbles" => true, "cancelable" => true, "button" => 0))
  when "getBoundingClientRect"
    DOMRect.new
  when "getClientRects"
    []
  when "scrollIntoView", "scroll", "scrollTo", "scrollBy"
    # No layout — record the request for tests to assert against.
    @scroll_log ||= []
    @scroll_log << [method, args]
    nil
  when "requestFullscreen"
    @document.__internal_set_fullscreen_element__(self)
    PromiseValue.resolve(@document.default_view, nil)
  when "showPopover"
    toggle_popover_state(true)
    nil
  when "hidePopover"
    toggle_popover_state(false)
    nil
  when "togglePopover"
    new_state = !@__popover_open__
    toggle_popover_state(new_state)
    new_state
  else
    nil
  end
end

#__js_get__(key) ⇒ Object



1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
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
1645
1646
1647
1648
1649
1650
1651
1652
1653
# File 'lib/dommy/element.rb', line 1554

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 "firstElementChild"
    @document.wrap_node(@__node__.element_children.first)
  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"
    if @__node__.name == "template"
      @document.template_content_inner_html(self)
    else
      @__node__.inner_html
    end

  when "tagName"
    @__node__.name.upcase
  when "classList"
    @class_list
  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"
    @__node__.name.upcase
  when "slot"
    slot
  when "role"
    role
  when "baseURI"
    base_uri
  when "shadowRoot"
    shadow_root
  when "ownerDocument"
    @document
  else
    # `el.onXxx` event handler property — returns the registered
    # callback (if any), or nil.
    if key.start_with?("on") && key.length > 2
      @on_handlers&.[](event_name_from_on(key))
    end
  end
end

#__js_method_names__Object



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

def __js_method_names__
  JS_METHOD_NAMES
end

#__js_set__(key, value) ⇒ Object



1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
# File 'lib/dommy/element.rb', line 1676

def __js_set__(key, value)
  case key
  when "textContent"
    # `node.content =` removes all existing children and (if
    # value is non-empty) appends a single text node. Capture
    # before/after to feed MutationObserver — mirrors the
    # innerHTML branch below.
    removed = @__node__.children.to_a
    @__node__.content = value.to_s
    added = @__node__.children.to_a
    if removed.any? || added.any?
      @document.notify_child_list_mutation(
        target_node: @__node__,
        added_nodes: added,
        removed_nodes: removed
      )
    end
  when "innerHTML"
    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]`). Mirror that here so child placeholders
      # inside the template don't pollute outer queries.
      @document.attach_template_content(self, value.to_s)
    else
      @__node__.inner_html = value.to_s
      @document.migrate_template_descendants(@__node__)
    end

    @document.notify_child_list_mutation(
      target_node: @__node__,
      added_nodes: @__node__.children.to_a,
      removed_nodes: removed
    )
  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 "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"
    set_attribute("role", value.to_s)
  else
    # `el.onXxx = fn` registers fn as a single named handler.
    # Setting to nil removes it. Mirrors HTMLElement IDL.
    if key.start_with?("on") && key.length > 2
      set_on_handler(event_name_from_on(key), value)
    else
      nil
    end
  end
end

#__test_scroll_log__Object

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



2247
2248
2249
# File 'lib/dommy/element.rb', line 2247

def __test_scroll_log__
  @scroll_log ||= []
end

#after(*args) ⇒ Object



1512
1513
1514
# File 'lib/dommy/element.rb', line 1512

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.



1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
# File 'lib/dommy/element.rb', line 1658

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



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

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

#append(*args) ⇒ Object

ParentNode mixin methods — append / prepend / replaceChildren take a mix of Node and String args (strings become text nodes).



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

def append(*args)
  append_nodes(args)
end

#append_child(child) ⇒ Object



2031
2032
2033
2034
2035
2036
2037
# File 'lib/dommy/element.rb', line 2031

def append_child(child)
  check_hierarchy!(child)
  nodes = detach_dom_nodes(child)
  append_dom_nodes(nodes)
  @document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
  child
end

#at_xpath(expression) ⇒ Object

XPath queries scoped to this element, returning wrapped nodes.



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

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


1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
# File 'lib/dommy/element.rb', line 1252

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



1121
1122
1123
# File 'lib/dommy/element.rb', line 1121

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



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

def base_uri
  @document.base_uri
end

#before(*args) ⇒ Object

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



1508
1509
1510
# File 'lib/dommy/element.rb', line 1508

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

#blurObject



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

def blur
  @document.__internal_set_active_element__(nil)
  nil
end

#child_element_countObject



937
938
939
# File 'lib/dommy/element.rb', line 937

def child_element_count
  @__node__.element_children.size
end

#child_nodesObject



941
942
943
# File 'lib/dommy/element.rb', line 941

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

#childrenObject



907
908
909
# File 'lib/dommy/element.rb', line 907

def children
  @live_children
end

#class_listObject



895
896
897
# File 'lib/dommy/element.rb', line 895

def class_list
  @class_list
end

#class_nameObject



887
888
889
# File 'lib/dommy/element.rb', line 887

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

#class_name=(value) ⇒ Object



891
892
893
# File 'lib/dommy/element.rb', line 891

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

#clickObject



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

def click
  __js_call__("click", [])
end

#clone_node(deep_arg) ⇒ Object



2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
# File 'lib/dommy/element.rb', line 2091

def clone_node(deep_arg)
  deep = !!deep_arg
  if deep
    @document.wrap_node(
      Parser.fragment(@__node__.to_html, owner_doc: @document.nokogiri_doc).children.find(&:element?)
    )
  else
    clone = @document.create_element(@__node__.name)
    @__node__.attribute_nodes.each do |attr|
      clone.__js_call__("setAttribute", [attr.name, attr.value])
    end

    clone
  end
end

#closest(selector) ⇒ Object



1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
# File 'lib/dommy/element.rb', line 1971

def closest(selector)
  return nil if selector.nil? || selector.to_s.empty?

  node = @__node__
  while node&.element?
    return @document.wrap_node(node) if matches_selector?(node, selector.to_s)

    node = node.parent
  end

  nil
end

#compare_document_position(other) ⇒ Object

Standard DOM compareDocumentPosition. Returns 0 for self, a CONTAINS/CONTAINED_BY bitmask for ancestor/descendant pairs, or PRECEDING/FOLLOWING for siblings (and DISCONNECTED for unrelated nodes).



1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
# File 'lib/dommy/element.rb', line 1392

def compare_document_position(other)
  return 0 if equal?(other)
  return DOCUMENT_POSITION_DISCONNECTED unless other.respond_to?(:__dommy_backend_node__)

  self_node = @__node__
  other_node = other.__dommy_backend_node__

  self_ancestors = ancestor_chain(self_node)
  other_ancestors = ancestor_chain(other_node)

  common = nil
  self_ancestors.each do |a|
    if other_ancestors.include?(a)
      common = a
      break
    end
  end

  unless common
    return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING
  end

  if common == self_node
    return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING
  elsif common == other_node
    return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING
  end

  # Sibling-of-some-level case: compare the two branch points
  # under the common ancestor.
  self_branch = branch_under(common, self_ancestors)
  other_branch = branch_under(common, other_ancestors)
  common.children.each do |child|
    if child == self_branch
      return DOCUMENT_POSITION_FOLLOWING
    elsif child == other_branch
      return DOCUMENT_POSITION_PRECEDING
    end
  end

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


1021
1022
1023
1024
1025
1026
1027
1028
# File 'lib/dommy/element.rb', line 1021

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



903
904
905
# File 'lib/dommy/element.rb', line 903

def dataset
  @dataset
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)


1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
# File 'lib/dommy/element.rb', line 1445

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



929
930
931
# File 'lib/dommy/element.rb', line 929

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

#first_element_childObject



921
922
923
# File 'lib/dommy/element.rb', line 921

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.



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

def focus
  @document.__internal_set_active_element__(self)
  nil
end

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



1998
1999
2000
# File 'lib/dommy/element.rb', line 1998

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

#get_attribute(name) ⇒ Object



1937
1938
1939
1940
1941
# File 'lib/dommy/element.rb', line 1937

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

  @__node__[normalize_attr_key(name)]
end

#get_attribute_node(name) ⇒ Object



1125
1126
1127
# File 'lib/dommy/element.rb', line 1125

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

#get_elements_by_class_name(name) ⇒ Object



1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
# File 'lib/dommy/element.rb', line 1096

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



1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
# File 'lib/dommy/element.rb', line 1108

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_html(_options = nil) ⇒ Object



1527
1528
1529
# File 'lib/dommy/element.rb', line 1527

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



1523
1524
1525
# File 'lib/dommy/element.rb', line 1523

def get_inner_html(_options = nil)
  inner_html
end

#has_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


1953
1954
1955
1956
1957
# File 'lib/dommy/element.rb', line 1953

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

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

#has_attributes?Boolean

Returns:

  • (Boolean)


957
958
959
# File 'lib/dommy/element.rb', line 957

def has_attributes?
  @__node__.attribute_nodes.any?
end

#has_child_nodes?Boolean

Returns:

  • (Boolean)


953
954
955
# File 'lib/dommy/element.rb', line 953

def has_child_nodes?
  @__node__.children.any?
end

#idObject



879
880
881
# File 'lib/dommy/element.rb', line 879

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

#id=(value) ⇒ Object



883
884
885
# File 'lib/dommy/element.rb', line 883

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

#inner_htmlObject



867
868
869
# File 'lib/dommy/element.rb', line 867

def inner_html
  __js_get__("innerHTML")
end

#inner_html=(value) ⇒ Object



871
872
873
# File 'lib/dommy/element.rb', line 871

def inner_html=(value)
  __js_set__("innerHTML", value)
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).



1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
# File 'lib/dommy/element.rb', line 1294

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)
    @document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: [node], removed_nodes: [])
  when "afterbegin"
    node = detach_for_insert(element)
    first = @__node__.children.first
    first ? first.add_previous_sibling(node) : @__node__.add_child(node)
    @document.notify_child_list_mutation(target_node: @__node__, added_nodes: [node], removed_nodes: [])
  when "beforeend"
    node = detach_for_insert(element)
    @__node__.add_child(node)
    @document.notify_child_list_mutation(target_node: @__node__, added_nodes: [node], removed_nodes: [])
  when "afterend"
    return nil unless @__node__.parent

    node = detach_for_insert(element)
    @__node__.add_next_sibling(node)
    @document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: [node], removed_nodes: [])
  else
    return nil
  end

  element
end

#insert_adjacent_html(position, html) ⇒ Object



1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
# File 'lib/dommy/element.rb', line 1326

def insert_adjacent_html(position, html)
  fragment = Parser.fragment(html.to_s, owner_doc: @__node__.document)
  nodes = fragment.children.to_a
  case position.to_s
  when "beforebegin"
    return nil unless @__node__.parent

    nodes.reverse_each { |n| @__node__.add_previous_sibling(n) }
    @document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: nodes, removed_nodes: [])
  when "afterbegin"
    first = @__node__.children.first
    if first
      nodes.reverse_each { |n| first.add_previous_sibling(n) }
    else
      nodes.each { |n| @__node__.add_child(n) }
    end

    @document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
  when "beforeend"
    nodes.each { |n| @__node__.add_child(n) }
    @document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
  when "afterend"
    return nil unless @__node__.parent

    nodes.reverse_each { |n| @__node__.add_next_sibling(n) }
    @document.notify_child_list_mutation(target_node: @__node__.parent, added_nodes: nodes, removed_nodes: [])
  end

  nil
end

#insert_adjacent_text(position, text) ⇒ Object



1357
1358
1359
1360
1361
# File 'lib/dommy/element.rb', line 1357

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



2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
# File 'lib/dommy/element.rb', line 2039

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

  @document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: [])
  child
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)


1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
# File 'lib/dommy/element.rb', line 1181

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



933
934
935
# File 'lib/dommy/element.rb', line 933

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

#last_element_childObject



925
926
927
# File 'lib/dommy/element.rb', line 925

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.



947
948
949
950
951
# File 'lib/dommy/element.rb', line 947

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

#local_nameObject



1145
1146
1147
# File 'lib/dommy/element.rb', line 1145

def local_name
  @__node__.name.downcase
end

#matches?(selector) ⇒ Boolean

Returns:

  • (Boolean)


1088
1089
1090
1091
1092
1093
1094
# File 'lib/dommy/element.rb', line 1088

def matches?(selector)
  return false if selector.nil? || selector.to_s.empty?

  # `:scope` pseudo — match against this element itself.
  sel = 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.



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

def namespace_uri
  ns = @__node__.namespace
  ns ? ns.href : "http://www.w3.org/1999/xhtml"
end

#next_element_siblingObject



969
970
971
972
973
# File 'lib/dommy/element.rb', line 969

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

#next_siblingObject



961
962
963
# File 'lib/dommy/element.rb', line 961

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

#normalizeObject

Merge adjacent text node siblings and drop empty text nodes.



1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/dommy/element.rb', line 1059

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.



1538
1539
1540
1541
# File 'lib/dommy/element.rb', line 1538

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.



983
984
985
# File 'lib/dommy/element.rb', line 983

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


994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'lib/dommy/element.rb', line 994

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

  @document.notify_child_list_mutation(target_node: parent, added_nodes: new_nodes, removed_nodes: [removed])
end

#owner_documentObject



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

def owner_document
  @document
end

#parent_elementObject Also known as: parent



911
912
913
# File 'lib/dommy/element.rb', line 911

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

#parent_nodeObject



917
918
919
# File 'lib/dommy/element.rb', line 917

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

#pathObject

The XPath string locating this element in its document.



2027
2028
2029
# File 'lib/dommy/element.rb', line 2027

def path
  @__node__.path
end

#prepend(*args) ⇒ Object



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

def prepend(*args)
  prepend_nodes(args)
end

#previous_element_siblingObject



975
976
977
978
979
# File 'lib/dommy/element.rb', line 975

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

#previous_siblingObject



965
966
967
# File 'lib/dommy/element.rb', line 965

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

#query_selector(selector) ⇒ Object



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

def query_selector(selector)
  return nil if selector.nil? || selector.to_s.empty?

  @document.wrap_node(@__node__.at_css(selector.to_s, Internal::CSS_PSEUDO_HANDLERS))
end

#query_selector_all(selector) ⇒ Object



2010
2011
2012
2013
2014
# File 'lib/dommy/element.rb', line 2010

def query_selector_all(selector)
  return NodeList.new if selector.nil? || selector.to_s.empty?

  NodeList.new(@__node__.css(selector.to_s, Internal::CSS_PSEUDO_HANDLERS).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`).



1672
1673
1674
# File 'lib/dommy/element.rb', line 1672

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

#removeObject



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

def remove
  __js_call__("remove", [])
end

#remove_attribute(name) ⇒ Object



1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
# File 'lib/dommy/element.rb', line 1959

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

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

  old = @__node__[key]
  @__node__.remove_attribute(key)
  @document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
  nil
end

#remove_attribute_node(attr) ⇒ Object



1133
1134
1135
1136
1137
# File 'lib/dommy/element.rb', line 1133

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

  attributes.remove_named_item(attr.name)
end

#remove_child(child) ⇒ Object



2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
# File 'lib/dommy/element.rb', line 2060

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

  node.unlink
  @document.notify_child_list_mutation(target_node: @__node__, added_nodes: [], removed_nodes: [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.



2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
# File 'lib/dommy/element.rb', line 2076

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
  @document.notify_child_list_mutation(
    target_node: @__node__,
    added_nodes: new_nodes,
    removed_nodes: [old_node]
  )
  old_child
end

#replace_children(*args) ⇒ Object



1497
1498
1499
1500
1501
1502
1503
1504
# File 'lib/dommy/element.rb', line 1497

def replace_children(*args)
  removed = @__node__.children.to_a
  removed.each(&:unlink)
  nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
  nodes.each { |n| @__node__.add_child(n) }
  @document.notify_child_list_mutation(target_node: @__node__, added_nodes: nodes, removed_nodes: removed)
  nil
end

#replace_with_nodes(*args) ⇒ Object



1516
1517
1518
# File 'lib/dommy/element.rb', line 1516

def replace_with_nodes(*args)
  replace_with(args)
end

#roleObject



1159
1160
1161
# File 'lib/dommy/element.rb', line 1159

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

#role=(value) ⇒ Object



1163
1164
1165
# File 'lib/dommy/element.rb', line 1163

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



1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
# File 'lib/dommy/element.rb', line 1034

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)


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

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

#set_attribute(name, value) ⇒ Object



1943
1944
1945
1946
1947
1948
1949
1950
1951
# File 'lib/dommy/element.rb', line 1943

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

  key = normalize_attr_key(name)
  old = @__node__[key]
  @__node__[key] = value.to_s
  @document.notify_attribute_mutation(target_node: @__node__, attribute_name: key, old_value: old)
  nil
end

#set_attribute_node(attr) ⇒ Object



1129
1130
1131
# File 'lib/dommy/element.rb', line 1129

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

#shadow_rootObject

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



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

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

  @__shadow_root
end

#slotObject

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



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

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

#slot=(value) ⇒ Object



1155
1156
1157
# File 'lib/dommy/element.rb', line 1155

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

#styleObject



899
900
901
# File 'lib/dommy/element.rb', line 899

def style
  @style
end

#tag_nameObject



875
876
877
# File 'lib/dommy/element.rb', line 875

def tag_name
  @__node__.name.upcase
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.



859
860
861
# File 'lib/dommy/element.rb', line 859

def text_content
  @__node__.text
end

#text_content=(value) ⇒ Object



863
864
865
# File 'lib/dommy/element.rb', line 863

def text_content=(value)
  __js_set__("textContent", value)
end

#to_sObject

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



1364
1365
1366
# File 'lib/dommy/element.rb', line 1364

def to_s
  outer_html
end

#toggle_attribute(name, force = nil) ⇒ Object



1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
# File 'lib/dommy/element.rb', line 1075

def toggle_attribute(name, force = nil)
  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

#xpath(expression) ⇒ Object



2022
2023
2024
# File 'lib/dommy/element.rb', line 2022

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