Class: Dommy::ShadowRoot

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

Overview

ShadowRoot — a DocumentFragment-shaped subtree attached to a host Element via attachShadow. Lives in its own Nokogiri fragment that's invisible to the outer document's tree walks (querySelector, getElementById, children, etc.), which is the core "encapsulation" the spec promises.

Tree manipulation works the same as a normal Element/Fragment; the boundary is enforced only on outer queries and event composition. CSS scoping (:host / ::slotted / ::part, and this tree's <style> rules) is handled by the cascade — see internal/css/rule_index.rb and css-cascade.md.

Constant Summary

Constants included from Node

Node::ATTRIBUTE_NODE, Node::CDATA_SECTION_NODE, Node::COMMENT_NODE, Node::DOCUMENT_FRAGMENT_NODE, Node::DOCUMENT_NODE, Node::DOCUMENT_POSITION_CONTAINED_BY, Node::DOCUMENT_POSITION_CONTAINS, Node::DOCUMENT_POSITION_DISCONNECTED, Node::DOCUMENT_POSITION_FOLLOWING, Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, Node::DOCUMENT_POSITION_PRECEDING, Node::DOCUMENT_TYPE_NODE, Node::ELEMENT_NODE, Node::HTML_NAMESPACE, Node::PROCESSING_INSTRUCTION_NODE, Node::TEXT_NODE, Node::XMLNS_NAMESPACE, Node::XML_NAMESPACE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Methods included from Internal::ParentNode

#append, #append_child, #normalize, #prepend, #replace_children

Methods included from Internal::ChildNode

#child_node_after, #child_node_before, #child_node_replace_with, #nullable_dom_string, #validate_insert_before_ref!

Methods included from Node

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

Methods included from EventTarget

#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler

Constructor Details

#initialize(host, mode:, delegates_focus: false, slot_assignment: "named") ⇒ ShadowRoot

Returns a new instance of ShadowRoot.



24
25
26
27
28
29
30
31
32
# File 'lib/dommy/shadow_root.rb', line 24

def initialize(host, mode:, delegates_focus: false, slot_assignment: "named")
  @host = host
  @mode = mode.to_s
  @delegates_focus = !!delegates_focus
  @slot_assignment = slot_assignment.to_s
  @document = host.document
  @__node__ = @document.backend_doc.fragment("")
  @document.__internal_register_shadow_fragment__(@__node__, self)
end

Instance Attribute Details

#delegates_focusObject (readonly)

Returns the value of attribute delegates_focus.



20
21
22
# File 'lib/dommy/shadow_root.rb', line 20

def delegates_focus
  @delegates_focus
end

#documentObject (readonly)

Returns the value of attribute document.



20
21
22
# File 'lib/dommy/shadow_root.rb', line 20

def document
  @document
end

#hostObject (readonly)

Returns the value of attribute host.



20
21
22
# File 'lib/dommy/shadow_root.rb', line 20

def host
  @host
end

#modeObject (readonly)

Returns the value of attribute mode.



20
21
22
# File 'lib/dommy/shadow_root.rb', line 20

def mode
  @mode
end

#slot_assignmentObject (readonly)

Returns the value of attribute slot_assignment.



20
21
22
# File 'lib/dommy/shadow_root.rb', line 20

def slot_assignment
  @slot_assignment
end

Instance Method Details

#[](key) ⇒ Object

[] accessor mirrors the bracket convention used elsewhere.



197
198
199
# File 'lib/dommy/shadow_root.rb', line 197

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

#[]=(k, v) ⇒ Object



201
202
203
# File 'lib/dommy/shadow_root.rb', line 201

def []=(k, v)
  __js_set__(k.to_s, v)
end

#__dommy_backend_node__Object



22
# File 'lib/dommy/shadow_root.rb', line 22

def __dommy_backend_node__ = @__node__

#__internal_event_parent__Object

Event bubbling stops at the ShadowRoot unless event has composed: true. The host is the bubble-path successor when composition crosses the boundary (handled in Event dispatch).



317
318
319
# File 'lib/dommy/shadow_root.rb', line 317

def __internal_event_parent__
  nil
end

#__js_call__(method, args) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/dommy/shadow_root.rb', line 269

def __js_call__(method, args)
  case method
  when "querySelector"
    query_selector(Internal.css_query_arg!(args))
  when "querySelectorAll"
    query_selector_all(Internal.css_query_arg!(args))
  when "getElementById"
    get_element_by_id(args[0])
  when "isEqualNode"
    is_equal_node(args[0])
  when "isSameNode"
    is_same_node(args[0])
  when "compareDocumentPosition"
    compare_document_position(args[0])
  when "hasChildNodes"
    @__node__.children.any?
  when "normalize"
    nil
  when "append"
    append(*args)
  when "prepend"
    prepend(*args)
  when "replaceChildren"
    replace_children(*args)
  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 "getRootNode"
    get_root_node(args[0])
  when "contains"
    contains?(args[0])
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1], args[2])
  when "dispatchEvent"
    dispatch_event(args[0])
  end
