All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.raquo.laminar.DomApi.scala Maven / Gradle / Ivy

The newest version!
package com.raquo.laminar

import com.raquo.ew._
import com.raquo.laminar.api.L.svg
import com.raquo.laminar.keys.{AriaAttr, EventProcessor, HtmlAttr, HtmlProp, StyleProp, SvgAttr}
import com.raquo.laminar.modifiers.EventListener
import com.raquo.laminar.nodes.{CommentNode, ReactiveElement, ReactiveHtmlElement, ReactiveSvgElement, TextNode}
import com.raquo.laminar.tags.{HtmlTag, SvgTag, Tag}
import org.scalajs.dom

import scala.annotation.tailrec
import scala.scalajs.js
import scala.scalajs.js.{JavaScriptException, |}

/** Low level DOM APIs used by Laminar.
  *
  * End users: Do not call any mutator methods here on Laminar-managed elements,
  * these methods do not make the necessary updates to Laminar internal state.
  * Instead, use regular Laminar API, or, if you must, the methods from
  * [[com.raquo.laminar.nodes.ParentNode]]
  */
object DomApi {

  /* Tree update functions */

  def appendChild(
    parent: dom.Node,
    child: dom.Node
  ): Boolean = {
    try {
      parent.appendChild(child)
      true
    } catch {
      // @TODO[Integrity] Does this only catch DOM exceptions? (here and in other methods)
      case JavaScriptException(_: dom.DOMException) => false
    }
  }

  def removeChild(
    parent: dom.Node,
    child: dom.Node
  ): Boolean = {
    try {
      parent.removeChild(child)
      true
    } catch {
      case JavaScriptException(_: dom.DOMException) => false
    }
  }

  def insertBefore(
    parent: dom.Node,
    newChild: dom.Node,
    referenceChild: dom.Node
  ): Boolean = {
    try {
      parent.insertBefore(newChild = newChild, refChild = referenceChild)
      true
    } catch {
      case JavaScriptException(_: dom.DOMException) => false
    }
  }

  def insertAfter(
    parent: dom.Node,
    newChild: dom.Node,
    referenceChild: dom.Node
  ): Boolean = {
    try {
      // Note: parent.insertBefore correctly handles the case of `refChild == null`
      parent.insertBefore(newChild = newChild, refChild = referenceChild.nextSibling)
      true
    } catch {
      case JavaScriptException(_: dom.DOMException) => false
    }
  }

  def replaceChild(
    parent: dom.Node,
    newChild: dom.Node,
    oldChild: dom.Node
  ): Boolean = {
    try {
      parent.replaceChild(newChild = newChild, oldChild = oldChild)
      true
    } catch {
      case JavaScriptException(_: dom.DOMException) => false
    }
  }


  /** Tree query functions */

  def indexOfChild(
    parent: dom.Node,
    child: dom.Node
  ): Int = {
    parent.childNodes.indexOf(child)
  }

  /** Note: This walks up the real DOM element tree, not the Laminar DOM tree.
    * See ChildNode.isDescendantOf if you want to walk up Laminar's tree instead.
    */
  @tailrec final def isDescendantOf(node: dom.Node, ancestor: dom.Node): Boolean = {
    // @TODO[Performance] Maybe use https://developer.mozilla.org/en-US/docs/Web/API/Node/contains instead (but IE only supports it for Elements)
    // For children of shadow roots, parentNode is null, but the host property contains a reference to the shadow root
    val effectiveParentNode = if (node.parentNode != null) {
      node.parentNode
    } else {
      val maybeShadowHost = node.asInstanceOf[js.Dynamic].selectDynamic("host").asInstanceOf[js.UndefOr[dom.Node]]
      maybeShadowHost.orNull
    }
    effectiveParentNode match {
      case null => false
      case `ancestor` => true
      case intermediateParent => isDescendantOf(intermediateParent, ancestor)
    }
  }



  /** Events */

