Class: Dommy::Element

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

Direct Known Subclasses

HTMLElement

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.



683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
# File 'lib/dommy/element.rb', line 683

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



681
682
683
# File 'lib/dommy/element.rb', line 681

def __node__
  @__node__
end

#documentObject (readonly)

Returns the value of attribute document.



681
682
683
# File 'lib/dommy/element.rb', line 681

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.



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

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

#[]=(key, value) ⇒ Object



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

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

#__js_call__(method, args) ⇒ Object



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
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
# File 'lib/dommy/element.rb', line 1576

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
  else
    nil
  end
end

#__js_get__(key) ⇒ Object



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

def __js_get__(key)
  case key
  when "nodeType"
    1
  when "isConnected"
    is_connected?
  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



1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
# File 'lib/dommy/element.rb', line 1503

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

#__shadow_root__Object

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



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

def __shadow_root__
  @__shadow_root
end

#after(*args) ⇒ Object



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

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.



1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
# File 'lib/dommy/element.rb', line 1485

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

#append(*args) ⇒ Object

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



1336
1337
1338
# File 'lib/dommy/element.rb', line 1336

def append(*args)
  append_nodes(args)
end

#append_child(child) ⇒ Object



1763
1764
1765
1766
1767
1768
1769
# File 'lib/dommy/element.rb', line 1763

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


1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
# File 'lib/dommy/element.rb', line 1099

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



968
969
970
# File 'lib/dommy/element.rb', line 968

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



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

def base_uri
  @document.base_uri
end

#before(*args) ⇒ Object

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



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

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

#blurObject



1063
1064
1065
1066
# File 'lib/dommy/element.rb', line 1063

def blur
  @document.__set_active_element__(nil)
  nil
end

#child_element_countObject



784
785
786
# File 'lib/dommy/element.rb', line 784

def child_element_count
  @__node__.element_children.size
end

#child_nodesObject



788
789
790
# File 'lib/dommy/element.rb', line 788

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

#childrenObject



754
755
756
# File 'lib/dommy/element.rb', line 754

def children
  @live_children
end

#class_listObject



742
743
744
# File 'lib/dommy/element.rb', line 742

def class_list
  @class_list
end

#class_nameObject



734
735
736
# File 'lib/dommy/element.rb', line 734

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

#class_name=(value) ⇒ Object



738
739
740
# File 'lib/dommy/element.rb', line 738

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

#clickObject



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

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

#clone_node(deep_arg) ⇒ Object



1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
# File 'lib/dommy/element.rb', line 1823

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



1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
# File 'lib/dommy/element.rb', line 1738

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



1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
# File 'lib/dommy/element.rb', line 1239

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)


868
869
870
871
872
873
874
875
# File 'lib/dommy/element.rb', line 868

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



750
751
752
# File 'lib/dommy/element.rb', line 750

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)


1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
# File 'lib/dommy/element.rb', line 1292

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



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

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

#first_element_childObject



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

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.



1058
1059
1060
1061
# File 'lib/dommy/element.rb', line 1058

def focus
  @document.__set_active_element__(self)
  nil
end

#get_attribute(name) ⇒ Object

