Module: Dommy::Js::DomInterfaces

Defined in:
lib/dommy/js/dom_interfaces.rb

Overview

Derives WebIDL interface metadata for a Dommy DOM object: the most-derived interface name and the single-inheritance chain up to the root (EventTarget for nodes). This mirrors the JS prototype chain the bridge builds so instanceof / Object.prototype.toString resolve correctly.

Engine-agnostic, and the single home for DOM interface hierarchy knowledge: BASE_CHAINS (seeded eagerly on the JS side) must stay consistent with what #chain_for derives from real objects.

Constant Summary collapse

NAME_OVERRIDES =

Dommy class basename -> WebIDL interface name, where they diverge. Anything not listed uses the class basename verbatim (HTMLDivElement, …).

{
  "TextNode" => "Text",
  "CommentNode" => "Comment",
  "ProcessingInstructionNode" => "ProcessingInstruction",
  "CharacterDataNode" => "CharacterData",
  "Fragment" => "DocumentFragment",
  "ClassList" => "DOMTokenList",
  "DatasetMap" => "DOMStringMap",
  "StyleDeclaration" => "CSSStyleDeclaration",
  "LiveNodeList" => "NodeList",
  "StandaloneEventTarget" => "EventTarget"
}.freeze
HTML_LEAF_INTERFACES =

Concrete HTML element interfaces. A browser exposes every one of these as a global constructor whether or not an instance exists, so a framework's bare instanceof HTMLInputElement feature check resolves regardless of page content (idiomorph, Turbo's morph engine, probes instanceof HTMLInputElement/HTMLTextAreaElement during focus restoration even when the page has no such element). Each is a direct HTMLElement subclass except the two media leaves, appended with their chains below. Mirrors the class HTMLxxxElement < HTMLElement set in the dommy gem's html_elements.

%w[
  HTMLAnchorElement HTMLAreaElement HTMLBaseElement HTMLBodyElement
  HTMLBRElement HTMLButtonElement HTMLCanvasElement HTMLDataElement HTMLDetailsElement
  HTMLDialogElement HTMLDirectoryElement HTMLDivElement HTMLDListElement
  HTMLDataListElement HTMLEmbedElement HTMLFieldSetElement HTMLFontElement
  HTMLFrameElement HTMLFrameSetElement HTMLParamElement HTMLTableColElement
  HTMLFormElement HTMLHeadElement HTMLHeadingElement HTMLHRElement
  HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement
  HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement
  HTMLMapElement HTMLMetaElement HTMLMeterElement HTMLModElement
  HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement
  HTMLOutputElement HTMLParagraphElement HTMLPictureElement HTMLPreElement
  HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement
  HTMLSlotElement HTMLSourceElement HTMLSpanElement HTMLStyleElement
  HTMLTableCaptionElement HTMLTableCellElement HTMLTableElement
  HTMLTableRowElement HTMLTableSectionElement HTMLTemplateElement
  HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement
  HTMLUListElement
].freeze
BASE_CHAINS =

Base interface chains seeded eagerly on the JS side so instanceof Node / typeof HTMLElement resolve before an instance of that exact type has crossed. Concrete leaves (HTMLButtonElement, …) are built lazily from #chain_for when an instance crosses. Keep consistent with #chain_for.