end

#__js_get__(key) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/dommy/shadow_root.rb', line 205

def __js_get__(key)
  case key
  when "host"
    @host
  when "mode"
    @mode
  when "delegatesFocus"
    @delegates_focus
  when "slotAssignment"
    @slot_assignment
  when "activeElement"
    active_element
  when "styleSheets"
    style_sheets
  when "innerHTML"
    inner_html
  when "textContent"
    text_content
  when "children"
    children
  when "childNodes"
    child_nodes
  when "childElementCount"
    child_element_count
  when "firstChild"
    first_child
  when "lastChild"
    last_child
  when "firstElementChild"
    first_element_child
  when "lastElementChild"
    last_element_child
  when "nodeType"
    11
  when "nodeName"
    "#document-fragment"
  else
    # Any unknown key (incl. framework-private `_`/`$` expandos like
    # lit-html's `_$litPart$`, which it probes with `=== undefined`) is
    # genuinely absent: JS `undefined`, `in` false — matching Element.
    Bridge::ABSENT
  end
end

#__js_set__(key, value) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/dommy/shadow_root.rb', line 249

def __js_set__(key, value)
  case key
  when "innerHTML"
    self.inner_html = value
  when "textContent"
    self.text_content = value
  else
    return Bridge::UNHANDLED
  end

  nil
end

#active_elementObject

shadowRoot.activeElement — the focused element, retargeted to this shadow tree: the document's focused element when it is inside this (connected) shadow root, else null.



97
98
99
100
101
102
# File 'lib/dommy/shadow_root.rb', line 97

def active_element
  return nil unless connected?

  focused = @document.__internal_focused_element__
  focused && contains?(focused) ? focused : nil
end

#child_element_countObject



67
68
69
# File 'lib/dommy/shadow_root.rb', line 67

def child_element_count
  @__node__.element_children.size
end

#child_nodesObject



63
64
65
# File 'lib/dommy/shadow_root.rb', line 63

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

#childrenObject



59
60
61
# File 'lib/dommy/shadow_root.rb', line 59

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

#connected?Boolean

Whether this shadow root participates in the document (its host is connected). A disconnected shadow tree has no active element and no live style sheets.

Returns:

  • (Boolean)


90
91
92
# File 'lib/dommy/shadow_root.rb', line 90

def connected?
  @host.respond_to?(:is_connected?) && @host.is_connected?
end

#contains?(other) ⇒ Boolean

Returns:

  • (Boolean)


187
188
189
190
191
192
193
194
# File 'lib/dommy/shadow_root.rb', line 187

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

#first_childObject



71
72
73
# File 'lib/dommy/shadow_root.rb', line 71

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

#first_element_childObject



79
80
81
# File 'lib/dommy/shadow_root.rb', line 79

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

#get_element_by_id(id) ⇒ Object



130
131
132
133
134
135
136
137
# File 'lib/dommy/shadow_root.rb', line 130

def get_element_by_id(id)
  return nil if id.nil? || id.to_s.empty?

  # getElementById matches the `id` attribute literally, not as a CSS
  # selector, so escape special characters (e.g. React `useId` `:rjm:`) to a
  # valid id-selector ident — a raw "##{id}" would be an invalid selector.
  @document.wrap_node(@__node__.at_css("##{Dommy::CSSNamespace.escape(id)}"))
end

#get_root_node(options = nil) ⇒ Object

getRootNode() returns the ShadowRoot itself; getRootNode({composed: true}) crosses the shadow boundary and returns the root of the host's tree (the document, or an outer shadow root for nested shadows).



178
179
180
181
182
183
184
185
# File 'lib/dommy/shadow_root.rb', line 178

