Coverage Summary for Class: ElementCreator (kweb)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
ElementCreator |
41.2%
(7/17)
|
77.8%
(28/36)
|
65.8%
(50/76)
|
65.7%
(331/504)
|
ElementCreator$cleanup$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/1)
|
ElementCreator$closeOnCleanup$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
ElementCreator$Companion |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(2/2)
|
ElementCreator$element$1 |
100%
(1/1)
|
|
100%
(2/2)
|
100%
(12/12)
|
ElementCreator$element$1$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(4/4)
|
ElementCreator$element$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/7)
|
ElementCreator$element$id$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(6/6)
|
ElementCreator$elementScope$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
ElementCreator$kval$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/10)
|
ElementCreator$kvar$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/10)
|
Total |
40.7%
(11/27)
|
77.8%
(28/36)
|
63.2%
(55/87)
|
62.8%
(355/565)
|
package kweb
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kweb.html.BodyElement
import kweb.html.HeadElement
import kweb.plugins.KwebPlugin
import kweb.state.CloseReason
import kweb.state.KVal
import kweb.state.KVar
import kweb.util.KWebDSL
import kweb.util.json
import mu.KLogging
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.KClass
typealias Cleaner = () -> Unit
/**
* Responsible for creating new DOM elements, and cleaning up [Cleaner]s, [KVar]s, and other
* related objects when DOM elements are deleted.
*
* [ElementCreator] is typically used as a [receiver](https://stackoverflow.com/a/45875492)
* for element creation functions like [p] or [element].
*/
@KWebDSL
open class ElementCreator<out PARENT_TYPE : Element>(
val element: PARENT_TYPE,
val parentCreator: ElementCreator<*>? = element.creator,
val insertBefore: String? = null
) {
companion object : KLogging()
@Volatile
private var cleanupListeners: MutableCollection<Cleaner>? = null
@Volatile
private var isCleanedUp = false
val elementsCreatedCount: Int get() = elementsCreatedCountAtomic.get()
private val elementsCreatedCountAtomic = AtomicInteger(0)
val browser: WebBrowser get() = element.browser
/**
* Create a new element, specifying its [tag](https://www.javatpoint.com/html-tags) and
* [attributes](https://www.javatpoint.com/html-attributes).
*
* Tag-specific functions like [p], [select], and others call this function and should
* be used in preference to it if available.
*
* @param tag The HTML tag, eg. "p", "select", "a", etc
* @param attributes The HTML element's attributes
* @param namespace If non-null elements will be created with [Document.createElementNS()](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS)
* with the specified namespace. If null then Kweb will use [Document.createElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement).
*/
fun element(tag: String, attributes: Map<String, JsonPrimitive> = attr, namespace: String? = null, new: (ElementCreator<*>.(Element) -> Unit)? = null): Element {
val mutAttributes = HashMap(attributes)
val id: String = mutAttributes.computeIfAbsent("id") { JsonPrimitive("K" + browser.generateId()) }.content
val htmlDoc = browser.htmlDocument.get()
val createElementStatement = when (namespace) {
null -> "document.createElement(tag);"
else -> "document.createElementNS(\"${namespace}\", tag);"
}
when {
htmlDoc != null -> {
val parentElement = when (element) {
is HeadElement -> htmlDoc.head()
is BodyElement -> htmlDoc.body()
else -> htmlDoc.getElementById(element.id)
} ?: error("Can't find element with id ${element.id}")
val jsElement =
if (insertBefore != null) {
val ne = htmlDoc.createElement(tag)
htmlDoc.getElementById(insertBefore)!!.before(ne)
ne
} else {
parentElement.appendElement(tag)
}
for ((k, v) in mutAttributes) {
jsElement.attr(k, v.content)
}
}
element.browser.isCatchingOutbound() != null -> {
//language=JavaScript
val createElementJs = """
console.log("Creating new element")
let tag = {};
let attributes = {};
let myId = {};
let parentId = {};
let insertBefore = {};
console.log("insertBefore = " + insertBefore)
let newEl = $createElementStatement
newEl.setAttribute("id", myId);
for (const key in attributes) {
if ( key !== "id") {
newEl.setAttribute(key, attributes[key]);
}
}
let parentElement = document.getElementById(parentId);
let startNode = document.getElementById(insertBefore)
if (insertBefore !== undefined) {
parentElement.insertBefore(newEl, startNode)
} else {
parentElement.appendChild(newEl);
}
""".trimIndent()
browser.callJsFunction(
createElementJs, JsonPrimitive(tag), JsonObject(mutAttributes), id.json,
JsonPrimitive(element.id), JsonPrimitive(insertBefore ?: ""), JsonPrimitive(elementsCreatedCount)
)
}
else -> {
//The way I have written this function, instead of attributes.get(), we now use attributes[].
//language=JavaScript
val createElementJs = """
console.log("Creating new element in other place")
let tag = {};
let attributes = {};
let myId = {};
let parentId = {};
let insertBefore = {};
let newEl = document.createElement(tag);
if (attributes["id"] === undefined) {
newEl.setAttribute("id", myId);
}
for (const key in attributes) {
newEl.setAttribute(key, attributes[key]);
}
let parentElement = document.getElementById(parentId);
let startNode = document.getElementById(insertBefore)
if (insertBefore !== undefined) {
parentElement.insertBefore(newEl, startNode)
} else {
parentElement.appendChild(newEl);
}
""".trimIndent()
element.browser.callJsFunction(
createElementJs, tag.json, JsonObject(mutAttributes), id.json,
element.id.json, JsonPrimitive(insertBefore ?: ""), JsonPrimitive(elementsCreatedCount)
)
}
}
val newElement = Element(element.browser, this, tag = tag, id = id)
elementsCreatedCountAtomic.incrementAndGet()
for (plugin in element.browser.kweb.plugins) {
plugin.elementCreationHook(newElement)
}
onCleanup(withParent = false) {
logger.debug { "Deleting element ${newElement.id}" }
newElement.deleteIfExists()
}
if (new != null) {
newElement.new { new(newElement) }
}
return newElement
}
/**
* Specify that a specific plugin be provided in [Kweb.plugins], throws an exception if not.
*/
fun require(vararg plugins: KClass<out KwebPlugin>) = element.browser.require(*plugins)
/**
* Specify a listener to be called when this element is removed from the DOM.
*
* @param withParent If `true` this cleaner will be called if this element is cleaned up, or if
* any ancestor element of this ElementCreator is cleaned up. Otherwise it will
* only be cleaned up if this ElementCreator is cleaned up specifically.
*
* As a rule-of-thumb, use 'true' for anything except deleting DOM elements.
*/
fun onCleanup(withParent: Boolean, f: Cleaner) {
if (withParent) {
parentCreator?.onCleanup(true, f)
}
if (cleanupListeners == null)
cleanupListeners = ConcurrentLinkedQueue()
cleanupListeners?.add(f)
}
fun cleanup() {
// TODO: Warn if called twice?
if (!isCleanedUp) {
isCleanedUp = true
try {
cleanupListeners?.forEach { it() }
} catch (e: Exception) {
logger.warn(e) { "Error while cleaning up ElementCreator" }
}
}
}
/**
* Close this AutoCloseable when this ElementCreator is cleaned up.
*/
fun closeOnCleanup(closeable: AutoCloseable) {
onCleanup(withParent = true) {
closeable.close()
}
}
// text() Deprecated because these may create confusion about whether element properties
// are set on the Element or the ElementCreator
@Deprecated("Use element.text() instead", ReplaceWith("element.text(text)"))
fun text(text: String) {
this.element.text(text)
}
@Deprecated("Use element.text() instead", ReplaceWith("element.text(text)"))
fun text(text: KVal<String>) {
this.element.text(text)
}
@Deprecated("Use element {} instead (as of v0.12.8)", ReplaceWith("element(receiver)", "kweb.ElementCreator.element"))
fun attr(receiver : (PARENT_TYPE).() -> Unit) {
receiver(element)
}
@Deprecated("Use element instead (as of v0.12.8)", ReplaceWith("element", "kweb.ElementCreator.element"))
val parent get() = element
@Deprecated("div { element { set(\"foo\", \"bar\")} } ===> div { it.set(\"foo\", \"bar\") }",
ReplaceWith("receiver(element)")
)
fun element(receiver : (PARENT_TYPE).() -> Unit) {
receiver(element)
}
/**
* Create a new [KVar], and call [KVar.close()] when this ElementCreator is cleaned up.
*/
fun <T> kvar(initialValue: T): KVar<T> {
val kv = KVar(initialValue)
onCleanup(withParent = true) {
kv.close(CloseReason("ElementCreator cleaned up"))
}
return kv
}
/**
* Create a new [KVar], and call [KVar.close()] when this ElementCreator is cleaned up.
*/
fun <T> kval(initialValue: T): KVal<T> {
val kv = KVal(initialValue)
onCleanup(withParent = true) {
kv.close(CloseReason("ElementCreator cleaned up"))
}
return kv
}
/**
* Creates a CoroutineScope that will be cancelled when this ElementCreator is cleaned up.
*/
@SinceKotlin("1.1.1")
fun elementScope(): CoroutineScope {
val scope = CoroutineScope(Dispatchers.IO)
onCleanup(withParent = true) {
scope.cancel()
}
return scope
}
}