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

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EventTarget

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



701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'lib/dommy/element.rb', line 701

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

#__node__Object (readonly)

Returns the value of attribute __node__.



699
700
701
# File 'lib/dommy/element.rb', line 699

def __node__
  @__node__
end

#documentObject (readonly)

Returns the value of attribute document.



699
700
701
# File 'lib/dommy/element.rb', line 699

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.



1411
1412
1413
# File 'lib/dommy/element.rb', line 1411

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

#[]=(key, value) ⇒ Object



1415
1416
1417
# File 'lib/dommy/element.rb', line 1415

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

#__js_call__(method, args) ⇒ Object



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
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
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
# File 'lib/dommy/element.rb', line 1614

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



1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
# File 'lib/dommy/element.rb', line 1419

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_set__(key, value) ⇒ Object



1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
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
# File 'lib/dommy/element.rb', line 1541

def __js_set__(key, value)
  case key
  when "textContent"
    @__node__.content = value.to_s
  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

#__scroll_log__Object

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



2069
2070
2071
# File 'lib/dommy/element.rb', line 2069

def __scroll_log__
  @__scroll_log__ ||= []
end

#__shadow_root__Object

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



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

def __shadow_root__
  @__shadow_root
end

#after(*args) ⇒ Object



1377
1378
1379
# File 'lib/dommy/element.rb', line 1377

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.



1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
# File 'lib/dommy/element.rb', line 1523

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



1826
1827
1828
1829
1830
1831
1832
1833
# File 'lib/dommy/element.rb', line 1826

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



1354
1355
1356
# File 'lib/dommy/element.rb', line 1354

def append(*args)
  append_nodes(args)
end

#append_child(child) ⇒ Object



1853
1854
1855
1856
1857
1858
1859
# File 'lib/dommy/element.rb', line 1853

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

#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


1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
# File 'lib/dommy/element.rb', line 1117

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



986
987
988
# File 'lib/dommy/element.rb', line 986

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



1035
1036
1037
# File 'lib/dommy/element.rb', line 1035

def base_uri
  @document.base_uri
end

#before(*args) ⇒ Object

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



1373
1374
1375
# File 'lib/dommy/element.rb', line 1373

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

#blurObject



1081
1082
1083
1084
# File 'lib/dommy/element.rb', line 1081

def blur
  @document.__set_active_element__(nil)
  nil
end

#child_element_countObject



802
803
804
# File 'lib/dommy/element.rb', line 802

def child_element_count
  @__node__.element_children.size
end

#child_nodesObject



806
807
808
# File 'lib/dommy/element.rb', line 806

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

#childrenObject



772
773
774
# File 'lib/dommy/element.rb', line 772

def children
  @live_children
end

#class_listObject



760
761
762
# File 'lib/dommy/element.rb', line 760

def class_list
  @class_list
end

#class_nameObject



752
753
754
# File 'lib/dommy/element.rb', line 752

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

#class_name=(value) ⇒ Object



756
757
758
# File 'lib/dommy/element.rb', line 756

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

#clickObject



1396
1397
1398
# File 'lib/dommy/element.rb', line 1396

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

#clone_node(deep_arg) ⇒ Object



1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
# File 'lib/dommy/element.rb', line 1913

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



1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
# File 'lib/dommy/element.rb', line 1808

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



1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
# File 'lib/dommy/element.rb', line 1257

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

  self_node = @__node__
  other_node = other.__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)


886
887
888
889
890
891
892
893
# File 'lib/dommy/element.rb', line 886

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

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

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

#datasetObject



768
769
770
# File 'lib/dommy/element.rb', line 768

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)