def get_root_node(options = nil)
  composed = options.is_a?(Hash) &&
    EventTarget.js_truthy?(options.key?("composed") ? options["composed"] : options[:composed])
  return self unless composed
  return @host.root_node({"composed" => true}) if @host.respond_to?(:root_node)

  self
end

#inner_htmlObject

---- Public Ruby API (ParentNode + DocumentFragment mixin) ----



36
37
38
# File 'lib/dommy/shadow_root.rb', line 36

def inner_html
  @__node__.children.map(&:to_html).join
end

#inner_html=(html) ⇒ Object



40
41
42
43
44
45
46
47
48
# File 'lib/dommy/shadow_root.rb', line 40

def inner_html=(html)
  removed = @__node__.children.to_a
  removed.each(&:unlink)
  fragment = Parser.fragment(html.to_s, owner_doc: @document.backend_doc)
  added = fragment.children.to_a
  added.each { |n| @__node__.add_child(n) }
  notify_child_list(added: added, removed: removed)
  nil
end

#insert_before(node, ref) ⇒ Object

Node-level child mutations the spec exposes on a DocumentFragment (and so on a ShadowRoot). lit-html drives rendering through insertBefore / removeChild / replaceChild against the render root, so a shadow root must support them — not just appendChild.



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/dommy/shadow_root.rb', line 143

def insert_before(node, ref)
  nodes = detach_dom_nodes(node)
  ref_bn = ref.respond_to?(:__dommy_backend_node__) ? ref.__dommy_backend_node__ : nil
  if ref_bn && ref_bn.parent == @__node__
    nodes.each { |n| ref_bn.add_previous_sibling(n) }
  else
    nodes.each { |n| @__node__.add_child(n) }
  end
  notify_child_list(added: nodes)
  node
end

#last_childObject



75
76
77
# File 'lib/dommy/shadow_root.rb', line 75

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

#last_element_childObject



83
84
85
# File 'lib/dommy/shadow_root.rb', line 83

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

#query_selector(selector) ⇒ Object



116
117
118
119
120
121
# File 'lib/dommy/shadow_root.rb', line 116

def query_selector(selector)
  return nil if selector.nil?

  ast = Internal::SelectorParser.parse!(selector)
  Internal::SelectorMatcher.query_first(self, ast, scope: self)
end

#query_selector_all(selector) ⇒ Object



123
124
125
126
127
128
# File 'lib/dommy/shadow_root.rb', line 123

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

  ast = Internal::SelectorParser.parse!(selector)
  NodeList.new(Internal::SelectorMatcher.query(self, ast, scope: self))
end

#remove_child(node) ⇒ Object



155
156
157
158
159
160
161
162
# File 'lib/dommy/shadow_root.rb', line 155

def remove_child(node)
  bn = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil
  raise DOMException::NotFoundError, "node is not a child of this shadow root" unless bn && bn.parent == @__node__

  bn.unlink
  notify_child_list(removed: [bn])
  node
end

#replace_child(new_child, old_child) ⇒ Object



164
165
166
167
168
169
170
171
172
173
# File 'lib/dommy/shadow_root.rb', line 164

def replace_child(new_child, old_child)
  old_bn = old_child.respond_to?(:__dommy_backend_node__) ? old_child.__dommy_backend_node__ : nil
  raise DOMException::NotFoundError, "node is not a child of this shadow root" unless old_bn && old_bn.parent == @__node__

  added = detach_dom_nodes(new_child)
  added.each { |n| old_bn.add_previous_sibling(n) }
  old_bn.unlink
  notify_child_list(added: added, removed: [old_bn])
  old_child
end

#style_sheetsObject

shadowRoot.styleSheets — the CSSStyleSheets of the <style> / <link> elements within this (connected) shadow tree (mirrors Document#style_sheets); empty for a disconnected shadow root.



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

def style_sheets
  return NodeList.new unless connected?

  sheets = query_selector_all("style, link").filter_map do |element|
    element.sheet if element.respond_to?(:sheet)
  end
  NodeList.new(sheets)
end

#text_contentObject



50
51
52
# File 'lib/dommy/shadow_root.rb', line 50

def text_content
  @__node__.text
end

#text_content=(value) ⇒ Object



54
55
56
57
# File 'lib/dommy/shadow_root.rb', line 54

def text_content=(value)
  @__node__.children.each(&:unlink)
  @__node__.add_child(Backend.create_text(value.to_s, @document.backend_doc))
end