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