From ebe8217862a940c09cdee258adffa985edd81fd2 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 26 Jul 2023 13:49:47 +0200 Subject: [PATCH] Add the logout button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conceptually, to logout we have to “delete the session”, thus the best HTTP verb would be `DELETE`. However, there is no way to send a `DELETE` request with a regular HTML form, and it seems that never will be[0]. I could use a POST, optionally with a “method override” technique, but i was planing to use HTMx anyway, so this was as good an opportunity to include it as any. In this application i am not concerned with people not having JavaScript enabled, because it is for a customer that has a known environment, and we do not have much time anyway. Therefore, i opted to forgo progressive enhancement in cases like this: if `DELETE` is needed, use `hx-delete`. Unfortunately, i can not use a
with a hidden for the CSRF token, because `DELETE` requests do not have body and the value should be added as query parameters, like a form with GET method, but HTMx does the incorrect thing here: sends the values in the request’s body. That’s why i have to use a custom header and the `hx-header` directive to include the CSRF token. Then, by default HTMx targets the triggered element for swap with the response from the server, but after a logout i want to redirect the user to the login form again. I could set the hx-target to button to replace the whole body, or tell the client to redirect to the new location. I actually do not know which one is “better”. Maybe the hx-target is best because then everything is handled by the client, but in the case of logout, since it is possible that i might want to load scripts only for logged-in users in the future, i opted for the full page reload. However, HTMx does not want to reload a page that return HTTP 401, hence i had to include the GET method to /login in order to return the login form with a response of HTTP 200, which also helps when reloading in the browser after a failed login attempt. I am not worried with the HTTP 401 when attempting to load a page as guest, because this request most probably comes from the browser, not HTMx, and it will show the login form as intended—even though it is not compliant, since it does not return the WWW-Authenticate header, but this is the best i can do given that no cookie-based authentication method has been accepted[1]. [0]: https://www.w3.org/Bugs/Public/show_bug.cgi?id=10671#c16 [1]: https://datatracker.ietf.org/doc/id/draft-broyer-http-cookie-auth-00.html --- pkg/app/app.go | 11 ++++++-- pkg/app/login.go | 17 ++++++++--- pkg/app/user.go | 21 +++++++++++++- pkg/auth/session.go | 17 +++-------- pkg/auth/user.go | 40 ++++++++++++++++++++++++++ pkg/database/db.go | 9 ++++++ pkg/http/htmx.go | 55 ++++++++++++++++++++++++++++++++++++ pkg/template/render.go | 10 +++++++ po/ca.po | 21 ++++++++++---- po/es.po | 21 ++++++++++---- web/static/htmx@1.9.3.min.js | 1 + web/templates/layout.gohtml | 5 ++++ 12 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 pkg/auth/user.go create mode 100644 pkg/http/htmx.go create mode 100644 web/static/htmx@1.9.3.min.js diff --git a/pkg/app/app.go b/pkg/app/app.go index ed3a2c7..0a3bfed 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -58,7 +58,7 @@ func New(db *database.DB) http.Handler { } func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { - requestURL := r.URL.Path + requestPath := r.URL.Path var head string head, r.URL.Path = shiftPath(r.URL.Path) if head == "static" { @@ -77,18 +77,23 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { if head == "login" { switch r.Method { + case http.MethodGet: + serveLoginForm(w, r, user, "/") case http.MethodPost: handleLogin(w, r, user, conn) default: - methodNotAllowed(w, r, http.MethodPost) + methodNotAllowed(w, r, http.MethodPost, http.MethodGet) } } else { if !user.LoggedIn { - serveLoginForm(w, r, user, requestURL) + w.WriteHeader(http.StatusUnauthorized) + serveLoginForm(w, r, user, requestPath) return } switch head { + case "me": + profileHandler(user, conn)(w, r) case "": switch r.Method { case http.MethodGet: diff --git a/pkg/app/login.go b/pkg/app/login.go index 99f1b79..987210f 100644 --- a/pkg/app/login.go +++ b/pkg/app/login.go @@ -60,10 +60,9 @@ func (f *loginForm) Valid(l *locale.Locale) bool { return v.AllOK } -func serveLoginForm(w http.ResponseWriter, _ *http.Request, user *auth.User, requestURL string) { +func serveLoginForm(w http.ResponseWriter, _ *http.Request, user *auth.User, redirectPath string) { login := newLoginForm() - login.Redirect.Val = requestURL - w.WriteHeader(http.StatusUnauthorized) + login.Redirect.Val = redirectPath template.MustRender(w, user, "login.gohtml", login) } @@ -77,7 +76,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn * cookie := conn.MustGetText(r.Context(), "select login($1, $2, $3)", login.Email, login.Password, httplib.RemoteAddr(r)) if cookie != "" { auth.SetSessionCookie(w, cookie) - http.Redirect(w, r, login.Redirect.Val, http.StatusSeeOther) + httplib.Redirect(w, r, login.Redirect.Val, http.StatusSeeOther) return } login.Error = errors.New(user.Locale.Gettext("Invalid user or password.")) @@ -87,3 +86,13 @@ func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn * } template.MustRender(w, user, "login.gohtml", login) } + +func handleLogout(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) { + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + conn.MustExec(r.Context(), "select logout()") + auth.DeleteSessionCookie(w) + httplib.Redirect(w, r, "/login", http.StatusSeeOther) +} diff --git a/pkg/app/user.go b/pkg/app/user.go index f778420..1696f08 100644 --- a/pkg/app/user.go +++ b/pkg/app/user.go @@ -23,7 +23,7 @@ func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) } row := conn.QueryRow(r.Context(), "select coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile") var langTag string - if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CsrfToken); err != nil { + if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CSRFToken); err != nil { return nil, err } if lang, err := language.Parse(langTag); err == nil { @@ -47,3 +47,22 @@ func (h *App) matchLocale(r *http.Request) *locale.Locale { } return l } + +func profileHandler(user *auth.User, conn *database.Conn) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = shiftPath(r.URL.Path) + + switch head { + case "session": + switch r.Method { + case http.MethodDelete: + handleLogout(w, r, user, conn) + default: + methodNotAllowed(w, r, http.MethodDelete) + } + default: + http.NotFound(w, r) + } + } +} diff --git a/pkg/auth/session.go b/pkg/auth/session.go index df29c55..320b245 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -8,25 +8,12 @@ package auth import ( "net/http" "time" - - "golang.org/x/text/language" - - "dev.tandem.ws/tandem/camper/pkg/locale" ) const ( sessionCookie = "camper-session" ) -type User struct { - Email string - LoggedIn bool - Role string - Language language.Tag - CsrfToken string - Locale *locale.Locale -} - func SetSessionCookie(w http.ResponseWriter, cookie string) { http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) } @@ -38,6 +25,10 @@ func GetSessionCookie(r *http.Request) string { return "" } +func DeleteSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, createSessionCookie("", -24*time.Hour)) +} + func createSessionCookie(value string, duration time.Duration) *http.Cookie { return &http.Cookie{ Name: sessionCookie, diff --git a/pkg/auth/user.go b/pkg/auth/user.go new file mode 100644 index 0000000..05be619 --- /dev/null +++ b/pkg/auth/user.go @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package auth + +import ( + "errors" + "net/http" + + "golang.org/x/text/language" + + "dev.tandem.ws/tandem/camper/pkg/locale" +) + +const ( + CSRFTokenField = "csrf_token" + CSRFTokenHeader = "X-CSRFToken" +) + +type User struct { + Email string + LoggedIn bool + Role string + Language language.Tag + CSRFToken string + Locale *locale.Locale +} + +func (user *User) VerifyCSRFToken(r *http.Request) error { + token := r.Header.Get(CSRFTokenHeader) + if token == "" { + token = r.FormValue(CSRFTokenField) + } + if user.CSRFToken == token { + return nil + } + return errors.New(user.Locale.Gettext("Cross-site request forgery detected.")) +} diff --git a/pkg/database/db.go b/pkg/database/db.go index 818afce..956d942 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -9,6 +9,7 @@ import ( "context" "log" + "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" ) @@ -57,6 +58,14 @@ type Conn struct { *pgxpool.Conn } +func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) pgconn.CommandTag { + tag, err := c.Conn.Exec(ctx, sql, args...) + if err != nil { + panic(err) + } + return tag +} + func (c *Conn) MustGetText(ctx context.Context, sql string, args ...interface{}) string { var result string if err := c.QueryRow(ctx, sql, args...).Scan(&result); err != nil { diff --git a/pkg/http/htmx.go b/pkg/http/htmx.go new file mode 100644 index 0000000..20ddeb7 --- /dev/null +++ b/pkg/http/htmx.go @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package http + +import ( + "encoding/json" + "net/http" +) + +const ( + HxLocation = "HX-Location" + HxRedirect = "HX-Redirect" + HxRequest = "HX-Request" +) + +func Relocate(w http.ResponseWriter, r *http.Request, url string, code int) { + if IsHTMxRequest(r) { + w.Header().Set(HxLocation, MustMarshalHTMxLocation(&HTMxLocation{ + Path: url, + Target: "main", + })) + w.WriteHeader(http.StatusNoContent) + } else { + http.Redirect(w, r, url, code) + } +} + +func Redirect(w http.ResponseWriter, r *http.Request, url string, code int) { + if IsHTMxRequest(r) { + w.Header().Set(HxRedirect, url) + w.WriteHeader(http.StatusNoContent) + } else { + http.Redirect(w, r, url, code) + } +} + +func IsHTMxRequest(r *http.Request) bool { + return r.Header.Get(HxRequest) == "true" +} + +type HTMxLocation struct { + Path string `json:"path"` + Target string `json:"target"` +} + +func MustMarshalHTMxLocation(location *HTMxLocation) string { + data, err := json.Marshal(location) + if err != nil { + panic(err) + } + return string(data) +} diff --git a/pkg/template/render.go b/pkg/template/render.go index b1affd9..5546639 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -6,6 +6,7 @@ package template import ( + "fmt" "html/template" "io" "net/http" @@ -30,6 +31,15 @@ func mustRenderLayout(w io.Writer, user *auth.User, layout string, filename stri "currentLocale": func() string { return user.Locale.Language.String() }, + "isLoggedIn": func() bool { + return user.LoggedIn + }, + "CSRFHeader": func() string { + return fmt.Sprintf(`"%s": "%s"`, auth.CSRFTokenHeader, user.CSRFToken) + }, + "CSRFInput": func() template.HTML { + return template.HTML(fmt.Sprintf(``, auth.CSRFTokenField, user.CSRFToken)) + }, }) if _, err := t.ParseFiles(templateFile(layout), templateFile(filename)); err != nil { panic(err) diff --git a/po/ca.po b/po/ca.po index 83d7117..05ba008 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-07-26 11:57+0200\n" +"POT-Creation-Date: 2023-07-26 13:28+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -43,22 +43,31 @@ msgctxt "action" msgid "Login" msgstr "Entra" -#: web/templates/layout.gohtml:15 +#: web/templates/layout.gohtml:16 msgid "Skip to main content" msgstr "Salta al contingut principal" -#: pkg/app/login.go:58 +#: web/templates/layout.gohtml:20 +msgctxt "action" +msgid "Logout" +msgstr "Surt" + +#: pkg/app/login.go:56 msgid "Email can not be empty." msgstr "No podeu deixar el correu en blanc." -#: pkg/app/login.go:59 +#: pkg/app/login.go:57 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." -#: pkg/app/login.go:61 +#: pkg/app/login.go:59 msgid "Password can not be empty." msgstr "No podeu deixar la contrasenya en blanc." -#: pkg/app/login.go:85 +#: pkg/app/login.go:82 msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." + +#: pkg/auth/user.go:39 +msgid "Cross-site request forgery detected." +msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." diff --git a/po/es.po b/po/es.po index c334f6b..67f090a 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-07-26 11:57+0200\n" +"POT-Creation-Date: 2023-07-26 13:28+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -43,22 +43,31 @@ msgctxt "action" msgid "Login" msgstr "Entrar" -#: web/templates/layout.gohtml:15 +#: web/templates/layout.gohtml:16 msgid "Skip to main content" msgstr "Saltar al contenido principal" -#: pkg/app/login.go:58 +#: web/templates/layout.gohtml:20 +msgctxt "action" +msgid "Logout" +msgstr "Salir" + +#: pkg/app/login.go:56 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/app/login.go:59 +#: pkg/app/login.go:57 msgid "This email is not valid. It should be like name@domain.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." -#: pkg/app/login.go:61 +#: pkg/app/login.go:59 msgid "Password can not be empty." msgstr "No podéis dejar la contraseña en blanco." -#: pkg/app/login.go:85 +#: pkg/app/login.go:82 msgid "Invalid user or password." msgstr "Usuario o contraseña incorrectos." + +#: pkg/auth/user.go:39 +msgid "Cross-site request forgery detected." +msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." diff --git a/web/static/htmx@1.9.3.min.js b/web/static/htmx@1.9.3.min.js new file mode 100644 index 0000000..7b239be --- /dev/null +++ b/web/static/htmx@1.9.3.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var G={onLoad:t,process:Nt,on:ue,off:fe,trigger:oe,ajax:mr,find:b,findAll:f,closest:d,values:function(e,t){var r=Qt(e,t||"post");return r.values},remove:B,addClass:V,removeClass:n,toggleClass:j,takeClass:W,defineExtension:Er,removeExtension:Cr,logAll:F,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"]},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=G.config.wsBinaryType;return t},version:"1.9.3"};var C={addTriggerHandler:bt,bodyContains:re,canAccessLocalStorage:D,findThisElement:de,filterValues:ir,hasAttribute:q,getAttributeValue:Z,getClosestAttributeValue:Y,getClosestMatch:c,getExpressionVars:vr,getHeaders:nr,getInputValues:Qt,getInternalData:ee,getSwapSpecification:or,getTriggerSpecs:Ge,getTarget:ve,makeFragment:l,mergeObjects:ne,makeSettleInfo:S,oobSwap:xe,querySelectorExt:ie,selectAndSwap:Xe,settleImmediately:Wt,shouldCancel:Qe,triggerEvent:oe,triggerErrorEvent:ae,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function J(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function Z(e,t){return J(e,t)||J(e,"data-"+t)}function u(e){return e.parentElement}function K(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=Z(t,r);var i=Z(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Y(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function H(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=K().createDocumentFragment()}return i}function L(e){return e.match(/",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",3);case"script":return i("
"+e+"
",1);default:return i(e,0)}}}function Q(e){if(e){e()}}function A(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function N(e){return A(e,"Function")}function I(e){return A(e,"Object")}function ee(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e){var t=[];if(e){for(var r=0;r=0}function re(e){if(e.getRootNode&&e.getRootNode()instanceof ShadowRoot){return K().body.contains(e.getRootNode().host)}else{return K().body.contains(e)}}function M(e){return e.trim().split(/\s+/)}function ne(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function D(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function X(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return cr(K().body,function(){return eval(e)})}function t(t){var e=G.on("htmx:load",function(e){t(e.detail.elt)});return e}function F(){G.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){G.logger=null}function b(e,t){if(t){return e.querySelector(t)}else{return b(K(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(K(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function V(e,t,r){e=s(e);if(r){setTimeout(function(){V(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function j(e,t){e=s(e);e.classList.toggle(t)}function W(e,t){e=s(e);te(e.parentElement.children,function(e){n(e,t)});V(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function _(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[z(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[$(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return K().querySelectorAll(r(t))}}var z=function(e,t){var r=K().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ie(e,t){if(t){return _(e,t)[0]}else{return _(K().body,e)[0]}}function s(e){if(A(e,"String")){return b(e)}else{return e}}function le(e,t,r){if(N(t)){return{target:K().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function ue(t,r,n){Or(function(){var e=le(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=N(r);return e?r:n}function fe(t,r,n){Or(function(){var e=le(t,r,n);e.target.removeEventListener(e.event,e.listener)});return N(r)?r:n}var ce=K().createElement("output");function he(e,t){var r=Y(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=_(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[ce]}else{return n}}}}function de(e,t){return c(e,function(e){return Z(e,t)!=null})}function ve(e){var t=Y(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return ie(e,t)}}else{var r=ee(e);if(r.boosted){return K().body}else{return e}}}function ge(e){var t=G.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=K().querySelectorAll(t);if(r){te(r,function(e){var t;var r=i.cloneNode(true);t=K().createDocumentFragment();t.appendChild(r);if(!me(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!oe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Me(o,e,e,t,a)}te(a.elts,function(e){oe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ae(K().body,"htmx:oobErrorNoTarget",{content:i})}return e}function ye(e,t,r){var n=Y(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var t=e.id.replace("'","\\'");var r=e.tagName.replace(":","\\:");var n=a.querySelector(r+"[id='"+t+"']");if(n&&n!==a){var i=e.cloneNode();pe(e,n);o.tasks.push(function(){pe(e,i)})}}})}function Se(e){return function(){n(e,G.config.addedClass);Nt(e);St(e);Ee(e);oe(e,"htmx:load")}}function Ee(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){we(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;V(i,G.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Se(i))}}}function Ce(e,t){var r=0;while(re!=t);while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}o(t);u(t).removeChild(t)}}function He(e,t,r){return a(e,e.firstChild,t,r)}function Le(e,t,r){return a(u(e),e,t,r)}function Ae(e,t,r){return a(e,null,t,r)}function Ne(e,t,r){return a(u(e),e.nextSibling,t,r)}function Ie(e,t,r){o(e);return u(e).removeChild(e)}function ke(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){o(n.nextSibling);e.removeChild(n.nextSibling)}o(n);e.removeChild(n)}}function Pe(e,t,r){var n=r||Y(e,"hx-select");if(n){var i=K().createDocumentFragment();te(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function Me(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":Te(r,n,i);return;case"afterbegin":He(r,n,i);return;case"beforebegin":Le(r,n,i);return;case"beforeend":Ae(r,n,i);return;case"afterend":Ne(r,n,i);return;case"delete":Ie(r,n,i);return;default:var a=Rr(t);for(var o=0;o-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Xe(e,t,r,n,i,a){i.title=De(n);var o=l(n);if(o){ye(r,o,i);o=Pe(r,o,a);be(o);return Me(e,r,t,o,i)}}function Fe(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=y(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!I(o)){o={value:o}}oe(r,a,o)}}}else{oe(r,n,[])}}var Ue=/\s/;var g=/[\s,]/;var Be=/[_$a-zA-Z]/;var Ve=/[_$a-zA-Z0-9]/;var je=['"',"'","/"];var p=/[^\s]/;function We(e){var t=[];var r=0;while(r0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=cr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ae(K().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(_e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function m(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var $e="input, textarea, select";function Ge(e){var t=Z(e,"hx-trigger");var r=[];if(t){var n=We(t);do{m(n,p);var i=n.length;var a=m(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};m(n,p);o.pollInterval=v(m(n,/[,\[\s]/));m(n,p);var s=ze(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=ze(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){m(n,p);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(m(n,g))}else if(u==="from"&&n[0]===":"){n.shift();var f=m(n,g);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+m(n,g)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=m(n,g)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(m(n,g))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=m(n,g)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=m(n,g)}else{ae(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ae(e,"htmx:syntax:error",{token:n.shift()})}m(n,p)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"]')){return[{trigger:"click"}]}else if(h(e,$e)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Je(e){ee(e).cancelled=true}function Ze(e,t,r){var n=ee(e);n.timeout=setTimeout(function(){if(re(e)&&n.cancelled!==true){if(!tt(r,e,kt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ze(e,t,r)}},r.pollInterval)}function Ke(e){return location.hostname===e.hostname&&J(e,"href")&&J(e,"href").indexOf("#")!==0}function Ye(t,r,e){if(t.tagName==="A"&&Ke(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=t.href}else{var a=J(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=J(t,"action")}e.forEach(function(e){rt(t,function(e,t){se(n,i,e,t)},r,e,true)})}}function Qe(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function et(e,t){return ee(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function tt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ae(K().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function rt(i,a,e,o,s){var l=ee(i);var t;if(o.from){t=_(i,o.from)}else{t=[i]}if(o.changed){l.lastValue=i.value}te(t,function(r){var n=function(e){if(!re(i)){r.removeEventListener(o.trigger,n);return}if(et(i,e)){return}if(s||Qe(e,i)){e.preventDefault()}if(tt(o,i,e)){return}var t=ee(e);t.triggerSpec=o;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(i)<0){t.handledFor.push(i);if(o.consume){e.stopPropagation()}if(o.target&&e.target){if(!h(e.target,o.target)){return}}if(o.once){if(l.triggeredOnce){return}else{l.triggeredOnce=true}}if(o.changed){if(l.lastValue===i.value){return}else{l.lastValue=i.value}}if(l.delayed){clearTimeout(l.delayed)}if(l.throttle){return}if(o.throttle){if(!l.throttle){a(i,e);l.throttle=setTimeout(function(){l.throttle=null},o.throttle)}}else if(o.delay){l.delayed=setTimeout(function(){a(i,e)},o.delay)}else{oe(i,"htmx:trigger");a(i,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:o.trigger,listener:n,on:r});r.addEventListener(o.trigger,n)})}var nt=false;var it=null;function at(){if(!it){it=function(){nt=true};window.addEventListener("scroll",it);setInterval(function(){if(nt){nt=false;te(K().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){ot(e)})}},200)}}function ot(t){if(!q(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=ee(t);if(e.initHash){oe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){oe(t,"revealed")},{once:true})}}}function st(e,t,r){var n=M(r);for(var i=0;i=0){var t=ct(n);setTimeout(function(){lt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ee(s).webSocket=t;t.addEventListener("message",function(e){if(ut(s)){return}var t=e.data;w(s,function(e){t=e.transformResponse(t,null,s)});var r=S(s);var n=l(t);var i=k(n.children);for(var a=0;a0){oe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Qe(e,u)){e.preventDefault()}})}else{ae(u,"htmx:noWebSocketSourceError")}}function ct(e){var t=G.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function ht(e,t,r){var n=M(r);for(var i=0;i0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=qt(o)}for(var l in r){Tt(e,l,r[l])}}}function Lt(t){Oe(t);for(const e of t.attributes){const{name:r,value:n}=e;if(r.startsWith("hx-on:")||r.startsWith("data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(e.startsWith(":"))e="htmx"+e;Tt(t,e,n)}}}function At(t){if(t.closest&&t.closest(G.config.disableSelector)){return}var r=ee(t);if(r.initHash!==Re(t)){r.initHash=Re(t);qe(t);Ht(t);oe(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Ge(t);var n=yt(t,r,e);if(!n){if(Y(t,"hx-boost")==="true"){Ye(t,r,e)}else if(q(t,"hx-trigger")){e.forEach(function(e){bt(t,e,r,function(){})})}}if(t.tagName==="FORM"){Ot(t)}var i=Z(t,"hx-sse");if(i){ht(t,r,i)}var a=Z(t,"hx-ws");if(a){st(t,r,a)}oe(t,"htmx:afterProcessNode")}}function Nt(e){e=s(e);At(e);te(Rt(e),function(e){At(e)});te(Ct(e),Lt)}function It(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function kt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=K().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ae(e,t,r){oe(e,t,ne({error:t},r))}function Pt(e){return e==="htmx:afterProcessNode"}function w(e,t){te(Rr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function oe(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=kt(t,r);if(G.logger&&!Pt(t)){G.logger(e,t,r)}if(r.error){x(r.error);oe(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=It(t);if(i&&a!==t){var o=kt(a,n.detail);i=i&&e.dispatchEvent(o)}w(e,function(e){i=i&&e.onEvent(t,n)!==false});return i}var Mt=location.pathname+location.search;function Dt(){var e=K().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||K().body}function Xt(e,t,r,n){if(!D()){return}e=X(e);var i=y(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;aG.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ae(K().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Ft(e){if(!D()){return null}e=X(e);var t=y(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){oe(K().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Dt();var r=S(t);var n=De(this.response);if(n){var i=b("title");if(i){i.innerHTML=n}else{window.document.title=n}}ke(t,e,r);Wt(r.tasks);Mt=a;oe(K().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ae(K().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function zt(e){Bt();e=e||location.pathname+location.search;var t=Ft(e);if(t){var r=l(t.content);var n=Dt();var i=S(n);ke(n,r,i);Wt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Mt=e;oe(K().body,"htmx:historyRestore",{path:e,item:t})}else{if(G.config.refreshOnHistoryMiss){window.location.reload(true)}else{_t(e)}}}function $t(e){var t=he(e,"hx-indicator");if(t==null){t=[e]}te(t,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,G.config.requestClass)});return t}function Gt(e){te(e,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,G.config.requestClass)}})}function Jt(e,t){for(var r=0;r=0}function or(e,t){var r=t?t:Y(e,"hx-swap");var n={swapStyle:ee(e).boosted?"innerHTML":G.config.defaultSwapStyle,swapDelay:G.config.defaultSwapDelay,settleDelay:G.config.defaultSettleDelay};if(ee(e).boosted&&!ar(e)){n["show"]="top"}if(r){var i=M(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function sr(e){return Y(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&J(e,"enctype")==="multipart/form-data"}function lr(t,r,n){var i=null;w(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(sr(r)){return rr(n)}else{return tr(n)}}}function S(e){return{tasks:[],elts:[e]}}function ur(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ie(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ie(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:G.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:G.config.scrollBehavior})}}}function fr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=Z(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=cr(e,function(){return Function("return ("+a+")")()},{})}else{s=y(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return fr(u(e),t,r,n)}function cr(e,t,r){if(G.config.allowEval){return t()}else{ae(e,"htmx:evalDisallowedError");return r}}function hr(e,t){return fr(e,"hx-vars",true,t)}function dr(e,t){return fr(e,"hx-vals",false,t)}function vr(e){return ne(hr(e),dr(e))}function gr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function pr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ae(K().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function E(e,t){return e.getAllResponseHeaders().match(t)}function mr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||A(r,"String")){return se(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return se(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return se(e,t,null,null,{returnPromise:true})}}function xr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function se(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=K().body}var D=i.handler||br;if(!re(n)){return}var l=i.targetOverride||ve(n);if(l==null||l==ce){ae(n,"htmx:targetError",{target:Z(n,"hx-target")});return}if(!M){var X=function(){return se(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(oe(n,"htmx:confirm",F)===false){return}}var u=n;var f=ee(n);var c=Y(n,"hx-sync");var h=null;var d=false;if(c){var v=c.split(":");var g=v[0].trim();if(g==="this"){u=de(n,"hx-sync")}else{u=ie(n,g)}c=(v[1]||"drop").trim();f=ee(u);if(c==="drop"&&f.xhr&&f.abortable!==true){return}else if(c==="abort"){if(f.xhr){return}else{d=true}}else if(c==="replace"){oe(u,"htmx:abort")}else if(c.indexOf("queue")===0){var U=c.split(" ");h=(U[1]||"last").trim()}}if(f.xhr){if(f.abortable){oe(u,"htmx:abort")}else{if(h==null){if(r){var p=ee(r);if(p&&p.triggerSpec&&p.triggerSpec.queue){h=p.triggerSpec.queue}}if(h==null){h="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(h==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="all"){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){se(e,t,n,r,i)})}return}}var m=new XMLHttpRequest;f.xhr=m;f.abortable=d;var x=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var y=Y(n,"hx-prompt");if(y){var b=prompt(y);if(b===null||!oe(n,"htmx:prompt",{prompt:b,target:l})){Q(a);x();return s}}var w=Y(n,"hx-confirm");if(w){if(!confirm(w)){Q(a);x();return s}}var S=nr(n,l,b);if(i.headers){S=ne(S,i.headers)}var E=Qt(n,e);var C=E.errors;var R=E.values;if(i.values){R=ne(R,i.values)}var B=vr(n);var O=ne(R,B);var q=ir(O,n);if(e!=="get"&&!sr(n)){S["Content-Type"]="application/x-www-form-urlencoded"}if(G.config.getCacheBusterParam&&e==="get"){q["org.htmx.cache-buster"]=J(l,"id")||"true"}if(t==null||t===""){t=K().location.href}var T=fr(n,"hx-request");var V=ee(n).boosted;var H=G.config.methodsThatUseUrlParams.indexOf(e)>=0;var L={boosted:V,useUrlParams:H,parameters:q,unfilteredParameters:O,headers:S,target:l,verb:e,errors:C,withCredentials:i.credentials||T.credentials||G.config.withCredentials,timeout:i.timeout||T.timeout||G.config.timeout,path:t,triggeringEvent:r};if(!oe(n,"htmx:configRequest",L)){Q(a);x();return s}t=L.path;e=L.verb;S=L.headers;q=L.parameters;C=L.errors;H=L.useUrlParams;if(C&&C.length>0){oe(n,"htmx:validation:halted",L);Q(a);x();return s}var j=t.split("#");var W=j[0];var A=j[1];var N=t;if(H){N=W;var _=Object.keys(q).length!==0;if(_){if(N.indexOf("?")<0){N+="?"}else{N+="&"}N+=tr(q);if(A){N+="#"+A}}}m.open(e.toUpperCase(),N,true);m.overrideMimeType("text/html");m.withCredentials=L.withCredentials;m.timeout=L.timeout;if(T.noHeaders){}else{for(var I in S){if(S.hasOwnProperty(I)){var z=S[I];gr(m,I,z)}}}var k={xhr:m,target:l,requestConfig:L,etc:i,boosted:V,pathInfo:{requestPath:t,finalRequestPath:N,anchor:A}};m.onload=function(){try{var e=xr(n);k.pathInfo.responsePath=pr(m);D(n,k);Gt(P);oe(n,"htmx:afterRequest",k);oe(n,"htmx:afterOnLoad",k);if(!re(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(re(r)){t=r}}if(t){oe(t,"htmx:afterRequest",k);oe(t,"htmx:afterOnLoad",k)}}Q(a);x()}catch(e){ae(n,"htmx:onLoadError",ne({error:e},k));throw e}};m.onerror=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:sendError",k);Q(o);x()};m.onabort=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:sendAbort",k);Q(o);x()};m.ontimeout=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:timeout",k);Q(o);x()};if(!oe(n,"htmx:beforeRequest",k)){Q(a);x();return s}var P=$t(n);te(["loadstart","loadend","progress","abort"],function(t){te([m,m.upload],function(e){e.addEventListener(t,function(e){oe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});oe(n,"htmx:beforeSend",k);var $=H?null:lr(m,n,q);m.send($);return s}function yr(e,t){var r=t.xhr;var n=null;var i=null;if(E(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(E(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(E(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=Y(e,"hx-push-url");var l=Y(e,"hx-replace-url");var u=ee(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function br(l,u){var f=u.xhr;var c=u.target;var e=u.etc;if(!oe(l,"htmx:beforeOnLoad",u))return;if(E(f,/HX-Trigger:/i)){Fe(f,"HX-Trigger",l)}if(E(f,/HX-Location:/i)){Bt();var t=f.getResponseHeader("HX-Location");var h;if(t.indexOf("{")===0){h=y(t);t=h["path"];delete h["path"]}mr("GET",t,h).then(function(){Vt(t)});return}if(E(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");return}if(E(f,/HX-Refresh:/i)){if("true"===f.getResponseHeader("HX-Refresh")){location.reload();return}}if(E(f,/HX-Retarget:/i)){u.target=K().querySelector(f.getResponseHeader("HX-Retarget"))}var d=yr(l,u);var r=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var n=f.status>=400;var i=ne({shouldSwap:r,serverResponse:v,isError:n},u);if(!oe(c,"htmx:beforeSwap",i))return;c=i.target;v=i.serverResponse;n=i.isError;u.target=c;u.failed=n;u.successful=!n;if(i.shouldSwap){if(f.status===286){Je(l)}w(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Bt()}var a=e.swapOverride;if(E(f,/HX-Reswap:/i)){a=f.getResponseHeader("HX-Reswap")}var h=or(l,a);c.classList.add(G.config.swappingClass);var g=null;var p=null;var o=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(E(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=S(c);Xe(h.swapStyle,c,l,v,n,r);if(t.elt&&!re(t.elt)&&t.elt.id){var i=document.getElementById(t.elt.id);var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!G.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(G.config.swappingClass);te(n.elts,function(e){if(e.classList){e.classList.add(G.config.settlingClass)}oe(e,"htmx:afterSwap",u)});if(E(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!re(l)){o=K().body}Fe(f,"HX-Trigger-After-Swap",o)}var s=function(){te(n.tasks,function(e){e.call()});te(n.elts,function(e){if(e.classList){e.classList.remove(G.config.settlingClass)}oe(e,"htmx:afterSettle",u)});if(d.type){if(d.type==="push"){Vt(d.path);oe(K().body,"htmx:pushedIntoHistory",{path:d.path})}else{jt(d.path);oe(K().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=b("#"+u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=b("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}ur(n.elts,h);if(E(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!re(l)){r=K().body}Fe(f,"HX-Trigger-After-Settle",r)}Q(g)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ae(l,"htmx:swapError",u);Q(p);throw e}};var s=G.config.globalViewTransitions;if(h.hasOwnProperty("transition")){s=h.transition}if(s&&oe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var m=new Promise(function(e,t){g=e;p=t});var x=o;o=function(){document.startViewTransition(function(){x();return m})}}if(h.swapDelay>0){setTimeout(o,h.swapDelay)}else{o()}}if(n){ae(l,"htmx:responseError",ne({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var wr={};function Sr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Er(e,t){if(t.init){t.init(C)}wr[e]=ne(Sr(),t)}function Cr(e){delete wr[e]}function Rr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=Z(e,"hx-ext");if(t){te(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=wr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Rr(u(e),r,n)}function Or(e){if(K().readyState!=="loading"){e()}else{K().addEventListener("DOMContentLoaded",e)}}function qr(){if(G.config.includeIndicatorStyles!==false){K().head.insertAdjacentHTML("beforeend","")}}function Tr(){var e=K().querySelector('meta[name="htmx-config"]');if(e){return y(e.content)}else{return null}}function Hr(){var e=Tr();if(e){G.config=ne(G.config,e)}}Or(function(){Hr();qr();var e=K().body;Nt(e);var t=K().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ee(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){zt();te(t,function(e){oe(e,"htmx:restored",{document:K(),triggerEvent:oe})})}else{if(r){r(e)}}};setTimeout(function(){oe(e,"htmx:load",{});e=null},0)});return G}()}); \ No newline at end of file diff --git a/web/templates/layout.gohtml b/web/templates/layout.gohtml index 3665ba9..3cb8da8 100644 --- a/web/templates/layout.gohtml +++ b/web/templates/layout.gohtml @@ -9,11 +9,16 @@ {{ template "title" . }} — Camper +
{{( gettext "Skip to main content" )}}

camper _ws

+ {{ if isLoggedIn -}} + + {{- end }}
{{- template "content" . }}