1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
# File 'lib/dommy/element.rb', line 1310

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

  @__node__.children.zip(other.__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



794
795
796
# File 'lib/dommy/element.rb', line 794

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

#first_element_childObject



786
787
788
# File 'lib/dommy/element.rb', line 786

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.



1076
1077
1078
1079
# File 'lib/dommy/element.rb', line 1076

def focus
  @document.__set_active_element__(self)
  nil
end

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



1835
1836
1837
# File 'lib/dommy/element.rb', line 1835

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

#get_attribute(name) ⇒ Object



1774
1775
1776
1777
1778
# File 'lib/dommy/element.rb', line 1774

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

  @__node__[normalize_attr_key(name)]
end

#get_attribute_node(name) ⇒ Object



990
991
992
# File 'lib/dommy/element.rb', line 990

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

#get_elements_by_class_name(name) ⇒ Object



961
962
963
964
965
966
967
968
969
970
971
# File 'lib/dommy/element.rb', line 961

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



973
974
975
976
977
978
979
980
981
982
# File 'lib/dommy/element.rb', line 973

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



1392
1393
1394
# File 'lib/dommy/element.rb', line 1392

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



1388
1389
1390
# File 'lib/dommy/element.rb', line 1388

def get_inner_html(_options = nil)
  inner_html
end

#has_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


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

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

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

#has_attributes?Boolean

Returns:

  • (Boolean)


822
823
824
# File 'lib/dommy/element.rb', line 822

def has_attributes?
  @__node__.attribute_nodes.any?
end

#has_child_nodes?Boolean

Returns:

  • (Boolean)


818
819
820
# File 'lib/dommy/element.rb', line 818

def has_child_nodes?
  @__node__.children.any?
end

#idObject



744
745
746
# File 'lib/dommy/element.rb', line 744

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

#id=(value) ⇒ Object



748
749
750
# File 'lib/dommy/element.rb', line 748

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

#inner_htmlObject



732
733
734
# File 'lib/dommy/element.rb', line 732

def inner_html
  __js_get__("innerHTML")
end

#inner_html=(value) ⇒ Object



736
737
738
# File 'lib/dommy/element.rb', line 736

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



1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
# File 'lib/dommy/element.rb', line 1159

def insert_adjacent_element(position, element)
  return nil unless element.respond_to?(:__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



1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
# File 'lib/dommy/element.rb', line 1191

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



1222
1223
1224
1225
1226
# File 'lib/dommy/element.rb', line 1222

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



1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
# File 'lib/dommy/element.rb', line 1861

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)


1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
# File 'lib/dommy/element.rb', line 1046

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?(Nokogiri::XML::Document)

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

      current = host.__node__
    else
      current = parent
    end
  end
end

#last_childObject



798
799
800
# File 'lib/dommy/element.rb', line 798

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

#last_element_childObject



790
791
792
# File 'lib/dommy/element.rb', line 790

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.



812
813
814
815
816
# File 'lib/dommy/element.rb', line 812

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

#local_nameObject



1010
1011
1012
# File 'lib/dommy/element.rb', line 1010

def local_name
  @__node__.name.downcase
end

#matches?(selector) ⇒ Boolean

Returns:

  • (Boolean)


953
954
955
956
957
958
959
# File 'lib/dommy/element.rb', line 953

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.



1005
1006
1007
1008
# File 'lib/dommy/element.rb', line 1005

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

#next_element_siblingObject



834
835
836
837
838
# File 'lib/dommy/element.rb', line 834

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

#next_siblingObject



826
827
828
# File 'lib/dommy/element.rb', line 826

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

#normalizeObject

Merge adjacent text node siblings and drop empty text nodes.



924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
# File 'lib/dommy/element.rb', line 924

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.



1403
1404
1405
1406
# File 'lib/dommy/element.rb', line 1403

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.



848
849
850
# File 'lib/dommy/element.rb', line 848

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


859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
# File 'lib/dommy/element.rb', line 859

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

  if parent.is_a?(Nokogiri::XML::Document)
    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



1039
1040
1041
# File 'lib/dommy/element.rb', line 1039

def owner_document
  @document
end

#parent_elementObject Also known as: parent



776
777
778
# File 'lib/dommy/element.rb', line 776

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

#parent_nodeObject



782
783
784
# File 'lib/dommy/element.rb', line 782

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

#prepend(*args) ⇒ Object



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

def prepend(*args)
  prepend_nodes(args)
end

#previous_element_siblingObject



840
841
842
843
844
# File 'lib/dommy/element.rb', line 840

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

#previous_siblingObject



830
831
832
# File 'lib/dommy/element.rb', line 830

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

#query_selector(selector) ⇒ Object



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

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

  @document.wrap_node(@__node__.at_css(selector.to_s))
end

#query_selector_all(selector) ⇒ Object



1847
1848
1849
1850
1851
# File 'lib/dommy/element.rb', line 1847

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

  NodeList.new(@__node__.css(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`).



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

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

#removeObject



1347
1348
1349
# File 'lib/dommy/element.rb', line 1347

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

#remove_attribute(name) ⇒ Object



1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
# File 'lib/dommy/element.rb', line 1796

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



998
999
1000
1001
1002
# File 'lib/dommy/element.rb', line 998

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

  attributes.remove_named_item(attr.name)
end

#remove_child(child) ⇒ Object



1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
# File 'lib/dommy/element.rb', line 1882

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.



1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
# File 'lib/dommy/element.rb', line 1898

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



1362
1363
1364
1365
1366
1367
1368
1369
# File 'lib/dommy/element.rb', line 1362

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



1381
1382
1383
# File 'lib/dommy/element.rb', line 1381

def replace_with_nodes(*args)
  replace_with(args)
end

#roleObject



1024
1025
1026
# File 'lib/dommy/element.rb', line 1024

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

#role=(value) ⇒ Object



1028
1029
1030
# File 'lib/dommy/element.rb', line 1028

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



899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# File 'lib/dommy/element.rb', line 899

def root_node
  sr = @document.__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?(Nokogiri::XML::Document)
      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)


1303
1304
1305
# File 'lib/dommy/element.rb', line 1303

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

#set_attribute(name, value) ⇒ Object



1780
1781
1782
1783
1784
1785
1786
1787
1788
# File 'lib/dommy/element.rb', line 1780

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



994
995
996
# File 'lib/dommy/element.rb', line 994

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.



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

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.



1016
1017
1018
# File 'lib/dommy/element.rb', line 1016

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

#slot=(value) ⇒ Object



1020
1021
1022
# File 'lib/dommy/element.rb', line 1020

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

#styleObject



764
765
766
# File 'lib/dommy/element.rb', line 764

def style
  @style
end

#tag_nameObject



740
741
742
# File 'lib/dommy/element.rb', line 740

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.



724
725
726
# File 'lib/dommy/element.rb', line 724

def text_content
  @__node__.text
end

#text_content=(value) ⇒ Object



728
729
730
# File 'lib/dommy/element.rb', line 728

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

#to_sObject

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



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

def to_s
  outer_html
end

#toggle_attribute(name, force = nil) ⇒ Object



940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/dommy/element.rb', line 940

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