HTML attribute names are case-insensitive — browser DOM stores them in lowercase regardless of the case passed to setAttribute. Matches that behavior so callers using ‘“SRC”` / `“Action”` / etc. interoperate with `getAttribute(“src”)` round-trips.



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

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

  @__node__[name.to_s.downcase]
end

#get_attribute_node(name) ⇒ Object



972
973
974
# File 'lib/dommy/element.rb', line 972

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

#get_elements_by_class_name(name) ⇒ Object



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

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



955
956
957
958
959
960
961
962
963
964
# File 'lib/dommy/element.rb', line 955

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



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

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



1370
1371
1372
# File 'lib/dommy/element.rb', line 1370

def get_inner_html(_options = nil)
  inner_html
end

#has_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


1720
1721
1722
1723
1724
# File 'lib/dommy/element.rb', line 1720

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

  @__node__.key?(name.to_s.downcase)
end

#has_attributes?Boolean

Returns:

  • (Boolean)


804
805
806
# File 'lib/dommy/element.rb', line 804

def has_attributes?
  @__node__.attribute_nodes.any?
end

#has_child_nodes?Boolean

Returns:

  • (Boolean)


800
801
802
# File 'lib/dommy/element.rb', line 800

def has_child_nodes?
  @__node__.children.any?
end

#idObject



726
727
728
# File 'lib/dommy/element.rb', line 726

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

#id=(value) ⇒ Object



730
731
732
# File 'lib/dommy/element.rb', line 730

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

#inner_htmlObject



714
715
716
# File 'lib/dommy/element.rb', line 714

def inner_html
  __js_get__("innerHTML")
end

#inner_html=(value) ⇒ Object



718
719
720
# File 'lib/dommy/element.rb', line 718

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



1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
# File 'lib/dommy/element.rb', line 1141

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



1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
# File 'lib/dommy/element.rb', line 1173

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



1204
1205
1206
1207
1208
# File 'lib/dommy/element.rb', line 1204

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



1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
# File 'lib/dommy/element.rb', line 1771

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)


1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
# File 'lib/dommy/element.rb', line 1028

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



780
781
782
# File 'lib/dommy/element.rb', line 780

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

#last_element_childObject



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

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.



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

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

#local_nameObject



992
993
994
# File 'lib/dommy/element.rb', line 992

def local_name
  @__node__.name.downcase
end

#matches?(selector) ⇒ Boolean

Returns:

  • (Boolean)


935
936
937
938
939
940
941
# File 'lib/dommy/element.rb', line 935

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.



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

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

#next_element_siblingObject



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

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

#next_siblingObject



808
809
810
# File 'lib/dommy/element.rb', line 808

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

#normalizeObject

Merge adjacent text node siblings and drop empty text nodes.



906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
# File 'lib/dommy/element.rb', line 906

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.



1385
1386
1387
1388
# File 'lib/dommy/element.rb', line 1385

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.



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

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


841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
# File 'lib/dommy/element.rb', line 841

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



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

def owner_document
  @document
end

#parent_elementObject Also known as: parent



758
759
760
# File 'lib/dommy/element.rb', line 758

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

#parent_nodeObject



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

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

#prepend(*args) ⇒ Object



1340
1341
1342
# File 'lib/dommy/element.rb', line 1340

def prepend(*args)
  prepend_nodes(args)
end

#previous_element_siblingObject



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

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

#previous_siblingObject



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

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

#query_selector(selector) ⇒ Object



1751
1752
1753
1754
1755
# File 'lib/dommy/element.rb', line 1751

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



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

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



1499
1500
1501
# File 'lib/dommy/element.rb', line 1499

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

#removeObject



1329
1330
1331
# File 'lib/dommy/element.rb', line 1329

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

#remove_attribute(name) ⇒ Object



1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
# File 'lib/dommy/element.rb', line 1726

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

  key = name.to_s.downcase
  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



980
981
982
983
984
# File 'lib/dommy/element.rb', line 980

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

  attributes.remove_named_item(attr.name)
end

#remove_child(child) ⇒ Object



1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
# File 'lib/dommy/element.rb', line 1792

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.



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

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



1344
1345
1346
1347
1348
1349
1350
1351
# File 'lib/dommy/element.rb', line 1344

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



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

def replace_with_nodes(*args)
  replace_with(args)
end

#roleObject



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

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

#role=(value) ⇒ Object



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

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



881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
# File 'lib/dommy/element.rb', line 881

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)


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

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

#set_attribute(name, value) ⇒ Object



1710
1711
1712
1713
1714
1715
1716
1717
1718
# File 'lib/dommy/element.rb', line 1710

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

  key = name.to_s.downcase
  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



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

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.



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

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.



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

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

#slot=(value) ⇒ Object



1002
1003
1004
# File 'lib/dommy/element.rb', line 1002

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

#styleObject



746
747
748
# File 'lib/dommy/element.rb', line 746

def style
  @style
end

#tag_nameObject



722
723
724
# File 'lib/dommy/element.rb', line 722

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.



706
707
708
# File 'lib/dommy/element.rb', line 706

def text_content
  @__node__.text
end

#text_content=(value) ⇒ Object



710
711
712
# File 'lib/dommy/element.rb', line 710

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

#to_sObject

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



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

def to_s
  outer_html
end

#toggle_attribute(name, force = nil) ⇒ Object



922
923
924
925
926
927
928
929
930
931
932
933
# File 'lib/dommy/element.rb', line 922

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