  def addEventListener[Ev <: dom.Event](
    element: dom.Element,
    listener: EventListener[Ev, _]
  ): Unit = {
    //println(s"> Adding listener on ${DomApi.debugNodeDescription(element.ref)} for `${eventPropSetter.key.name}` with useCapture=${eventPropSetter.useCapture}")
    element.addEventListener(
      `type` = EventProcessor.eventProp(listener.eventProcessor).name,
      listener = listener.domCallback,
      options = listener.options
    )
  }

  def removeEventListener[Ev <: dom.Event](
    element: dom.Element,
    listener: EventListener[Ev, _]
  ): Unit = {
    element.removeEventListener(
      `type` = EventProcessor.eventProp(listener.eventProcessor).name,
      listener = listener.domCallback,
      options = listener.options
    )
  }


  /** HTML Elements */

  def createHtmlElement[Ref <: dom.html.Element](tag: HtmlTag[Ref]): Ref = {
    dom.document.createElement(tag.name).asInstanceOf[Ref]
  }


  /** HTML Attributes */

  def getHtmlAttribute[V](
    element: ReactiveHtmlElement.Base,
    attr: HtmlAttr[V]
  ): js.UndefOr[V] = {
    getHtmlAttributeRaw(element, attr).map(attr.codec.decode)
  }

  def getHtmlAttributeRaw(
    element: ReactiveHtmlElement.Base,
    attr: HtmlAttr[_]
  ): js.UndefOr[String] = {
    val domValue = element.ref.getAttributeNS(namespaceURI = null, localName = attr.name)
    if (domValue != null) {
      domValue
    } else {
      js.undefined
    }
  }

  def setHtmlAttribute[V](
    element: ReactiveHtmlElement.Base,
    attr: HtmlAttr[V],
    value: V
  ): Unit = {
    val domValue = attr.codec.encode(value)
    setHtmlAttributeRaw(element, attr, domValue)
  }

  private[laminar] def setHtmlAttributeRaw(
    element: ReactiveHtmlElement.Base,
    attr: HtmlAttr[_],
    domValue: String
  ): Unit = {
    if (domValue == null) { // End users should use `removeHtmlAttribute` instead. This is to support boolean attributes.
      removeHtmlAttribute(element, attr)
    } else {
      element.ref.setAttribute(attr.name, domValue)
    }
  }

  def removeHtmlAttribute(
    element: ReactiveHtmlElement.Base,
    attr: HtmlAttr[_]
  ): Unit = {
    element.ref.removeAttribute(attr.name)
  }


  /** HTML Properties */

  /** Returns `js.undefined` when the property is missing on the element.
    * If the element type supports this property, it should never be js.undefined.
    */
  def getHtmlProperty[V, DomV](
    element: ReactiveHtmlElement.Base,
    prop: HtmlProp[V, DomV]
  ): js.UndefOr[V] = {
    val domValue = getHtmlPropertyRaw(element, prop)
    domValue.map(prop.codec.decode)
  }

  def getHtmlPropertyRaw[V, DomV](
    element: ReactiveHtmlElement.Base,
    prop: HtmlProp[V, DomV]
  ): js.UndefOr[DomV] = {
    element.ref.asInstanceOf[js.Dynamic].selectDynamic(prop.name).asInstanceOf[js.UndefOr[DomV]]
  }

  def setHtmlProperty[V, DomV](
    element: ReactiveHtmlElement.Base,
    prop: HtmlProp[V, DomV],
    value: V
  ): Unit = {
    val domValue = prop.codec.encode(value)
    setHtmlPropertyRaw(element, prop, domValue)
  }

  private[laminar] def setHtmlPropertyRaw[V, DomV](
    element: ReactiveHtmlElement.Base,
    prop: HtmlProp[V, DomV],
    value: DomV
  ): Unit = {
    element.ref.asInstanceOf[js.Dynamic].updateDynamic(prop.name)(value.asInstanceOf[js.Any])
  }


  /** CSS Style Properties */

  /** Note: this only gets inline style values – those set via the `style` attribute, which includes
    * all style props set by Laminar. It does not account for CSS declarations in `