[
  %w[Node EventTarget],
  %w[Element Node EventTarget],
  %w[HTMLElement Element Node EventTarget],
  %w[SVGElement Element Node EventTarget],
  %w[CharacterData Node EventTarget],
  %w[Text CharacterData Node EventTarget],
  %w[Comment CharacterData Node EventTarget],
  %w[ProcessingInstruction CharacterData Node EventTarget],
  %w[Document Node EventTarget],
  %w[DocumentFragment Node EventTarget],
  # ShadowRoot is a DocumentFragment subclass; seeded so bare `node
  # instanceof ShadowRoot` (Alpine.js walks the tree with this) resolves.
  %w[ShadowRoot DocumentFragment Node EventTarget],
  %w[DocumentType Node EventTarget],
  %w[Attr Node EventTarget],
  %w[Event],
  %w[CustomEvent Event],
  %w[MessageEvent Event],
  %w[PopStateEvent Event],
  %w[HashChangeEvent Event],
  %w[SubmitEvent Event],
  %w[CloseEvent Event],
  %w[UIEvent Event],
  %w[MouseEvent UIEvent Event],
  %w[WheelEvent MouseEvent UIEvent Event],
  %w[FocusEvent UIEvent Event],
  %w[KeyboardEvent UIEvent Event],
  %w[CompositionEvent UIEvent Event],
  %w[PromiseRejectionEvent Event],
  %w[ToggleEvent Event],
  %w[ErrorEvent Event],
  %w[DOMException],
  # Window-exposed constructors that frameworks call bare (new X(...)).
  # Seeding them creates the global; construction routes to the window.
  %w[MutationObserver], %w[IntersectionObserver], %w[ResizeObserver],
  %w[PerformanceObserver], %w[AbortController], %w[AbortSignal EventTarget],
  %w[FormData], %w[URL], %w[URLSearchParams], %w[Headers], %w[Request], %w[Response],
  %w[Blob], %w[File], %w[FileList], %w[FileReader], %w[XMLHttpRequest],
  %w[TextEncoder], %w[TextDecoder], %w[DOMParser], %w[XMLSerializer],
  %w[MessageChannel], %w[BroadcastChannel], %w[WebSocket], %w[EventSource],
  %w[Notification], %w[Worker], %w[DataTransfer],
  %w[ReadableStream], %w[WritableStream], %w[TransformStream],
  %w[Range],
  # Web Storage: seeded so `localStorage instanceof Storage` resolves and
  # `Storage.prototype` exists (a global constructor, construction routes
  # to the window / throws like the browser's illegal constructor).
  %w[Storage],
  # CSSOM stylesheet interfaces. Seeded so `style instanceof CSSStyleSheet`
  # resolves: Lit's css-tag runs that check while deciding whether to use
  # constructable stylesheets; with the interface present but
  # `adoptedStyleSheets` unsupported it falls back to injecting a <style>
  # element, which Dommy handles.
  %w[CSSStyleSheet StyleSheet], %w[StyleSheet],
  # Collection interfaces, seeded so `result instanceof NodeList` /
  # `instanceof HTMLCollection` resolve (querySelectorAll, children, …).
  %w[NodeList], %w[HTMLCollection], %w[RadioNodeList NodeList],
  %w[HTMLFormControlsCollection HTMLCollection],
  # Traversal: NodeFilter exposes only [Constant]s (NodeFilter.SHOW_ELEMENT,
  # .FILTER_ACCEPT, …); TreeWalker/NodeIterator are instances.
  %w[NodeFilter], %w[TreeWalker], %w[NodeIterator],
  # Concrete HTML element interfaces (see HTML_LEAF_INTERFACES) + the media
  # subtree, so bare `instanceof HTMLInputElement` always resolves.
  *HTML_LEAF_INTERFACES.map { |n| [n, "HTMLElement", "Element", "Node", "EventTarget"] },
  # createElementNS with an unrecognized HTML-namespace local name yields an
  # HTMLUnknownElement; seed it so a bare `instanceof HTMLUnknownElement`
  # resolves even before such an element crosses.
  %w[HTMLUnknownElement HTMLElement Element Node EventTarget],
  %w[HTMLMediaElement HTMLElement Element Node EventTarget],
  %w[HTMLAudioElement HTMLMediaElement HTMLElement Element Node EventTarget],
  %w[HTMLVideoElement HTMLMediaElement HTMLElement Element Node EventTarget]
].freeze

Class Method Summary collapse

Class Method Details

.chain_for(obj) ⇒ Object

Walk the Dommy class superclass chain (HTMLDivElement < HTMLElement < Element), then append the module-provided base interfaces (Node -> EventTarget) for nodes, since Dommy models Node as a mixin rather than a superclass. Stops at the first foreign superclass (Object, or StandardError for DOMException) so non-DOM ancestors stay out.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/dommy/js/dom_interfaces.rb', line 147

def chain_for(obj)
  # WHATWG models DOMException as a single interface distinguished by its
  # `name` property — there are no per-name subclasses. Collapse Dommy's
  # convenience subclasses (AbortError < DOMException) to the one interface
  # so a DOMException crossing as a value (e.g. `signal.reason`) reports
  # `constructor === DOMException`, which assert_throws_dom checks.
  return ["DOMException"] if defined?(Dommy::DOMException) && obj.is_a?(Dommy::DOMException)

  names = []
  klass = obj.class
  while klass && klass.name&.start_with?("Dommy::")
    name = name_for(klass)
    names << name if name && !names.include?(name)
    klass = klass.superclass
  end
  if defined?(Dommy::Node) && obj.is_a?(Dommy::Node)
    names << "Node" unless names.include?("Node")
    names << "EventTarget" unless names.include?("EventTarget")
  end
  names
end

.info(obj) ⇒ Object

{ "name" => most-derived interface, "chain" => [...] } for a host object.



137
138
139
140
# File 'lib/dommy/js/dom_interfaces.rb', line 137

def info(obj)
  chain = chain_for(obj)
  {"name" => chain.first, "chain" => chain}
end

.name_for(klass) ⇒ Object



169
170
171
172
173
174
# File 'lib/dommy/js/dom_interfaces.rb', line 169

def name_for(klass)
  base = klass.name&.split("::")&.last
  return nil unless base

  NAME_OVERRIDES.fetch(base, base)
end