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

org.http4k.webdriver.HtmxCommand.kt Maven / Gradle / Ivy

There is a newer version: 5.41.0.0
Show newest version
package org.http4k.webdriver

import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.urlEncoded
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import org.jsoup.parser.Parser

data class HtmxCommand(
    val method: Method,
    val uri: String,
    val target: Element,
    val swap: HtmxSwap,
) {
    fun performOn(element: HtmxJsoupWebElement) {
        // TODO: re-use headers from http4k.http4k-htmx instead of repeating?
        val headers = listOfNotNull(
            "hx-request" to "true",
            element.getAttribute("id")?.let { "hx-trigger" to it },
            element.getAttribute("name")?.let { "hx-trigger-name" to it },
            if (target.hasAttr("id")) "hx-target" to target.attr("id") else null,
        )

        val response = element
            .handler(
                request(element.delegate.element)
                    .headers(headers)
            )

        val mimeType = response.header("content-type")?.split(";")?.firstOrNull()

        val responseBody =
            when (mimeType) {
                "text/html" -> Jsoup.parse(response.bodyString(), Parser.xmlParser()).root().children()
                "text/plain" -> listOf(TextNode(response.bodyString()))
                null -> throw RuntimeException("No content type on response")
                else -> throw RuntimeException("Unsupported content type on response ${response.header("content-type")}")
            }

        swap
            .performSwap(
                element = target,
                newElements = responseBody ?: emptyList()
            )
    }

    private fun request(element: Element): Request {
        val formBody = formBodyOfElement(element)
        val isInput = listOf("input", "textarea", "select", "button").contains(element.tagName())

        return when {
            isInput && element.hasAttr("name") && method == Method.GET ->
                Request(Method.GET, uri).query(element.attr("name"), element.attr("value"))

            formBody != null && method == Method.GET ->
                Request(Method.GET, "$uri?$formBody")

            formBody != null ->
                Request(method, uri).body(formBody)

            else ->
                Request(method, uri)
        }
    }

    private fun formBodyOfElement(element: Element): String? =
        if (element.tagName() == "form")
            formBody(element)
        else
            element
                .parents()
                .toList()
                .firstOrNull { it.tagName() == "form" }
                ?.let { formBody(it) }

    private fun formBody(formElement: Element): String {
        // TODO: lots of duplication with JSoupWebElement
        // and slightly different approaches
        val inputs =
            formElement
                .getElementsByTag("input")
                .toList()
                .filter { it.hasAttr("name") }
                .map { it.attr("name") to it.attr("value") }

        val textAreas =
            formElement
                .getElementsByTag("textarea")
                .toList()
                .filter { it.hasAttr("name") }
                .map { it.attr("name") to it.text() }

        val selects =
            formElement
                .getElementsByTag("select")
                .toList()
                .filter { it.hasAttr("name") }
                .mapNotNull {
                    it.getElementsByTag("option")
                        .toList()
                        .find { option -> option.hasAttr("selected") }
                        ?.let { option -> it.attr("name") to option.attr("value") }
                }

        val buttons =
            formElement
                .getElementsByTag("button")
                .toList()
                .filter { it.hasAttr("name") }
                .map { it.attr("name") to it.attr("value") }

        val all = (inputs + textAreas + selects + buttons)

        return all.joinToString("&") { "${it.first.urlEncoded()}=${it.second.urlEncoded()}" }
    }

    companion object {

        private fun withDataPrefix(p: Pair): List> =
            listOf(p, "data-${p.first}" to p.second)

        private val hxAttrs = listOf(
            "hx-get" to Method.GET,
            "hx-post" to Method.POST,
            "hx-delete" to Method.DELETE,
            "hx-patch" to Method.PATCH,
            "hx-put" to Method.PUT,
        ).flatMap(::withDataPrefix)

        private fun Element.hxAttr(key: String): String? =
            this.attr("hx-$key").takeIf { it.isNotEmpty() }
                ?: this.attr("data-hx-$key").takeIf { it.isNotEmpty() }

        private fun  Element.findInheritedValue(f: (Element) -> T?): T? =
            f(this) ?: this.parents().firstNotNullOfOrNull { f(it) }

        private fun targetElement(element: Element): Element? =
            element
                .hxAttr("target")
                ?.let {
                    when {
                        it == "this" -> element
                        else -> element.root().select(it).firstOrNull()
                    }
                }

        private fun swap(element: Element): HtmxSwap? =
            element
                .hxAttr("swap")
                ?.let(::swapFromString)

        private fun swapFromString(s: String): HtmxSwap? =
            HtmxSwap
                .entries
                .firstOrNull { s.lowercase().startsWith(it.toString().lowercase()) }

        private fun fromElement(element: Element): HtmxCommand? =
            hxAttrs
                .firstOrNull { element.hasAttr(it.first) }
                ?.let {
                    HtmxCommand(
                        method = it.second,
                        uri = element.attr(it.first),
                        target = element.findInheritedValue(::targetElement) ?: element,
                        swap = element.findInheritedValue(::swap) ?: HtmxSwap.InnerHtml,
                    )
                }

        fun from(element: HtmxJsoupWebElement): HtmxCommand? =
            fromElement(element.delegate.element)
    }
}

interface HtmxSwapAction {
    fun performSwap(element: Element, newElements: List)
}

enum class HtmxSwap : HtmxSwapAction {
    InnerHtml {
        override fun performSwap(element: Element, newElements: List) {
            element.empty()
            element.appendChildren(newElements)
        }
    },
    OuterHtml {
        override fun performSwap(element: Element, newElements: List) {
            newElements.forEach { element.before(it) }
            element.remove()
        }
    },
    BeforeBegin {
        override fun performSwap(element: Element, newElements: List) {
            newElements.forEach { element.before(it) }
        }
    },
    AfterBegin {
        override fun performSwap(element: Element, newElements: List) {
            element.insertChildren(0, newElements)
        }
    },
    BeforeEnd {
        override fun performSwap(element: Element, newElements: List) {
            element.appendChildren(newElements)
        }
    },
    AfterEnd {
        override fun performSwap(element: Element, newElements: List) {
            newElements.reversed().forEach { element.after(it) }
        }
    },
    Delete {
        override fun performSwap(element: Element, newElements: List) {
            element.remove()
        }
    },
    None {
        override fun performSwap(element: Element, newElements: List) {}
    },
}






© 2015 - 2025 Weber Informatics LLC | Privacy Policy