diff --git a/.github/workflows/release-ws-test.yml b/.github/workflows/release-ws-test.yml
new file mode 100644
index 0000000..acb64cb
--- /dev/null
+++ b/.github/workflows/release-ws-test.yml
@@ -0,0 +1,48 @@
+name: Build and Deploy ws-test
+
+on:
+ workflow_run:
+ workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
+ types:
+ - completed
+ workflow_dispatch:
+ push:
+ branches:
+ - ws-testing
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get short commit hash
+ id: vars
+ run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
+
+ - name: Build Docker image
+ run: |
+ cd ./examples/ws-example && docker build -t ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} .
+
+ - name: Tag as latest Docker image
+ run: |
+ docker tag ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/ws-example:latest
+
+ - name: Log in to GitHub Container Registry
+ run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
+
+ - name: Push Docker image
+ run: |
+ docker push ghcr.io/${{ github.repository_owner }}/ws-example:latest
diff --git a/examples/chat/sse/handler.go b/examples/chat/sse/handler.go
index b4f8000..cb39bfc 100644
--- a/examples/chat/sse/handler.go
+++ b/examples/chat/sse/handler.go
@@ -50,7 +50,6 @@ func Handle() http.HandlerFunc {
defer manager.Disconnect(sessionId)
defer func() {
- fmt.Printf("empting channels\n")
for len(writer) > 0 {
<-writer
}
diff --git a/examples/chat/sse/manager.go b/examples/chat/sse/manager.go
index 485afb2..d812c6d 100644
--- a/examples/chat/sse/manager.go
+++ b/examples/chat/sse/manager.go
@@ -70,16 +70,14 @@ func (manager *SocketManager) Listen(listener chan SocketEvent) {
}
func (manager *SocketManager) dispatch(event SocketEvent) {
- fmt.Printf("dispatching event: %s\n", event.Type)
done := make(chan struct{}, 1)
go func() {
for {
select {
case <-done:
- fmt.Printf("dispatched event: %s\n", event.Type)
return
case <-time.After(5 * time.Second):
- fmt.Printf("havent dispatched event after 5s, chan blocked: %s\n", event.Type)
+ fmt.Printf("havent dispatched listener event after 5s, chan blocked: %s\n", event.Type)
}
}
}()
diff --git a/examples/ws-example/.dockerignore b/examples/ws-example/.dockerignore
new file mode 100644
index 0000000..fb47686
--- /dev/null
+++ b/examples/ws-example/.dockerignore
@@ -0,0 +1,11 @@
+# Project exclude paths
+/tmp/
+node_modules/
+dist/
+js/dist
+js/node_modules
+go.work
+go.work.sum
+.idea
+!framework/assets/dist
+__htmgo
\ No newline at end of file
diff --git a/examples/ws-example/.gitignore b/examples/ws-example/.gitignore
new file mode 100644
index 0000000..3d6a979
--- /dev/null
+++ b/examples/ws-example/.gitignore
@@ -0,0 +1,6 @@
+/assets/dist
+tmp
+node_modules
+.idea
+__htmgo
+dist
\ No newline at end of file
diff --git a/examples/ws-example/Dockerfile b/examples/ws-example/Dockerfile
new file mode 100644
index 0000000..0471f13
--- /dev/null
+++ b/examples/ws-example/Dockerfile
@@ -0,0 +1,38 @@
+# Stage 1: Build the Go binary
+FROM golang:1.23-alpine AS builder
+
+RUN apk update
+RUN apk add git
+RUN apk add curl
+
+# Set the working directory inside the container
+WORKDIR /app
+
+# Copy go.mod and go.sum files
+COPY go.mod go.sum ./
+
+# Download and cache the Go modules
+RUN go mod download
+
+# Copy the source code into the container
+COPY . .
+
+# Build the Go binary for Linux
+RUN GOPRIVATE=github.com/maddalax GOPROXY=direct go run github.com/maddalax/htmgo/cli/htmgo@latest build
+
+
+# Stage 2: Create the smallest possible image
+FROM gcr.io/distroless/base-debian11
+
+# Set the working directory inside the container
+WORKDIR /app
+
+# Copy the Go binary from the builder stage
+COPY --from=builder /app/dist .
+
+# Expose the necessary port (replace with your server port)
+EXPOSE 3000
+
+
+# Command to run the binary
+CMD ["./ws-example"]
diff --git a/examples/ws-example/Taskfile.yml b/examples/ws-example/Taskfile.yml
new file mode 100644
index 0000000..695006f
--- /dev/null
+++ b/examples/ws-example/Taskfile.yml
@@ -0,0 +1,20 @@
+version: '3'
+
+tasks:
+ run:
+ cmds:
+ - go run github.com/maddalax/htmgo/cli/htmgo@latest run
+ silent: true
+
+ build:
+ cmds:
+ - go run github.com/maddalax/htmgo/cli/htmgo@latest build
+
+ docker:
+ cmds:
+ - docker build .
+
+ watch:
+ cmds:
+ - go run github.com/maddalax/htmgo/cli/htmgo@latest watch
+ silent: true
\ No newline at end of file
diff --git a/examples/ws-example/assets.go b/examples/ws-example/assets.go
new file mode 100644
index 0000000..a864128
--- /dev/null
+++ b/examples/ws-example/assets.go
@@ -0,0 +1,13 @@
+//go:build !prod
+// +build !prod
+
+package main
+
+import (
+ "io/fs"
+ "ws-example/internal/embedded"
+)
+
+func GetStaticAssets() fs.FS {
+ return embedded.NewOsFs()
+}
diff --git a/examples/ws-example/assets/css/input.css b/examples/ws-example/assets/css/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/ws-example/assets/css/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/ws-example/assets_prod.go b/examples/ws-example/assets_prod.go
new file mode 100644
index 0000000..f0598e1
--- /dev/null
+++ b/examples/ws-example/assets_prod.go
@@ -0,0 +1,16 @@
+//go:build prod
+// +build prod
+
+package main
+
+import (
+ "embed"
+ "io/fs"
+)
+
+//go:embed assets/dist/*
+var staticAssets embed.FS
+
+func GetStaticAssets() fs.FS {
+ return staticAssets
+}
diff --git a/examples/ws-example/go.mod b/examples/ws-example/go.mod
new file mode 100644
index 0000000..62ab62f
--- /dev/null
+++ b/examples/ws-example/go.mod
@@ -0,0 +1,18 @@
+module ws-example
+
+go 1.23.0
+
+require (
+ github.com/go-chi/chi/v5 v5.1.0
+ github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241104193946-1ddeceaa8286
+ github.com/maddalax/htmgo/framework v1.0.3-0.20241104193946-1ddeceaa8286
+)
+
+require (
+ github.com/gobwas/httphead v0.1.0 // indirect
+ github.com/gobwas/pool v0.2.1 // indirect
+ github.com/gobwas/ws v1.4.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+)
diff --git a/examples/ws-example/go.sum b/examples/ws-example/go.sum
new file mode 100644
index 0000000..6e8ae0b
--- /dev/null
+++ b/examples/ws-example/go.sum
@@ -0,0 +1,28 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
+github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
+github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241104193946-1ddeceaa8286 h1:5Z848JJUQ3OACuKWOX8XbzFb9krKwaiJb7inYSKCRKY=
+github.com/maddalax/htmgo/extensions/websocket v0.0.0-20241104193946-1ddeceaa8286/go.mod h1:r6/VqntLp7VlAUpIXy3MWZMHs2EkPKJP5rJdDL8lFP4=
+github.com/maddalax/htmgo/framework v1.0.3-0.20241104193946-1ddeceaa8286 h1:Z7L0W9OZyjrICsnCoLu/GVM33cj4YP7GHlj6/fHPplw=
+github.com/maddalax/htmgo/framework v1.0.3-0.20241104193946-1ddeceaa8286/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/ws-example/internal/embedded/os.go b/examples/ws-example/internal/embedded/os.go
new file mode 100644
index 0000000..ddfd55f
--- /dev/null
+++ b/examples/ws-example/internal/embedded/os.go
@@ -0,0 +1,17 @@
+package embedded
+
+import (
+ "io/fs"
+ "os"
+)
+
+type OsFs struct {
+}
+
+func (receiver OsFs) Open(name string) (fs.File, error) {
+ return os.Open(name)
+}
+
+func NewOsFs() OsFs {
+ return OsFs{}
+}
diff --git a/examples/ws-example/k6.js b/examples/ws-example/k6.js
new file mode 100644
index 0000000..e69de29
diff --git a/examples/ws-example/main.go b/examples/ws-example/main.go
new file mode 100644
index 0000000..7b880d3
--- /dev/null
+++ b/examples/ws-example/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket"
+ ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+ "io/fs"
+ "net/http"
+ "ws-example/__htmgo"
+)
+
+func main() {
+ locator := service.NewLocator()
+
+ h.Start(h.AppOpts{
+ ServiceLocator: locator,
+ LiveReload: true,
+ Register: func(app *h.App) {
+
+ app.Use(func(ctx *h.RequestContext) {
+ session.CreateSession(ctx)
+ })
+
+ websocket.EnableExtension(app, ws2.ExtensionOpts{
+ WsPath: "/ws",
+ RoomName: func(ctx *h.RequestContext) string {
+ return "all"
+ },
+ SessionId: func(ctx *h.RequestContext) string {
+ return ctx.QueryParam("sessionId")
+ },
+ })
+
+ sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
+
+ if err != nil {
+ panic(err)
+ }
+
+ http.FileServerFS(sub)
+
+ app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
+ __htmgo.Register(app.Router)
+ },
+ })
+}
diff --git a/examples/ws-example/pages/index.go b/examples/ws-example/pages/index.go
new file mode 100644
index 0000000..2c87461
--- /dev/null
+++ b/examples/ws-example/pages/index.go
@@ -0,0 +1,57 @@
+package pages
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/extensions/websocket/ws"
+ "github.com/maddalax/htmgo/framework/h"
+ "ws-example/partials"
+)
+
+func IndexPage(ctx *h.RequestContext) *h.Page {
+ sessionId := session.GetSessionId(ctx)
+
+ return h.NewPage(
+ RootPage(
+ ctx,
+ h.Div(
+ h.Attribute("ws-connect", fmt.Sprintf("/ws?sessionId=%s", sessionId)),
+ h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
+ h.H3(
+ h.Id("intro-text"),
+ h.Text("Repeater Example"),
+ h.Class("text-2xl"),
+ ),
+ h.Div(
+ h.Id("ws-metrics"),
+ ),
+ partials.CounterForm(ctx, partials.CounterProps{Id: "counter-1"}),
+ partials.Repeater(ctx, partials.RepeaterProps{
+ Id: "repeater-1",
+ OnAdd: func(data ws.HandlerData) {
+ //ws.BroadcastServerSideEvent("increment", map[string]any{})
+ },
+ OnRemove: func(data ws.HandlerData, index int) {
+ //ws.BroadcastServerSideEvent("decrement", map[string]any{})
+ },
+ AddButton: h.Button(
+ h.Text("+ Add Item"),
+ ),
+ RemoveButton: func(index int, children ...h.Ren) *h.Element {
+ return h.Button(
+ h.Text("Remove"),
+ h.Children(children...),
+ )
+ },
+ Item: func(index int) *h.Element {
+ return h.Input(
+ "text",
+ h.Class("border border-gray-300 rounded p-2"),
+ h.Value(fmt.Sprintf("item %d", index)),
+ )
+ },
+ }),
+ ),
+ ),
+ )
+}
diff --git a/examples/ws-example/pages/root.go b/examples/ws-example/pages/root.go
new file mode 100644
index 0000000..0e847dd
--- /dev/null
+++ b/examples/ws-example/pages/root.go
@@ -0,0 +1,26 @@
+package pages
+
+import (
+ "github.com/maddalax/htmgo/framework/h"
+)
+
+func RootPage(ctx *h.RequestContext, children ...h.Ren) h.Ren {
+ return h.Html(
+ h.JoinExtensions(
+ h.HxExtension(
+ h.BaseExtensions(),
+ ),
+ h.HxExtension("ws"),
+ ),
+ h.Head(
+ h.Link("/public/main.css", "stylesheet"),
+ h.Script("/public/htmgo.js"),
+ ),
+ h.Body(
+ h.Div(
+ h.Class("flex flex-col gap-2 bg-white h-full"),
+ h.Fragment(children...),
+ ),
+ ),
+ )
+}
diff --git a/examples/ws-example/pages/ws/metrics.go b/examples/ws-example/pages/ws/metrics.go
new file mode 100644
index 0000000..c13faad
--- /dev/null
+++ b/examples/ws-example/pages/ws/metrics.go
@@ -0,0 +1,129 @@
+package ws
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/extensions/websocket/ws"
+ "github.com/maddalax/htmgo/framework/h"
+ "runtime"
+ "time"
+ "ws-example/pages"
+)
+
+func Metrics(ctx *h.RequestContext) *h.Page {
+
+ ws.RunOnConnected(ctx, func() {
+ ws.PushElementCtx(
+ ctx,
+ metricsView(ctx),
+ )
+ })
+
+ ws.Every(ctx, time.Second, func() bool {
+ return ws.PushElementCtx(
+ ctx,
+ metricsView(ctx),
+ )
+ })
+
+ return h.NewPage(
+ pages.RootPage(
+ ctx,
+ h.Div(
+ h.Attribute("ws-connect", fmt.Sprintf("/ws?sessionId=%s", session.GetSessionId(ctx))),
+ h.Class("flex flex-col gap-4 items-center min-h-screen max-w-2xl mx-auto mt-8"),
+ h.H3(
+ h.Id("intro-text"),
+ h.Text("Websocket Metrics"),
+ h.Class("text-2xl"),
+ ),
+ h.Div(
+ h.Id("ws-metrics"),
+ ),
+ ),
+ ),
+ )
+}
+
+func metricsView(ctx *h.RequestContext) *h.Element {
+ metrics := ws.MetricsFromCtx(ctx)
+
+ return h.Div(
+ h.Id("ws-metrics"),
+ List(metrics),
+ )
+}
+
+func List(metrics ws.Metrics) *h.Element {
+ return h.Body(
+ h.Div(
+ h.Class("flow-root rounded-lg border border-gray-100 py-3 shadow-sm"),
+ h.Dl(
+ h.Class("-my-3 divide-y divide-gray-100 text-sm"),
+ ListItem("Current Time", time.Now().Format("15:04:05")),
+ ListItem("Seconds Elapsed", fmt.Sprintf("%d", metrics.Manager.SecondsElapsed)),
+ ListItem("Total Messages", fmt.Sprintf("%d", metrics.Manager.TotalMessages)),
+ ListItem("Messages Per Second", fmt.Sprintf("%d", metrics.Manager.MessagesPerSecond)),
+ ListItem("Total Goroutines For ws.Every", fmt.Sprintf("%d", metrics.Manager.RunningGoroutines)),
+ ListItem("Total Goroutines In System", fmt.Sprintf("%d", runtime.NumGoroutine())),
+ ListItem("Sockets", fmt.Sprintf("%d", metrics.Manager.TotalSockets)),
+ ListItem("Rooms", fmt.Sprintf("%d", metrics.Manager.TotalRooms)),
+ ListItem("Session Id To Hashes", fmt.Sprintf("%d", metrics.Handler.SessionIdToHashesCount)),
+ ListItem("Total Handlers", fmt.Sprintf("%d", metrics.Handler.TotalHandlers)),
+ ListItem("Server Event Names To Hash", fmt.Sprintf("%d", metrics.Handler.ServerEventNamesToHashCount)),
+ ListItem("Total Listeners", fmt.Sprintf("%d", metrics.Manager.TotalListeners)),
+ h.IterMap(metrics.Manager.SocketsPerRoom, func(key string, value []string) *h.Element {
+ return ListBlock(
+ fmt.Sprintf("Sockets In Room - %s", key),
+ h.IfElse(
+ len(value) > 100,
+ h.Div(
+ h.Pf("%d total sockets", len(value)),
+ ),
+ h.Div(
+ h.List(value, func(item string, index int) *h.Element {
+ return h.Div(
+ h.Pf("%s", item),
+ )
+ }),
+ ),
+ ),
+ )
+ }),
+ ),
+ ),
+ )
+}
+
+func ListItem(term, description string) *h.Element {
+ return h.Div(
+ h.Class("grid grid-cols-1 gap-1 p-3 even:bg-gray-50 sm:grid-cols-3 sm:gap-4"),
+ DescriptionTerm(term),
+ DescriptionDetail(description),
+ )
+}
+
+func ListBlock(title string, children *h.Element) *h.Element {
+ return h.Div(
+ h.Class("grid grid-cols-1 gap-1 p-3 even:bg-gray-50 sm:grid-cols-3 sm:gap-4"),
+ DescriptionTerm(title),
+ h.Dd(
+ h.Class("text-gray-700 sm:col-span-2"),
+ children,
+ ),
+ )
+}
+
+func DescriptionTerm(term string) *h.Element {
+ return h.Dt(
+ h.Class("font-medium text-gray-900"),
+ h.Text(term),
+ )
+}
+
+func DescriptionDetail(detail string) *h.Element {
+ return h.Dd(
+ h.Class("text-gray-700 sm:col-span-2"),
+ h.Text(detail),
+ )
+}
diff --git a/examples/ws-example/partials/index.go b/examples/ws-example/partials/index.go
new file mode 100644
index 0000000..5054084
--- /dev/null
+++ b/examples/ws-example/partials/index.go
@@ -0,0 +1,72 @@
+package partials
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/extensions/websocket/ws"
+ "github.com/maddalax/htmgo/framework/h"
+)
+
+type Counter struct {
+ Count func() int
+ Increment func()
+ Decrement func()
+}
+
+func UseCounter(ctx *h.RequestContext, id string) Counter {
+ sessionId := session.GetSessionId(ctx)
+ get, set := session.UseState(sessionId, id, 0)
+
+ var increment = func() {
+ set(get() + 1)
+ }
+
+ var decrement = func() {
+ set(get() - 1)
+ }
+
+ return Counter{
+ Count: get,
+ Increment: increment,
+ Decrement: decrement,
+ }
+}
+
+type CounterProps struct {
+ Id string
+}
+
+func CounterForm(ctx *h.RequestContext, props CounterProps) *h.Element {
+ if props.Id == "" {
+ props.Id = h.GenId(6)
+ }
+ counter := UseCounter(ctx, props.Id)
+
+ return h.Div(
+ h.Attribute("hx-swap", "none"),
+ h.Class("flex flex-col gap-3 items-center"),
+ h.Id(props.Id),
+ h.P(
+ h.Id("counter-text-"+props.Id),
+ h.AttributePairs(
+ "id", "counter",
+ "class", "text-xl",
+ "name", "count",
+ "text", "count",
+ ),
+ h.TextF("Count: %d", counter.Count()),
+ ),
+ h.Button(
+ h.Class("bg-rose-400 hover:bg-rose-500 text-white font-bold py-2 px-4 rounded"),
+ h.Type("submit"),
+ h.Text("Increment"),
+ ws.OnServerEvent(ctx, "increment", func(data ws.HandlerData) {
+ counter.Increment()
+ ws.PushElement(data, CounterForm(ctx, props))
+ }),
+ ws.OnServerEvent(ctx, "decrement", func(data ws.HandlerData) {
+ counter.Decrement()
+ ws.PushElement(data, CounterForm(ctx, props))
+ }),
+ ),
+ )
+}
diff --git a/examples/ws-example/partials/repeater.go b/examples/ws-example/partials/repeater.go
new file mode 100644
index 0000000..c357c83
--- /dev/null
+++ b/examples/ws-example/partials/repeater.go
@@ -0,0 +1,84 @@
+package partials
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/extensions/websocket/ws"
+ "github.com/maddalax/htmgo/framework/h"
+)
+
+type RepeaterProps struct {
+ Item func(index int) *h.Element
+ RemoveButton func(index int, children ...h.Ren) *h.Element
+ AddButton *h.Element
+ DefaultItems []*h.Element
+ Id string
+ currentIndex int
+ OnAdd func(data ws.HandlerData)
+ OnRemove func(data ws.HandlerData, index int)
+}
+
+func (props *RepeaterProps) itemId(index int) string {
+ return fmt.Sprintf("%s-repeater-item-%d", props.Id, index)
+}
+
+func (props *RepeaterProps) addButtonId() string {
+ return fmt.Sprintf("%s-repeater-add-button", props.Id)
+}
+
+func repeaterItem(ctx *h.RequestContext, item *h.Element, index int, props *RepeaterProps) *h.Element {
+ id := props.itemId(index)
+ return h.Div(
+ h.Class("flex gap-2 items-center"),
+ h.Id(id),
+ item,
+ props.RemoveButton(
+ index,
+ h.ClassIf(index == 0, "opacity-0 disabled"),
+ h.If(
+ index == 0,
+ h.Disabled(),
+ ),
+ ws.OnClick(ctx, func(data ws.HandlerData) {
+ props.OnRemove(data, index)
+ props.currentIndex--
+ ws.PushElement(
+ data,
+ h.Div(
+ h.Attribute("hx-swap-oob", fmt.Sprintf("delete:#%s", id)),
+ h.Div(),
+ ),
+ )
+ }),
+ ),
+ )
+}
+
+func Repeater(ctx *h.RequestContext, props RepeaterProps) *h.Element {
+ if props.Id == "" {
+ props.Id = h.GenId(6)
+ }
+ return h.Div(
+ h.Class("flex flex-col gap-2"),
+ h.List(props.DefaultItems, func(item *h.Element, index int) *h.Element {
+ return repeaterItem(ctx, item, index, &props)
+ }),
+ h.Div(
+ h.Id(props.addButtonId()),
+ h.Class("flex justify-center"),
+ props.AddButton,
+ ws.OnClick(ctx, func(data ws.HandlerData) {
+ props.OnAdd(data)
+ ws.PushElement(
+ data,
+ h.Div(
+ h.Attribute("hx-swap-oob", "beforebegin:#"+props.addButtonId()),
+ repeaterItem(
+ ctx, props.Item(props.currentIndex), props.currentIndex, &props,
+ ),
+ ),
+ )
+ props.currentIndex++
+ }),
+ ),
+ )
+}
diff --git a/examples/ws-example/tailwind.config.js b/examples/ws-example/tailwind.config.js
new file mode 100644
index 0000000..b18125c
--- /dev/null
+++ b/examples/ws-example/tailwind.config.js
@@ -0,0 +1,5 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["**/*.go"],
+ plugins: [],
+};
diff --git a/extensions/websocket/go.mod b/extensions/websocket/go.mod
new file mode 100644
index 0000000..edc9faf
--- /dev/null
+++ b/extensions/websocket/go.mod
@@ -0,0 +1,17 @@
+module github.com/maddalax/htmgo/extensions/websocket
+
+go 1.23.0
+
+require (
+ github.com/gobwas/ws v1.4.0
+ github.com/maddalax/htmgo/framework v0.0.0-20241014151703-8503dffa4e7d
+ github.com/puzpuzpuz/xsync/v3 v3.4.0
+)
+
+require (
+ github.com/go-chi/chi/v5 v5.1.0 // indirect
+ github.com/gobwas/httphead v0.1.0 // indirect
+ github.com/gobwas/pool v0.2.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+)
diff --git a/extensions/websocket/go.sum b/extensions/websocket/go.sum
new file mode 100644
index 0000000..c6aaae7
--- /dev/null
+++ b/extensions/websocket/go.sum
@@ -0,0 +1,26 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
+github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
+github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/maddalax/htmgo/framework v0.0.0-20241014151703-8503dffa4e7d h1:oysEaiKB7/WbvEklkyQ7SEE1xmDeGLrBUvF3BAsBUns=
+github.com/maddalax/htmgo/framework v0.0.0-20241014151703-8503dffa4e7d/go.mod h1:HYKI49Pb6oyY2opSJdTt145B1vWgfWIDohvlolynv80=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/extensions/websocket/init.go b/extensions/websocket/init.go
new file mode 100644
index 0000000..9261063
--- /dev/null
+++ b/extensions/websocket/init.go
@@ -0,0 +1,31 @@
+package websocket
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/opts"
+ "github.com/maddalax/htmgo/extensions/websocket/ws"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+)
+
+func EnableExtension(app *h.App, opts opts.ExtensionOpts) {
+ if app.Opts.ServiceLocator == nil {
+ app.Opts.ServiceLocator = service.NewLocator()
+ }
+
+ if opts.WsPath == "" {
+ panic("websocket: WsPath is required")
+ }
+
+ if opts.SessionId == nil {
+ panic("websocket: SessionId func is required")
+ }
+
+ service.Set[wsutil.SocketManager](app.Opts.ServiceLocator, service.Singleton, func() *wsutil.SocketManager {
+ manager := wsutil.NewSocketManager(&opts)
+ manager.StartMetrics()
+ return manager
+ })
+ ws.StartListener(app.Opts.ServiceLocator)
+ app.Router.Handle(opts.WsPath, wsutil.WsHttpHandler(&opts))
+}
diff --git a/extensions/websocket/internal/wsutil/handler.go b/extensions/websocket/internal/wsutil/handler.go
new file mode 100644
index 0000000..1096dd5
--- /dev/null
+++ b/extensions/websocket/internal/wsutil/handler.go
@@ -0,0 +1,115 @@
+package wsutil
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/gobwas/ws"
+ "github.com/gobwas/ws/wsutil"
+ ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+ "log/slog"
+ "net/http"
+ "sync"
+ "time"
+)
+
+func WsHttpHandler(opts *ws2.ExtensionOpts) http.HandlerFunc {
+
+ if opts.RoomName == nil {
+ opts.RoomName = func(ctx *h.RequestContext) string {
+ return "all"
+ }
+ }
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
+ locator := cc.ServiceLocator()
+ manager := service.Get[SocketManager](locator)
+
+ sessionId := opts.SessionId(cc)
+
+ if sessionId == "" {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ conn, _, _, err := ws.UpgradeHTTP(r, w)
+ if err != nil {
+ slog.Info("failed to upgrade", slog.String("error", err.Error()))
+ return
+ }
+
+ roomId := opts.RoomName(cc)
+ /*
+ Large buffer in case the client disconnects while we are writing
+ we don't want to block the writer
+ */
+ done := make(chan bool, 1000)
+ writer := make(WriterChan, 1000)
+
+ wg := sync.WaitGroup{}
+
+ manager.Add(roomId, sessionId, writer, done)
+
+ /*
+ * This goroutine is responsible for writing messages to the client
+ */
+ wg.Add(1)
+ go func() {
+ defer manager.Disconnect(sessionId)
+ defer wg.Done()
+
+ defer func() {
+ for len(writer) > 0 {
+ <-writer
+ }
+ for len(done) > 0 {
+ <-done
+ }
+ }()
+
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-done:
+ fmt.Printf("closing connection: \n")
+ return
+ case <-ticker.C:
+ manager.Ping(sessionId)
+ case message := <-writer:
+ err = wsutil.WriteServerMessage(conn, ws.OpText, []byte(message))
+ if err != nil {
+ return
+ }
+ }
+ }
+ }()
+
+ /*
+ * This goroutine is responsible for reading messages from the client
+ */
+ go func() {
+ defer conn.Close()
+ for {
+ msg, op, err := wsutil.ReadClientData(conn)
+ if err != nil {
+ return
+ }
+ if op != ws.OpText {
+ return
+ }
+ m := make(map[string]any)
+ err = json.Unmarshal(msg, &m)
+ if err != nil {
+ return
+ }
+ manager.OnMessage(sessionId, m)
+ }
+ }()
+
+ wg.Wait()
+ }
+}
diff --git a/extensions/websocket/internal/wsutil/manager.go b/extensions/websocket/internal/wsutil/manager.go
new file mode 100644
index 0000000..d274fe1
--- /dev/null
+++ b/extensions/websocket/internal/wsutil/manager.go
@@ -0,0 +1,365 @@
+package wsutil
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/extensions/websocket/opts"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+ "github.com/puzpuzpuz/xsync/v3"
+ "log/slog"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+type EventType string
+type WriterChan chan string
+type DoneChan chan bool
+
+const (
+ ConnectedEvent EventType = "connected"
+ DisconnectedEvent EventType = "disconnected"
+ MessageEvent EventType = "message"
+)
+
+type SocketEvent struct {
+ SessionId string
+ RoomId string
+ Type EventType
+ Payload map[string]any
+}
+
+type CloseEvent struct {
+ Code int
+ Reason string
+}
+
+type SocketConnection struct {
+ Id string
+ RoomId string
+ Done DoneChan
+ Writer WriterChan
+}
+
+type ManagerMetrics struct {
+ RunningGoroutines int32
+ TotalSockets int
+ TotalRooms int
+ TotalListeners int
+ SocketsPerRoomCount map[string]int
+ SocketsPerRoom map[string][]string
+ TotalMessages int64
+ MessagesPerSecond int
+ SecondsElapsed int
+}
+
+type SocketManager struct {
+ sockets *xsync.MapOf[string, *xsync.MapOf[string, SocketConnection]]
+ idToRoom *xsync.MapOf[string, string]
+ listeners []chan SocketEvent
+ goroutinesRunning atomic.Int32
+ opts *opts.ExtensionOpts
+ lock sync.Mutex
+ totalMessages atomic.Int64
+ messagesPerSecond int
+ secondsElapsed int
+}
+
+func (manager *SocketManager) StartMetrics() {
+ go func() {
+ for {
+ time.Sleep(time.Second)
+ manager.lock.Lock()
+ manager.secondsElapsed++
+ totalMessages := manager.totalMessages.Load()
+ manager.messagesPerSecond = int(float64(totalMessages) / float64(manager.secondsElapsed))
+ manager.lock.Unlock()
+ }
+ }()
+}
+
+func (manager *SocketManager) Metrics() ManagerMetrics {
+ manager.lock.Lock()
+ defer manager.lock.Unlock()
+ count := manager.goroutinesRunning.Load()
+ metrics := ManagerMetrics{
+ RunningGoroutines: count,
+ TotalSockets: 0,
+ TotalRooms: 0,
+ TotalListeners: len(manager.listeners),
+ SocketsPerRoom: make(map[string][]string),
+ SocketsPerRoomCount: make(map[string]int),
+ TotalMessages: manager.totalMessages.Load(),
+ MessagesPerSecond: manager.messagesPerSecond,
+ SecondsElapsed: manager.secondsElapsed,
+ }
+
+ roomMap := make(map[string]int)
+
+ manager.idToRoom.Range(func(socketId string, roomId string) bool {
+ roomMap[roomId]++
+ return true
+ })
+
+ metrics.TotalRooms = len(roomMap)
+
+ manager.sockets.Range(func(roomId string, sockets *xsync.MapOf[string, SocketConnection]) bool {
+ metrics.SocketsPerRoomCount[roomId] = sockets.Size()
+ sockets.Range(func(socketId string, conn SocketConnection) bool {
+ if metrics.SocketsPerRoom[roomId] == nil {
+ metrics.SocketsPerRoom[roomId] = []string{}
+ }
+ metrics.SocketsPerRoom[roomId] = append(metrics.SocketsPerRoom[roomId], socketId)
+ metrics.TotalSockets++
+ return true
+ })
+ return true
+ })
+
+ return metrics
+}
+
+func SocketManagerFromCtx(ctx *h.RequestContext) *SocketManager {
+ locator := ctx.ServiceLocator()
+ return service.Get[SocketManager](locator)
+}
+
+func NewSocketManager(opts *opts.ExtensionOpts) *SocketManager {
+ return &SocketManager{
+ sockets: xsync.NewMapOf[string, *xsync.MapOf[string, SocketConnection]](),
+ idToRoom: xsync.NewMapOf[string, string](),
+ opts: opts,
+ goroutinesRunning: atomic.Int32{},
+ }
+}
+
+func (manager *SocketManager) ForEachSocket(roomId string, cb func(conn SocketConnection)) {
+ sockets, ok := manager.sockets.Load(roomId)
+ if !ok {
+ return
+ }
+ sockets.Range(func(id string, conn SocketConnection) bool {
+ cb(conn)
+ return true
+ })
+}
+
+func (manager *SocketManager) RunIntervalWithSocket(socketId string, interval time.Duration, cb func() bool) {
+ socketIdSlog := slog.String("socketId", socketId)
+ slog.Debug("ws-extension: starting every loop", socketIdSlog, slog.Duration("duration", interval))
+
+ go func() {
+ manager.goroutinesRunning.Add(1)
+ defer manager.goroutinesRunning.Add(-1)
+ tries := 0
+ for {
+ socket := manager.Get(socketId)
+ // This can run before the socket is established, lets try a few times and kill it if socket isn't connected after a bit.
+ if socket == nil {
+ if tries > 200 {
+ slog.Debug("ws-extension: socket disconnected, killing goroutine", socketIdSlog)
+ return
+ } else {
+ time.Sleep(time.Millisecond * 15)
+ tries++
+ slog.Debug("ws-extension: socket not connected yet, trying again", socketIdSlog, slog.Int("attempt", tries))
+ continue
+ }
+ }
+ success := cb()
+ if !success {
+ return
+ }
+ time.Sleep(interval)
+ }
+ }()
+}
+
+func (manager *SocketManager) Listen(listener chan SocketEvent) {
+ if manager.listeners == nil {
+ manager.listeners = make([]chan SocketEvent, 0)
+ }
+ if listener != nil {
+ manager.listeners = append(manager.listeners, listener)
+ }
+}
+
+func (manager *SocketManager) dispatch(event SocketEvent) {
+ done := make(chan struct{}, 1)
+ go func() {
+ for {
+ select {
+ case <-done:
+ return
+ case <-time.After(5 * time.Second):
+ fmt.Printf("havent dispatched event after 5s, chan blocked: %s\n", event.Type)
+ }
+ }
+ }()
+ for _, listener := range manager.listeners {
+ listener <- event
+ }
+ done <- struct{}{}
+}
+
+func (manager *SocketManager) OnMessage(id string, message map[string]any) {
+ socket := manager.Get(id)
+ if socket == nil {
+ return
+ }
+
+ manager.totalMessages.Add(1)
+ manager.dispatch(SocketEvent{
+ SessionId: id,
+ Type: MessageEvent,
+ Payload: message,
+ RoomId: socket.RoomId,
+ })
+}
+
+func (manager *SocketManager) Add(roomId string, id string, writer WriterChan, done DoneChan) {
+ manager.idToRoom.Store(id, roomId)
+
+ sockets, ok := manager.sockets.LoadOrCompute(roomId, func() *xsync.MapOf[string, SocketConnection] {
+ return xsync.NewMapOf[string, SocketConnection]()
+ })
+
+ sockets.Store(id, SocketConnection{
+ Id: id,
+ Writer: writer,
+ RoomId: roomId,
+ Done: done,
+ })
+
+ s, ok := sockets.Load(id)
+ if !ok {
+ return
+ }
+
+ manager.dispatch(SocketEvent{
+ SessionId: s.Id,
+ Type: ConnectedEvent,
+ RoomId: s.RoomId,
+ Payload: map[string]any{},
+ })
+}
+
+func (manager *SocketManager) OnClose(id string) {
+ socket := manager.Get(id)
+ if socket == nil {
+ return
+ }
+ slog.Debug("ws-extension: removing socket from manager", slog.String("socketId", id))
+ manager.dispatch(SocketEvent{
+ SessionId: id,
+ Type: DisconnectedEvent,
+ RoomId: socket.RoomId,
+ Payload: map[string]any{},
+ })
+ roomId, ok := manager.idToRoom.Load(id)
+ if !ok {
+ return
+ }
+ sockets, ok := manager.sockets.Load(roomId)
+ if !ok {
+ return
+ }
+ sockets.Delete(id)
+ manager.idToRoom.Delete(id)
+ slog.Debug("ws-extension: removed socket from manager", slog.String("socketId", id))
+
+}
+
+func (manager *SocketManager) CloseWithMessage(id string, message string) {
+ conn := manager.Get(id)
+ if conn != nil {
+ defer manager.OnClose(id)
+ manager.writeText(*conn, message)
+ conn.Done <- true
+ }
+}
+
+func (manager *SocketManager) Disconnect(id string) {
+ conn := manager.Get(id)
+ if conn != nil {
+ manager.OnClose(id)
+ conn.Done <- true
+ }
+}
+
+func (manager *SocketManager) Get(id string) *SocketConnection {
+ roomId, ok := manager.idToRoom.Load(id)
+ if !ok {
+ return nil
+ }
+ sockets, ok := manager.sockets.Load(roomId)
+ if !ok {
+ return nil
+ }
+ conn, ok := sockets.Load(id)
+ return &conn
+}
+
+func (manager *SocketManager) Ping(id string) bool {
+ conn := manager.Get(id)
+ if conn != nil {
+ return manager.writeText(*conn, "ping")
+ }
+ return false
+}
+
+func (manager *SocketManager) writeCloseRaw(writer WriterChan, message string) {
+ manager.writeTextRaw(writer, message)
+}
+
+func (manager *SocketManager) writeTextRaw(writer WriterChan, message string) {
+ timeout := 3 * time.Second
+ select {
+ case writer <- message:
+ case <-time.After(timeout):
+ fmt.Printf("could not send %s to channel after %s\n", message, timeout)
+ }
+}
+
+func (manager *SocketManager) writeText(socket SocketConnection, message string) bool {
+ if socket.Writer == nil {
+ return false
+ }
+ manager.writeTextRaw(socket.Writer, message)
+ return true
+}
+
+func (manager *SocketManager) BroadcastText(roomId string, message string, predicate func(conn SocketConnection) bool) {
+ sockets, ok := manager.sockets.Load(roomId)
+
+ if !ok {
+ return
+ }
+
+ sockets.Range(func(id string, conn SocketConnection) bool {
+ if predicate(conn) {
+ manager.writeText(conn, message)
+ }
+ return true
+ })
+}
+
+func (manager *SocketManager) SendHtml(id string, message string) bool {
+ conn := manager.Get(id)
+ minified := strings.ReplaceAll(message, "\n", "")
+ minified = strings.ReplaceAll(minified, "\t", "")
+ minified = strings.TrimSpace(minified)
+ if conn != nil {
+ return manager.writeText(*conn, minified)
+ }
+ return false
+}
+
+func (manager *SocketManager) SendText(id string, message string) bool {
+ conn := manager.Get(id)
+ if conn != nil {
+ return manager.writeText(*conn, message)
+ }
+ return false
+}
diff --git a/extensions/websocket/internal/wsutil/manager_test.go b/extensions/websocket/internal/wsutil/manager_test.go
new file mode 100644
index 0000000..0ba95d4
--- /dev/null
+++ b/extensions/websocket/internal/wsutil/manager_test.go
@@ -0,0 +1,202 @@
+package wsutil
+
+import (
+ ws2 "github.com/maddalax/htmgo/extensions/websocket/opts"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func createManager() *SocketManager {
+ return NewSocketManager(&ws2.ExtensionOpts{
+ WsPath: "/ws",
+ SessionId: func(ctx *h.RequestContext) string {
+ return "test"
+ },
+ })
+}
+
+func addSocket(manager *SocketManager, roomId string, id string) (socketId string, writer WriterChan, done DoneChan) {
+ writer = make(chan string, 10)
+ done = make(chan bool, 10)
+ manager.Add(roomId, id, writer, done)
+ return id, writer, done
+}
+
+func TestManager(t *testing.T) {
+ manager := createManager()
+ socketId, _, _ := addSocket(manager, "123", "456")
+ socket := manager.Get(socketId)
+ assert.NotNil(t, socket)
+ assert.Equal(t, socketId, socket.Id)
+
+ manager.OnClose(socketId)
+ socket = manager.Get(socketId)
+ assert.Nil(t, socket)
+}
+
+func TestManagerForEachSocket(t *testing.T) {
+ manager := createManager()
+ addSocket(manager, "all", "456")
+ addSocket(manager, "all", "789")
+ var count int
+ manager.ForEachSocket("all", func(conn SocketConnection) {
+ count++
+ })
+ assert.Equal(t, 2, count)
+}
+
+func TestSendText(t *testing.T) {
+ manager := createManager()
+ socketId, writer, done := addSocket(manager, "all", "456")
+ manager.SendText(socketId, "hello")
+ assert.Equal(t, "hello", <-writer)
+ manager.SendText(socketId, "hello2")
+ assert.Equal(t, "hello2", <-writer)
+ done <- true
+ assert.Equal(t, true, <-done)
+}
+
+func TestBroadcastText(t *testing.T) {
+ manager := createManager()
+ _, w1, d1 := addSocket(manager, "all", "456")
+ _, w2, d2 := addSocket(manager, "all", "789")
+ manager.BroadcastText("all", "hello", func(conn SocketConnection) bool {
+ return true
+ })
+ assert.Equal(t, "hello", <-w1)
+ assert.Equal(t, "hello", <-w2)
+ d1 <- true
+ d2 <- true
+ assert.Equal(t, true, <-d1)
+ assert.Equal(t, true, <-d2)
+}
+
+func TestBroadcastTextWithPredicate(t *testing.T) {
+ manager := createManager()
+ _, w1, _ := addSocket(manager, "all", "456")
+ _, w2, _ := addSocket(manager, "all", "789")
+ manager.BroadcastText("all", "hello", func(conn SocketConnection) bool {
+ return conn.Id != "456"
+ })
+
+ assert.Equal(t, 0, len(w1))
+ assert.Equal(t, 1, len(w2))
+}
+
+func TestSendHtml(t *testing.T) {
+ manager := createManager()
+ socketId, writer, _ := addSocket(manager, "all", "456")
+ rendered := h.Render(
+ h.Div(
+ h.P(
+ h.Text("hello"),
+ ),
+ ))
+ manager.SendHtml(socketId, rendered)
+ assert.Equal(t, "
", <-writer)
+}
+
+func TestOnMessage(t *testing.T) {
+ manager := createManager()
+ socketId, _, _ := addSocket(manager, "all", "456")
+
+ listener := make(chan SocketEvent, 10)
+
+ manager.Listen(listener)
+
+ manager.OnMessage(socketId, map[string]any{
+ "message": "hello",
+ })
+
+ event := <-listener
+ assert.Equal(t, "hello", event.Payload["message"])
+ assert.Equal(t, "456", event.SessionId)
+ assert.Equal(t, MessageEvent, event.Type)
+ assert.Equal(t, "all", event.RoomId)
+}
+
+func TestOnClose(t *testing.T) {
+ manager := createManager()
+ socketId, _, _ := addSocket(manager, "all", "456")
+ listener := make(chan SocketEvent, 10)
+ manager.Listen(listener)
+ manager.OnClose(socketId)
+ event := <-listener
+ assert.Equal(t, "456", event.SessionId)
+ assert.Equal(t, DisconnectedEvent, event.Type)
+ assert.Equal(t, "all", event.RoomId)
+}
+
+func TestOnAdd(t *testing.T) {
+ manager := createManager()
+
+ listener := make(chan SocketEvent, 10)
+ manager.Listen(listener)
+
+ socketId, _, _ := addSocket(manager, "all", "456")
+ event := <-listener
+
+ assert.Equal(t, socketId, event.SessionId)
+ assert.Equal(t, ConnectedEvent, event.Type)
+ assert.Equal(t, "all", event.RoomId)
+}
+
+func TestCloseWithMessage(t *testing.T) {
+ manager := createManager()
+ socketId, w, _ := addSocket(manager, "all", "456")
+ manager.CloseWithMessage(socketId, "internal error")
+ assert.Equal(t, "internal error", <-w)
+ assert.Nil(t, manager.Get(socketId))
+}
+
+func TestDisconnect(t *testing.T) {
+ manager := createManager()
+ socketId, _, _ := addSocket(manager, "all", "456")
+ manager.Disconnect(socketId)
+ assert.Nil(t, manager.Get(socketId))
+}
+
+func TestPing(t *testing.T) {
+ manager := createManager()
+ socketId, w, _ := addSocket(manager, "all", "456")
+ manager.Ping(socketId)
+ assert.Equal(t, "ping", <-w)
+}
+
+func TestMultipleRooms(t *testing.T) {
+ manager := createManager()
+ socketId1, _, _ := addSocket(manager, "room1", "456")
+ socketId2, _, _ := addSocket(manager, "room2", "789")
+
+ room1Count := 0
+ room2Count := 0
+
+ manager.ForEachSocket("room1", func(conn SocketConnection) {
+ room1Count++
+ })
+
+ manager.ForEachSocket("room2", func(conn SocketConnection) {
+ room2Count++
+ })
+
+ assert.Equal(t, 1, room1Count)
+ assert.Equal(t, 1, room2Count)
+
+ room1Count = 0
+ room2Count = 0
+
+ manager.OnClose(socketId1)
+ manager.OnClose(socketId2)
+
+ manager.ForEachSocket("room1", func(conn SocketConnection) {
+ room1Count++
+ })
+
+ manager.ForEachSocket("room2", func(conn SocketConnection) {
+ room2Count++
+ })
+
+ assert.Equal(t, 0, room1Count)
+ assert.Equal(t, 0, room2Count)
+}
diff --git a/extensions/websocket/opts/opts.go b/extensions/websocket/opts/opts.go
new file mode 100644
index 0000000..b59373b
--- /dev/null
+++ b/extensions/websocket/opts/opts.go
@@ -0,0 +1,9 @@
+package opts
+
+import "github.com/maddalax/htmgo/framework/h"
+
+type ExtensionOpts struct {
+ WsPath string
+ RoomName func(ctx *h.RequestContext) string
+ SessionId func(ctx *h.RequestContext) string
+}
diff --git a/extensions/websocket/session/session.go b/extensions/websocket/session/session.go
new file mode 100644
index 0000000..24d35ef
--- /dev/null
+++ b/extensions/websocket/session/session.go
@@ -0,0 +1,77 @@
+package session
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/puzpuzpuz/xsync/v3"
+)
+
+type Id string
+
+var cache = xsync.NewMapOf[Id, *xsync.MapOf[string, any]]()
+
+type State struct {
+ SessionId Id
+}
+
+func NewState(ctx *h.RequestContext) *State {
+ id := GetSessionId(ctx)
+ cache.Store(id, xsync.NewMapOf[string, any]())
+ return &State{
+ SessionId: id,
+ }
+}
+
+func CreateSession(ctx *h.RequestContext) Id {
+ sessionId := fmt.Sprintf("session-id-%s", h.GenId(30))
+ ctx.Set("session-id", sessionId)
+ return Id(sessionId)
+}
+
+func GetSessionId(ctx *h.RequestContext) Id {
+ sessionIdRaw := ctx.Get("session-id")
+ sessionId := ""
+
+ if sessionIdRaw == "" || sessionIdRaw == nil {
+ panic("session id is not set, please use session.CreateSession(ctx) in middleware to create a session id")
+ } else {
+ sessionId = sessionIdRaw.(string)
+ }
+
+ return Id(sessionId)
+}
+
+func Update[T any](sessionId Id, key string, compute func(prev T) T) T {
+ actual := Get[T](sessionId, key, *new(T))
+ next := compute(actual)
+ Set(sessionId, key, next)
+ return next
+}
+
+func Get[T any](sessionId Id, key string, fallback T) T {
+ actual, _ := cache.LoadOrCompute(sessionId, func() *xsync.MapOf[string, any] {
+ return xsync.NewMapOf[string, any]()
+ })
+ value, exists := actual.Load(key)
+ if exists {
+ return value.(T)
+ }
+ return fallback
+}
+
+func Set(sessionId Id, key string, value any) {
+ actual, _ := cache.LoadOrCompute(sessionId, func() *xsync.MapOf[string, any] {
+ return xsync.NewMapOf[string, any]()
+ })
+ actual.Store(key, value)
+}
+
+func UseState[T any](sessionId Id, key string, initial T) (func() T, func(T)) {
+ var get = func() T {
+ return Get[T](sessionId, key, initial)
+ }
+ var set = func(value T) {
+ Set(sessionId, key, value)
+ }
+ return get, set
+}
diff --git a/extensions/websocket/ws/access.go b/extensions/websocket/ws/access.go
new file mode 100644
index 0000000..efa63c5
--- /dev/null
+++ b/extensions/websocket/ws/access.go
@@ -0,0 +1,10 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/framework/h"
+)
+
+func ManagerFromCtx(ctx *h.RequestContext) *wsutil.SocketManager {
+ return wsutil.SocketManagerFromCtx(ctx)
+}
diff --git a/extensions/websocket/ws/attribute.go b/extensions/websocket/ws/attribute.go
new file mode 100644
index 0000000..40a048a
--- /dev/null
+++ b/extensions/websocket/ws/attribute.go
@@ -0,0 +1,20 @@
+package ws
+
+import "github.com/maddalax/htmgo/framework/h"
+
+func OnClick(ctx *h.RequestContext, handler Handler) *h.AttributeMapOrdered {
+ return AddClientSideHandler(ctx, "click", handler)
+}
+
+func OnClientEvent(ctx *h.RequestContext, eventName string, handler Handler) *h.AttributeMapOrdered {
+ return AddClientSideHandler(ctx, eventName, handler)
+}
+
+func OnServerEvent(ctx *h.RequestContext, eventName string, handler Handler) h.Ren {
+ AddServerSideHandler(ctx, eventName, handler)
+ return h.Attribute("data-handler-id", "")
+}
+
+func OnMouseOver(ctx *h.RequestContext, handler Handler) *h.AttributeMapOrdered {
+ return AddClientSideHandler(ctx, "mouseover", handler)
+}
diff --git a/extensions/websocket/ws/dispatch.go b/extensions/websocket/ws/dispatch.go
new file mode 100644
index 0000000..248b649
--- /dev/null
+++ b/extensions/websocket/ws/dispatch.go
@@ -0,0 +1,47 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+)
+
+// PushServerSideEvent sends a server side event this specific session
+func PushServerSideEvent(data HandlerData, event string, value map[string]any) {
+ serverSideMessageListener <- ServerSideEvent{
+ Event: event,
+ Payload: value,
+ SessionId: data.SessionId,
+ }
+}
+
+// BroadcastServerSideEvent sends a server side event to all clients that have a handler for the event, not just the current session
+func BroadcastServerSideEvent(event string, value map[string]any) {
+ serverSideMessageListener <- ServerSideEvent{
+ Event: event,
+ Payload: value,
+ SessionId: "*",
+ }
+}
+
+// PushElement sends an element to the current session and swaps it into the page
+func PushElement(data HandlerData, el *h.Element) bool {
+ return data.Manager.SendHtml(data.Socket.Id, h.Render(el))
+}
+
+// PushElementCtx sends an element to the current session and swaps it into the page
+func PushElementCtx(ctx *h.RequestContext, el *h.Element) bool {
+ locator := ctx.ServiceLocator()
+ socketManager := service.Get[wsutil.SocketManager](locator)
+ socketId := session.GetSessionId(ctx)
+ socket := socketManager.Get(string(socketId))
+ if socket == nil {
+ return false
+ }
+ return PushElement(HandlerData{
+ Socket: socket,
+ Manager: socketManager,
+ SessionId: socketId,
+ }, el)
+}
diff --git a/extensions/websocket/ws/every.go b/extensions/websocket/ws/every.go
new file mode 100644
index 0000000..679d3d5
--- /dev/null
+++ b/extensions/websocket/ws/every.go
@@ -0,0 +1,29 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/maddalax/htmgo/framework/service"
+ "time"
+)
+
+// Every executes the given callback every interval, until the socket is disconnected, or the callback returns false.
+func Every(ctx *h.RequestContext, interval time.Duration, cb func() bool) {
+ socketId := session.GetSessionId(ctx)
+ locator := ctx.ServiceLocator()
+ manager := service.Get[wsutil.SocketManager](locator)
+ manager.RunIntervalWithSocket(string(socketId), interval, cb)
+}
+
+func Once(ctx *h.RequestContext, cb func()) {
+ // time is irrelevant, we just need to run the callback once, it will exit after because of the false return
+ Every(ctx, time.Millisecond, func() bool {
+ cb()
+ return false
+ })
+}
+
+func RunOnConnected(ctx *h.RequestContext, cb func()) {
+ Once(ctx, cb)
+}
diff --git a/extensions/websocket/ws/handler.go b/extensions/websocket/ws/handler.go
new file mode 100644
index 0000000..2d0bbb7
--- /dev/null
+++ b/extensions/websocket/ws/handler.go
@@ -0,0 +1,90 @@
+package ws
+
+import (
+ "fmt"
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "sync"
+)
+
+type MessageHandler struct {
+ manager *wsutil.SocketManager
+}
+
+func NewMessageHandler(manager *wsutil.SocketManager) *MessageHandler {
+ return &MessageHandler{manager: manager}
+}
+
+func (h *MessageHandler) OnServerSideEvent(e ServerSideEvent) {
+ fmt.Printf("received server side event: %s\n", e.Event)
+ hashes, ok := serverEventNamesToHash.Load(e.Event)
+
+ // If we are not broadcasting to everyone, filter it down to just the current session that invoked the event
+ // TODO optimize this
+ if e.SessionId != "*" {
+ hashesForSession, ok2 := sessionIdToHashes.Load(e.SessionId)
+
+ if ok2 {
+ subset := make(map[KeyHash]bool)
+ for hash := range hashes {
+ if _, ok := hashesForSession[hash]; ok {
+ subset[hash] = true
+ }
+ }
+ hashes = subset
+ }
+ }
+
+ if ok {
+ lock.Lock()
+ callingHandler.Store(true)
+ wg := sync.WaitGroup{}
+ for hash := range hashes {
+ cb, ok := handlers.Load(hash)
+ if ok {
+ wg.Add(1)
+ go func(e ServerSideEvent) {
+ defer wg.Done()
+ sessionId, ok2 := hashesToSessionId.Load(hash)
+ if ok2 {
+ cb(HandlerData{
+ SessionId: sessionId,
+ Socket: h.manager.Get(string(sessionId)),
+ Manager: h.manager,
+ })
+ }
+ }(e)
+ }
+ }
+ wg.Wait()
+ callingHandler.Store(false)
+ lock.Unlock()
+ }
+}
+
+func (h *MessageHandler) OnClientSideEvent(handlerId string, sessionId session.Id) {
+ cb, ok := handlers.Load(handlerId)
+ if ok {
+ cb(HandlerData{
+ SessionId: sessionId,
+ Socket: h.manager.Get(string(sessionId)),
+ Manager: h.manager,
+ })
+ }
+}
+
+func (h *MessageHandler) OnDomElementRemoved(handlerId string) {
+ handlers.Delete(handlerId)
+}
+
+func (h *MessageHandler) OnSocketDisconnected(event wsutil.SocketEvent) {
+ sessionId := session.Id(event.SessionId)
+ hashes, ok := sessionIdToHashes.Load(sessionId)
+ if ok {
+ for hash := range hashes {
+ hashesToSessionId.Delete(hash)
+ handlers.Delete(hash)
+ }
+ sessionIdToHashes.Delete(sessionId)
+ }
+}
diff --git a/extensions/websocket/ws/listener.go b/extensions/websocket/ws/listener.go
new file mode 100644
index 0000000..5556331
--- /dev/null
+++ b/extensions/websocket/ws/listener.go
@@ -0,0 +1,46 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/framework/service"
+)
+
+func StartListener(locator *service.Locator) {
+ manager := service.Get[wsutil.SocketManager](locator)
+ manager.Listen(socketMessageListener)
+ handler := NewMessageHandler(manager)
+ go func() {
+ for {
+ handle(handler)
+ }
+ }()
+}
+
+func handle(handler *MessageHandler) {
+ select {
+ case event := <-serverSideMessageListener:
+ handler.OnServerSideEvent(event)
+ case event := <-socketMessageListener:
+ switch event.Type {
+ case wsutil.DisconnectedEvent:
+ handler.OnSocketDisconnected(event)
+ case wsutil.MessageEvent:
+
+ handlerId, ok := event.Payload["id"].(string)
+ eventName, ok2 := event.Payload["event"].(string)
+
+ if !ok || !ok2 {
+ return
+ }
+
+ sessionId := session.Id(event.SessionId)
+ if eventName == "dom-element-removed" {
+ handler.OnDomElementRemoved(handlerId)
+ return
+ } else {
+ handler.OnClientSideEvent(handlerId, sessionId)
+ }
+ }
+ }
+}
diff --git a/extensions/websocket/ws/metrics.go b/extensions/websocket/ws/metrics.go
new file mode 100644
index 0000000..258493b
--- /dev/null
+++ b/extensions/websocket/ws/metrics.go
@@ -0,0 +1,19 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/framework/h"
+)
+
+type Metrics struct {
+ Manager wsutil.ManagerMetrics
+ Handler HandlerMetrics
+}
+
+func MetricsFromCtx(ctx *h.RequestContext) Metrics {
+ manager := ManagerFromCtx(ctx)
+ return Metrics{
+ Manager: manager.Metrics(),
+ Handler: GetHandlerMetics(),
+ }
+}
diff --git a/extensions/websocket/ws/register.go b/extensions/websocket/ws/register.go
new file mode 100644
index 0000000..b7350fd
--- /dev/null
+++ b/extensions/websocket/ws/register.go
@@ -0,0 +1,92 @@
+package ws
+
+import (
+ "github.com/maddalax/htmgo/extensions/websocket/internal/wsutil"
+ "github.com/maddalax/htmgo/extensions/websocket/session"
+ "github.com/maddalax/htmgo/framework/h"
+ "github.com/puzpuzpuz/xsync/v3"
+ "sync"
+ "sync/atomic"
+)
+
+type HandlerData struct {
+ SessionId session.Id
+ Socket *wsutil.SocketConnection
+ Manager *wsutil.SocketManager
+}
+
+type Handler func(data HandlerData)
+
+type ServerSideEvent struct {
+ Event string
+ Payload map[string]any
+ SessionId session.Id
+}
+type KeyHash = string
+
+var handlers = xsync.NewMapOf[KeyHash, Handler]()
+var sessionIdToHashes = xsync.NewMapOf[session.Id, map[KeyHash]bool]()
+var hashesToSessionId = xsync.NewMapOf[KeyHash, session.Id]()
+var serverEventNamesToHash = xsync.NewMapOf[string, map[KeyHash]bool]()
+
+var socketMessageListener = make(chan wsutil.SocketEvent, 100)
+var serverSideMessageListener = make(chan ServerSideEvent, 100)
+var lock = sync.Mutex{}
+var callingHandler = atomic.Bool{}
+
+type HandlerMetrics struct {
+ TotalHandlers int
+ ServerEventNamesToHashCount int
+ SessionIdToHashesCount int
+}
+
+func GetHandlerMetics() HandlerMetrics {
+ metrics := HandlerMetrics{
+ TotalHandlers: handlers.Size(),
+ ServerEventNamesToHashCount: serverEventNamesToHash.Size(),
+ SessionIdToHashesCount: sessionIdToHashes.Size(),
+ }
+ return metrics
+}
+
+func makeId() string {
+ return h.GenId(30)
+}
+
+func AddServerSideHandler(ctx *h.RequestContext, event string, handler Handler) *h.AttributeMapOrdered {
+ // If we are already in a handler, we don't want to add another handler
+ // this can happen if the handler renders another element that has a handler
+ if callingHandler.Load() {
+ return h.NewAttributeMap()
+ }
+ sessionId := session.GetSessionId(ctx)
+ hash := makeId()
+ handlers.LoadOrStore(hash, handler)
+ m, _ := serverEventNamesToHash.LoadOrCompute(event, func() map[KeyHash]bool {
+ return make(map[KeyHash]bool)
+ })
+ m[hash] = true
+ storeHashForSession(sessionId, hash)
+ storeSessionIdForHash(sessionId, hash)
+ return h.AttributePairs("data-handler-id", hash, "data-handler-event", event)
+}
+
+func AddClientSideHandler(ctx *h.RequestContext, event string, handler Handler) *h.AttributeMapOrdered {
+ hash := makeId()
+ handlers.LoadOrStore(hash, handler)
+ sessionId := session.GetSessionId(ctx)
+ storeHashForSession(sessionId, hash)
+ storeSessionIdForHash(sessionId, hash)
+ return h.AttributePairs("data-handler-id", hash, "data-handler-event", event)
+}
+
+func storeHashForSession(sessionId session.Id, hash KeyHash) {
+ m, _ := sessionIdToHashes.LoadOrCompute(sessionId, func() map[KeyHash]bool {
+ return make(map[KeyHash]bool)
+ })
+ m[hash] = true
+}
+
+func storeSessionIdForHash(sessionId session.Id, hash KeyHash) {
+ hashesToSessionId.Store(hash, sessionId)
+}
diff --git a/framework/assets/dist/htmgo.js b/framework/assets/dist/htmgo.js
index df752a2..b49560d 100644
--- a/framework/assets/dist/htmgo.js
+++ b/framework/assets/dist/htmgo.js
@@ -1 +1 @@
-var ne=function(){let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){return getInputValues(e,t||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0},parseInterval:null,_:null,version:"2.0.2"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(e){return "[hx-"+e+"], [data-hx-"+e+"]"}).join(", "),HEAD_TAG_REGEX=makeTagRegEx("head");function makeTagRegEx(e,t=!1){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function parseInterval(e){if(e==null)return;let t=NaN;return e.slice(-2)=="ms"?t=parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?t=parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?t=parseFloat(e.slice(0,-1))*1e3*60:t=parseFloat(e),isNaN(t)?void 0:t}function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)}function hasAttribute(e,t){return !!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)}function parentElt(e){let t=e.parentElement;return !t&&e.parentNode instanceof ShadowRoot?e.parentNode:t}function getDocument(){return document}function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()}function getClosestMatch(e,t){for(;e&&!t(e);)e=parentElt(e);return e||null}function getAttributeValueWithDisinheritance(e,t,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var i=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return i&&(i==="*"||i.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return "unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return !!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){let n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return !!n&&n.call(e,t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[1].toLowerCase():""}function parseHTML(e){return new DOMParser().parseFromString(e,"text/html")}function takeChildrenFor(e,t){for(;t.childNodes.length>0;)e.append(t.childNodes[0]);}function duplicateScript(e){let t=getDocument().createElement("script");return forEach(e.attributes,function(n){t.setAttribute(n.name,n.value);}),t.textContent=e.textContent,t.async=!1,htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce),t}function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function normalizeScriptTags(e){Array.from(e.querySelectorAll("script")).forEach(t=>{if(isJavaScriptScriptNode(t)){let n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t);}catch(o){logError(o);}finally{t.remove();}}});}function makeFragment(e){let t=e.replace(HEAD_TAG_REGEX,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let i=parseHTML(e);takeChildrenFor(r,i.body),r.title=i.title;}else if(n==="body"){r=new DocumentFragment;let i=parseHTML(t);takeChildrenFor(r,i.body),r.title=i.title;}else {let i=parseHTML(''+t+"");r=i.querySelector("template").content,r.title=i.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText);}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(i=>i.remove())),r}function maybeCall(e){e&&e();}function isType(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function isFunction(e){return typeof e=="function"}function isRawObject(e){return isType(e,"Object")}function getInternalData(e){let t="htmx-internal-data",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n=0}function bodyContains(e){let t=e.getRootNode&&e.getRootNode();return t&&t instanceof window.ShadowRoot?getDocument().body.contains(t.host):getDocument().body.contains(e)}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function parseJSON(e){try{return JSON.parse(e)}catch(t){return logError(t),null}}function canAccessLocalStorage(){let e="htmx:localStorageTest";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch{return !1}}function normalizePath(e){try{let t=new URL(e);return t&&(e=t.pathname+t.search),/^\/$/.test(e)||(e=e.replace(/\/+$/,"")),e}catch{return e}}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(e){return htmx.on("htmx:load",function(n){e(n.detail.elt);})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n);};}function logNone(){htmx.logger=null;}function find(e,t){return typeof e!="string"?e.querySelector(t):find(getDocument(),e)}function findAll(e,t){return typeof e!="string"?e.querySelectorAll(t):findAll(getDocument(),e)}function getWindow(){return window}function removeElement(e,t){e=resolveTarget(e),t?getWindow().setTimeout(function(){removeElement(e),e=null;},t):parentElt(e).removeChild(e);}function asElement(e){return e instanceof Element?e:null}function asHtmlElement(e){return e instanceof HTMLElement?e:null}function asString(e){return typeof e=="string"?e:null}function asParentNode(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function addClassToElement(e,t,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null;},n):e.classList&&e.classList.add(t));}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null;},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.removeAttribute("class")));}function toggleClassOnElement(e,t){e=resolveTarget(e),e.classList.toggle(t);}function takeClassForElement(e,t){e=resolveTarget(e),forEach(e.parentElement.children,function(n){removeClassFromElement(n,t);}),addClassToElement(asElement(e),t);}function closest(e,t){if(e=asElement(resolveTarget(e)),e&&e.closest)return e.closest(t);do if(e==null||matches(e,t))return e;while(e=e&&asElement(parentElt(e)));return null}function startsWith(e,t){return e.substring(0,t.length)===t}function endsWith(e,t){return e.substring(e.length-t.length)===t}function normalizeSelector(e){let t=e.trim();return startsWith(t,"<")&&endsWith(t,"/>")?t.substring(1,t.length-2):t}function querySelectorAllExt(e,t,n){return e=resolveTarget(e),t.indexOf("closest ")===0?[closest(asElement(e),normalizeSelector(t.substr(8)))]:t.indexOf("find ")===0?[find(asParentNode(e),normalizeSelector(t.substr(5)))]:t==="next"?[asElement(e).nextElementSibling]:t.indexOf("next ")===0?[scanForwardQuery(e,normalizeSelector(t.substr(5)),!!n)]:t==="previous"?[asElement(e).previousElementSibling]:t.indexOf("previous ")===0?[scanBackwardsQuery(e,normalizeSelector(t.substr(9)),!!n)]:t==="document"?[document]:t==="window"?[window]:t==="body"?[document.body]:t==="root"?[getRootNode(e,!!n)]:t.indexOf("global ")===0?querySelectorAllExt(e,t.slice(7),!0):toArray(asParentNode(getRootNode(e,!!n)).querySelectorAll(normalizeSelector(t)))}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o=0;o--){let i=r[o];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}};function querySelectorExt(e,t){return typeof e!="string"?querySelectorAllExt(e,t)[0]:querySelectorAllExt(getDocument().body,e)[0]}function resolveTarget(e,t){return typeof e=="string"?find(asParentNode(t)||document,e):e}function processEventArgs(e,t,n){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t}:{target:resolveTarget(e),event:asString(t),listener:n}}function addEventListenerImpl(e,t,n){return ready(function(){let o=processEventArgs(e,t,n);o.target.addEventListener(o.event,o.listener);}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener);}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return [findThisElement(e,t)];{let r=querySelectorAllExt(e,n);return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),t)!=null}))}function getTarget(e){let t=getClosestAttributeValue(e,"hx-target");return t?t==="this"?findThisElement(e,"hx-target"):querySelectorExt(e,t):getInternalData(e).boosted?getDocument().body:e}function shouldSettleAttribute(e){let t=htmx.config.attributesToSettle;for(let n=0;n0?(o=e.substr(0,e.indexOf(":")),r=e.substr(e.indexOf(":")+1,e.length)):o=e);let i=getDocument().querySelectorAll(r);return i?(forEach(i,function(s){let l,a=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(a),isInlineSwap(o,s)||(l=asParentNode(a));let u={shouldSwap:!0,target:s,fragment:l};triggerEvent(s,"htmx:oobBeforeSwap",u)&&(s=u.target,u.shouldSwap&&swapWithStyle(o,s,s,l,n),forEach(n.elts,function(f){triggerEvent(f,"htmx:oobAfterSwap",u);}));}),t.parentNode.removeChild(t)):(t.parentNode.removeChild(t),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:t})),e}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);r!=null&&t.parentNode.replaceChild(r,t);});}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let i=o.replace("'","\\'"),s=r.tagName.replace(":","\\:"),l=asParentNode(e),a=l&&l.querySelector(s+"[id='"+i+"']");if(a&&a!==l){let u=r.cloneNode();cloneAttributes(r,a),n.tasks.push(function(){cloneAttributes(r,u);});}}});}function makeAjaxLoadTask(e){return function(){removeClassFromElement(e,htmx.config.addedClass),processNode(asElement(e)),processFocus(asParentNode(e)),triggerEvent(e,"htmx:load");}}function processFocus(e){let t="[autofocus]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus();}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o));}}function stringHash(e,t){let n=0;for(;n0}function swap(e,t,n,r){r||(r={}),e=resolveTarget(e);let o=document.activeElement,i={};try{i={elt:o,start:o?o.selectionStart:null,end:o?o.selectionEnd:null};}catch{}let s=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else {let a=makeFragment(t);if(s.title=a.title,r.selectOOB){let u=r.selectOOB.split(",");for(let f=0;f0?getWindow().setTimeout(l,n.settleDelay):l();}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let i in o)if(o.hasOwnProperty(i)){let s=o[i];isRawObject(s)?n=s.target!==void 0?s.target:n:s={value:s},triggerEvent(n,i,s);}}else {let o=r.split(",");for(let i=0;i0;){let s=t[0];if(s==="]"){if(r--,r===0){i===null&&(o=o+"true"),t.shift(),o+=")})";try{let l=maybeEval(e,function(){return Function(o)()},function(){return !0});return l.source=o,l}catch(l){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:l,source:o}),null}}}else s==="["&&r++;isPossibleRelativeReference(s,i,n)?o+="(("+n+"."+s+") ? ("+n+"."+s+") : (window."+s+"))":o=o+s,i=t.shift();}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}function consumeCSSSelector(e){let t;return e.length>0&&COMBINED_SELECTOR_START.test(e[0])?(e.shift(),t=consumeUntil(e,COMBINED_SELECTOR_END).trim(),e.shift()):t=consumeUntil(e,WHITESPACE_OR_COMMA),t}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(e,t,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let l=o.length,a=consumeUntil(o,/[,\[\s]/);if(a!=="")if(a==="every"){let u={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),u.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var i=maybeGenerateConditional(e,o,"event");i&&(u.eventFilter=i),r.push(u);}else {let u={trigger:a};var i=maybeGenerateConditional(e,o,"event");for(i&&(u.eventFilter=i);o.length>0&&o[0]!==",";){consumeUntil(o,NOT_WHITESPACE);let c=o.shift();if(c==="changed")u.changed=!0;else if(c==="once")u.once=!0;else if(c==="consume")u.consume=!0;else if(c==="delay"&&o[0]===":")o.shift(),u.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(c==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var s=consumeCSSSelector(o);else {var s=consumeUntil(o,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();let b=consumeCSSSelector(o);b.length>0&&(s+=" "+b);}}u.from=s;}else c==="target"&&o[0]===":"?(o.shift(),u.target=consumeCSSSelector(o)):c==="throttle"&&o[0]===":"?(o.shift(),u.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):c==="queue"&&o[0]===":"?(o.shift(),u.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):c==="root"&&o[0]===":"?(o.shift(),u[c]=consumeCSSSelector(o)):c==="threshold"&&o[0]===":"?(o.shift(),u[c]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});}r.push(u);}o.length===l&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE);}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r);}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0;}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n));},n.pollInterval);}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else {let i=getRawAttribute(e,"method");r=i?i.toLowerCase():"get",o=getRawAttribute(e,"action");}n.forEach(function(i){addEventListener(e,function(s,l){let a=asElement(s);if(eltIsDisabled(a)){cleanUpElement(a);return}issueAjaxRequest(r,o,a,l);},t,i,!0);});}}function shouldCancel(e,t){let n=asElement(t);return n?!!((e.type==="submit"||e.type==="click")&&(n.tagName==="FORM"||matches(n,'input[type="submit"], button')&&closest(n,"form")!==null||n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0))):!1}function ignoreBoostedAnchorCtrlClick(e,t){return getInternalData(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function maybeFilterEvent(e,t,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let i=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:i}),!0}return !1}function addEventListener(e,t,n,r,o){let i=getInternalData(e),s;r.from?s=querySelectorAllExt(e,r.from):s=[e],r.changed&&s.forEach(function(l){let a=getInternalData(l);a.lastValue=l.value;}),forEach(s,function(l){let a=function(u){if(!bodyContains(e)){l.removeEventListener(r.trigger,a);return}if(ignoreBoostedAnchorCtrlClick(e,u)||((o||shouldCancel(u,e))&&u.preventDefault(),maybeFilterEvent(r,e,u)))return;let f=getInternalData(u);if(f.triggerSpec=r,f.handledFor==null&&(f.handledFor=[]),f.handledFor.indexOf(e)<0){if(f.handledFor.push(e),r.consume&&u.stopPropagation(),r.target&&u.target&&!matches(asElement(u.target),r.target))return;if(r.once){if(i.triggeredOnce)return;i.triggeredOnce=!0;}if(r.changed){let c=getInternalData(l),d=l.value;if(c.lastValue===d)return;c.lastValue=d;}if(i.delayed&&clearTimeout(i.delayed),i.throttle)return;r.throttle>0?i.throttle||(triggerEvent(e,"htmx:trigger"),t(e,u),i.throttle=getWindow().setTimeout(function(){i.throttle=null;},r.throttle)):r.delay>0?i.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,u);},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,u));}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:a,on:l}),l.addEventListener(r.trigger,a);});}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0;},window.addEventListener("scroll",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){maybeReveal(e);}));},200));}function maybeReveal(e){!hasAttribute(e,"data-hx-revealed")&&isScrolledIntoView(e)&&(e.setAttribute("data-hx-revealed","true"),getInternalData(e).initHash?triggerEvent(e,"revealed"):e.addEventListener("htmx:afterProcessNode",function(){triggerEvent(e,"revealed");},{once:!0}));}function loadImmediately(e,t,n,r){let o=function(){n.loaded||(n.loaded=!0,t(e));};r>0?getWindow().setTimeout(o,r):o();}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let i=getAttributeValue(e,"hx-"+o);r=!0,t.path=i,t.verb=o,n.forEach(function(s){addTriggerHandler(e,s,t,function(l,a){let u=asElement(l);if(closest(u,htmx.config.disableSelector)){cleanUpElement(u);return}issueAjaxRequest(o,i,u,a);});});}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,t),maybeReveal(asElement(e));else if(t.trigger==="intersect"){let o={};t.root&&(o.root=querySelectorExt(e,t.root)),t.threshold&&(o.threshold=parseFloat(t.threshold)),new IntersectionObserver(function(s){for(let l=0;l0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t);}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return !1;let n=t.attributes;for(let r=0;r", "+i).join(""))}else return []}function maybeSetLastButtonClicked(e){let t=closest(asElement(e.target),"button, input[type='submit']"),n=getRelatedFormData(e);n&&(n.lastButtonClicked=t);}function maybeUnsetLastButtonClicked(e){let t=getRelatedFormData(e);t&&(t.lastButtonClicked=null);}function getRelatedFormData(e){let t=closest(asElement(e.target),"button, input[type='submit']");if(!t)return;let n=resolveTarget("#"+getRawAttribute(t,"form"),t.getRootNode())||closest(t,"form");if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked);}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,i=function(s){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,s));});};e.addEventListener(t,i),r.onHandlers.push({event:t,listener:i});}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;thtmx.config.historyCacheSize;)i.shift();for(;i.length>0;)try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(l){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:l,cache:i}),i.shift();}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(localStorage.getItem("htmx-history-cache"))||[];for(let n=0;n=200&&this.status<400){triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",n);let r=makeFragment(this.response),o=r.querySelector("[hx-history-elt],[data-hx-history-elt]")||r,i=getHistoryElement(),s=makeSettleInfo(i);handleTitle(r.title),swapInnerHTML(i,o,s),settleImmediately(s.tasks),currentPathForHistory=e,triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:this.response});}else triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",n);},t.send();}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n=makeFragment(t.content),r=getHistoryElement(),o=makeSettleInfo(r);handleTitle(n.title),swapInnerHTML(r,n,o),settleImmediately(o.tasks),getWindow().setTimeout(function(){window.scrollTo(0,t.scroll);},0),currentPathForHistory=e,triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,item:t});}else htmx.config.refreshOnHistoryMiss?window.location.reload(!0):loadHistoryFromServer(e);}function addRequestIndicatorClasses(e){let t=findAttributeTargets(e,"hx-indicator");return t==null&&(t=[e]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass);}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","");}),t}function removeRequestIndicators(e,t){forEach(e,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)-1,r.requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass);}),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)-1,r.requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"));});}function haveSeenNode(e,t){for(let n=0;nt.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.append(e,o));}}function processInputValue(e,t,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let i=getRawAttribute(r,"name"),s=r.value;r instanceof HTMLSelectElement&&r.multiple&&(s=toArray(r.querySelectorAll("option:checked")).map(function(l){return l.value})),r instanceof HTMLInputElement&&r.files&&(s=toArray(r.files)),addValueToFormData(i,s,t),o&&validateElement(r,n);}r instanceof HTMLFormElement&&(forEach(r.elements,function(i){e.indexOf(i)>=0?removeValueFromFormData(i.name,i.value,t):e.push(i),o&&validateElement(i,n);}),new FormData(r).forEach(function(i,s){i instanceof File&&i.name===""||addValueToFormData(s,i,t);}));}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(t.push({elt:n,message:n.validationMessage,validity:n.validity}),triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})));}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n);}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,i=[],s=getInternalData(e);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let l=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(s.lastButtonClicked&&(l=l&&s.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,i,closest(e,"form"),l),processInputValue(n,r,i,e,l),s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let u=s.lastButtonClicked||e,f=getRawAttribute(u,"name");addValueToFormData(f,u.value,o);}let a=findAttributeTargets(e,"hx-include");return forEach(a,function(u){processInputValue(n,r,i,asElement(u),l),matches(u,"form")||forEach(asParentNode(u).querySelectorAll(INPUT_SELECTOR),function(f){processInputValue(n,r,i,f,l);});}),overrideFormData(r,o),{errors:i,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n);}),t}function getHeaders(e,t,n){let r={"HX-Request":"true","HX-Trigger":getRawAttribute(e,"id"),"HX-Trigger-Name":getRawAttribute(e,"name"),"HX-Target":getAttributeValue(t,"id"),"HX-Current-URL":getDocument().location.href};return getValuesForElement(e,"hx-headers",!1,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.substr(4).split(","),function(r){r=r.trim(),e.delete(r);}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(i){r.append(o,i);});}),r}}else return e}function isAnchorLink(e){return !!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={swapStyle:getInternalData(e).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(e).boosted&&!isAnchorLink(e)&&(r.show="top"),n){let s=splitOnWhitespace(n);if(s.length>0)for(let l=0;l0?o.join(":"):null;r.scroll=f,r.scrollTarget=i;}else if(a.indexOf("show:")===0){var o=a.substr(5).split(":");let c=o.pop();var i=o.length>0?o.join(":"):null;r.show=c,r.showTarget=i;}else if(a.indexOf("focus-scroll:")===0){let u=a.substr(13);r.focusScroll=u=="true";}else l==0?r.swapStyle=a:logError("Unknown modifier in hx-swap: "+a);}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t));}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return {tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,o.scrollTop=o.scrollHeight);}if(t.show){var o=null;if(t.showTarget){let s=t.showTarget;t.showTarget==="window"&&(s="body"),o=asElement(querySelectorExt(n,s));}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}));}}function getValuesForElement(e,t,n,r){if(r==null&&(r={}),e==null)return r;let o=getAttributeValue(e,t);if(o){let i=o.trim(),s=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.substr(11),s=!0):i.indexOf("js:")===0&&(i=i.substr(3),s=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let l;s?l=maybeEval(e,function(){return Function("return ("+i+")")()},{}):l=parseJSON(i);for(let a in l)l.hasOwnProperty(a)&&r[a]==null&&(r[a]=l[a]);}return getValuesForElement(asElement(parentElt(e)),t,n,r)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t){return getValuesForElement(e,"hx-vars",!0,t)}function getHXValsForElement(e,t){return getValuesForElement(e,"hx-vals",!1,t)}function getExpressionVars(e){return mergeObjects(getHXVarsForElement(e),getHXValsForElement(e))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n);}catch{e.setRequestHeader(t,encodeURIComponent(n)),e.setRequestHeader(t+"-URI-AutoEncoded","true");}}function getPathFromResponse(e){if(e.responseURL&&typeof URL<"u")try{let t=new URL(e.responseURL);return t.pathname+t.search}catch{triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:e.responseURL});}}function hasHeader(e,t){return t.test(e.getAllResponseHeaders())}function ajaxHelper(e,t,n){return e=e.toLowerCase(),n?n instanceof Element||typeof n=="string"?issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n),returnPromise:!0}):issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:resolveTarget(n.target),swapOverride:n.swap,select:n.select,returnPromise:!0}):issueAjaxRequest(e,t,null,null,{returnPromise:!0})}function hierarchyForElt(e){let t=[];for(;e;)t.push(e),e=e.parentElement;return t}function verifyPath(e,t,n){let r,o;return typeof URL=="function"?(o=new URL(t,document.location.href),r=document.location.origin===o.origin):(o=t,r=startsWith(t,document.location.origin)),htmx.config.selfRequestsOnly&&!r?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:o,sameHost:r},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r);}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(i){r.push(i),e.append(t,i);}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(i){e.append(t,i);});}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,i){return r[o]=i,e.delete(t),r.forEach(function(s){e.append(t,s);}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol")return Reflect.get(t,n);if(n==="toJSON")return ()=>Object.fromEntries(e);if(n in t)return typeof t[n]=="function"?function(){return e[n].apply(e,arguments)}:t[n];let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o);}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,i){let s=null,l=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var a=new Promise(function(g,E){s=g,l=E;});n==null&&(n=getDocument().body);let u=o.handler||handleAjaxResponse,f=o.select||null;if(!bodyContains(n))return maybeCall(s),a;let c=o.targetOverride||asElement(getTarget(n));if(c==null||c==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getAttributeValue(n,"hx-target")}),maybeCall(l),a;let d=getInternalData(n),b=d.lastButtonClicked;if(b){let g=getRawAttribute(b,"formaction");g!=null&&(t=g);let E=getRawAttribute(b,"formmethod");E!=null&&E.toLowerCase()!=="dialog"&&(e=E);}let S=getClosestAttributeValue(n,"hx-confirm");if(i===void 0&&triggerEvent(n,"htmx:confirm",{target:c,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(O){return issueAjaxRequest(e,t,n,r,o,!!O)},question:S})===!1)return maybeCall(s),a;let A=n,p=getClosestAttributeValue(n,"hx-sync"),x=null,T=!1;if(p){let g=p.split(":"),E=g[0].trim();if(E==="this"?A=findThisElement(n,"hx-sync"):A=asElement(querySelectorExt(n,E)),p=(g[1]||"drop").trim(),d=getInternalData(A),p==="drop"&&d.xhr&&d.abortable!==!0)return maybeCall(s),a;if(p==="abort"){if(d.xhr)return maybeCall(s),a;T=!0;}else p==="replace"?triggerEvent(A,"htmx:abort"):p.indexOf("queue")===0&&(x=(p.split(" ")[1]||"last").trim());}if(d.xhr)if(d.abortable)triggerEvent(A,"htmx:abort");else {if(x==null){if(r){let g=getInternalData(r);g&&g.triggerSpec&&g.triggerSpec.queue&&(x=g.triggerSpec.queue);}x==null&&(x="last");}return d.queuedRequests==null&&(d.queuedRequests=[]),x==="first"&&d.queuedRequests.length===0?d.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);}):x==="all"?d.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);}):x==="last"&&(d.queuedRequests=[],d.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);})),maybeCall(s),a}let m=new XMLHttpRequest;d.xhr=m,d.abortable=T;let H=function(){d.xhr=null,d.abortable=!1,d.queuedRequests!=null&&d.queuedRequests.length>0&&d.queuedRequests.shift()();},N=getClosestAttributeValue(n,"hx-prompt");if(N){var I=prompt(N);if(I===null||!triggerEvent(n,"htmx:prompt",{prompt:I,target:c}))return maybeCall(s),H(),a}if(S&&!i&&!confirm(S))return maybeCall(s),H(),a;let R=getHeaders(n,c,I);e!=="get"&&!usesFormData(n)&&(R["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(R=mergeObjects(R,o.headers));let v=getInputValues(n,e),q=v.errors,F=v.formData;o.values&&overrideFormData(F,formDataFromObject(o.values));let _=formDataFromObject(getExpressionVars(n)),W=overrideFormData(F,_),L=filterValues(W,n);htmx.config.getCacheBusterParam&&e==="get"&&L.set("org.htmx.cache-buster",getRawAttribute(c,"id")||"true"),(t==null||t==="")&&(t=getDocument().location.href);let X=getValuesForElement(n,"hx-request"),Y=getInternalData(n).boosted,M=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,w={boosted:Y,useUrlParams:M,formData:L,parameters:formDataProxy(L),unfilteredFormData:W,unfilteredParameters:formDataProxy(W),headers:R,target:c,verb:e,errors:q,withCredentials:o.credentials||X.credentials||htmx.config.withCredentials,timeout:o.timeout||X.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",w))return maybeCall(s),H(),a;if(t=w.path,e=w.verb,R=w.headers,L=formDataFromObject(w.parameters),q=w.errors,M=w.useUrlParams,q&&q.length>0)return triggerEvent(n,"htmx:validation:halted",w),maybeCall(s),H(),a;let G=t.split("#"),ee=G[0],j=G[1],D=t;if(M&&(D=ee,!L.keys().next().done&&(D.indexOf("?")<0?D+="?":D+="&",D+=urlEncode(L),j&&(D+="#"+j))),!verifyPath(n,D,w))return triggerErrorEvent(n,"htmx:invalidPath",w),maybeCall(l),a;if(m.open(e.toUpperCase(),D,!0),m.overrideMimeType("text/html"),m.withCredentials=w.withCredentials,m.timeout=w.timeout,!X.noHeaders){for(let g in R)if(R.hasOwnProperty(g)){let E=R[g];safelySetHeaderValue(m,g,E);}}let y={xhr:m,target:c,requestConfig:w,etc:o,boosted:Y,select:f,pathInfo:{requestPath:t,finalRequestPath:D,responsePath:null,anchor:j}};if(m.onload=function(){try{let g=hierarchyForElt(n);if(y.pathInfo.responsePath=getPathFromResponse(m),u(n,y),y.keepIndicators!==!0&&removeRequestIndicators(V,k),triggerEvent(n,"htmx:afterRequest",y),triggerEvent(n,"htmx:afterOnLoad",y),!bodyContains(n)){let E=null;for(;g.length>0&&E==null;){let O=g.shift();bodyContains(O)&&(E=O);}E&&(triggerEvent(E,"htmx:afterRequest",y),triggerEvent(E,"htmx:afterOnLoad",y));}maybeCall(s),H();}catch(g){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:g},y)),g}},m.onerror=function(){removeRequestIndicators(V,k),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:sendError",y),maybeCall(l),H();},m.onabort=function(){removeRequestIndicators(V,k),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:sendAbort",y),maybeCall(l),H();},m.ontimeout=function(){removeRequestIndicators(V,k),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:timeout",y),maybeCall(l),H();},!triggerEvent(n,"htmx:beforeRequest",y))return maybeCall(s),H(),a;var V=addRequestIndicatorClasses(n),k=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(g){forEach([m,m.upload],function(E){E.addEventListener(g,function(O){triggerEvent(n,"htmx:xhr:"+g,{lengthComputable:O.lengthComputable,loaded:O.loaded,total:O.total});});});}),triggerEvent(n,"htmx:beforeSend",y);let te=M?null:encodeParamsForBody(m,n,L);return m.send(te),a}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let i=t.pathInfo.finalRequestPath,s=t.pathInfo.responsePath,l=getClosestAttributeValue(e,"hx-push-url"),a=getClosestAttributeValue(e,"hx-replace-url"),u=getInternalData(e).boosted,f=null,c=null;return l?(f="push",c=l):a?(f="replace",c=a):u&&(f="push",c=s||i),c?c==="false"?{}:(c==="true"&&(c=s||i),t.pathInfo.anchor&&c.indexOf("#")===-1&&(c=c+"#"+t.pathInfo.anchor),{type:f,path:c}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t0?getWindow().setTimeout(I,x.swapDelay):I();}c&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.status+" from "+t.pathInfo.requestPath},t));}}let extensions={};function extensionBase(){return {init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return !0},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return !1},handleSwap:function(e,t,n,r){return !1},encodeParameters:function(e,t,n){return null}}}function defineExtension(e,t){t.init&&t.init(internalAPI),extensions[e]=mergeObjects(extensionBase(),t);}function removeExtension(e){delete extensions[e];}function getExtensions(e,t,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let i=extensions[o];i&&t.indexOf(i)<0&&t.push(i);}}),getExtensions(asElement(parentElt(e)),t,n)}var isReady=!1;getDocument().addEventListener("DOMContentLoaded",function(){isReady=!0;});function ready(e){isReady||getDocument().readyState==="complete"?e():getDocument().addEventListener("DOMContentLoaded",e);}function insertIndicatorStyles(){if(htmx.config.includeIndicatorStyles!==!1){let e=htmx.config.inlineStyleNonce?` nonce="${htmx.config.inlineStyleNonce}"`:"";getDocument().head.insertAdjacentHTML("beforeend","");}}function getMetaConfig(){let e=getDocument().querySelector('meta[name="htmx-config"]');return e?parseJSON(e.content):null}function mergeMetaConfig(){let e=getMetaConfig();e&&(htmx.config=mergeObjects(htmx.config,e));}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let e=getDocument().body;processNode(e);let t=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(r){let o=r.target,i=getInternalData(o);i&&i.xhr&&i.xhr.abort();});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent});})):n&&n(r);},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null;},0);}),htmx}(),h=ne;function re(e,t){if(e==="ignore")return !1;let n=e.split("/"),r=t.split("/");for(let o=0;o{let s=ie(t).replace("htmx:","hx-on::");r.has(o)||(o.hasAttribute(s)&&setTimeout(()=>{let l=ae(s.replace("hx-on::","htmx:"),{...n.detail,target:o});l.detail.meta="trigger-children",o.dispatchEvent(l),r.add(o);},1),o.children&&$(o,t,n,r));});}h.defineExtension("trigger-children",{onEvent:(e,t)=>{if(!(t instanceof CustomEvent)||t.detail.meta==="trigger-children")return !1;let n=new Set,r=t.target||t.detail.target;return $(r,e,t,n),!0}});h.defineExtension("debug",{onEvent:function(e,t){console.debug?console.debug(e,t):console&&console.log("DEBUG:",e,t);}});var C=h.config,B,le="hx-target-";function Q(e,t){return e.substring(0,t.length)===t}function ue(e,t){if(!e||!t)return null;let n=t.toString(),r=[n,n.substr(0,2)+"*",n.substr(0,2)+"x",n.substr(0,1)+"*",n.substr(0,1)+"x",n.substr(0,1)+"**",n.substr(0,1)+"xx","*","x","***","xxx"];(Q(n,"4")||Q(n,"5"))&&r.push("error");for(let o=0;o{B=e,C.responseTargetUnsetsError===void 0&&(C.responseTargetUnsetsError=!0),C.responseTargetSetsError===void 0&&(C.responseTargetSetsError=!1),C.responseTargetPrefersExisting===void 0&&(C.responseTargetPrefersExisting=!1),C.responseTargetPrefersRetargetHeader===void 0&&(C.responseTargetPrefersRetargetHeader=!0);},onEvent:(e,t)=>{if(!(t instanceof CustomEvent))return !1;if(e==="htmx:beforeSwap"&&t.detail.xhr&&t.detail.xhr.status!==200){if(t.detail.target&&(C.responseTargetPrefersExisting||C.responseTargetPrefersRetargetHeader&&t.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)))return t.detail.shouldSwap=!0,z(t),!0;if(!t.detail.requestConfig)return !0;let n=ue(t.detail.requestConfig.elt,t.detail.xhr.status);return n&&(z(t),t.detail.shouldSwap=!0,t.detail.target=n),!0}}});h.defineExtension("mutation-error",{onEvent:(e,t)=>{if(!(t instanceof CustomEvent))return !1;if(e==="htmx:afterRequest"){if(!t.detail||!t.detail.xhr)return;let n=t.detail.xhr.status;n>=400&&document.querySelectorAll("*").forEach(r=>{r.hasAttribute("hx-on::on-mutation-error")&&h.trigger(r,"htmx:on-mutation-error",{status:n});});}}});var U="";h.defineExtension("livereload",{init:function(){let e=!1;for(let n of Array.from(h.findAll("[hx-ext]")))if(n.getAttribute("hx-ext")?.split(" ").includes("livereload")){e=!0;break}if(!e)return;console.log("livereload extension initialized.");let t=new EventSource("/dev/livereload");t.onmessage=function(n){let r=n.data;U===""&&(U=r),U!==r&&(U=r,ce());},t.onerror=function(n){console.error("EventSource error:",n);};},onEvent:function(e,t){}});function ce(){window.location.reload();}var fe=/__eval_[A-Za-z0-9]+\([a-z]+\)/gm;h.defineExtension("htmgo",{onEvent:function(e,t){e==="htmx:beforeCleanupElement"&&t.target&&J(t.target),e==="htmx:load"&&t.target&&K(t.target);}});function K(e){if(e==null||!(e instanceof HTMLElement))return;["SCRIPT","LINK","STYLE","META","BASE","TITLE","HEAD","HTML","BODY"].includes(e.tagName)||e.hasAttribute("onload")&&e.onload(new Event("load")),e.querySelectorAll("[onload]").forEach(K);}function J(e){let t=Array.from(e.attributes);for(let n of t){let r=n.value.match(fe)||[];for(let o of r){let i=o.replace("()","").replace("(this)","").replace(";",""),s=document.getElementById(i);s&&s.tagName==="SCRIPT"&&(console.debug("removing associated script with id",i),s.remove());}}}var P=null,Z=new Set;h.defineExtension("sse",{init:function(e){P=e;},onEvent:function(e,t){let n=t.target;if(n instanceof HTMLElement&&(e==="htmx:beforeCleanupElement"&&J(n),e==="htmx:beforeProcessNode")){let r=document.querySelectorAll("[sse-connect]");for(let o of Array.from(r)){let i=o.getAttribute("sse-connect");i&&!Z.has(i)&&(de(o,i),Z.add(i));}}}});function de(e,t){if(!t)return;console.info("Connecting to EventSource",t);let n=new EventSource(t);n.addEventListener("close",function(r){h.trigger(e,"htmx:sseClose",{event:r});}),n.onopen=function(r){h.trigger(e,"htmx:sseOpen",{event:r});},n.onerror=function(r){h.trigger(e,"htmx:sseError",{event:r}),n.readyState==EventSource.CLOSED&&h.trigger(e,"htmx:sseClose",{event:r});},n.onmessage=function(r){let o=P.makeSettleInfo(e);h.trigger(e,"htmx:sseBeforeMessage",{event:r});let i=r.data,s=P.makeFragment(i),l=Array.from(s.children);for(let a of l)P.oobSwap(P.getAttributeValue(a,"hx-swap-oob")||"true",a,o),a.tagName==="SCRIPT"&&a.id.startsWith("__eval")&&document.body.appendChild(a);h.trigger(e,"htmx:sseAfterMessage",{event:r});};}window.htmx=h;function he(e){let t=window.location.href;setInterval(()=>{window.location.href!==t&&(e(t,window.location.href),t=window.location.href);},101);}he((e,t)=>{ge(t);});function ge(e){let t=new URL(e);document.querySelectorAll("[hx-trigger]").forEach(function(n){let r=n.getAttribute("hx-trigger");if(!r)return;if(r.split(", ").find(i=>i==="url"))h.swap(n,"url",{swapStyle:"outerHTML",swapDelay:0,settleDelay:0});else for(let[i,s]of t.searchParams){let l="qs:"+i;if(r.includes(l)){console.log("triggering",l),h.trigger(n,l,null);break}}}),document.querySelectorAll("[hx-match-qp]").forEach(n=>{let r=!1;for(let o of n.getAttributeNames())if(o.startsWith("hx-match-qp-mapping:")){let i=o.replace("hx-match-qp-mapping:","");if(t.searchParams.get(i)){h.swap(n,n.getAttribute(o)??"",{swapStyle:"innerHTML",swapDelay:0,settleDelay:0}),r=!0;break}}if(!r){let o=n.getAttribute("hx-match-qp-default");o&&h.swap(n,n.getAttribute("hx-match-qp-mapping:"+o)??"",{swapStyle:"innerHTML",swapDelay:0,settleDelay:0});}});}document.addEventListener("htmx:beforeSwap",function(e){e instanceof CustomEvent&&(e.detail.xhr.status===422||e.detail.xhr.status===400)&&(e.detail.shouldSwap=!0,e.detail.isError=!1);});
+var le=function(){let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){return getInputValues(e,t||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0},parseInterval:null,_:null,version:"2.0.2"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(e){return "[hx-"+e+"], [data-hx-"+e+"]"}).join(", "),HEAD_TAG_REGEX=makeTagRegEx("head");function makeTagRegEx(e,t=!1){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function parseInterval(e){if(e==null)return;let t=NaN;return e.slice(-2)=="ms"?t=parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?t=parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?t=parseFloat(e.slice(0,-1))*1e3*60:t=parseFloat(e),isNaN(t)?void 0:t}function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)}function hasAttribute(e,t){return !!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)}function parentElt(e){let t=e.parentElement;return !t&&e.parentNode instanceof ShadowRoot?e.parentNode:t}function getDocument(){return document}function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()}function getClosestMatch(e,t){for(;e&&!t(e);)e=parentElt(e);return e||null}function getAttributeValueWithDisinheritance(e,t,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var i=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return i&&(i==="*"||i.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return "unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return !!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){let n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return !!n&&n.call(e,t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[1].toLowerCase():""}function parseHTML(e){return new DOMParser().parseFromString(e,"text/html")}function takeChildrenFor(e,t){for(;t.childNodes.length>0;)e.append(t.childNodes[0]);}function duplicateScript(e){let t=getDocument().createElement("script");return forEach(e.attributes,function(n){t.setAttribute(n.name,n.value);}),t.textContent=e.textContent,t.async=!1,htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce),t}function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function normalizeScriptTags(e){Array.from(e.querySelectorAll("script")).forEach(t=>{if(isJavaScriptScriptNode(t)){let n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t);}catch(o){logError(o);}finally{t.remove();}}});}function makeFragment(e){let t=e.replace(HEAD_TAG_REGEX,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let i=parseHTML(e);takeChildrenFor(r,i.body),r.title=i.title;}else if(n==="body"){r=new DocumentFragment;let i=parseHTML(t);takeChildrenFor(r,i.body),r.title=i.title;}else {let i=parseHTML(''+t+"");r=i.querySelector("template").content,r.title=i.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText);}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(i=>i.remove())),r}function maybeCall(e){e&&e();}function isType(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function isFunction(e){return typeof e=="function"}function isRawObject(e){return isType(e,"Object")}function getInternalData(e){let t="htmx-internal-data",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n=0}function bodyContains(e){let t=e.getRootNode&&e.getRootNode();return t&&t instanceof window.ShadowRoot?getDocument().body.contains(t.host):getDocument().body.contains(e)}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function parseJSON(e){try{return JSON.parse(e)}catch(t){return logError(t),null}}function canAccessLocalStorage(){let e="htmx:localStorageTest";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch{return !1}}function normalizePath(e){try{let t=new URL(e);return t&&(e=t.pathname+t.search),/^\/$/.test(e)||(e=e.replace(/\/+$/,"")),e}catch{return e}}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(e){return htmx.on("htmx:load",function(n){e(n.detail.elt);})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n);};}function logNone(){htmx.logger=null;}function find(e,t){return typeof e!="string"?e.querySelector(t):find(getDocument(),e)}function findAll(e,t){return typeof e!="string"?e.querySelectorAll(t):findAll(getDocument(),e)}function getWindow(){return window}function removeElement(e,t){e=resolveTarget(e),t?getWindow().setTimeout(function(){removeElement(e),e=null;},t):parentElt(e).removeChild(e);}function asElement(e){return e instanceof Element?e:null}function asHtmlElement(e){return e instanceof HTMLElement?e:null}function asString(e){return typeof e=="string"?e:null}function asParentNode(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function addClassToElement(e,t,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null;},n):e.classList&&e.classList.add(t));}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null;},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.removeAttribute("class")));}function toggleClassOnElement(e,t){e=resolveTarget(e),e.classList.toggle(t);}function takeClassForElement(e,t){e=resolveTarget(e),forEach(e.parentElement.children,function(n){removeClassFromElement(n,t);}),addClassToElement(asElement(e),t);}function closest(e,t){if(e=asElement(resolveTarget(e)),e&&e.closest)return e.closest(t);do if(e==null||matches(e,t))return e;while(e=e&&asElement(parentElt(e)));return null}function startsWith(e,t){return e.substring(0,t.length)===t}function endsWith(e,t){return e.substring(e.length-t.length)===t}function normalizeSelector(e){let t=e.trim();return startsWith(t,"<")&&endsWith(t,"/>")?t.substring(1,t.length-2):t}function querySelectorAllExt(e,t,n){return e=resolveTarget(e),t.indexOf("closest ")===0?[closest(asElement(e),normalizeSelector(t.substr(8)))]:t.indexOf("find ")===0?[find(asParentNode(e),normalizeSelector(t.substr(5)))]:t==="next"?[asElement(e).nextElementSibling]:t.indexOf("next ")===0?[scanForwardQuery(e,normalizeSelector(t.substr(5)),!!n)]:t==="previous"?[asElement(e).previousElementSibling]:t.indexOf("previous ")===0?[scanBackwardsQuery(e,normalizeSelector(t.substr(9)),!!n)]:t==="document"?[document]:t==="window"?[window]:t==="body"?[document.body]:t==="root"?[getRootNode(e,!!n)]:t.indexOf("global ")===0?querySelectorAllExt(e,t.slice(7),!0):toArray(asParentNode(getRootNode(e,!!n)).querySelectorAll(normalizeSelector(t)))}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o=0;o--){let i=r[o];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}};function querySelectorExt(e,t){return typeof e!="string"?querySelectorAllExt(e,t)[0]:querySelectorAllExt(getDocument().body,e)[0]}function resolveTarget(e,t){return typeof e=="string"?find(asParentNode(t)||document,e):e}function processEventArgs(e,t,n){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t}:{target:resolveTarget(e),event:asString(t),listener:n}}function addEventListenerImpl(e,t,n){return ready(function(){let o=processEventArgs(e,t,n);o.target.addEventListener(o.event,o.listener);}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener);}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return [findThisElement(e,t)];{let r=querySelectorAllExt(e,n);return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),t)!=null}))}function getTarget(e){let t=getClosestAttributeValue(e,"hx-target");return t?t==="this"?findThisElement(e,"hx-target"):querySelectorExt(e,t):getInternalData(e).boosted?getDocument().body:e}function shouldSettleAttribute(e){let t=htmx.config.attributesToSettle;for(let n=0;n0?(o=e.substr(0,e.indexOf(":")),r=e.substr(e.indexOf(":")+1,e.length)):o=e);let i=getDocument().querySelectorAll(r);return i?(forEach(i,function(s){let l,a=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(a),isInlineSwap(o,s)||(l=asParentNode(a));let u={shouldSwap:!0,target:s,fragment:l};triggerEvent(s,"htmx:oobBeforeSwap",u)&&(s=u.target,u.shouldSwap&&swapWithStyle(o,s,s,l,n),forEach(n.elts,function(d){triggerEvent(d,"htmx:oobAfterSwap",u);}));}),t.parentNode.removeChild(t)):(t.parentNode.removeChild(t),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:t})),e}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);r!=null&&t.parentNode.replaceChild(r,t);});}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let i=o.replace("'","\\'"),s=r.tagName.replace(":","\\:"),l=asParentNode(e),a=l&&l.querySelector(s+"[id='"+i+"']");if(a&&a!==l){let u=r.cloneNode();cloneAttributes(r,a),n.tasks.push(function(){cloneAttributes(r,u);});}}});}function makeAjaxLoadTask(e){return function(){removeClassFromElement(e,htmx.config.addedClass),processNode(asElement(e)),processFocus(asParentNode(e)),triggerEvent(e,"htmx:load");}}function processFocus(e){let t="[autofocus]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus();}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o));}}function stringHash(e,t){let n=0;for(;n0}function swap(e,t,n,r){r||(r={}),e=resolveTarget(e);let o=document.activeElement,i={};try{i={elt:o,start:o?o.selectionStart:null,end:o?o.selectionEnd:null};}catch{}let s=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else {let a=makeFragment(t);if(s.title=a.title,r.selectOOB){let u=r.selectOOB.split(",");for(let d=0;d0?getWindow().setTimeout(l,n.settleDelay):l();}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let i in o)if(o.hasOwnProperty(i)){let s=o[i];isRawObject(s)?n=s.target!==void 0?s.target:n:s={value:s},triggerEvent(n,i,s);}}else {let o=r.split(",");for(let i=0;i0;){let s=t[0];if(s==="]"){if(r--,r===0){i===null&&(o=o+"true"),t.shift(),o+=")})";try{let l=maybeEval(e,function(){return Function(o)()},function(){return !0});return l.source=o,l}catch(l){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:l,source:o}),null}}}else s==="["&&r++;isPossibleRelativeReference(s,i,n)?o+="(("+n+"."+s+") ? ("+n+"."+s+") : (window."+s+"))":o=o+s,i=t.shift();}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}function consumeCSSSelector(e){let t;return e.length>0&&COMBINED_SELECTOR_START.test(e[0])?(e.shift(),t=consumeUntil(e,COMBINED_SELECTOR_END).trim(),e.shift()):t=consumeUntil(e,WHITESPACE_OR_COMMA),t}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(e,t,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let l=o.length,a=consumeUntil(o,/[,\[\s]/);if(a!=="")if(a==="every"){let u={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),u.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var i=maybeGenerateConditional(e,o,"event");i&&(u.eventFilter=i),r.push(u);}else {let u={trigger:a};var i=maybeGenerateConditional(e,o,"event");for(i&&(u.eventFilter=i);o.length>0&&o[0]!==",";){consumeUntil(o,NOT_WHITESPACE);let c=o.shift();if(c==="changed")u.changed=!0;else if(c==="once")u.once=!0;else if(c==="consume")u.consume=!0;else if(c==="delay"&&o[0]===":")o.shift(),u.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(c==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var s=consumeCSSSelector(o);else {var s=consumeUntil(o,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();let b=consumeCSSSelector(o);b.length>0&&(s+=" "+b);}}u.from=s;}else c==="target"&&o[0]===":"?(o.shift(),u.target=consumeCSSSelector(o)):c==="throttle"&&o[0]===":"?(o.shift(),u.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):c==="queue"&&o[0]===":"?(o.shift(),u.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):c==="root"&&o[0]===":"?(o.shift(),u[c]=consumeCSSSelector(o)):c==="threshold"&&o[0]===":"?(o.shift(),u[c]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});}r.push(u);}o.length===l&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE);}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r);}return n.length>0?n:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0;}function processPolling(e,t,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n));},n.pollInterval);}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else {let i=getRawAttribute(e,"method");r=i?i.toLowerCase():"get",o=getRawAttribute(e,"action");}n.forEach(function(i){addEventListener(e,function(s,l){let a=asElement(s);if(eltIsDisabled(a)){cleanUpElement(a);return}issueAjaxRequest(r,o,a,l);},t,i,!0);});}}function shouldCancel(e,t){let n=asElement(t);return n?!!((e.type==="submit"||e.type==="click")&&(n.tagName==="FORM"||matches(n,'input[type="submit"], button')&&closest(n,"form")!==null||n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0))):!1}function ignoreBoostedAnchorCtrlClick(e,t){return getInternalData(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function maybeFilterEvent(e,t,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let i=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:i}),!0}return !1}function addEventListener(e,t,n,r,o){let i=getInternalData(e),s;r.from?s=querySelectorAllExt(e,r.from):s=[e],r.changed&&s.forEach(function(l){let a=getInternalData(l);a.lastValue=l.value;}),forEach(s,function(l){let a=function(u){if(!bodyContains(e)){l.removeEventListener(r.trigger,a);return}if(ignoreBoostedAnchorCtrlClick(e,u)||((o||shouldCancel(u,e))&&u.preventDefault(),maybeFilterEvent(r,e,u)))return;let d=getInternalData(u);if(d.triggerSpec=r,d.handledFor==null&&(d.handledFor=[]),d.handledFor.indexOf(e)<0){if(d.handledFor.push(e),r.consume&&u.stopPropagation(),r.target&&u.target&&!matches(asElement(u.target),r.target))return;if(r.once){if(i.triggeredOnce)return;i.triggeredOnce=!0;}if(r.changed){let c=getInternalData(l),h=l.value;if(c.lastValue===h)return;c.lastValue=h;}if(i.delayed&&clearTimeout(i.delayed),i.throttle)return;r.throttle>0?i.throttle||(triggerEvent(e,"htmx:trigger"),t(e,u),i.throttle=getWindow().setTimeout(function(){i.throttle=null;},r.throttle)):r.delay>0?i.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,u);},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,u));}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:a,on:l}),l.addEventListener(r.trigger,a);});}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0;},window.addEventListener("scroll",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){maybeReveal(e);}));},200));}function maybeReveal(e){!hasAttribute(e,"data-hx-revealed")&&isScrolledIntoView(e)&&(e.setAttribute("data-hx-revealed","true"),getInternalData(e).initHash?triggerEvent(e,"revealed"):e.addEventListener("htmx:afterProcessNode",function(){triggerEvent(e,"revealed");},{once:!0}));}function loadImmediately(e,t,n,r){let o=function(){n.loaded||(n.loaded=!0,t(e));};r>0?getWindow().setTimeout(o,r):o();}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let i=getAttributeValue(e,"hx-"+o);r=!0,t.path=i,t.verb=o,n.forEach(function(s){addTriggerHandler(e,s,t,function(l,a){let u=asElement(l);if(closest(u,htmx.config.disableSelector)){cleanUpElement(u);return}issueAjaxRequest(o,i,u,a);});});}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,t),maybeReveal(asElement(e));else if(t.trigger==="intersect"){let o={};t.root&&(o.root=querySelectorExt(e,t.root)),t.threshold&&(o.threshold=parseFloat(t.threshold)),new IntersectionObserver(function(s){for(let l=0;l0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t);}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return !1;let n=t.attributes;for(let r=0;r", "+i).join(""))}else return []}function maybeSetLastButtonClicked(e){let t=closest(asElement(e.target),"button, input[type='submit']"),n=getRelatedFormData(e);n&&(n.lastButtonClicked=t);}function maybeUnsetLastButtonClicked(e){let t=getRelatedFormData(e);t&&(t.lastButtonClicked=null);}function getRelatedFormData(e){let t=closest(asElement(e.target),"button, input[type='submit']");if(!t)return;let n=resolveTarget("#"+getRawAttribute(t,"form"),t.getRootNode())||closest(t,"form");if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked);}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,i=function(s){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,s));});};e.addEventListener(t,i),r.onHandlers.push({event:t,listener:i});}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;thtmx.config.historyCacheSize;)i.shift();for(;i.length>0;)try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(l){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:l,cache:i}),i.shift();}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(localStorage.getItem("htmx-history-cache"))||[];for(let n=0;n=200&&this.status<400){triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",n);let r=makeFragment(this.response),o=r.querySelector("[hx-history-elt],[data-hx-history-elt]")||r,i=getHistoryElement(),s=makeSettleInfo(i);handleTitle(r.title),swapInnerHTML(i,o,s),settleImmediately(s.tasks),currentPathForHistory=e,triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:this.response});}else triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",n);},t.send();}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n=makeFragment(t.content),r=getHistoryElement(),o=makeSettleInfo(r);handleTitle(n.title),swapInnerHTML(r,n,o),settleImmediately(o.tasks),getWindow().setTimeout(function(){window.scrollTo(0,t.scroll);},0),currentPathForHistory=e,triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,item:t});}else htmx.config.refreshOnHistoryMiss?window.location.reload(!0):loadHistoryFromServer(e);}function addRequestIndicatorClasses(e){let t=findAttributeTargets(e,"hx-indicator");return t==null&&(t=[e]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass);}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","");}),t}function removeRequestIndicators(e,t){forEach(e,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)-1,r.requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass);}),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)-1,r.requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"));});}function haveSeenNode(e,t){for(let n=0;nt.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.append(e,o));}}function processInputValue(e,t,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let i=getRawAttribute(r,"name"),s=r.value;r instanceof HTMLSelectElement&&r.multiple&&(s=toArray(r.querySelectorAll("option:checked")).map(function(l){return l.value})),r instanceof HTMLInputElement&&r.files&&(s=toArray(r.files)),addValueToFormData(i,s,t),o&&validateElement(r,n);}r instanceof HTMLFormElement&&(forEach(r.elements,function(i){e.indexOf(i)>=0?removeValueFromFormData(i.name,i.value,t):e.push(i),o&&validateElement(i,n);}),new FormData(r).forEach(function(i,s){i instanceof File&&i.name===""||addValueToFormData(s,i,t);}));}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(t.push({elt:n,message:n.validationMessage,validity:n.validity}),triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})));}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n);}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,i=[],s=getInternalData(e);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let l=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(s.lastButtonClicked&&(l=l&&s.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,i,closest(e,"form"),l),processInputValue(n,r,i,e,l),s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let u=s.lastButtonClicked||e,d=getRawAttribute(u,"name");addValueToFormData(d,u.value,o);}let a=findAttributeTargets(e,"hx-include");return forEach(a,function(u){processInputValue(n,r,i,asElement(u),l),matches(u,"form")||forEach(asParentNode(u).querySelectorAll(INPUT_SELECTOR),function(d){processInputValue(n,r,i,d,l);});}),overrideFormData(r,o),{errors:i,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n);}),t}function getHeaders(e,t,n){let r={"HX-Request":"true","HX-Trigger":getRawAttribute(e,"id"),"HX-Trigger-Name":getRawAttribute(e,"name"),"HX-Target":getAttributeValue(t,"id"),"HX-Current-URL":getDocument().location.href};return getValuesForElement(e,"hx-headers",!1,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.substr(4).split(","),function(r){r=r.trim(),e.delete(r);}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(i){r.append(o,i);});}),r}}else return e}function isAnchorLink(e){return !!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={swapStyle:getInternalData(e).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(e).boosted&&!isAnchorLink(e)&&(r.show="top"),n){let s=splitOnWhitespace(n);if(s.length>0)for(let l=0;l0?o.join(":"):null;r.scroll=d,r.scrollTarget=i;}else if(a.indexOf("show:")===0){var o=a.substr(5).split(":");let c=o.pop();var i=o.length>0?o.join(":"):null;r.show=c,r.showTarget=i;}else if(a.indexOf("focus-scroll:")===0){let u=a.substr(13);r.focusScroll=u=="true";}else l==0?r.swapStyle=a:logError("Unknown modifier in hx-swap: "+a);}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t));}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return {tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,o.scrollTop=o.scrollHeight);}if(t.show){var o=null;if(t.showTarget){let s=t.showTarget;t.showTarget==="window"&&(s="body"),o=asElement(querySelectorExt(n,s));}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}));}}function getValuesForElement(e,t,n,r){if(r==null&&(r={}),e==null)return r;let o=getAttributeValue(e,t);if(o){let i=o.trim(),s=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.substr(11),s=!0):i.indexOf("js:")===0&&(i=i.substr(3),s=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let l;s?l=maybeEval(e,function(){return Function("return ("+i+")")()},{}):l=parseJSON(i);for(let a in l)l.hasOwnProperty(a)&&r[a]==null&&(r[a]=l[a]);}return getValuesForElement(asElement(parentElt(e)),t,n,r)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t){return getValuesForElement(e,"hx-vars",!0,t)}function getHXValsForElement(e,t){return getValuesForElement(e,"hx-vals",!1,t)}function getExpressionVars(e){return mergeObjects(getHXVarsForElement(e),getHXValsForElement(e))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n);}catch{e.setRequestHeader(t,encodeURIComponent(n)),e.setRequestHeader(t+"-URI-AutoEncoded","true");}}function getPathFromResponse(e){if(e.responseURL&&typeof URL<"u")try{let t=new URL(e.responseURL);return t.pathname+t.search}catch{triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:e.responseURL});}}function hasHeader(e,t){return t.test(e.getAllResponseHeaders())}function ajaxHelper(e,t,n){return e=e.toLowerCase(),n?n instanceof Element||typeof n=="string"?issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n),returnPromise:!0}):issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:resolveTarget(n.target),swapOverride:n.swap,select:n.select,returnPromise:!0}):issueAjaxRequest(e,t,null,null,{returnPromise:!0})}function hierarchyForElt(e){let t=[];for(;e;)t.push(e),e=e.parentElement;return t}function verifyPath(e,t,n){let r,o;return typeof URL=="function"?(o=new URL(t,document.location.href),r=document.location.origin===o.origin):(o=t,r=startsWith(t,document.location.origin)),htmx.config.selfRequestsOnly&&!r?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:o,sameHost:r},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r);}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(i){r.push(i),e.append(t,i);}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(i){e.append(t,i);});}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,i){return r[o]=i,e.delete(t),r.forEach(function(s){e.append(t,s);}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol")return Reflect.get(t,n);if(n==="toJSON")return ()=>Object.fromEntries(e);if(n in t)return typeof t[n]=="function"?function(){return e[n].apply(e,arguments)}:t[n];let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o);}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,i){let s=null,l=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var a=new Promise(function(g,E){s=g,l=E;});n==null&&(n=getDocument().body);let u=o.handler||handleAjaxResponse,d=o.select||null;if(!bodyContains(n))return maybeCall(s),a;let c=o.targetOverride||asElement(getTarget(n));if(c==null||c==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getAttributeValue(n,"hx-target")}),maybeCall(l),a;let h=getInternalData(n),b=h.lastButtonClicked;if(b){let g=getRawAttribute(b,"formaction");g!=null&&(t=g);let E=getRawAttribute(b,"formmethod");E!=null&&E.toLowerCase()!=="dialog"&&(e=E);}let S=getClosestAttributeValue(n,"hx-confirm");if(i===void 0&&triggerEvent(n,"htmx:confirm",{target:c,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(L){return issueAjaxRequest(e,t,n,r,o,!!L)},question:S})===!1)return maybeCall(s),a;let C=n,p=getClosestAttributeValue(n,"hx-sync"),x=null,T=!1;if(p){let g=p.split(":"),E=g[0].trim();if(E==="this"?C=findThisElement(n,"hx-sync"):C=asElement(querySelectorExt(n,E)),p=(g[1]||"drop").trim(),h=getInternalData(C),p==="drop"&&h.xhr&&h.abortable!==!0)return maybeCall(s),a;if(p==="abort"){if(h.xhr)return maybeCall(s),a;T=!0;}else p==="replace"?triggerEvent(C,"htmx:abort"):p.indexOf("queue")===0&&(x=(p.split(" ")[1]||"last").trim());}if(h.xhr)if(h.abortable)triggerEvent(C,"htmx:abort");else {if(x==null){if(r){let g=getInternalData(r);g&&g.triggerSpec&&g.triggerSpec.queue&&(x=g.triggerSpec.queue);}x==null&&(x="last");}return h.queuedRequests==null&&(h.queuedRequests=[]),x==="first"&&h.queuedRequests.length===0?h.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);}):x==="all"?h.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);}):x==="last"&&(h.queuedRequests=[],h.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o);})),maybeCall(s),a}let m=new XMLHttpRequest;h.xhr=m,h.abortable=T;let H=function(){h.xhr=null,h.abortable=!1,h.queuedRequests!=null&&h.queuedRequests.length>0&&h.queuedRequests.shift()();},k=getClosestAttributeValue(n,"hx-prompt");if(k){var I=prompt(k);if(I===null||!triggerEvent(n,"htmx:prompt",{prompt:I,target:c}))return maybeCall(s),H(),a}if(S&&!i&&!confirm(S))return maybeCall(s),H(),a;let R=getHeaders(n,c,I);e!=="get"&&!usesFormData(n)&&(R["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(R=mergeObjects(R,o.headers));let v=getInputValues(n,e),P=v.errors,V=v.formData;o.values&&overrideFormData(V,formDataFromObject(o.values));let j=formDataFromObject(getExpressionVars(n)),z=overrideFormData(V,j),q=filterValues(z,n);htmx.config.getCacheBusterParam&&e==="get"&&q.set("org.htmx.cache-buster",getRawAttribute(c,"id")||"true"),(t==null||t==="")&&(t=getDocument().location.href);let J=getValuesForElement(n,"hx-request"),$=getInternalData(n).boosted,B=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,w={boosted:$,useUrlParams:B,formData:q,parameters:formDataProxy(q),unfilteredFormData:z,unfilteredParameters:formDataProxy(z),headers:R,target:c,verb:e,errors:P,withCredentials:o.credentials||J.credentials||htmx.config.withCredentials,timeout:o.timeout||J.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",w))return maybeCall(s),H(),a;if(t=w.path,e=w.verb,R=w.headers,q=formDataFromObject(w.parameters),P=w.errors,B=w.useUrlParams,P&&P.length>0)return triggerEvent(n,"htmx:validation:halted",w),maybeCall(s),H(),a;let Q=t.split("#"),se=Q[0],Y=Q[1],O=t;if(B&&(O=se,!q.keys().next().done&&(O.indexOf("?")<0?O+="?":O+="&",O+=urlEncode(q),Y&&(O+="#"+Y))),!verifyPath(n,O,w))return triggerErrorEvent(n,"htmx:invalidPath",w),maybeCall(l),a;if(m.open(e.toUpperCase(),O,!0),m.overrideMimeType("text/html"),m.withCredentials=w.withCredentials,m.timeout=w.timeout,!J.noHeaders){for(let g in R)if(R.hasOwnProperty(g)){let E=R[g];safelySetHeaderValue(m,g,E);}}let y={xhr:m,target:c,requestConfig:w,etc:o,boosted:$,select:d,pathInfo:{requestPath:t,finalRequestPath:O,responsePath:null,anchor:Y}};if(m.onload=function(){try{let g=hierarchyForElt(n);if(y.pathInfo.responsePath=getPathFromResponse(m),u(n,y),y.keepIndicators!==!0&&removeRequestIndicators(U,_),triggerEvent(n,"htmx:afterRequest",y),triggerEvent(n,"htmx:afterOnLoad",y),!bodyContains(n)){let E=null;for(;g.length>0&&E==null;){let L=g.shift();bodyContains(L)&&(E=L);}E&&(triggerEvent(E,"htmx:afterRequest",y),triggerEvent(E,"htmx:afterOnLoad",y));}maybeCall(s),H();}catch(g){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:g},y)),g}},m.onerror=function(){removeRequestIndicators(U,_),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:sendError",y),maybeCall(l),H();},m.onabort=function(){removeRequestIndicators(U,_),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:sendAbort",y),maybeCall(l),H();},m.ontimeout=function(){removeRequestIndicators(U,_),triggerErrorEvent(n,"htmx:afterRequest",y),triggerErrorEvent(n,"htmx:timeout",y),maybeCall(l),H();},!triggerEvent(n,"htmx:beforeRequest",y))return maybeCall(s),H(),a;var U=addRequestIndicatorClasses(n),_=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(g){forEach([m,m.upload],function(E){E.addEventListener(g,function(L){triggerEvent(n,"htmx:xhr:"+g,{lengthComputable:L.lengthComputable,loaded:L.loaded,total:L.total});});});}),triggerEvent(n,"htmx:beforeSend",y);let ae=B?null:encodeParamsForBody(m,n,q);return m.send(ae),a}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let i=t.pathInfo.finalRequestPath,s=t.pathInfo.responsePath,l=getClosestAttributeValue(e,"hx-push-url"),a=getClosestAttributeValue(e,"hx-replace-url"),u=getInternalData(e).boosted,d=null,c=null;return l?(d="push",c=l):a?(d="replace",c=a):u&&(d="push",c=s||i),c?c==="false"?{}:(c==="true"&&(c=s||i),t.pathInfo.anchor&&c.indexOf("#")===-1&&(c=c+"#"+t.pathInfo.anchor),{type:d,path:c}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t0?getWindow().setTimeout(I,x.swapDelay):I();}c&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.status+" from "+t.pathInfo.requestPath},t));}}let extensions={};function extensionBase(){return {init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return !0},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return !1},handleSwap:function(e,t,n,r){return !1},encodeParameters:function(e,t,n){return null}}}function defineExtension(e,t){t.init&&t.init(internalAPI),extensions[e]=mergeObjects(extensionBase(),t);}function removeExtension(e){delete extensions[e];}function getExtensions(e,t,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let i=extensions[o];i&&t.indexOf(i)<0&&t.push(i);}}),getExtensions(asElement(parentElt(e)),t,n)}var isReady=!1;getDocument().addEventListener("DOMContentLoaded",function(){isReady=!0;});function ready(e){isReady||getDocument().readyState==="complete"?e():getDocument().addEventListener("DOMContentLoaded",e);}function insertIndicatorStyles(){if(htmx.config.includeIndicatorStyles!==!1){let e=htmx.config.inlineStyleNonce?` nonce="${htmx.config.inlineStyleNonce}"`:"";getDocument().head.insertAdjacentHTML("beforeend","");}}function getMetaConfig(){let e=getDocument().querySelector('meta[name="htmx-config"]');return e?parseJSON(e.content):null}function mergeMetaConfig(){let e=getMetaConfig();e&&(htmx.config=mergeObjects(htmx.config,e));}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let e=getDocument().body;processNode(e);let t=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(r){let o=r.target,i=getInternalData(o);i&&i.xhr&&i.xhr.abort();});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent});})):n&&n(r);},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null;},0);}),htmx}(),f=le;function ue(e,t){if(e==="ignore")return !1;let n=e.split("/"),r=t.split("/");for(let o=0;o{let s=fe(t).replace("htmx:","hx-on::");r.has(o)||(o.hasAttribute(s)&&setTimeout(()=>{let l=he(s.replace("hx-on::","htmx:"),{...n.detail,target:o});l.detail.meta="trigger-children",o.dispatchEvent(l),r.add(o);},1),o.children&&K(o,t,n,r));});}f.defineExtension("trigger-children",{onEvent:(e,t)=>{if(!(t instanceof CustomEvent)||t.detail.meta==="trigger-children")return !1;let n=new Set,r=t.target||t.detail.target;return K(r,e,t,n),!0}});f.defineExtension("debug",{onEvent:function(e,t){console.debug?console.debug(e,t):console&&console.log("DEBUG:",e,t);}});var A=f.config,W,ge="hx-target-";function Z(e,t){return e.substring(0,t.length)===t}function me(e,t){if(!e||!t)return null;let n=t.toString(),r=[n,n.substr(0,2)+"*",n.substr(0,2)+"x",n.substr(0,1)+"*",n.substr(0,1)+"x",n.substr(0,1)+"**",n.substr(0,1)+"xx","*","x","***","xxx"];(Z(n,"4")||Z(n,"5"))&&r.push("error");for(let o=0;o{W=e,A.responseTargetUnsetsError===void 0&&(A.responseTargetUnsetsError=!0),A.responseTargetSetsError===void 0&&(A.responseTargetSetsError=!1),A.responseTargetPrefersExisting===void 0&&(A.responseTargetPrefersExisting=!1),A.responseTargetPrefersRetargetHeader===void 0&&(A.responseTargetPrefersRetargetHeader=!0);},onEvent:(e,t)=>{if(!(t instanceof CustomEvent))return !1;if(e==="htmx:beforeSwap"&&t.detail.xhr&&t.detail.xhr.status!==200){if(t.detail.target&&(A.responseTargetPrefersExisting||A.responseTargetPrefersRetargetHeader&&t.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)))return t.detail.shouldSwap=!0,G(t),!0;if(!t.detail.requestConfig)return !0;let n=me(t.detail.requestConfig.elt,t.detail.xhr.status);return n&&(G(t),t.detail.shouldSwap=!0,t.detail.target=n),!0}}});f.defineExtension("mutation-error",{onEvent:(e,t)=>{if(!(t instanceof CustomEvent))return !1;if(e==="htmx:afterRequest"){if(!t.detail||!t.detail.xhr)return;let n=t.detail.xhr.status;n>=400&&document.querySelectorAll("*").forEach(r=>{r.hasAttribute("hx-on::on-mutation-error")&&f.trigger(r,"htmx:on-mutation-error",{status:n});});}}});var X="";f.defineExtension("livereload",{init:function(){let e=!1;for(let n of Array.from(f.findAll("[hx-ext]")))if(n.getAttribute("hx-ext")?.split(" ").includes("livereload")){e=!0;break}if(!e)return;console.log("livereload extension initialized.");let t=new EventSource("/dev/livereload");t.onmessage=function(n){let r=n.data;X===""&&(X=r),X!==r&&(X=r,Ee());},t.onerror=function(n){console.error("EventSource error:",n);};},onEvent:function(e,t){}});function Ee(){window.location.reload();}var pe=/__eval_[A-Za-z0-9]+\([a-z]+\)/gm;f.defineExtension("htmgo",{onEvent:function(e,t){e==="htmx:beforeCleanupElement"&&t.target&&N(t.target),e==="htmx:load"&&t.target&&ee(t.target);}});function ee(e){if(e==null||!(e instanceof HTMLElement))return;["SCRIPT","LINK","STYLE","META","BASE","TITLE","HEAD","HTML","BODY"].includes(e.tagName)||e.hasAttribute("onload")&&e.onload(new Event("load")),e.querySelectorAll("[onload]").forEach(ee);}function N(e){let t=Array.from(e.attributes);for(let n of t){let r=n.value.match(pe)||[];for(let o of r){let i=o.replace("()","").replace("(this)","").replace(";",""),s=document.getElementById(i);s&&s.tagName==="SCRIPT"&&(console.debug("removing associated script with id",i),s.remove());}}}var F=null,te=new Set;f.defineExtension("sse",{init:function(e){F=e;},onEvent:function(e,t){let n=t.target;if(n instanceof HTMLElement&&(e==="htmx:beforeCleanupElement"&&N(n),e==="htmx:beforeProcessNode")){let r=document.querySelectorAll("[sse-connect]");for(let o of Array.from(r)){let i=o.getAttribute("sse-connect");i&&!te.has(i)&&(xe(o,i),te.add(i));}}}});function xe(e,t){if(!t)return;console.info("Connecting to EventSource",t);let n=new EventSource(t);n.addEventListener("close",function(r){f.trigger(e,"htmx:sseClose",{event:r});}),n.onopen=function(r){f.trigger(e,"htmx:sseOpen",{event:r});},n.onerror=function(r){f.trigger(e,"htmx:sseError",{event:r}),n.readyState==EventSource.CLOSED&&f.trigger(e,"htmx:sseClose",{event:r});},n.onmessage=function(r){let o=F.makeSettleInfo(e);f.trigger(e,"htmx:sseBeforeMessage",{event:r});let i=r.data,s=F.makeFragment(i),l=Array.from(s.children);for(let a of l)F.oobSwap(F.getAttributeValue(a,"hx-swap-oob")||"true",a,o),a.tagName==="SCRIPT"&&a.id.startsWith("__eval")&&document.body.appendChild(a);f.trigger(e,"htmx:sseAfterMessage",{event:r});};}var M=null,ne=new Set,D=null;f.defineExtension("ws",{init:function(e){M=e;},onEvent:function(e,t){let n=t.target;if(n instanceof HTMLElement&&(e==="htmx:beforeCleanupElement"&&N(n),e==="htmx:beforeProcessNode")){let r=document.querySelectorAll("[ws-connect]");for(let o of Array.from(r)){let i=o.getAttribute("ws-connect");i&&!ne.has(i)&&(re(o,i),ne.add(i));}}}});function ye(e,t=100,n=1e4){let r=Math.random();return Math.min(t*Math.pow(2,e)*r,n)}function re(e,t,n=0){if(t)return !t.startsWith("ws://")&&!t.startsWith("wss://")&&(t=(window.location.protocol==="https:"?"wss://":"ws://")+window.location.host+t),console.info("connecting to ws",t),D=new WebSocket(t),D.addEventListener("close",function(r){f.trigger(e,"htmx:wsClose",{event:r});let o=ye(n);setTimeout(()=>{re(e,t,n+1);},o);}),D.addEventListener("open",function(r){f.trigger(e,"htmx:wsOpen",{event:r});}),D.addEventListener("error",function(r){f.trigger(e,"htmx:wsError",{event:r});}),D.addEventListener("message",function(r){let o=M.makeSettleInfo(e);f.trigger(e,"htmx:wsBeforeMessage",{event:r});let i=r.data,s=M.makeFragment(i),l=Array.from(s.children);for(let a of l)M.oobSwap(M.getAttributeValue(a,"hx-swap-oob")||"true",a,o),a.tagName==="SCRIPT"&&a.id.startsWith("__eval")&&document.body.appendChild(a);f.trigger(e,"htmx:wsAfterMessage",{event:r});}),D}window.onload=function(){let e=document.querySelectorAll("[hx-extension]");for(let t of Array.from(e)){let n=t.getAttribute("hx-extension");if(n!=null&&n.split(" ").includes("ws")){be();break}}};function oe(e){D!=null&&D.readyState===WebSocket.OPEN&&D.send(JSON.stringify(e));}function ie(e,t){t(e);for(let n of Array.from(e.childNodes))ie(n,t);}function be(){new MutationObserver(n).observe(document.body,{childList:!0,subtree:!0});let t=new Set;function n(r){for(let i of r)for(let s of Array.from(i.removedNodes))ie(s,l=>{if(l instanceof HTMLElement){let a=l.getAttribute("data-handler-id");a&&(t.delete(a),oe({id:a,event:"dom-element-removed"}));}});let o=new Set;document.querySelectorAll("[data-handler-id]").forEach(i=>{let s=i.getAttribute("data-handler-id"),l=i.getAttribute("data-handler-event");s==null||l==null||(o.add(s),!t.has(s)&&(t.add(s),i.addEventListener(l,a=>{oe({id:s,event:l});})));});for(let i of t)o.has(i)||t.delete(i);}n([]);}window.htmx=f;function ve(e){let t=window.location.href;setInterval(()=>{window.location.href!==t&&(e(t,window.location.href),t=window.location.href);},101);}ve((e,t)=>{we(t);});function we(e){let t=new URL(e);document.querySelectorAll("[hx-trigger]").forEach(function(n){let r=n.getAttribute("hx-trigger");if(!r)return;if(r.split(", ").find(i=>i==="url"))f.swap(n,"url",{swapStyle:"outerHTML",swapDelay:0,settleDelay:0});else for(let[i,s]of t.searchParams){let l="qs:"+i;if(r.includes(l)){f.trigger(n,l,null);break}}}),document.querySelectorAll("[hx-match-qp]").forEach(n=>{let r=!1;for(let o of n.getAttributeNames())if(o.startsWith("hx-match-qp-mapping:")){let i=o.replace("hx-match-qp-mapping:","");if(t.searchParams.get(i)){f.swap(n,n.getAttribute(o)??"",{swapStyle:"innerHTML",swapDelay:0,settleDelay:0}),r=!0;break}}if(!r){let o=n.getAttribute("hx-match-qp-default");o&&f.swap(n,n.getAttribute("hx-match-qp-mapping:"+o)??"",{swapStyle:"innerHTML",swapDelay:0,settleDelay:0});}});}document.addEventListener("htmx:beforeSwap",function(e){e instanceof CustomEvent&&(e.detail.xhr.status===422||e.detail.xhr.status===400)&&(e.detail.shouldSwap=!0,e.detail.isError=!1);});
diff --git a/framework/assets/js/htmgo.ts b/framework/assets/js/htmgo.ts
index 7b33524..f8f081d 100644
--- a/framework/assets/js/htmgo.ts
+++ b/framework/assets/js/htmgo.ts
@@ -7,6 +7,8 @@ import "./htmxextensions/mutation-error";
import "./htmxextensions/livereload"
import "./htmxextensions/htmgo";
import "./htmxextensions/sse"
+import "./htmxextensions/ws"
+import "./htmxextensions/ws-event-handler"
// @ts-ignore
window.htmx = htmx;
@@ -44,7 +46,6 @@ function onUrlChange(newUrl: string) {
for (let [key, values] of url.searchParams) {
let eventName = "qs:" + key;
if (triggers.includes(eventName)) {
- console.log("triggering", eventName);
htmx.trigger(element, eventName, null);
break;
}
diff --git a/framework/assets/js/htmxextensions/ws-event-handler.ts b/framework/assets/js/htmxextensions/ws-event-handler.ts
new file mode 100644
index 0000000..8c4622e
--- /dev/null
+++ b/framework/assets/js/htmxextensions/ws-event-handler.ts
@@ -0,0 +1,77 @@
+import {ws} from "./ws";
+
+window.onload = function () {
+ const elements = document.querySelectorAll("[hx-extension]");
+ for (let element of Array.from(elements)) {
+ const value = element.getAttribute("hx-extension");
+ if(value != null && value.split(" ").includes("ws")) {
+ addWsEventHandlers()
+ break;
+ }
+ }
+};
+
+function sendWs(message: Record) {
+ if(ws != null && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(message));
+ }
+}
+
+function walk(node: Node, cb: (node: Node) => void) {
+ cb(node);
+ for (let child of Array.from(node.childNodes)) {
+ walk(child, cb);
+ }
+}
+
+export function addWsEventHandlers() {
+ const observer = new MutationObserver(register)
+ observer.observe(document.body, {childList: true, subtree: true})
+
+ let added = new Set();
+
+ function register(mutations: MutationRecord[]) {
+ for (let mutation of mutations) {
+ for (let removedNode of Array.from(mutation.removedNodes)) {
+ walk(removedNode, (node) => {
+ if (node instanceof HTMLElement) {
+ const handlerId = node.getAttribute("data-handler-id")
+ if(handlerId) {
+ added.delete(handlerId)
+ sendWs({id: handlerId, event: 'dom-element-removed'})
+ }
+ }
+ })
+ }
+
+ }
+
+
+ let ids = new Set();
+ document.querySelectorAll("[data-handler-id]").forEach(element => {
+ const id = element.getAttribute("data-handler-id");
+ const event = element.getAttribute("data-handler-event");
+
+ if(id == null || event == null) {
+ return;
+ }
+
+ ids.add(id);
+ if (added.has(id)) {
+ return;
+ }
+ added.add(id);
+ element.addEventListener(event, (e) => {
+ sendWs({id, event})
+ });
+ })
+ for (let id of added) {
+ if (!ids.has(id)) {
+ added.delete(id);
+ }
+ }
+ }
+
+ register([])
+}
+
diff --git a/framework/assets/js/htmxextensions/ws.ts b/framework/assets/js/htmxextensions/ws.ts
new file mode 100644
index 0000000..337490f
--- /dev/null
+++ b/framework/assets/js/htmxextensions/ws.ts
@@ -0,0 +1,87 @@
+import htmx from 'htmx.org'
+import {removeAssociatedScripts} from "./htmgo";
+
+let api : any = null;
+let processed = new Set()
+export let ws: WebSocket | null = null;
+
+htmx.defineExtension("ws", {
+ init: function (apiRef) {
+ api = apiRef;
+ },
+ // @ts-ignore
+ onEvent: function (name, evt) {
+ const target = evt.target;
+ if(!(target instanceof HTMLElement)) {
+ return
+ }
+
+ if(name === 'htmx:beforeCleanupElement') {
+ removeAssociatedScripts(target);
+ }
+
+ if(name === 'htmx:beforeProcessNode') {
+ const elements = document.querySelectorAll('[ws-connect]');
+ for (let element of Array.from(elements)) {
+ const url = element.getAttribute("ws-connect")!;
+ if(url && !processed.has(url)) {
+ connectWs(element, url)
+ processed.add(url)
+ }
+ }
+ }
+ }
+})
+
+function exponentialBackoff(attempt: number, baseDelay = 100, maxDelay = 10000) {
+ // Exponential backoff: baseDelay * (2 ^ attempt) with jitter
+ const jitter = Math.random(); // Adding randomness to prevent collisions
+ return Math.min(baseDelay * Math.pow(2, attempt) * jitter, maxDelay);
+}
+
+function connectWs(ele: Element, url: string, attempt: number = 0) {
+ if(!url) {
+ return
+ }
+ if(!url.startsWith('ws://') && !url.startsWith('wss://')) {
+ const isSecure = window.location.protocol === 'https:'
+ url = (isSecure ? 'wss://' : 'ws://') + window.location.host + url
+ }
+ console.info('connecting to ws', url)
+
+ ws = new WebSocket(url);
+
+ ws.addEventListener("close", function(event) {
+ htmx.trigger(ele, "htmx:wsClose", {event: event});
+ const delay = exponentialBackoff(attempt);
+ setTimeout(() => {
+ connectWs(ele, url, attempt + 1)
+ }, delay)
+ })
+
+ ws.addEventListener("open", function(event) {
+ htmx.trigger(ele, "htmx:wsOpen", {event: event});
+ })
+
+ ws.addEventListener("error", function(event) {
+ htmx.trigger(ele, "htmx:wsError", {event: event});
+ })
+
+ ws.addEventListener("message", function(event) {
+ const settleInfo = api.makeSettleInfo(ele);
+ htmx.trigger(ele, "htmx:wsBeforeMessage", {event: event});
+ const response = event.data
+ const fragment = api.makeFragment(response) as DocumentFragment;
+ const children = Array.from(fragment.children);
+ for (let child of children) {
+ api.oobSwap(api.getAttributeValue(child, 'hx-swap-oob') || 'true', child, settleInfo);
+ // support htmgo eval__ scripts
+ if(child.tagName === 'SCRIPT' && child.id.startsWith("__eval")) {
+ document.body.appendChild(child);
+ }
+ }
+ htmx.trigger(ele, "htmx:wsAfterMessage", {event: event});
+ })
+
+ return ws
+}
diff --git a/framework/h/app.go b/framework/h/app.go
index 3c095a5..f45b43b 100644
--- a/framework/h/app.go
+++ b/framework/h/app.go
@@ -174,6 +174,16 @@ func (app *App) UseWithContext(h func(w http.ResponseWriter, r *http.Request, co
})
}
+func (app *App) Use(h func(ctx *RequestContext)) {
+ app.Router.Use(func(handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ cc := r.Context().Value(RequestContextKey).(*RequestContext)
+ h(cc)
+ handler.ServeHTTP(w, r)
+ })
+ })
+}
+
func GetLogLevel() slog.Level {
// Get the log level from the environment variable
logLevel := os.Getenv("LOG_LEVEL")
diff --git a/framework/h/attribute.go b/framework/h/attribute.go
index be20b92..806b408 100644
--- a/framework/h/attribute.go
+++ b/framework/h/attribute.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/maddalax/htmgo/framework/datastructure/orderedmap"
"github.com/maddalax/htmgo/framework/hx"
+ "github.com/maddalax/htmgo/framework/internal/util"
"strings"
)
@@ -358,3 +359,7 @@ func AriaHidden(value bool) *AttributeR {
func TabIndex(value int) *AttributeR {
return Attribute("tabindex", fmt.Sprintf("%d", value))
}
+
+func GenId(len int) string {
+ return util.RandSeq(len)
+}
diff --git a/framework/h/tag.go b/framework/h/tag.go
index c1f6079..93352fd 100644
--- a/framework/h/tag.go
+++ b/framework/h/tag.go
@@ -161,6 +161,18 @@ func Div(children ...Ren) *Element {
return Tag("div", children...)
}
+func Dl(children ...Ren) *Element {
+ return Tag("dl", children...)
+}
+
+func Dt(children ...Ren) *Element {
+ return Tag("dt", children...)
+}
+
+func Dd(children ...Ren) *Element {
+ return Tag("dd", children...)
+}
+
func Article(children ...Ren) *Element {
return Tag("article", children...)
}