opal/opal-browser

View on GitHub
opal/browser/dom/node.rb

Summary

Maintainability
D
1 day
Test Coverage
# backtick_javascript: true
module Browser; module DOM

# Abstract class for all DOM node types.
#
# @see https://developer.mozilla.org/en-US/docs/Web/API/Node
class Node
  include Browser::NativeCachedWrapper

  ELEMENT_NODE                = 1
  ATTRIBUTE_NODE              = 2
  TEXT_NODE                   = 3
  CDATA_SECTION_NODE          = 4
  ENTITY_REFERENCE_NOCE       = 5
  ENTITY_NODE                 = 6
  PROCESSING_INSTRUCTION_NODE = 7
  COMMENT_NODE                = 8
  DOCUMENT_NODE               = 9
  DOCUMENT_TYPE_NODE          = 10
  DOCUMENT_FRAGMENT_NODE      = 11
  NOTATION_NODE               = 12

  # Wrap a native DOM node.
  #
  # @param value [native] the native DOM node
  #
  # @return [Node]
  def self.new(value)
    if self == Node
      @classes ||= [nil, Element, Attribute, Text, CDATA, nil, nil, nil, Comment, Document, nil, DocumentFragment]

      if klass = @classes[`value.nodeType`]
        klass.new(value)
      else
        raise ArgumentError, 'cannot instantiate a non derived Node object'
      end
    else
      super
    end
  end

  def initialize(node)
    raise ArgumentError, "Please ensure that #initialize of #{self.class} accepts one argument" unless node
    super
  end

  # Return true of the other element is the same underlying DOM node.
  #
  # @return [Boolean]
  def ==(other)
    `#@native === #{Native.convert(other)}`
  end

  # Initialize a new node after `#dup` or `#clone`.
  #
  # This method is not to be called directly. Use `Node#dup` or
  # `Node#clone`.
  #
  # This method creates a deep detached clone of a DOM subtree to be used
  # in the same document. The new node will have all events detached.
  def initialize_copy(old)
    set_native_reference `#{old.to_n}.cloneNode(true)`
  end

  # Append a child to the node.
  #
  # When passing a {String} a text node will be created.
  #
  # When passing an Object that responds to #each, every yielded element
  # will be added following the same logic.
  #
  # @param node [String, Node, #each, #to_n] the node to append
  #
  # @return [self]
  def <<(node)
    if Opal.respond_to? node, :each
      node.each { |n| self << n }
      return self
    elsif Opal.respond_to? node, :to_dom
      node = node.to_dom(document)
    end

    unless native?(node)
      if String === node
        node = `#@native.ownerDocument.createTextNode(node)`
      else
        node = Native.convert(node)
      end
    end

    `#@native.appendChild(node)`

    self
  end

  def >>(node)
    if Opal.respond_to? node, :each
      node.each { |n| self >> n }
      return self
    elsif Opal.respond_to? node, :to_dom
      node = node.to_dom(document)
    end

    unless native?(node)
      if String === node
        node = `#@native.ownerDocument.createTextNode(node)`
      else
        node = Native.convert(node)
      end
    end

    if `#@native.firstChild == null`
      `#@native.appendChild(node)`
    else
      `#@native.insertBefore(node, #@native.firstChild)`
    end

    self
  end

  def add_child(node = nil, &block)
    unless node
      node = DOM(&block)
    end

    self << node
  end

  # Add the passed node after this one.
  #
  # When passing a {String} a text node will be created.
  #
  # @param node [String, Node, #to_n] the node to add
  def add_next_sibling(node = nil, &block)
    unless node
      node = DOM(&block)
    end
    node = node.to_dom(document) if Opal.respond_to? node, :to_dom

    unless native?(node)
      if String === node
        node = `#@native.ownerDocument.createTextNode(node)`
      else
        node = Native.convert(node)
      end
    end

    `#@native.parentNode.insertBefore(node, #@native.nextSibling)`
  end

  # Add the passed node before this one.
  #
  # When passing a {String} a text node will be created.
  #
  # @param node [String, Node, #to_n] the node to add
  def add_previous_sibling(node = nil, &block)
    unless node
      node = DOM(&block)
    end
    node = node.to_dom(document) if Opal.respond_to? node, :to_dom

    unless native?(node)
      if String === node
        node = `#@native.ownerDocument.createTextNode(node)`
      else
        node = Native.convert(node)
      end
    end

    `#@native.parentNode.insertBefore(node, #@native)`
  end

  alias after add_next_sibling

  # Append the node to the passed one.
  #
  # @param node [Node] the node to append to
  def append_to(node)
    node << self
    self
  end

  # Get an array of ancestors.
  #
  # Passing a selector will select the ancestors matching it.
  #
  # @param expression [String] the selector to use as filter
  #
  # @return [NodeSet]
  def ancestors(expression = nil)
    return NodeSet[] unless parent

    parents = [parent]

    while parent = parents.last.parent
      parents << parent
    end

    if Document === parents.last
      parents.pop
    end

    if expression
      parents.select! { |p| p =~ expression }
    end

    NodeSet.new(parents)
  end

  def attached?
    `#@native.isConnected`
  end

  alias before add_previous_sibling

  # Remove the node from its parent.
  def remove
    parent.remove_child(self) if parent
    self
  end

  # Remove all the children of the node.
  def clear
    children.remove
  end

  # @!attribute content
  # @return [String] the inner text content of the node
  if Browser.supports? 'Element.textContent'
    def content
      `#@native.textContent`
    end

    def content=(value)
      `#@native.textContent = #{value}`
    end
  elsif Browser.supports? 'Element.innerText'
    def content
      `#@native.innerText`
    end

    def content=(value)
      `#@native.innerText = #{value}`
    end
  else
    def content
      raise NotImplementedError, 'node text content unsupported'
    end

    def content=(value)
      raise NotImplementedError, 'node text content unsupported'
    end
  end

  def blank?
    raise NotImplementedError
  end

  # Return true if the node is a CDATA section.
  def cdata?
    node_type == CDATA_SECTION_NODE
  end

  # @!attribute [r] child
  # @return [Node?] the first child of the node
  def child
    children.first
  end

  # @!attribute children
  # @return [NodeSet] the children of the node
  def children
    NodeSet[Native::Array.new(`#@native.childNodes`)]
  end

  def children=(node)
    raise NotImplementedError
  end

  # Return true if the node is a comment.
  def comment?
    node_type == COMMENT_NODE
  end

  # Return true if the node is a custom element.
  def custom?
    false
  end

  # @!attribute [rw] document
  # @return [Document?] the document the node is attached to
  def document
    DOM(`#@native.ownerDocument`) if defined?(`#@native.ownerDocument`)
  end

  # Detach a node and transfer it to another document.
  def document=(new_document)
    `#{Native.try_convert(new_document, new_document)}.adoptNode(#@native)`
  end

  # Return true if the node is a document.
  def document?
    node_type == DOCUMENT_NODE
  end

  # Return true if the node is an element.
  def elem?
    node_type == ELEMENT_NODE
  end

  alias element? elem?

  # @!attribute [r] element_children
  # @return [NodeSet] all the children which are elements
  def element_children
    children.select(&:element?)
  end

  alias elements element_children

  # @!attribute [r] first_element_child
  # @return [Element?] the first element child
  def first_element_child
    element_children.first
  end

  # Return true if the node is a document fragment.
  def fragment?
    node_type == DOCUMENT_FRAGMENT_NODE
  end

  alias inner_text content
  alias inner_text= content=

  # @!attribute [r] last_element_child
  # @return [Element?] the last element child
  def last_element_child
    element_children.last
  end

  # @!attribute name
  # @return [String] the name of the node
  def name
    `#@native.nodeName || nil`
  end

  def name=(value)
    `#@native.nodeName = #{value.to_s}`
  end

  # @!attribute [r] namespace
  # @return [String] the namespace of the node
  def namespace
    `#@native.namespaceURI || nil`
  end

  # @!attribute next
  # @return [Node?] the next sibling of the node
  def next
    DOM(`#@native.nextSibling`) if `#@native.nextSibling != null`
  end

  alias next= add_next_sibling

  # @!attribute [r] next_element
  # @return [Element?] the next element sibling of the node
  def next_element
    current = self.next

    while current && !current.element?
      current = current.next
    end

    current
  end

  alias next_sibling next

  alias node_name name

  alias node_name= name=

  # @!attribute [r] node_type
  # @return [Symbol] the type of the node
  def node_type
    `#@native.nodeType`
  end

  # @!attribute outer_html
  # @return [String] the simulated outer html of the node
  def outer_html
    div = $document.create_element("DIV")
    div << self.dup
    div.inner_html
  end

  # @!attribute parent
  # @return [Element?] the parent of the node
  def parent
    DOM(`#@native.parentNode`) if `#@native.parentNode != null`
  end

  def parent=(node)
    `#@native.parentNode = #{Native.convert(node)}`
  end

  def parse(text, options = {})
    raise NotImplementedError
  end

  def path
    raise NotImplementedError
  end

  # Prepend the node to the passed one.
  #
  # @param node [Node] the node to prepend to
  def prepend_to(node)
    node >> self
    self
  end

  # @!attribute previous
  # @return [Node?] the previous sibling of the node
  def previous
    DOM(`#@native.previousSibling`) if `#@native.previousSibling != null`
  end

  alias previous= add_previous_sibling

  # @!attribute [r] previous_element
  # @return [Element?] the previous element sibling of the node
  def previous_element
    current = self.previous

    while current && !current.element?
      current = current.previous
    end

    current
  end

  alias previous_sibling previous

  # Remove the given node from the children of this node.
  def remove_child(node)
    `#@native.removeChild(#{Native.try_convert(node)})`
    self
  end

  # Replace the node with the given one.
  #
  # @todo implement for NodeSet
  #
  # @param node [Node] the node to replace with
  # @return [Node] the passed node
  def replace(node)
    node = node.to_dom(document) if Opal.respond_to? node, :to_dom

    unless native?(node)
      if String === node
        node = `#@native.ownerDocument.createTextNode(node)`
      else
        node = Native.convert(node)
      end
    end

    `#@native.parentNode.replaceChild(node, #@native)`

    DOM(node)
  end

  alias replace_with replace

  alias text content
  alias text= content=

  # Return true if the node is a text node.
  def text?
    node_type == TEXT_NODE
  end

  def traverse(&block)
    raise NotImplementedError
  end

  alias type node_type

  # @!attribute value
  # @return [String] the value of the node
  def value
    `#@native.nodeValue || nil`
  end

  def value=(value)
    `#@native.nodeValue = value`
  end

  # @private
  def inspect
    "#<DOM::Node: #{name}>"
  end
end

end; end