detached 7becef6

Refactor action response handling and enhance UI toggle states

Backend:
- Refactor common action responses to use a centralized function ActionSuccess.
- Modify HTTP responses to use ActionSuccess in multiple handlers.

Frontend:
- Implement Hyperscript for toggle state management in upload and create-container buttons.
- Update HMTX target to a common div for all action responses.
- Remove redundant JavaScript for toggle states and element removal.
- Update libraries and fix broken links in header.
commit
7becef6c836e2d4c4fd367d1afcd7e3c524956c4
author
bnema · · 2023-10-20 16:32 UTC
parent
82ff1b2
signature
unsigned
15 files · +3907 -134
M internal/httpserve/handler/containers.go +20 -8
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/templating/load"
1011 "github.com/bnema/gordon/internal/templating/render"
1112 "github.com/bnema/gordon/pkg/docker"
1213 "github.com/bnema/gordon/pkg/humanize"
@@ -14,8 +15,10 @@ import (
1415 "github.com/labstack/echo/v4"
1516 )
1617
17-var idMap = make(map[string]string)
18-var idMapMutex sync.Mutex
18+var (
19+ idMap = make(map[string]string)
20+ idMapMutex sync.Mutex
21+)
1922
2023 type HumanReadableContainerImage struct {
2124 *types.ImageSummary
@@ -36,6 +39,16 @@ type HumanReadableContainer struct {
3639 StateColor string
3740 }
3841
42+func ActionSuccess(a *app.App) string {
43+ successFragment, err := load.Fragment(a, "success")
44+ if err != nil {
45+ fmt.Println(err)
46+ return ""
47+ }
48+
49+ return successFragment
50+}
51+
3952 // ImageManagerComponent handles the /image-manager route (HTMX route)
4053 func ImageManagerComponent(c echo.Context, a *app.App) error {
4154 images, err := docker.ListContainerImages()
@@ -103,8 +116,7 @@ func ImageManagerDelete(c echo.Context, a *app.App) error {
103116 delete(idMap, ShortID)
104117 idMapMutex.Unlock()
105118
106- // Since it is HTMX we return a HTML div with the message "Removed"
107- return c.HTML(http.StatusOK, `<div>Removed</div>`)
119+ return c.String(http.StatusOK, ActionSuccess(a))
108120 }
109121
110122 // ContainerManagerComponent handles the /container-manager route
@@ -169,7 +181,7 @@ func ContainerManagerDelete(c echo.Context, a *app.App) error {
169181 if err != nil {
170182 return sendError(c, err)
171183 }
172- return c.HTML(http.StatusOK, `Success`)
184+ return c.HTML(http.StatusOK, ActionSuccess(a))
173185 }
174186
175187 // ContainerManagerStop handles the /container-manager/stop route
@@ -182,7 +194,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
182194 }
183195 // if stopped is true, the container was stopped gracefully
184196 if stopped {
185- return c.HTML(http.StatusOK, `Success`)
197+ return c.HTML(http.StatusOK, ActionSuccess(a))
186198 }
187199 // If stopped is false, the container was not stopped gracefully we force stop it
188200 err = docker.StopContainerRagefully(ID)
@@ -190,7 +202,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
190202 return sendError(c, err)
191203 }
192204
193- return c.HTML(http.StatusOK, `Success`)
205+ return c.HTML(http.StatusOK, ActionSuccess(a))
194206 }
195207
196208 // ContainerManagerStart handles the /container-manager/start route
@@ -200,5 +212,5 @@ func ContainerManagerStart(c echo.Context, a *app.App) error {
200212 if err != nil {
201213 return sendError(c, err)
202214 }
203- return c.HTML(http.StatusOK, `Success`)
215+ return c.HTML(http.StatusOK, ActionSuccess(a))
204216 }
M internal/httpserve/handler/uploadcontainerimage.go +1 -1
@@ -60,5 +60,5 @@ func UploadImagePOSTHandler(c echo.Context, a *app.App) error {
6060 if err != nil {
6161 return sendError(c, err)
6262 }
63- return c.HTML(http.StatusOK, "Success")
63+ return c.HTML(http.StatusOK, ActionSuccess(a))
6464 }
A internal/templating/load/loadfragment.go +28 -0
@@ -0,0 +1,28 @@
1+package load
2+
3+import (
4+ "fmt"
5+ "io"
6+
7+ "github.com/bnema/gordon/internal/app"
8+)
9+
10+// LoadFragment loads a specific HTML fragment from the app's TemplateFS
11+func Fragment(a *app.App, fragmentName string) (string, error) {
12+ filePath := fmt.Sprintf("html/fragments/%s.gohtml", fragmentName)
13+
14+ // Open the file
15+ file, err := a.TemplateFS.Open(filePath)
16+ if err != nil {
17+ return "", err
18+ }
19+ defer file.Close()
20+
21+ // Read the file's content
22+ content, err := io.ReadAll(file)
23+ if err != nil {
24+ return "", err
25+ }
26+
27+ return string(content), nil
28+}
M internal/templating/models/html/admin/manager.gohtml +0 -3
@@ -9,9 +9,6 @@
99 <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
1010 <div class="flex flex-col md:flex-row w-full flex-grow">
1111 <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
12- <h1 class="text-lg md:text-2xl font-bold text-gray-400 IBMPlexSans justify-center text-center">
13- {{index .Lang "manager" "content" "title"}}
14- </h1>
1512 <!-- List all the containers -->
1613 <div
1714 id="container-manager"
M internal/templating/models/html/fragments/containerlist.gohtml +6 -4
@@ -1,3 +1,5 @@
1+<div id="actionResponse"></div>
2+
13 <div class="flex flex-col p-4" hx-boost="true">
24 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
35 {{index .Lang "manager" "content" "h1-containers"}}
@@ -33,7 +35,7 @@
3335 <button
3436 id="start-button-{{.ID}}"
3537 hx-post="/htmx/container-manager/start/{{.ID}}"
36- hx-target="#container-{{.ID}}"
38+ hx-target="#actionResponse"
3739 hx-target-*="#error-message-{{.ID}}"
3840 hx-swap="outerHTML"
3941 class="py-2 px-4"
@@ -44,7 +46,7 @@
4446 <button
4547 id="edit-button-{{.ID}}"
4648 hx-get="/htmx/container-manager/edit/{{.ID}}"
47- hx-target="#edit-container-{{.ID}}"
49+ hx-target="#actionResponse"
4850 hx-target-*="#error-message-{{.ID}}"
4951 hx-swap="innerHTML"
5052 class="py-2 px-4"
@@ -56,7 +58,7 @@
5658 <button
5759 id="stop-button-{{.ID}}"
5860 hx-post="/htmx/container-manager/stop/{{.ID}}"
59- hx-target="#container-{{.ID}}"
61+ hx-target="#actionResponse"
6062 hx-target-*="#error-message-{{.ID}}"
6163 hx-swap="innerHTML"
6264 hx-confirm="{{index $Lang "manager" "content" "confirm-container-stop"}}"
@@ -68,7 +70,7 @@
6870 <button
6971 id="remove-button-img-{{.ID}}"
7072 hx-delete="/htmx/container-manager/delete/{{.ID}}"
71- hx-target="#container-{{.ID}}"
73+ hx-target="#actionResponse"
7274 hx-target-*="#error-message-{{.ID}}"
7375 hx-swap="innerHTML"
7476 hx-confirm="{{index $Lang "manager" "content" "confirm-delete"}}"
M internal/templating/models/html/fragments/header.gohtml +2 -2
@@ -4,11 +4,11 @@
44 <title>{{ .header.meta.title }}</title>
55 <link rel="stylesheet" href="/assets/css/tailwind.css" />
66
7- <script src="/libs/htmx.min.js"></script>
7+ <script src="/libs/htmx.js"></script>
88 <script src="/libs/htmx-response-targets.js"></script>
99 <script src="/libs/htmx-debug.js"></script>
1010 <script src="/libs/htmx-sse.js"></script>
11- <script defer src="https://unpkg.com/hyperscript.org@0.9.11"></script>
11+ <script defer src="/libs/hyperscript.min.js"></script>
1212 <script defer src="/assets/js/custom.js"></script>
1313 <!-- script wait for the page to be loaded and insert <link rel="stylesheet" href="/libs/iconoir.css" /> -->
1414 <script>
M internal/templating/models/html/fragments/imagelist.gohtml +5 -26
@@ -3,13 +3,14 @@
33 <div class="flex justify-between items-center mb-1">
44 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
55 <button
6+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
67 id="upload-button"
78 hx-get="/htmx/upload-image"
89 hx-target="#upload-image"
910 hx-swap="innerHTML"
10- data-active="false"
11- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
11+ _="on click toggle between .visible and .hidden on #upload-image then add .hidden to #create-container"
1212 >
13+
1314 <span class="iconoir-upload text-xl align-middle"></span>
1415 Upload
1516 </button>
@@ -36,6 +37,7 @@
3637 hx-target-*="#error-message-div-{{.ID}}"
3738 hx-swap="innerHTML"
3839 class="py-2 px-4"
40+ _="on click toggle between .visible and .hidden on #create-container then add .hidden to #upload-image"
3941 >
4042 <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span>
4143 </button>
@@ -69,27 +71,4 @@
6971 </table>
7072 <div id="upload-image"></div>
7173 <div id="create-container"></div>
72-</div>
73-
74-<script>
75- // Hide upload form if add button is clicked
76- document.querySelectorAll('[id^="add-button-img-"]').forEach((button) => {
77- button.addEventListener("click", () => {
78- document.getElementById("upload-image").innerHTML = "";
79- });
80- });
81-
82- // Hide create container form if upload button is clicke
83- document.getElementById("upload-button").addEventListener("click", () => {
84- document.getElementById("create-container").innerHTML = "";
85- });
86-
87- // Remove image from list if remove button is clicked (after confirmation)
88- document.querySelectorAll('[id^="remove-button-img-"]').forEach((button) => {
89- button.addEventListener("click", () => {
90- const id = button.id.split("-")[3];
91- const tr = document.getElementById(`img-${id}`);
92- tr.style.display = "none";
93- });
94- });
95-</script>⏎̸
74+</div>⏎̸
A internal/templating/models/html/fragments/success.gohtml +6 -0
@@ -0,0 +1,6 @@
1+<div
2+ id="success"
3+ _="on load wait 1s then transition opacity to 0 then remove me then wait 1s then call window.location.reload()"
4+ class="flex text-blue-800 content-center justify-center items-center w-full h-full">
5+ <p class="text-2xl text-center">Success!</p>
6+</div>
M internal/templating/models/html/fragments/uploadimage.gohtml +6 -10
@@ -2,8 +2,7 @@
22 <div
33 class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
44 hx-ext="response-targets"
5- hx-boost="true"
6->
5+ hx-boost="true">
76 <div class="flex-grow bg-slate-100 p-4 rounded-md">
87 <h2>Upload Image Containers</h2>
98 <!-- Upload Form -->
@@ -14,25 +13,22 @@
1413 hx-post="/htmx/upload-image"
1514 hx-swap="innerHTML"
1615 hx-target="#image-manager"
17- hx-target-*="#error-message-div"
18- >
16+ hx-target-*="#error-message-div">
1917 <div
2018 class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4"
21- hx-drop="true"
22- >
19+ hx-drop="true">
2320 Drag & Drop your file here or
2421 <input
2522 type="file"
2623 name="file"
2724 accept=".tar"
28- class="bg-transparent outline-none"
29- />
25+ class="bg-transparent outline-none" />
3026 </div>
3127 <input
3228 type="submit"
3329 value="Upload"
34- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
35- />
30+ _="on click toggle @disabled until htmx:afterOnLoad"
31+ class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
3632 </form>
3733 <div id="error-message-div" class="text-red-500"></div>
3834 </div>
M internal/webui/public/assets/css/custom.css +7 -5
@@ -37,7 +37,8 @@
3737 /* Import Jetbrains Mono Nerd Font Bold */
3838 @font-face {
3939 font-family: "JetBrainsMono-Nerd-Font-Bold";
40- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");
40+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
41+ format("woff2");
4142 font-weight: bold;
4243 font-style: normal;
4344 }
@@ -84,17 +85,18 @@
8485 }
8586
8687 ::view-transition-old(slide-it) {
87- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
88+ animation:
89+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
8890 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
8991 }
9092 ::view-transition-new(slide-it) {
91- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
93+ animation:
94+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
9295 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
9396 }
9497
9598 .gordon-title {
9699 font-family: "LilitaOne-Regular", sans-serif;
97-
98100 }
99101
100102 .IBMPlexSans {
@@ -107,4 +109,4 @@
107109
108110 .JetBrainsMonoBoldNF {
109111 font-family: "JetBrainsMono-Nerd-Font-Bold", sans-serif;
110-}⏎̸
112+}
M internal/webui/public/assets/css/tailwind.css +31 -4
@@ -575,6 +575,18 @@ video {
575575 }
576576 }
577577
578+.visible {
579+ visibility: visible;
580+}
581+
582+.invisible {
583+ visibility: hidden;
584+}
585+
586+.collapse {
587+ visibility: collapse;
588+}
589+
578590 .fixed {
579591 position: fixed;
580592 }
@@ -708,6 +720,10 @@ video {
708720 flex-direction: column;
709721 }
710722
723+.content-center {
724+ align-content: center;
725+}
726+
711727 .items-center {
712728 align-items: center;
713729 }
@@ -1027,6 +1043,14 @@ video {
10271043 outline-offset: 2px;
10281044 }
10291045
1046+.transition {
1047+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1048+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1049+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1050+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1051+ transition-duration: 150ms;
1052+}
1053+
10301054 /* Import IBM Plex Fonts */
10311055
10321056 @font-face {
@@ -1067,7 +1091,8 @@ video {
10671091 @font-face {
10681092 font-family: "JetBrainsMono-Nerd-Font-Bold";
10691093
1070- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");
1094+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
1095+ format("woff2");
10711096
10721097 font-weight: bold;
10731098
@@ -1120,12 +1145,14 @@ video {
11201145 }
11211146
11221147 ::view-transition-old(slide-it) {
1123- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
1148+ animation:
1149+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
11241150 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
11251151 }
11261152
11271153 ::view-transition-new(slide-it) {
1128- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
1154+ animation:
1155+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
11291156 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
11301157 }
11311158
@@ -1284,4 +1311,4 @@ video {
12841311 font-size: 3rem;
12851312 line-height: 1;
12861313 }
1287-}⏎̸
1314+}
M internal/webui/public/assets/js/custom.js +1 -67
@@ -1,67 +1,1 @@
1-document.body.addEventListener("htmx:afterOnLoad", function(event) {
2- if (
3- event.target.id === "image-manager" &&
4- event.detail.xhr.responseText === "Success"
5- ) {
6- setTimeout(function() {
7- location.reload();
8- }, 2000);
9- }
10-});
11-
12-function toggleButtonState(event) {
13- const button = event.currentTarget;
14- const isActive = button.getAttribute('data-active') === 'true';
15-
16- // Toggle the state
17- button.setAttribute('data-active', !isActive);
18-
19- // Get the related component based on hx-target attribute
20- const hxTarget = button.getAttribute('hx-target');
21- const relatedComponent = document.querySelector(hxTarget);
22-
23- if (relatedComponent) {
24- if (isActive) {
25- relatedComponent.style.display = 'none'; // Hide
26- } else {
27- relatedComponent.style.display = 'block'; // Show
28-
29- // Adjust textarea height if it exists within the relatedComponent
30- const textarea = relatedComponent.querySelector("#container_config");
31- if (textarea) {
32- textarea.style.height = ""; // Reset the height
33- textarea.style.height = textarea.scrollHeight + "px";
34- }
35- }
36- }
37-}
38-
39-// Attach event listeners to buttons that needs to toggle state
40-document.body.addEventListener("htmx:afterSwap", function (event) {
41- const buttons = document.querySelectorAll('[id^="edit-button-"], [id^="add-button-img-"]', '[id^="upload-button"]');
42- buttons.forEach(button => {
43- button.addEventListener('click', toggleButtonState);
44- });
45-});
46-
47-// // Attach event listeners to buttons that needs to set a full location reload
48-// document.body.addEventListener("htmx:afterSwap", function (event) {
49-// const xhr = event.detail.xhr;
50-// const statusCode = xhr.status;
51-
52-// // Check for 500 status code
53-// if (statusCode >= 500 && statusCode < 600) {
54-// // Handle the error (e.g., log it or show an alert)
55-// console.error("Server returned error: ", statusCode);
56-// return; // Do not reload the page
57-// }
58-
59-// const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]');
60-// buttons.forEach(button => {
61-// button.addEventListener('click', function(event) {
62-// setTimeout(function() {
63-// location.reload();
64-// }, 5000);
65-// });
66-// });
67-// });
1+// switched to hyperscript for the most part⏎̸
A internal/webui/public/libs/htmx.js +3789 -0
@@ -0,0 +1,3789 @@
1+// UMD insanity
2+// This code sets up support for (in order) AMD, ES6 modules, and globals.
3+(function (root, factory) {
4+ //@ts-ignore
5+ if (typeof define === 'function' && define.amd) {
6+ // AMD. Register as an anonymous module.
7+ //@ts-ignore
8+ define([], factory);
9+ } else if (typeof module === 'object' && module.exports) {
10+ // Node. Does not work with strict CommonJS, but
11+ // only CommonJS-like environments that support module.exports,
12+ // like Node.
13+ module.exports = factory();
14+ } else {
15+ // Browser globals
16+ root.htmx = root.htmx || factory();
17+ }
18+}(typeof self !== 'undefined' ? self : this, function () {
19+return (function () {
20+ 'use strict';
21+
22+ // Public API
23+ //** @type {import("./htmx").HtmxApi} */
24+ // TODO: list all methods in public API
25+ var htmx = {
26+ onLoad: onLoadHelper,
27+ process: processNode,
28+ on: addEventListenerImpl,
29+ off: removeEventListenerImpl,
30+ trigger : triggerEvent,
31+ ajax : ajaxHelper,
32+ find : find,
33+ findAll : findAll,
34+ closest : closest,
35+ values : function(elt, type){
36+ var inputValues = getInputValues(elt, type || "post");
37+ return inputValues.values;
38+ },
39+ remove : removeElement,
40+ addClass : addClassToElement,
41+ removeClass : removeClassFromElement,
42+ toggleClass : toggleClassOnElement,
43+ takeClass : takeClassForElement,
44+ defineExtension : defineExtension,
45+ removeExtension : removeExtension,
46+ logAll : logAll,
47+ logNone : logNone,
48+ logger : null,
49+ config : {
50+ historyEnabled:true,
51+ historyCacheSize:10,
52+ refreshOnHistoryMiss:false,
53+ defaultSwapStyle:'innerHTML',
54+ defaultSwapDelay:0,
55+ defaultSettleDelay:20,
56+ includeIndicatorStyles:true,
57+ indicatorClass:'htmx-indicator',
58+ requestClass:'htmx-request',
59+ addedClass:'htmx-added',
60+ settlingClass:'htmx-settling',
61+ swappingClass:'htmx-swapping',
62+ allowEval:true,
63+ allowScriptTags:true,
64+ inlineScriptNonce:'',
65+ attributesToSettle:["class", "style", "width", "height"],
66+ withCredentials:false,
67+ timeout:0,
68+ wsReconnectDelay: 'full-jitter',
69+ wsBinaryType: 'blob',
70+ disableSelector: "[hx-disable], [data-hx-disable]",
71+ useTemplateFragments: false,
72+ scrollBehavior: 'smooth',
73+ defaultFocusScroll: false,
74+ getCacheBusterParam: false,
75+ globalViewTransitions: false,
76+ methodsThatUseUrlParams: ["get"],
77+ selfRequestsOnly: false
78+ },
79+ parseInterval:parseInterval,
80+ _:internalEval,
81+ createEventSource: function(url){
82+ return new EventSource(url, {withCredentials:true})
83+ },
84+ createWebSocket: function(url){
85+ var sock = new WebSocket(url, []);
86+ sock.binaryType = htmx.config.wsBinaryType;
87+ return sock;
88+ },
89+ version: "1.9.6"
90+ };
91+
92+ /** @type {import("./htmx").HtmxInternalApi} */
93+ var internalAPI = {
94+ addTriggerHandler: addTriggerHandler,
95+ bodyContains: bodyContains,
96+ canAccessLocalStorage: canAccessLocalStorage,
97+ findThisElement: findThisElement,
98+ filterValues: filterValues,
99+ hasAttribute: hasAttribute,
100+ getAttributeValue: getAttributeValue,
101+ getClosestAttributeValue: getClosestAttributeValue,
102+ getClosestMatch: getClosestMatch,
103+ getExpressionVars: getExpressionVars,
104+ getHeaders: getHeaders,
105+ getInputValues: getInputValues,
106+ getInternalData: getInternalData,
107+ getSwapSpecification: getSwapSpecification,
108+ getTriggerSpecs: getTriggerSpecs,
109+ getTarget: getTarget,
110+ makeFragment: makeFragment,
111+ mergeObjects: mergeObjects,
112+ makeSettleInfo: makeSettleInfo,
113+ oobSwap: oobSwap,
114+ querySelectorExt: querySelectorExt,
115+ selectAndSwap: selectAndSwap,
116+ settleImmediately: settleImmediately,
117+ shouldCancel: shouldCancel,
118+ triggerEvent: triggerEvent,
119+ triggerErrorEvent: triggerErrorEvent,
120+ withExtensions: withExtensions,
121+ }
122+
123+ var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
124+ var VERB_SELECTOR = VERBS.map(function(verb){
125+ return "[hx-" + verb + "], [data-hx-" + verb + "]"
126+ }).join(", ");
127+
128+ //====================================================================
129+ // Utilities
130+ //====================================================================
131+
132+ function parseInterval(str) {
133+ if (str == undefined) {
134+ return undefined
135+ }
136+ if (str.slice(-2) == "ms") {
137+ return parseFloat(str.slice(0,-2)) || undefined
138+ }
139+ if (str.slice(-1) == "s") {
140+ return (parseFloat(str.slice(0,-1)) * 1000) || undefined
141+ }
142+ if (str.slice(-1) == "m") {
143+ return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined
144+ }
145+ return parseFloat(str) || undefined
146+ }
147+
148+ /**
149+ * @param {HTMLElement} elt
150+ * @param {string} name
151+ * @returns {(string | null)}
152+ */
153+ function getRawAttribute(elt, name) {
154+ return elt.getAttribute && elt.getAttribute(name);
155+ }
156+
157+ // resolve with both hx and data-hx prefixes
158+ function hasAttribute(elt, qualifiedName) {
159+ return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
160+ elt.hasAttribute("data-" + qualifiedName));
161+ }
162+
163+ /**
164+ *
165+ * @param {HTMLElement} elt
166+ * @param {string} qualifiedName
167+ * @returns {(string | null)}
168+ */
169+ function getAttributeValue(elt, qualifiedName) {
170+ return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
171+ }
172+
173+ /**
174+ * @param {HTMLElement} elt
175+ * @returns {HTMLElement | null}
176+ */
177+ function parentElt(elt) {
178+ return elt.parentElement;
179+ }
180+
181+ /**
182+ * @returns {Document}
183+ */
184+ function getDocument() {
185+ return document;
186+ }
187+
188+ /**
189+ * @param {HTMLElement} elt
190+ * @param {(e:HTMLElement) => boolean} condition
191+ * @returns {HTMLElement | null}
192+ */
193+ function getClosestMatch(elt, condition) {
194+ while (elt && !condition(elt)) {
195+ elt = parentElt(elt);
196+ }
197+
198+ return elt ? elt : null;
199+ }
200+
201+ function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName){
202+ var attributeValue = getAttributeValue(ancestor, attributeName);
203+ var disinherit = getAttributeValue(ancestor, "hx-disinherit");
204+ if (initialElement !== ancestor && disinherit && (disinherit === "*" || disinherit.split(" ").indexOf(attributeName) >= 0)) {
205+ return "unset";
206+ } else {
207+ return attributeValue
208+ }
209+ }
210+
211+ /**
212+ * @param {HTMLElement} elt
213+ * @param {string} attributeName
214+ * @returns {string | null}
215+ */
216+ function getClosestAttributeValue(elt, attributeName) {
217+ var closestAttr = null;
218+ getClosestMatch(elt, function (e) {
219+ return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName);
220+ });
221+ if (closestAttr !== "unset") {
222+ return closestAttr;
223+ }
224+ }
225+
226+ /**
227+ * @param {HTMLElement} elt
228+ * @param {string} selector
229+ * @returns {boolean}
230+ */
231+ function matches(elt, selector) {
232+ // @ts-ignore: non-standard properties for browser compatibility
233+ // noinspection JSUnresolvedVariable
234+ var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector;
235+ return matchesFunction && matchesFunction.call(elt, selector);
236+ }
237+
238+ /**
239+ * @param {string} str
240+ * @returns {string}
241+ */
242+ function getStartTag(str) {
243+ var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
244+ var match = tagMatcher.exec( str );
245+ if (match) {
246+ return match[1].toLowerCase();
247+ } else {
248+ return "";
249+ }
250+ }
251+
252+ /**
253+ *
254+ * @param {string} resp
255+ * @param {number} depth
256+ * @returns {Element}
257+ */
258+ function parseHTML(resp, depth) {
259+ var parser = new DOMParser();
260+ var responseDoc = parser.parseFromString(resp, "text/html");
261+
262+ /** @type {Element} */
263+ var responseNode = responseDoc.body;
264+ while (depth > 0) {
265+ depth--;
266+ // @ts-ignore
267+ responseNode = responseNode.firstChild;
268+ }
269+ if (responseNode == null) {
270+ // @ts-ignore
271+ responseNode = getDocument().createDocumentFragment();
272+ }
273+ return responseNode;
274+ }
275+
276+ function aFullPageResponse(resp) {
277+ return resp.match(/<body/);
278+ }
279+
280+ /**
281+ *
282+ * @param {string} resp
283+ * @returns {Element}
284+ */
285+ function makeFragment(resp) {
286+ var partialResponse = !aFullPageResponse(resp);
287+ if (htmx.config.useTemplateFragments && partialResponse) {
288+ var documentFragment = parseHTML("<body><template>" + resp + "</template></body>", 0);
289+ // @ts-ignore type mismatch between DocumentFragment and Element.
290+ // TODO: Are these close enough for htmx to use interchangeably?
291+ return documentFragment.querySelector('template').content;
292+ } else {
293+ var startTag = getStartTag(resp);
294+ switch (startTag) {
295+ case "thead":
296+ case "tbody":
297+ case "tfoot":
298+ case "colgroup":
299+ case "caption":
300+ return parseHTML("<table>" + resp + "</table>", 1);
301+ case "col":
302+ return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
303+ case "tr":
304+ return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
305+ case "td":
306+ case "th":
307+ return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
308+ case "script":
309+ case "style":
310+ return parseHTML("<div>" + resp + "</div>", 1);
311+ default:
312+ return parseHTML(resp, 0);
313+ }
314+ }
315+ }
316+
317+ /**
318+ * @param {Function} func
319+ */
320+ function maybeCall(func){
321+ if(func) {
322+ func();
323+ }
324+ }
325+
326+ /**
327+ * @param {any} o
328+ * @param {string} type
329+ * @returns
330+ */
331+ function isType(o, type) {
332+ return Object.prototype.toString.call(o) === "[object " + type + "]";
333+ }
334+
335+ /**
336+ * @param {*} o
337+ * @returns {o is Function}
338+ */
339+ function isFunction(o) {
340+ return isType(o, "Function");
341+ }
342+
343+ /**
344+ * @param {*} o
345+ * @returns {o is Object}
346+ */
347+ function isRawObject(o) {
348+ return isType(o, "Object");
349+ }
350+
351+ /**
352+ * getInternalData retrieves "private" data stored by htmx within an element
353+ * @param {HTMLElement} elt
354+ * @returns {*}
355+ */
356+ function getInternalData(elt) {
357+ var dataProp = 'htmx-internal-data';
358+ var data = elt[dataProp];
359+ if (!data) {
360+ data = elt[dataProp] = {};
361+ }
362+ return data;
363+ }
364+
365+ /**
366+ * toArray converts an ArrayLike object into a real array.
367+ * @param {ArrayLike} arr
368+ * @returns {any[]}
369+ */
370+ function toArray(arr) {
371+ var returnArr = [];
372+ if (arr) {
373+ for (var i = 0; i < arr.length; i++) {
374+ returnArr.push(arr[i]);
375+ }
376+ }
377+ return returnArr
378+ }
379+
380+ function forEach(arr, func) {
381+ if (arr) {
382+ for (var i = 0; i < arr.length; i++) {
383+ func(arr[i]);
384+ }
385+ }
386+ }
387+
388+ function isScrolledIntoView(el) {
389+ var rect = el.getBoundingClientRect();
390+ var elemTop = rect.top;
391+ var elemBottom = rect.bottom;
392+ return elemTop < window.innerHeight && elemBottom >= 0;
393+ }
394+
395+ function bodyContains(elt) {
396+ // IE Fix
397+ if (elt.getRootNode && elt.getRootNode() instanceof window.ShadowRoot) {
398+ return getDocument().body.contains(elt.getRootNode().host);
399+ } else {
400+ return getDocument().body.contains(elt);
401+ }
402+ }
403+
404+ function splitOnWhitespace(trigger) {
405+ return trigger.trim().split(/\s+/);
406+ }
407+
408+ /**
409+ * mergeObjects takes all of the keys from
410+ * obj2 and duplicates them into obj1
411+ * @param {Object} obj1
412+ * @param {Object} obj2
413+ * @returns {Object}
414+ */
415+ function mergeObjects(obj1, obj2) {
416+ for (var key in obj2) {
417+ if (obj2.hasOwnProperty(key)) {
418+ obj1[key] = obj2[key];
419+ }
420+ }
421+ return obj1;
422+ }
423+
424+ function parseJSON(jString) {
425+ try {
426+ return JSON.parse(jString);
427+ } catch(error) {
428+ logError(error);
429+ return null;
430+ }
431+ }
432+
433+ function canAccessLocalStorage() {
434+ var test = 'htmx:localStorageTest';
435+ try {
436+ localStorage.setItem(test, test);
437+ localStorage.removeItem(test);
438+ return true;
439+ } catch(e) {
440+ return false;
441+ }
442+ }
443+
444+ function normalizePath(path) {
445+ try {
446+ var url = new URL(path);
447+ if (url) {
448+ path = url.pathname + url.search;
449+ }
450+ // remove trailing slash, unless index page
451+ if (!path.match('^/$')) {
452+ path = path.replace(/\/+$/, '');
453+ }
454+ return path;
455+ } catch (e) {
456+ // be kind to IE11, which doesn't support URL()
457+ return path;
458+ }
459+ }
460+
461+ //==========================================================================================
462+ // public API
463+ //==========================================================================================
464+
465+ function internalEval(str){
466+ return maybeEval(getDocument().body, function () {
467+ return eval(str);
468+ });
469+ }
470+
471+ function onLoadHelper(callback) {
472+ var value = htmx.on("htmx:load", function(evt) {
473+ callback(evt.detail.elt);
474+ });
475+ return value;
476+ }
477+
478+ function logAll(){
479+ htmx.logger = function(elt, event, data) {
480+ if(console) {
481+ console.log(event, elt, data);
482+ }
483+ }
484+ }
485+
486+ function logNone() {
487+ htmx.logger = null
488+ }
489+
490+ function find(eltOrSelector, selector) {
491+ if (selector) {
492+ return eltOrSelector.querySelector(selector);
493+ } else {
494+ return find(getDocument(), eltOrSelector);
495+ }
496+ }
497+
498+ function findAll(eltOrSelector, selector) {
499+ if (selector) {
500+ return eltOrSelector.querySelectorAll(selector);
501+ } else {
502+ return findAll(getDocument(), eltOrSelector);
503+ }
504+ }
505+
506+ function removeElement(elt, delay) {
507+ elt = resolveTarget(elt);
508+ if (delay) {
509+ setTimeout(function(){
510+ removeElement(elt);
511+ elt = null;
512+ }, delay);
513+ } else {
514+ elt.parentElement.removeChild(elt);
515+ }
516+ }
517+
518+ function addClassToElement(elt, clazz, delay) {
519+ elt = resolveTarget(elt);
520+ if (delay) {
521+ setTimeout(function(){
522+ addClassToElement(elt, clazz);
523+ elt = null;
524+ }, delay);
525+ } else {
526+ elt.classList && elt.classList.add(clazz);
527+ }
528+ }
529+
530+ function removeClassFromElement(elt, clazz, delay) {
531+ elt = resolveTarget(elt);
532+ if (delay) {
533+ setTimeout(function(){
534+ removeClassFromElement(elt, clazz);
535+ elt = null;
536+ }, delay);
537+ } else {
538+ if (elt.classList) {
539+ elt.classList.remove(clazz);
540+ // if there are no classes left, remove the class attribute
541+ if (elt.classList.length === 0) {
542+ elt.removeAttribute("class");
543+ }
544+ }
545+ }
546+ }
547+
548+ function toggleClassOnElement(elt, clazz) {
549+ elt = resolveTarget(elt);
550+ elt.classList.toggle(clazz);
551+ }
552+
553+ function takeClassForElement(elt, clazz) {
554+ elt = resolveTarget(elt);
555+ forEach(elt.parentElement.children, function(child){
556+ removeClassFromElement(child, clazz);
557+ })
558+ addClassToElement(elt, clazz);
559+ }
560+
561+ function closest(elt, selector) {
562+ elt = resolveTarget(elt);
563+ if (elt.closest) {
564+ return elt.closest(selector);
565+ } else {
566+ // TODO remove when IE goes away
567+ do{
568+ if (elt == null || matches(elt, selector)){
569+ return elt;
570+ }
571+ }
572+ while (elt = elt && parentElt(elt));
573+ return null;
574+ }
575+ }
576+
577+ function startsWith(str, prefix) {
578+ return str.substring(0, prefix.length) === prefix
579+ }
580+
581+ function endsWith(str, suffix) {
582+ return str.substring(str.length - suffix.length) === suffix
583+ }
584+
585+ function normalizeSelector(selector) {
586+ var trimmedSelector = selector.trim();
587+ if (startsWith(trimmedSelector, "<") && endsWith(trimmedSelector, "/>")) {
588+ return trimmedSelector.substring(1, trimmedSelector.length - 2);
589+ } else {
590+ return trimmedSelector;
591+ }
592+ }
593+
594+ function querySelectorAllExt(elt, selector) {
595+ if (selector.indexOf("closest ") === 0) {
596+ return [closest(elt, normalizeSelector(selector.substr(8)))];
597+ } else if (selector.indexOf("find ") === 0) {
598+ return [find(elt, normalizeSelector(selector.substr(5)))];
599+ } else if (selector.indexOf("next ") === 0) {
600+ return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))];
601+ } else if (selector.indexOf("previous ") === 0) {
602+ return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))];
603+ } else if (selector === 'document') {
604+ return [document];
605+ } else if (selector === 'window') {
606+ return [window];
607+ } else if (selector === 'body') {
608+ return [document.body];
609+ } else {
610+ return getDocument().querySelectorAll(normalizeSelector(selector));
611+ }
612+ }
613+
614+ var scanForwardQuery = function(start, match) {
615+ var results = getDocument().querySelectorAll(match);
616+ for (var i = 0; i < results.length; i++) {
617+ var elt = results[i];
618+ if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) {
619+ return elt;
620+ }
621+ }
622+ }
623+
624+ var scanBackwardsQuery = function(start, match) {
625+ var results = getDocument().querySelectorAll(match);
626+ for (var i = results.length - 1; i >= 0; i--) {
627+ var elt = results[i];
628+ if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) {
629+ return elt;
630+ }
631+ }
632+ }
633+
634+ function querySelectorExt(eltOrSelector, selector) {
635+ if (selector) {
636+ return querySelectorAllExt(eltOrSelector, selector)[0];
637+ } else {
638+ return querySelectorAllExt(getDocument().body, eltOrSelector)[0];
639+ }
640+ }
641+
642+ function resolveTarget(arg2) {
643+ if (isType(arg2, 'String')) {
644+ return find(arg2);
645+ } else {
646+ return arg2;
647+ }
648+ }
649+
650+ function processEventArgs(arg1, arg2, arg3) {
651+ if (isFunction(arg2)) {
652+ return {
653+ target: getDocument().body,
654+ event: arg1,
655+ listener: arg2
656+ }
657+ } else {
658+ return {
659+ target: resolveTarget(arg1),
660+ event: arg2,
661+ listener: arg3
662+ }
663+ }
664+
665+ }
666+
667+ function addEventListenerImpl(arg1, arg2, arg3) {
668+ ready(function(){
669+ var eventArgs = processEventArgs(arg1, arg2, arg3);
670+ eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener);
671+ })
672+ var b = isFunction(arg2);
673+ return b ? arg2 : arg3;
674+ }
675+
676+ function removeEventListenerImpl(arg1, arg2, arg3) {
677+ ready(function(){
678+ var eventArgs = processEventArgs(arg1, arg2, arg3);
679+ eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener);
680+ })
681+ return isFunction(arg2) ? arg2 : arg3;
682+ }
683+
684+ //====================================================================
685+ // Node processing
686+ //====================================================================
687+
688+ var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors
689+ function findAttributeTargets(elt, attrName) {
690+ var attrTarget = getClosestAttributeValue(elt, attrName);
691+ if (attrTarget) {
692+ if (attrTarget === "this") {
693+ return [findThisElement(elt, attrName)];
694+ } else {
695+ var result = querySelectorAllExt(elt, attrTarget);
696+ if (result.length === 0) {
697+ logError('The selector "' + attrTarget + '" on ' + attrName + " returned no matches!");
698+ return [DUMMY_ELT]
699+ } else {
700+ return result;
701+ }
702+ }
703+ }
704+ }
705+
706+ function findThisElement(elt, attribute){
707+ return getClosestMatch(elt, function (elt) {
708+ return getAttributeValue(elt, attribute) != null;
709+ })
710+ }
711+
712+ function getTarget(elt) {
713+ var targetStr = getClosestAttributeValue(elt, "hx-target");
714+ if (targetStr) {
715+ if (targetStr === "this") {
716+ return findThisElement(elt,'hx-target');
717+ } else {
718+ return querySelectorExt(elt, targetStr)
719+ }
720+ } else {
721+ var data = getInternalData(elt);
722+ if (data.boosted) {
723+ return getDocument().body;
724+ } else {
725+ return elt;
726+ }
727+ }
728+ }
729+
730+ function shouldSettleAttribute(name) {
731+ var attributesToSettle = htmx.config.attributesToSettle;
732+ for (var i = 0; i < attributesToSettle.length; i++) {
733+ if (name === attributesToSettle[i]) {
734+ return true;
735+ }
736+ }
737+ return false;
738+ }
739+
740+ function cloneAttributes(mergeTo, mergeFrom) {
741+ forEach(mergeTo.attributes, function (attr) {
742+ if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
743+ mergeTo.removeAttribute(attr.name)
744+ }
745+ });
746+ forEach(mergeFrom.attributes, function (attr) {
747+ if (shouldSettleAttribute(attr.name)) {
748+ mergeTo.setAttribute(attr.name, attr.value);
749+ }
750+ });
751+ }
752+
753+ function isInlineSwap(swapStyle, target) {
754+ var extensions = getExtensions(target);
755+ for (var i = 0; i < extensions.length; i++) {
756+ var extension = extensions[i];
757+ try {
758+ if (extension.isInlineSwap(swapStyle)) {
759+ return true;
760+ }
761+ } catch(e) {
762+ logError(e);
763+ }
764+ }
765+ return swapStyle === "outerHTML";
766+ }
767+
768+ /**
769+ *
770+ * @param {string} oobValue
771+ * @param {HTMLElement} oobElement
772+ * @param {*} settleInfo
773+ * @returns
774+ */
775+ function oobSwap(oobValue, oobElement, settleInfo) {
776+ var selector = "#" + getRawAttribute(oobElement, "id");
777+ var swapStyle = "outerHTML";
778+ if (oobValue === "true") {
779+ // do nothing
780+ } else if (oobValue.indexOf(":") > 0) {
781+ swapStyle = oobValue.substr(0, oobValue.indexOf(":"));
782+ selector = oobValue.substr(oobValue.indexOf(":") + 1, oobValue.length);
783+ } else {
784+ swapStyle = oobValue;
785+ }
786+
787+ var targets = getDocument().querySelectorAll(selector);
788+ if (targets) {
789+ forEach(
790+ targets,
791+ function (target) {
792+ var fragment;
793+ var oobElementClone = oobElement.cloneNode(true);
794+ fragment = getDocument().createDocumentFragment();
795+ fragment.appendChild(oobElementClone);
796+ if (!isInlineSwap(swapStyle, target)) {
797+ fragment = oobElementClone; // if this is not an inline swap, we use the content of the node, not the node itself
798+ }
799+
800+ var beforeSwapDetails = {shouldSwap: true, target: target, fragment:fragment };
801+ if (!triggerEvent(target, 'htmx:oobBeforeSwap', beforeSwapDetails)) return;
802+
803+ target = beforeSwapDetails.target; // allow re-targeting
804+ if (beforeSwapDetails['shouldSwap']){
805+ swap(swapStyle, target, target, fragment, settleInfo);
806+ }
807+ forEach(settleInfo.elts, function (elt) {
808+ triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails);
809+ });
810+ }
811+ );
812+ oobElement.parentNode.removeChild(oobElement);
813+ } else {
814+ oobElement.parentNode.removeChild(oobElement);
815+ triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: oobElement});
816+ }
817+ return oobValue;
818+ }
819+
820+ function handleOutOfBandSwaps(elt, fragment, settleInfo) {
821+ var oobSelects = getClosestAttributeValue(elt, "hx-select-oob");
822+ if (oobSelects) {
823+ var oobSelectValues = oobSelects.split(",");
824+ for (let i = 0; i < oobSelectValues.length; i++) {
825+ var oobSelectValue = oobSelectValues[i].split(":", 2);
826+ var id = oobSelectValue[0].trim();
827+ if (id.indexOf("#") === 0) {
828+ id = id.substring(1);
829+ }
830+ var oobValue = oobSelectValue[1] || "true";
831+ var oobElement = fragment.querySelector("#" + id);
832+ if (oobElement) {
833+ oobSwap(oobValue, oobElement, settleInfo);
834+ }
835+ }
836+ }
837+ forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) {
838+ var oobValue = getAttributeValue(oobElement, "hx-swap-oob");
839+ if (oobValue != null) {
840+ oobSwap(oobValue, oobElement, settleInfo);
841+ }
842+ });
843+ }
844+
845+ function handlePreservedElements(fragment) {
846+ forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
847+ var id = getAttributeValue(preservedElt, "id");
848+ var oldElt = getDocument().getElementById(id);
849+ if (oldElt != null) {
850+ preservedElt.parentNode.replaceChild(oldElt, preservedElt);
851+ }
852+ });
853+ }
854+
855+ function handleAttributes(parentNode, fragment, settleInfo) {
856+ forEach(fragment.querySelectorAll("[id]"), function (newNode) {
857+ var id = getRawAttribute(newNode, "id")
858+ if (id && id.length > 0) {
859+ var normalizedId = id.replace("'", "\\'");
860+ var normalizedTag = newNode.tagName.replace(':', '\\:');
861+ var oldNode = parentNode.querySelector(normalizedTag + "[id='" + normalizedId + "']");
862+ if (oldNode && oldNode !== parentNode) {
863+ var newAttributes = newNode.cloneNode();
864+ cloneAttributes(newNode, oldNode);
865+ settleInfo.tasks.push(function () {
866+ cloneAttributes(newNode, newAttributes);
867+ });
868+ }
869+ }
870+ });
871+ }
872+
873+ function makeAjaxLoadTask(child) {
874+ return function () {
875+ removeClassFromElement(child, htmx.config.addedClass);
876+ processNode(child);
877+ processScripts(child);
878+ processFocus(child)
879+ triggerEvent(child, 'htmx:load');
880+ };
881+ }
882+
883+ function processFocus(child) {
884+ var autofocus = "[autofocus]";
885+ var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
886+ if (autoFocusedElt != null) {
887+ autoFocusedElt.focus();
888+ }
889+ }
890+
891+ function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
892+ handleAttributes(parentNode, fragment, settleInfo);
893+ while(fragment.childNodes.length > 0){
894+ var child = fragment.firstChild;
895+ addClassToElement(child, htmx.config.addedClass);
896+ parentNode.insertBefore(child, insertBefore);
897+ if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
898+ settleInfo.tasks.push(makeAjaxLoadTask(child));
899+ }
900+ }
901+ }
902+
903+ // based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0,
904+ // derived from Java's string hashcode implementation
905+ function stringHash(string, hash) {
906+ var char = 0;
907+ while (char < string.length){
908+ hash = (hash << 5) - hash + string.charCodeAt(char++) | 0; // bitwise or ensures we have a 32-bit int
909+ }
910+ return hash;
911+ }
912+
913+ function attributeHash(elt) {
914+ var hash = 0;
915+ // IE fix
916+ if (elt.attributes) {
917+ for (var i = 0; i < elt.attributes.length; i++) {
918+ var attribute = elt.attributes[i];
919+ if(attribute.value){ // only include attributes w/ actual values (empty is same as non-existent)
920+ hash = stringHash(attribute.name, hash);
921+ hash = stringHash(attribute.value, hash);
922+ }
923+ }
924+ }
925+ return hash;
926+ }
927+
928+ function deInitOnHandlers(elt) {
929+ var internalData = getInternalData(elt);
930+ if (internalData.onHandlers) {
931+ for (let i = 0; i < internalData.onHandlers.length; i++) {
932+ const handlerInfo = internalData.onHandlers[i];
933+ elt.removeEventListener(handlerInfo.event, handlerInfo.listener);
934+ }
935+ delete internalData.onHandlers
936+ }
937+ }
938+
939+ function deInitNode(element) {
940+ var internalData = getInternalData(element);
941+ if (internalData.timeout) {
942+ clearTimeout(internalData.timeout);
943+ }
944+ if (internalData.webSocket) {
945+ internalData.webSocket.close();
946+ }
947+ if (internalData.sseEventSource) {
948+ internalData.sseEventSource.close();
949+ }
950+ if (internalData.listenerInfos) {
951+ forEach(internalData.listenerInfos, function (info) {
952+ if (info.on) {
953+ info.on.removeEventListener(info.trigger, info.listener);
954+ }
955+ });
956+ }
957+ if (internalData.initHash) {
958+ internalData.initHash = null
959+ }
960+ deInitOnHandlers(element);
961+ }
962+
963+ function cleanUpElement(element) {
964+ triggerEvent(element, "htmx:beforeCleanupElement")
965+ deInitNode(element);
966+ if (element.children) { // IE
967+ forEach(element.children, function(child) { cleanUpElement(child) });
968+ }
969+ }
970+
971+ function swapOuterHTML(target, fragment, settleInfo) {
972+ if (target.tagName === "BODY") {
973+ return swapInnerHTML(target, fragment, settleInfo);
974+ } else {
975+ // @type {HTMLElement}
976+ var newElt
977+ var eltBeforeNewContent = target.previousSibling;
978+ insertNodesBefore(parentElt(target), target, fragment, settleInfo);
979+ if (eltBeforeNewContent == null) {
980+ newElt = parentElt(target).firstChild;
981+ } else {
982+ newElt = eltBeforeNewContent.nextSibling;
983+ }
984+ getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
985+ settleInfo.elts = settleInfo.elts.filter(function(e) { return e != target });
986+ while(newElt && newElt !== target) {
987+ if (newElt.nodeType === Node.ELEMENT_NODE) {
988+ settleInfo.elts.push(newElt);
989+ }
990+ newElt = newElt.nextElementSibling;
991+ }
992+ cleanUpElement(target);
993+ parentElt(target).removeChild(target);
994+ }
995+ }
996+
997+ function swapAfterBegin(target, fragment, settleInfo) {
998+ return insertNodesBefore(target, target.firstChild, fragment, settleInfo);
999+ }
1000+
1001+ function swapBeforeBegin(target, fragment, settleInfo) {
1002+ return insertNodesBefore(parentElt(target), target, fragment, settleInfo);
1003+ }
1004+
1005+ function swapBeforeEnd(target, fragment, settleInfo) {
1006+ return insertNodesBefore(target, null, fragment, settleInfo);
1007+ }
1008+
1009+ function swapAfterEnd(target, fragment, settleInfo) {
1010+ return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo);
1011+ }
1012+ function swapDelete(target, fragment, settleInfo) {
1013+ cleanUpElement(target);
1014+ return parentElt(target).removeChild(target);
1015+ }
1016+
1017+ function swapInnerHTML(target, fragment, settleInfo) {
1018+ var firstChild = target.firstChild;
1019+ insertNodesBefore(target, firstChild, fragment, settleInfo);
1020+ if (firstChild) {
1021+ while (firstChild.nextSibling) {
1022+ cleanUpElement(firstChild.nextSibling)
1023+ target.removeChild(firstChild.nextSibling);
1024+ }
1025+ cleanUpElement(firstChild)
1026+ target.removeChild(firstChild);
1027+ }
1028+ }
1029+
1030+ function maybeSelectFromResponse(elt, fragment, selectOverride) {
1031+ var selector = selectOverride || getClosestAttributeValue(elt, "hx-select");
1032+ if (selector) {
1033+ var newFragment = getDocument().createDocumentFragment();
1034+ forEach(fragment.querySelectorAll(selector), function (node) {
1035+ newFragment.appendChild(node);
1036+ });
1037+ fragment = newFragment;
1038+ }
1039+ return fragment;
1040+ }
1041+
1042+ function swap(swapStyle, elt, target, fragment, settleInfo) {
1043+ switch (swapStyle) {
1044+ case "none":
1045+ return;
1046+ case "outerHTML":
1047+ swapOuterHTML(target, fragment, settleInfo);
1048+ return;
1049+ case "afterbegin":
1050+ swapAfterBegin(target, fragment, settleInfo);
1051+ return;
1052+ case "beforebegin":
1053+ swapBeforeBegin(target, fragment, settleInfo);
1054+ return;
1055+ case "beforeend":
1056+ swapBeforeEnd(target, fragment, settleInfo);
1057+ return;
1058+ case "afterend":
1059+ swapAfterEnd(target, fragment, settleInfo);
1060+ return;
1061+ case "delete":
1062+ swapDelete(target, fragment, settleInfo);
1063+ return;
1064+ default:
1065+ var extensions = getExtensions(elt);
1066+ for (var i = 0; i < extensions.length; i++) {
1067+ var ext = extensions[i];
1068+ try {
1069+ var newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo);
1070+ if (newElements) {
1071+ if (typeof newElements.length !== 'undefined') {
1072+ // if handleSwap returns an array (like) of elements, we handle them
1073+ for (var j = 0; j < newElements.length; j++) {
1074+ var child = newElements[j];
1075+ if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
1076+ settleInfo.tasks.push(makeAjaxLoadTask(child));
1077+ }
1078+ }
1079+ }
1080+ return;
1081+ }
1082+ } catch (e) {
1083+ logError(e);
1084+ }
1085+ }
1086+ if (swapStyle === "innerHTML") {
1087+ swapInnerHTML(target, fragment, settleInfo);
1088+ } else {
1089+ swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo);
1090+ }
1091+ }
1092+ }
1093+
1094+ function findTitle(content) {
1095+ if (content.indexOf('<title') > -1) {
1096+ var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
1097+ var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
1098+
1099+ if (result) {
1100+ return result[2];
1101+ }
1102+ }
1103+ }
1104+
1105+ function selectAndSwap(swapStyle, target, elt, responseText, settleInfo, selectOverride) {
1106+ settleInfo.title = findTitle(responseText);
1107+ var fragment = makeFragment(responseText);
1108+ if (fragment) {
1109+ handleOutOfBandSwaps(elt, fragment, settleInfo);
1110+ fragment = maybeSelectFromResponse(elt, fragment, selectOverride);
1111+ handlePreservedElements(fragment);
1112+ return swap(swapStyle, elt, target, fragment, settleInfo);
1113+ }
1114+ }
1115+
1116+ function handleTrigger(xhr, header, elt) {
1117+ var triggerBody = xhr.getResponseHeader(header);
1118+ if (triggerBody.indexOf("{") === 0) {
1119+ var triggers = parseJSON(triggerBody);
1120+ for (var eventName in triggers) {
1121+ if (triggers.hasOwnProperty(eventName)) {
1122+ var detail = triggers[eventName];
1123+ if (!isRawObject(detail)) {
1124+ detail = {"value": detail}
1125+ }
1126+ triggerEvent(elt, eventName, detail);
1127+ }
1128+ }
1129+ } else {
1130+ var eventNames = triggerBody.split(",")
1131+ for (var i = 0; i < eventNames.length; i++) {
1132+ triggerEvent(elt, eventNames[i].trim(), []);
1133+ }
1134+ }
1135+ }
1136+
1137+ var WHITESPACE = /\s/;
1138+ var WHITESPACE_OR_COMMA = /[\s,]/;
1139+ var SYMBOL_START = /[_$a-zA-Z]/;
1140+ var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
1141+ var STRINGISH_START = ['"', "'", "/"];
1142+ var NOT_WHITESPACE = /[^\s]/;
1143+ function tokenizeString(str) {
1144+ var tokens = [];
1145+ var position = 0;
1146+ while (position < str.length) {
1147+ if(SYMBOL_START.exec(str.charAt(position))) {
1148+ var startPosition = position;
1149+ while (SYMBOL_CONT.exec(str.charAt(position + 1))) {
1150+ position++;
1151+ }
1152+ tokens.push(str.substr(startPosition, position - startPosition + 1));
1153+ } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) {
1154+ var startChar = str.charAt(position);
1155+ var startPosition = position;
1156+ position++;
1157+ while (position < str.length && str.charAt(position) !== startChar ) {
1158+ if (str.charAt(position) === "\\") {
1159+ position++;
1160+ }
1161+ position++;
1162+ }
1163+ tokens.push(str.substr(startPosition, position - startPosition + 1));
1164+ } else {
1165+ var symbol = str.charAt(position);
1166+ tokens.push(symbol);
1167+ }
1168+ position++;
1169+ }
1170+ return tokens;
1171+ }
1172+
1173+ function isPossibleRelativeReference(token, last, paramName) {
1174+ return SYMBOL_START.exec(token.charAt(0)) &&
1175+ token !== "true" &&
1176+ token !== "false" &&
1177+ token !== "this" &&
1178+ token !== paramName &&
1179+ last !== ".";
1180+ }
1181+
1182+ function maybeGenerateConditional(elt, tokens, paramName) {
1183+ if (tokens[0] === '[') {
1184+ tokens.shift();
1185+ var bracketCount = 1;
1186+ var conditionalSource = " return (function(" + paramName + "){ return (";
1187+ var last = null;
1188+ while (tokens.length > 0) {
1189+ var token = tokens[0];
1190+ if (token === "]") {
1191+ bracketCount--;
1192+ if (bracketCount === 0) {
1193+ if (last === null) {
1194+ conditionalSource = conditionalSource + "true";
1195+ }
1196+ tokens.shift();
1197+ conditionalSource += ")})";
1198+ try {
1199+ var conditionFunction = maybeEval(elt,function () {
1200+ return Function(conditionalSource)();
1201+ },
1202+ function(){return true})
1203+ conditionFunction.source = conditionalSource;
1204+ return conditionFunction;
1205+ } catch (e) {
1206+ triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource})
1207+ return null;
1208+ }
1209+ }
1210+ } else if (token === "[") {
1211+ bracketCount++;
1212+ }
1213+ if (isPossibleRelativeReference(token, last, paramName)) {
1214+ conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
1215+ } else {
1216+ conditionalSource = conditionalSource + token;
1217+ }
1218+ last = tokens.shift();
1219+ }
1220+ }
1221+ }
1222+
1223+ function consumeUntil(tokens, match) {
1224+ var result = "";
1225+ while (tokens.length > 0 && !tokens[0].match(match)) {
1226+ result += tokens.shift();
1227+ }
1228+ return result;
1229+ }
1230+
1231+ var INPUT_SELECTOR = 'input, textarea, select';
1232+
1233+ /**
1234+ * @param {HTMLElement} elt
1235+ * @returns {import("./htmx").HtmxTriggerSpecification[]}
1236+ */
1237+ function getTriggerSpecs(elt) {
1238+ var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
1239+ var triggerSpecs = [];
1240+ if (explicitTrigger) {
1241+ var tokens = tokenizeString(explicitTrigger);
1242+ do {
1243+ consumeUntil(tokens, NOT_WHITESPACE);
1244+ var initialLength = tokens.length;
1245+ var trigger = consumeUntil(tokens, /[,\[\s]/);
1246+ if (trigger !== "") {
1247+ if (trigger === "every") {
1248+ var every = {trigger: 'every'};
1249+ consumeUntil(tokens, NOT_WHITESPACE);
1250+ every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
1251+ consumeUntil(tokens, NOT_WHITESPACE);
1252+ var eventFilter = maybeGenerateConditional(elt, tokens, "event");
1253+ if (eventFilter) {
1254+ every.eventFilter = eventFilter;
1255+ }
1256+ triggerSpecs.push(every);
1257+ } else if (trigger.indexOf("sse:") === 0) {
1258+ triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
1259+ } else {
1260+ var triggerSpec = {trigger: trigger};
1261+ var eventFilter = maybeGenerateConditional(elt, tokens, "event");
1262+ if (eventFilter) {
1263+ triggerSpec.eventFilter = eventFilter;
1264+ }
1265+ while (tokens.length > 0 && tokens[0] !== ",") {
1266+ consumeUntil(tokens, NOT_WHITESPACE)
1267+ var token = tokens.shift();
1268+ if (token === "changed") {
1269+ triggerSpec.changed = true;
1270+ } else if (token === "once") {
1271+ triggerSpec.once = true;
1272+ } else if (token === "consume") {
1273+ triggerSpec.consume = true;
1274+ } else if (token === "delay" && tokens[0] === ":") {
1275+ tokens.shift();
1276+ triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
1277+ } else if (token === "from" && tokens[0] === ":") {
1278+ tokens.shift();
1279+ var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1280+ if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
1281+ tokens.shift();
1282+ from_arg +=
1283+ " " +
1284+ consumeUntil(
1285+ tokens,
1286+ WHITESPACE_OR_COMMA
1287+ );
1288+ }
1289+ triggerSpec.from = from_arg;
1290+ } else if (token === "target" && tokens[0] === ":") {
1291+ tokens.shift();
1292+ triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1293+ } else if (token === "throttle" && tokens[0] === ":") {
1294+ tokens.shift();
1295+ triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
1296+ } else if (token === "queue" && tokens[0] === ":") {
1297+ tokens.shift();
1298+ triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1299+ } else if ((token === "root" || token === "threshold") && tokens[0] === ":") {
1300+ tokens.shift();
1301+ triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1302+ } else {
1303+ triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
1304+ }
1305+ }
1306+ triggerSpecs.push(triggerSpec);
1307+ }
1308+ }
1309+ if (tokens.length === initialLength) {
1310+ triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
1311+ }
1312+ consumeUntil(tokens, NOT_WHITESPACE);
1313+ } while (tokens[0] === "," && tokens.shift())
1314+ }
1315+
1316+ if (triggerSpecs.length > 0) {
1317+ return triggerSpecs;
1318+ } else if (matches(elt, 'form')) {
1319+ return [{trigger: 'submit'}];
1320+ } else if (matches(elt, 'input[type="button"], input[type="submit"]')){
1321+ return [{trigger: 'click'}];
1322+ } else if (matches(elt, INPUT_SELECTOR)) {
1323+ return [{trigger: 'change'}];
1324+ } else {
1325+ return [{trigger: 'click'}];
1326+ }
1327+ }
1328+
1329+ function cancelPolling(elt) {
1330+ getInternalData(elt).cancelled = true;
1331+ }
1332+
1333+ function processPolling(elt, handler, spec) {
1334+ var nodeData = getInternalData(elt);
1335+ nodeData.timeout = setTimeout(function () {
1336+ if (bodyContains(elt) && nodeData.cancelled !== true) {
1337+ if (!maybeFilterEvent(spec, elt, makeEvent('hx:poll:trigger', {
1338+ triggerSpec: spec,
1339+ target: elt
1340+ }))) {
1341+ handler(elt);
1342+ }
1343+ processPolling(elt, handler, spec);
1344+ }
1345+ }, spec.pollInterval);
1346+ }
1347+
1348+ function isLocalLink(elt) {
1349+ return location.hostname === elt.hostname &&
1350+ getRawAttribute(elt,'href') &&
1351+ getRawAttribute(elt,'href').indexOf("#") !== 0;
1352+ }
1353+
1354+ function boostElement(elt, nodeData, triggerSpecs) {
1355+ if ((elt.tagName === "A" && isLocalLink(elt) && (elt.target === "" || elt.target === "_self")) || elt.tagName === "FORM") {
1356+ nodeData.boosted = true;
1357+ var verb, path;
1358+ if (elt.tagName === "A") {
1359+ verb = "get";
1360+ path = getRawAttribute(elt, 'href')
1361+ } else {
1362+ var rawAttribute = getRawAttribute(elt, "method");
1363+ verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
1364+ if (verb === "get") {
1365+ }
1366+ path = getRawAttribute(elt, 'action');
1367+ }
1368+ triggerSpecs.forEach(function(triggerSpec) {
1369+ addEventListener(elt, function(elt, evt) {
1370+ if (closest(elt, htmx.config.disableSelector)) {
1371+ cleanUpElement(elt)
1372+ return
1373+ }
1374+ issueAjaxRequest(verb, path, elt, evt)
1375+ }, nodeData, triggerSpec, true);
1376+ });
1377+ }
1378+ }
1379+
1380+ /**
1381+ *
1382+ * @param {Event} evt
1383+ * @param {HTMLElement} elt
1384+ * @returns
1385+ */
1386+ function shouldCancel(evt, elt) {
1387+ if (evt.type === "submit" || evt.type === "click") {
1388+ if (elt.tagName === "FORM") {
1389+ return true;
1390+ }
1391+ if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) {
1392+ return true;
1393+ }
1394+ if (elt.tagName === "A" && elt.href &&
1395+ (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf("#") !== 0)) {
1396+ return true;
1397+ }
1398+ }
1399+ return false;
1400+ }
1401+
1402+ function ignoreBoostedAnchorCtrlClick(elt, evt) {
1403+ return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && (evt.ctrlKey || evt.metaKey);
1404+ }
1405+
1406+ function maybeFilterEvent(triggerSpec, elt, evt) {
1407+ var eventFilter = triggerSpec.eventFilter;
1408+ if(eventFilter){
1409+ try {
1410+ return eventFilter.call(elt, evt) !== true;
1411+ } catch(e) {
1412+ triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source});
1413+ return true;
1414+ }
1415+ }
1416+ return false;
1417+ }
1418+
1419+ function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
1420+ var elementData = getInternalData(elt);
1421+ var eltsToListenOn;
1422+ if (triggerSpec.from) {
1423+ eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from);
1424+ } else {
1425+ eltsToListenOn = [elt];
1426+ }
1427+ // store the initial values of the elements, so we can tell if they change
1428+ if (triggerSpec.changed) {
1429+ eltsToListenOn.forEach(function (eltToListenOn) {
1430+ var eltToListenOnData = getInternalData(eltToListenOn);
1431+ eltToListenOnData.lastValue = eltToListenOn.value;
1432+ })
1433+ }
1434+ forEach(eltsToListenOn, function (eltToListenOn) {
1435+ var eventListener = function (evt) {
1436+ if (!bodyContains(elt)) {
1437+ eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener);
1438+ return;
1439+ }
1440+ if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
1441+ return;
1442+ }
1443+ if (explicitCancel || shouldCancel(evt, elt)) {
1444+ evt.preventDefault();
1445+ }
1446+ if (maybeFilterEvent(triggerSpec, elt, evt)) {
1447+ return;
1448+ }
1449+ var eventData = getInternalData(evt);
1450+ eventData.triggerSpec = triggerSpec;
1451+ if (eventData.handledFor == null) {
1452+ eventData.handledFor = [];
1453+ }
1454+ if (eventData.handledFor.indexOf(elt) < 0) {
1455+ eventData.handledFor.push(elt);
1456+ if (triggerSpec.consume) {
1457+ evt.stopPropagation();
1458+ }
1459+ if (triggerSpec.target && evt.target) {
1460+ if (!matches(evt.target, triggerSpec.target)) {
1461+ return;
1462+ }
1463+ }
1464+ if (triggerSpec.once) {
1465+ if (elementData.triggeredOnce) {
1466+ return;
1467+ } else {
1468+ elementData.triggeredOnce = true;
1469+ }
1470+ }
1471+ if (triggerSpec.changed) {
1472+ var eltToListenOnData = getInternalData(eltToListenOn)
1473+ if (eltToListenOnData.lastValue === eltToListenOn.value) {
1474+ return;
1475+ }
1476+ eltToListenOnData.lastValue = eltToListenOn.value
1477+ }
1478+ if (elementData.delayed) {
1479+ clearTimeout(elementData.delayed);
1480+ }
1481+ if (elementData.throttle) {
1482+ return;
1483+ }
1484+
1485+ if (triggerSpec.throttle) {
1486+ if (!elementData.throttle) {
1487+ handler(elt, evt);
1488+ elementData.throttle = setTimeout(function () {
1489+ elementData.throttle = null;
1490+ }, triggerSpec.throttle);
1491+ }
1492+ } else if (triggerSpec.delay) {
1493+ elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
1494+ } else {
1495+ triggerEvent(elt, 'htmx:trigger')
1496+ handler(elt, evt);
1497+ }
1498+ }
1499+ };
1500+ if (nodeData.listenerInfos == null) {
1501+ nodeData.listenerInfos = [];
1502+ }
1503+ nodeData.listenerInfos.push({
1504+ trigger: triggerSpec.trigger,
1505+ listener: eventListener,
1506+ on: eltToListenOn
1507+ })
1508+ eltToListenOn.addEventListener(triggerSpec.trigger, eventListener);
1509+ });
1510+ }
1511+
1512+ var windowIsScrolling = false // used by initScrollHandler
1513+ var scrollHandler = null;
1514+ function initScrollHandler() {
1515+ if (!scrollHandler) {
1516+ scrollHandler = function() {
1517+ windowIsScrolling = true
1518+ };
1519+ window.addEventListener("scroll", scrollHandler)
1520+ setInterval(function() {
1521+ if (windowIsScrolling) {
1522+ windowIsScrolling = false;
1523+ forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
1524+ maybeReveal(elt);
1525+ })
1526+ }
1527+ }, 200);
1528+ }
1529+ }
1530+
1531+ function maybeReveal(elt) {
1532+ if (!hasAttribute(elt,'data-hx-revealed') && isScrolledIntoView(elt)) {
1533+ elt.setAttribute('data-hx-revealed', 'true');
1534+ var nodeData = getInternalData(elt);
1535+ if (nodeData.initHash) {
1536+ triggerEvent(elt, 'revealed');
1537+ } else {
1538+ // if the node isn't initialized, wait for it before triggering the request
1539+ elt.addEventListener("htmx:afterProcessNode", function(evt) { triggerEvent(elt, 'revealed') }, {once: true});
1540+ }
1541+ }
1542+ }
1543+
1544+ //====================================================================
1545+ // Web Sockets
1546+ //====================================================================
1547+
1548+ function processWebSocketInfo(elt, nodeData, info) {
1549+ var values = splitOnWhitespace(info);
1550+ for (var i = 0; i < values.length; i++) {
1551+ var value = values[i].split(/:(.+)/);
1552+ if (value[0] === "connect") {
1553+ ensureWebSocket(elt, value[1], 0);
1554+ }
1555+ if (value[0] === "send") {
1556+ processWebSocketSend(elt);
1557+ }
1558+ }
1559+ }
1560+
1561+ function ensureWebSocket(elt, wssSource, retryCount) {
1562+ if (!bodyContains(elt)) {
1563+ return; // stop ensuring websocket connection when socket bearing element ceases to exist
1564+ }
1565+
1566+ if (wssSource.indexOf("/") == 0) { // complete absolute paths only
1567+ var base_part = location.hostname + (location.port ? ':'+location.port: '');
1568+ if (location.protocol == 'https:') {
1569+ wssSource = "wss://" + base_part + wssSource;
1570+ } else if (location.protocol == 'http:') {
1571+ wssSource = "ws://" + base_part + wssSource;
1572+ }
1573+ }
1574+ var socket = htmx.createWebSocket(wssSource);
1575+ socket.onerror = function (e) {
1576+ triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
1577+ maybeCloseWebSocketSource(elt);
1578+ };
1579+
1580+ socket.onclose = function (e) {
1581+ if ([1006, 1012, 1013].indexOf(e.code) >= 0) { // Abnormal Closure/Service Restart/Try Again Later
1582+ var delay = getWebSocketReconnectDelay(retryCount);
1583+ setTimeout(function() {
1584+ ensureWebSocket(elt, wssSource, retryCount+1); // creates a websocket with a new timeout
1585+ }, delay);
1586+ }
1587+ };
1588+ socket.onopen = function (e) {
1589+ retryCount = 0;
1590+ }
1591+
1592+ getInternalData(elt).webSocket = socket;
1593+ socket.addEventListener('message', function (event) {
1594+ if (maybeCloseWebSocketSource(elt)) {
1595+ return;
1596+ }
1597+
1598+ var response = event.data;
1599+ withExtensions(elt, function(extension){
1600+ response = extension.transformResponse(response, null, elt);
1601+ });
1602+
1603+ var settleInfo = makeSettleInfo(elt);
1604+ var fragment = makeFragment(response);
1605+ var children = toArray(fragment.children);
1606+ for (var i = 0; i < children.length; i++) {
1607+ var child = children[i];
1608+ oobSwap(getAttributeValue(child, "hx-swap-oob") || "true", child, settleInfo);
1609+ }
1610+
1611+ settleImmediately(settleInfo.tasks);
1612+ });
1613+ }
1614+
1615+ function maybeCloseWebSocketSource(elt) {
1616+ if (!bodyContains(elt)) {
1617+ getInternalData(elt).webSocket.close();
1618+ return true;
1619+ }
1620+ }
1621+
1622+ function processWebSocketSend(elt) {
1623+ var webSocketSourceElt = getClosestMatch(elt, function (parent) {
1624+ return getInternalData(parent).webSocket != null;
1625+ });
1626+ if (webSocketSourceElt) {
1627+ elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) {
1628+ var webSocket = getInternalData(webSocketSourceElt).webSocket;
1629+ var headers = getHeaders(elt, webSocketSourceElt);
1630+ var results = getInputValues(elt, 'post');
1631+ var errors = results.errors;
1632+ var rawParameters = results.values;
1633+ var expressionVars = getExpressionVars(elt);
1634+ var allParameters = mergeObjects(rawParameters, expressionVars);
1635+ var filteredParameters = filterValues(allParameters, elt);
1636+ filteredParameters['HEADERS'] = headers;
1637+ if (errors && errors.length > 0) {
1638+ triggerEvent(elt, 'htmx:validation:halted', errors);
1639+ return;
1640+ }
1641+ webSocket.send(JSON.stringify(filteredParameters));
1642+ if(shouldCancel(evt, elt)){
1643+ evt.preventDefault();
1644+ }
1645+ });
1646+ } else {
1647+ triggerErrorEvent(elt, "htmx:noWebSocketSourceError");
1648+ }
1649+ }
1650+
1651+ function getWebSocketReconnectDelay(retryCount) {
1652+ var delay = htmx.config.wsReconnectDelay;
1653+ if (typeof delay === 'function') {
1654+ // @ts-ignore
1655+ return delay(retryCount);
1656+ }
1657+ if (delay === 'full-jitter') {
1658+ var exp = Math.min(retryCount, 6);
1659+ var maxDelay = 1000 * Math.pow(2, exp);
1660+ return maxDelay * Math.random();
1661+ }
1662+ logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
1663+ }
1664+
1665+ //====================================================================
1666+ // Server Sent Events
1667+ //====================================================================
1668+
1669+ function processSSEInfo(elt, nodeData, info) {
1670+ var values = splitOnWhitespace(info);
1671+ for (var i = 0; i < values.length; i++) {
1672+ var value = values[i].split(/:(.+)/);
1673+ if (value[0] === "connect") {
1674+ processSSESource(elt, value[1]);
1675+ }
1676+
1677+ if ((value[0] === "swap")) {
1678+ processSSESwap(elt, value[1])
1679+ }
1680+ }
1681+ }
1682+
1683+ function processSSESource(elt, sseSrc) {
1684+ var source = htmx.createEventSource(sseSrc);
1685+ source.onerror = function (e) {
1686+ triggerErrorEvent(elt, "htmx:sseError", {error:e, source:source});
1687+ maybeCloseSSESource(elt);
1688+ };
1689+ getInternalData(elt).sseEventSource = source;
1690+ }
1691+
1692+ function processSSESwap(elt, sseEventName) {
1693+ var sseSourceElt = getClosestMatch(elt, hasEventSource);
1694+ if (sseSourceElt) {
1695+ var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
1696+ var sseListener = function (event) {
1697+ if (maybeCloseSSESource(sseSourceElt)) {
1698+ return;
1699+ }
1700+ if (!bodyContains(elt)) {
1701+ sseEventSource.removeEventListener(sseEventName, sseListener);
1702+ return;
1703+ }
1704+
1705+ ///////////////////////////
1706+ // TODO: merge this code with AJAX and WebSockets code in the future.
1707+
1708+ var response = event.data;
1709+ withExtensions(elt, function(extension){
1710+ response = extension.transformResponse(response, null, elt);
1711+ });
1712+
1713+ var swapSpec = getSwapSpecification(elt)
1714+ var target = getTarget(elt)
1715+ var settleInfo = makeSettleInfo(elt);
1716+
1717+ selectAndSwap(swapSpec.swapStyle, target, elt, response, settleInfo)
1718+ settleImmediately(settleInfo.tasks)
1719+ triggerEvent(elt, "htmx:sseMessage", event)
1720+ };
1721+
1722+ getInternalData(elt).sseListener = sseListener;
1723+ sseEventSource.addEventListener(sseEventName, sseListener);
1724+ } else {
1725+ triggerErrorEvent(elt, "htmx:noSSESourceError");
1726+ }
1727+ }
1728+
1729+ function processSSETrigger(elt, handler, sseEventName) {
1730+ var sseSourceElt = getClosestMatch(elt, hasEventSource);
1731+ if (sseSourceElt) {
1732+ var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
1733+ var sseListener = function () {
1734+ if (!maybeCloseSSESource(sseSourceElt)) {
1735+ if (bodyContains(elt)) {
1736+ handler(elt);
1737+ } else {
1738+ sseEventSource.removeEventListener(sseEventName, sseListener);
1739+ }
1740+ }
1741+ };
1742+ getInternalData(elt).sseListener = sseListener;
1743+ sseEventSource.addEventListener(sseEventName, sseListener);
1744+ } else {
1745+ triggerErrorEvent(elt, "htmx:noSSESourceError");
1746+ }
1747+ }
1748+
1749+ function maybeCloseSSESource(elt) {
1750+ if (!bodyContains(elt)) {
1751+ getInternalData(elt).sseEventSource.close();
1752+ return true;
1753+ }
1754+ }
1755+
1756+ function hasEventSource(node) {
1757+ return getInternalData(node).sseEventSource != null;
1758+ }
1759+
1760+ //====================================================================
1761+
1762+ function loadImmediately(elt, handler, nodeData, delay) {
1763+ var load = function(){
1764+ if (!nodeData.loaded) {
1765+ nodeData.loaded = true;
1766+ handler(elt);
1767+ }
1768+ }
1769+ if (delay) {
1770+ setTimeout(load, delay);
1771+ } else {
1772+ load();
1773+ }
1774+ }
1775+
1776+ function processVerbs(elt, nodeData, triggerSpecs) {
1777+ var explicitAction = false;
1778+ forEach(VERBS, function (verb) {
1779+ if (hasAttribute(elt,'hx-' + verb)) {
1780+ var path = getAttributeValue(elt, 'hx-' + verb);
1781+ explicitAction = true;
1782+ nodeData.path = path;
1783+ nodeData.verb = verb;
1784+ triggerSpecs.forEach(function(triggerSpec) {
1785+ addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
1786+ if (closest(elt, htmx.config.disableSelector)) {
1787+ cleanUpElement(elt)
1788+ return
1789+ }
1790+ issueAjaxRequest(verb, path, elt, evt)
1791+ })
1792+ });
1793+ }
1794+ });
1795+ return explicitAction;
1796+ }
1797+
1798+ function addTriggerHandler(elt, triggerSpec, nodeData, handler) {
1799+ if (triggerSpec.sseEvent) {
1800+ processSSETrigger(elt, handler, triggerSpec.sseEvent);
1801+ } else if (triggerSpec.trigger === "revealed") {
1802+ initScrollHandler();
1803+ addEventListener(elt, handler, nodeData, triggerSpec);
1804+ maybeReveal(elt);
1805+ } else if (triggerSpec.trigger === "intersect") {
1806+ var observerOptions = {};
1807+ if (triggerSpec.root) {
1808+ observerOptions.root = querySelectorExt(elt, triggerSpec.root)
1809+ }
1810+ if (triggerSpec.threshold) {
1811+ observerOptions.threshold = parseFloat(triggerSpec.threshold);
1812+ }
1813+ var observer = new IntersectionObserver(function (entries) {
1814+ for (var i = 0; i < entries.length; i++) {
1815+ var entry = entries[i];
1816+ if (entry.isIntersecting) {
1817+ triggerEvent(elt, "intersect");
1818+ break;
1819+ }
1820+ }
1821+ }, observerOptions);
1822+ observer.observe(elt);
1823+ addEventListener(elt, handler, nodeData, triggerSpec);
1824+ } else if (triggerSpec.trigger === "load") {
1825+ if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", {elt: elt}))) {
1826+ loadImmediately(elt, handler, nodeData, triggerSpec.delay);
1827+ }
1828+ } else if (triggerSpec.pollInterval) {
1829+ nodeData.polling = true;
1830+ processPolling(elt, handler, triggerSpec);
1831+ } else {
1832+ addEventListener(elt, handler, nodeData, triggerSpec);
1833+ }
1834+ }
1835+
1836+ function evalScript(script) {
1837+ if (htmx.config.allowScriptTags && (script.type === "text/javascript" || script.type === "module" || script.type === "") ) {
1838+ var newScript = getDocument().createElement("script");
1839+ forEach(script.attributes, function (attr) {
1840+ newScript.setAttribute(attr.name, attr.value);
1841+ });
1842+ newScript.textContent = script.textContent;
1843+ newScript.async = false;
1844+ if (htmx.config.inlineScriptNonce) {
1845+ newScript.nonce = htmx.config.inlineScriptNonce;
1846+ }
1847+ var parent = script.parentElement;
1848+
1849+ try {
1850+ parent.insertBefore(newScript, script);
1851+ } catch (e) {
1852+ logError(e);
1853+ } finally {
1854+ // remove old script element, but only if it is still in DOM
1855+ if (script.parentElement) {
1856+ script.parentElement.removeChild(script);
1857+ }
1858+ }
1859+ }
1860+ }
1861+
1862+ function processScripts(elt) {
1863+ if (matches(elt, "script")) {
1864+ evalScript(elt);
1865+ }
1866+ forEach(findAll(elt, "script"), function (script) {
1867+ evalScript(script);
1868+ });
1869+ }
1870+
1871+ function hasChanceOfBeingBoosted() {
1872+ return document.querySelector("[hx-boost], [data-hx-boost]");
1873+ }
1874+
1875+ function findHxOnWildcardElements(elt) {
1876+ var node = null
1877+ var elements = []
1878+
1879+ if (document.evaluate) {
1880+ var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
1881+ while (node = iter.iterateNext()) elements.push(node)
1882+ } else {
1883+ var allElements = document.getElementsByTagName("*")
1884+ for (var i = 0; i < allElements.length; i++) {
1885+ var attributes = allElements[i].attributes
1886+ for (var j = 0; j < attributes.length; j++) {
1887+ var attrName = attributes[j].name
1888+ if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) {
1889+ elements.push(allElements[i])
1890+ }
1891+ }
1892+ }
1893+ }
1894+
1895+ return elements
1896+ }
1897+
1898+ function findElementsToProcess(elt) {
1899+ if (elt.querySelectorAll) {
1900+ var boostedElts = hasChanceOfBeingBoosted() ? ", a" : "";
1901+ var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
1902+ " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
1903+ return results;
1904+ } else {
1905+ return [];
1906+ }
1907+ }
1908+
1909+ function initButtonTracking(elt) {
1910+ // Handle submit buttons/inputs that have the form attribute set
1911+ // see https://developer.mozilla.org/docs/Web/HTML/Element/button
1912+ var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
1913+ if (!form) {
1914+ return
1915+ }
1916+
1917+ var maybeSetLastButtonClicked = function (evt) {
1918+ var elt = closest(evt.target, "button, input[type='submit']");
1919+ if (elt !== null) {
1920+ var internalData = getInternalData(form);
1921+ internalData.lastButtonClicked = elt;
1922+ }
1923+ };
1924+
1925+ // need to handle both click and focus in:
1926+ // focusin - in case someone tabs in to a button and hits the space bar
1927+ // click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
1928+
1929+ elt.addEventListener('click', maybeSetLastButtonClicked)
1930+ elt.addEventListener('focusin', maybeSetLastButtonClicked)
1931+ elt.addEventListener('focusout', function(evt){
1932+ var internalData = getInternalData(form);
1933+ internalData.lastButtonClicked = null;
1934+ })
1935+ }
1936+
1937+ function countCurlies(line) {
1938+ var tokens = tokenizeString(line);
1939+ var netCurlies = 0;
1940+ for (let i = 0; i < tokens.length; i++) {
1941+ const token = tokens[i];
1942+ if (token === "{") {
1943+ netCurlies++;
1944+ } else if (token === "}") {
1945+ netCurlies--;
1946+ }
1947+ }
1948+ return netCurlies;
1949+ }
1950+
1951+ function addHxOnEventHandler(elt, eventName, code) {
1952+ var nodeData = getInternalData(elt);
1953+ nodeData.onHandlers = [];
1954+ var func;
1955+ var listener = function (e) {
1956+ return maybeEval(elt, function() {
1957+ if (!func) {
1958+ func = new Function("event", code);
1959+ }
1960+ func.call(elt, e);
1961+ });
1962+ };
1963+ elt.addEventListener(eventName, listener);
1964+ nodeData.onHandlers.push({event:eventName, listener:listener});
1965+ }
1966+
1967+ function processHxOn(elt) {
1968+ var hxOnValue = getAttributeValue(elt, 'hx-on');
1969+ if (hxOnValue) {
1970+ var handlers = {}
1971+ var lines = hxOnValue.split("\n");
1972+ var currentEvent = null;
1973+ var curlyCount = 0;
1974+ while (lines.length > 0) {
1975+ var line = lines.shift();
1976+ var match = line.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);
1977+ if (curlyCount === 0 && match) {
1978+ line.split(":")
1979+ currentEvent = match[1].slice(0, -1); // strip last colon
1980+ handlers[currentEvent] = match[2];
1981+ } else {
1982+ handlers[currentEvent] += line;
1983+ }
1984+ curlyCount += countCurlies(line);
1985+ }
1986+
1987+ for (var eventName in handlers) {
1988+ addHxOnEventHandler(elt, eventName, handlers[eventName]);
1989+ }
1990+ }
1991+ }
1992+
1993+ function processHxOnWildcard(elt) {
1994+ // wipe any previous on handlers so that this function takes precedence
1995+ deInitOnHandlers(elt)
1996+
1997+ for (var i = 0; i < elt.attributes.length; i++) {
1998+ var name = elt.attributes[i].name
1999+ var value = elt.attributes[i].value
2000+ if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) {
2001+ let eventName = name.slice(name.indexOf(":") + 1)
2002+ // if the eventName starts with a colon, prepend "htmx" for shorthand support
2003+ if (startsWith(eventName, ":")) eventName = "htmx" + eventName
2004+
2005+ addHxOnEventHandler(elt, eventName, value)
2006+ }
2007+ }
2008+ }
2009+
2010+ function initNode(elt) {
2011+ if (closest(elt, htmx.config.disableSelector)) {
2012+ cleanUpElement(elt)
2013+ return;
2014+ }
2015+ var nodeData = getInternalData(elt);
2016+ if (nodeData.initHash !== attributeHash(elt)) {
2017+ // clean up any previously processed info
2018+ deInitNode(elt);
2019+
2020+ nodeData.initHash = attributeHash(elt);
2021+
2022+ processHxOn(elt);
2023+
2024+ triggerEvent(elt, "htmx:beforeProcessNode")
2025+
2026+ if (elt.value) {
2027+ nodeData.lastValue = elt.value;
2028+ }
2029+
2030+ var triggerSpecs = getTriggerSpecs(elt);
2031+ var hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs);
2032+
2033+ if (!hasExplicitHttpAction) {
2034+ if (getClosestAttributeValue(elt, "hx-boost") === "true") {
2035+ boostElement(elt, nodeData, triggerSpecs);
2036+ } else if (hasAttribute(elt, 'hx-trigger')) {
2037+ triggerSpecs.forEach(function (triggerSpec) {
2038+ // For "naked" triggers, don't do anything at all
2039+ addTriggerHandler(elt, triggerSpec, nodeData, function () {
2040+ })
2041+ })
2042+ }
2043+ }
2044+
2045+ // Handle submit buttons/inputs that have the form attribute set
2046+ // see https://developer.mozilla.org/docs/Web/HTML/Element/button
2047+ if (elt.tagName === "FORM" || (getRawAttribute(elt, "type") === "submit" && hasAttribute(elt, "form"))) {
2048+ initButtonTracking(elt)
2049+ }
2050+
2051+ var sseInfo = getAttributeValue(elt, 'hx-sse');
2052+ if (sseInfo) {
2053+ processSSEInfo(elt, nodeData, sseInfo);
2054+ }
2055+
2056+ var wsInfo = getAttributeValue(elt, 'hx-ws');
2057+ if (wsInfo) {
2058+ processWebSocketInfo(elt, nodeData, wsInfo);
2059+ }
2060+ triggerEvent(elt, "htmx:afterProcessNode");
2061+ }
2062+ }
2063+
2064+ function processNode(elt) {
2065+ elt = resolveTarget(elt);
2066+ if (closest(elt, htmx.config.disableSelector)) {
2067+ cleanUpElement(elt)
2068+ return;
2069+ }
2070+ initNode(elt);
2071+ forEach(findElementsToProcess(elt), function(child) { initNode(child) });
2072+ // Because it happens second, the new way of adding onHandlers superseeds the old one
2073+ // i.e. if there are any hx-on:eventName attributes, the hx-on attribute will be ignored
2074+ forEach(findHxOnWildcardElements(elt), processHxOnWildcard);
2075+ }
2076+
2077+ //====================================================================
2078+ // Event/Log Support
2079+ //====================================================================
2080+
2081+ function kebabEventName(str) {
2082+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
2083+ }
2084+
2085+ function makeEvent(eventName, detail) {
2086+ var evt;
2087+ if (window.CustomEvent && typeof window.CustomEvent === 'function') {
2088+ evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail});
2089+ } else {
2090+ evt = getDocument().createEvent('CustomEvent');
2091+ evt.initCustomEvent(eventName, true, true, detail);
2092+ }
2093+ return evt;
2094+ }
2095+
2096+ function triggerErrorEvent(elt, eventName, detail) {
2097+ triggerEvent(elt, eventName, mergeObjects({error:eventName}, detail));
2098+ }
2099+
2100+ function ignoreEventForLogging(eventName) {
2101+ return eventName === "htmx:afterProcessNode"
2102+ }
2103+
2104+ /**
2105+ * `withExtensions` locates all active extensions for a provided element, then
2106+ * executes the provided function using each of the active extensions. It should
2107+ * be called internally at every extendable execution point in htmx.
2108+ *
2109+ * @param {HTMLElement} elt
2110+ * @param {(extension:import("./htmx").HtmxExtension) => void} toDo
2111+ * @returns void
2112+ */
2113+ function withExtensions(elt, toDo) {
2114+ forEach(getExtensions(elt), function(extension){
2115+ try {
2116+ toDo(extension);
2117+ } catch (e) {
2118+ logError(e);
2119+ }
2120+ });
2121+ }
2122+
2123+ function logError(msg) {
2124+ if(console.error) {
2125+ console.error(msg);
2126+ } else if (console.log) {
2127+ console.log("ERROR: ", msg);
2128+ }
2129+ }
2130+
2131+ function triggerEvent(elt, eventName, detail) {
2132+ elt = resolveTarget(elt);
2133+ if (detail == null) {
2134+ detail = {};
2135+ }
2136+ detail["elt"] = elt;
2137+ var event = makeEvent(eventName, detail);
2138+ if (htmx.logger && !ignoreEventForLogging(eventName)) {
2139+ htmx.logger(elt, eventName, detail);
2140+ }
2141+ if (detail.error) {
2142+ logError(detail.error);
2143+ triggerEvent(elt, "htmx:error", {errorInfo:detail})
2144+ }
2145+ var eventResult = elt.dispatchEvent(event);
2146+ var kebabName = kebabEventName(eventName);
2147+ if (eventResult && kebabName !== eventName) {
2148+ var kebabedEvent = makeEvent(kebabName, event.detail);
2149+ eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
2150+ }
2151+ withExtensions(elt, function (extension) {
2152+ eventResult = eventResult && (extension.onEvent(eventName, event) !== false && !event.defaultPrevented)
2153+ });
2154+ return eventResult;
2155+ }
2156+
2157+ //====================================================================
2158+ // History Support
2159+ //====================================================================
2160+ var currentPathForHistory = location.pathname+location.search;
2161+
2162+ function getHistoryElement() {
2163+ var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]');
2164+ return historyElt || getDocument().body;
2165+ }
2166+
2167+ function saveToHistoryCache(url, content, title, scroll) {
2168+ if (!canAccessLocalStorage()) {
2169+ return;
2170+ }
2171+
2172+ url = normalizePath(url);
2173+
2174+ var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
2175+ for (var i = 0; i < historyCache.length; i++) {
2176+ if (historyCache[i].url === url) {
2177+ historyCache.splice(i, 1);
2178+ break;
2179+ }
2180+ }
2181+ var newHistoryItem = {url:url, content: content, title:title, scroll:scroll};
2182+ triggerEvent(getDocument().body, "htmx:historyItemCreated", {item:newHistoryItem, cache: historyCache})
2183+ historyCache.push(newHistoryItem)
2184+ while (historyCache.length > htmx.config.historyCacheSize) {
2185+ historyCache.shift();
2186+ }
2187+ while(historyCache.length > 0){
2188+ try {
2189+ localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
2190+ break;
2191+ } catch (e) {
2192+ triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache})
2193+ historyCache.shift(); // shrink the cache and retry
2194+ }
2195+ }
2196+ }
2197+
2198+ function getCachedHistory(url) {
2199+ if (!canAccessLocalStorage()) {
2200+ return null;
2201+ }
2202+
2203+ url = normalizePath(url);
2204+
2205+ var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
2206+ for (var i = 0; i < historyCache.length; i++) {
2207+ if (historyCache[i].url === url) {
2208+ return historyCache[i];
2209+ }
2210+ }
2211+ return null;
2212+ }
2213+
2214+ function cleanInnerHtmlForHistory(elt) {
2215+ var className = htmx.config.requestClass;
2216+ var clone = elt.cloneNode(true);
2217+ forEach(findAll(clone, "." + className), function(child){
2218+ removeClassFromElement(child, className);
2219+ });
2220+ return clone.innerHTML;
2221+ }
2222+
2223+ function saveCurrentPageToHistory() {
2224+ var elt = getHistoryElement();
2225+ var path = currentPathForHistory || location.pathname+location.search;
2226+
2227+ // Allow history snapshot feature to be disabled where hx-history="false"
2228+ // is present *anywhere* in the current document we're about to save,
2229+ // so we can prevent privileged data entering the cache.
2230+ // The page will still be reachable as a history entry, but htmx will fetch it
2231+ // live from the server onpopstate rather than look in the localStorage cache
2232+ var disableHistoryCache
2233+ try {
2234+ disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
2235+ } catch (e) {
2236+ // IE11: insensitive modifier not supported so fallback to case sensitive selector
2237+ disableHistoryCache = getDocument().querySelector('[hx-history="false"],[data-hx-history="false"]')
2238+ }
2239+ if (!disableHistoryCache) {
2240+ triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path: path, historyElt: elt});
2241+ saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY);
2242+ }
2243+
2244+ if (htmx.config.historyEnabled) history.replaceState({htmx: true}, getDocument().title, window.location.href);
2245+ }
2246+
2247+ function pushUrlIntoHistory(path) {
2248+ // remove the cache buster parameter, if any
2249+ if (htmx.config.getCacheBusterParam) {
2250+ path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '')
2251+ if (endsWith(path, '&') || endsWith(path, "?")) {
2252+ path = path.slice(0, -1);
2253+ }
2254+ }
2255+ if(htmx.config.historyEnabled) {
2256+ history.pushState({htmx:true}, "", path);
2257+ }
2258+ currentPathForHistory = path;
2259+ }
2260+
2261+ function replaceUrlInHistory(path) {
2262+ if(htmx.config.historyEnabled) history.replaceState({htmx:true}, "", path);
2263+ currentPathForHistory = path;
2264+ }
2265+
2266+ function settleImmediately(tasks) {
2267+ forEach(tasks, function (task) {
2268+ task.call();
2269+ });
2270+ }
2271+
2272+ function loadHistoryFromServer(path) {
2273+ var request = new XMLHttpRequest();
2274+ var details = {path: path, xhr:request};
2275+ triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
2276+ request.open('GET', path, true);
2277+ request.setRequestHeader("HX-History-Restore-Request", "true");
2278+ request.onload = function () {
2279+ if (this.status >= 200 && this.status < 400) {
2280+ triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
2281+ var fragment = makeFragment(this.response);
2282+ // @ts-ignore
2283+ fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment;
2284+ var historyElement = getHistoryElement();
2285+ var settleInfo = makeSettleInfo(historyElement);
2286+ var title = findTitle(this.response);
2287+ if (title) {
2288+ var titleElt = find("title");
2289+ if (titleElt) {
2290+ titleElt.innerHTML = title;
2291+ } else {
2292+ window.document.title = title;
2293+ }
2294+ }
2295+ // @ts-ignore
2296+ swapInnerHTML(historyElement, fragment, settleInfo)
2297+ settleImmediately(settleInfo.tasks);
2298+ currentPathForHistory = path;
2299+ triggerEvent(getDocument().body, "htmx:historyRestore", {path: path, cacheMiss:true, serverResponse:this.response});
2300+ } else {
2301+ triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details);
2302+ }
2303+ };
2304+ request.send();
2305+ }
2306+
2307+ function restoreHistory(path) {
2308+ saveCurrentPageToHistory();
2309+ path = path || location.pathname+location.search;
2310+ var cached = getCachedHistory(path);
2311+ if (cached) {
2312+ var fragment = makeFragment(cached.content);
2313+ var historyElement = getHistoryElement();
2314+ var settleInfo = makeSettleInfo(historyElement);
2315+ swapInnerHTML(historyElement, fragment, settleInfo)
2316+ settleImmediately(settleInfo.tasks);
2317+ document.title = cached.title;
2318+ setTimeout(function () {
2319+ window.scrollTo(0, cached.scroll);
2320+ }, 0); // next 'tick', so browser has time to render layout
2321+ currentPathForHistory = path;
2322+ triggerEvent(getDocument().body, "htmx:historyRestore", {path:path, item:cached});
2323+ } else {
2324+ if (htmx.config.refreshOnHistoryMiss) {
2325+
2326+ // @ts-ignore: optional parameter in reload() function throws error
2327+ window.location.reload(true);
2328+ } else {
2329+ loadHistoryFromServer(path);
2330+ }
2331+ }
2332+ }
2333+
2334+ function addRequestIndicatorClasses(elt) {
2335+ var indicators = findAttributeTargets(elt, 'hx-indicator');
2336+ if (indicators == null) {
2337+ indicators = [elt];
2338+ }
2339+ forEach(indicators, function (ic) {
2340+ var internalData = getInternalData(ic);
2341+ internalData.requestCount = (internalData.requestCount || 0) + 1;
2342+ ic.classList["add"].call(ic.classList, htmx.config.requestClass);
2343+ });
2344+ return indicators;
2345+ }
2346+
2347+ function disableElements(elt) {
2348+ var disabledElts = findAttributeTargets(elt, 'hx-disabled-elt');
2349+ if (disabledElts == null) {
2350+ disabledElts = [];
2351+ }
2352+ forEach(disabledElts, function (disabledElement) {
2353+ var internalData = getInternalData(disabledElement);
2354+ internalData.requestCount = (internalData.requestCount || 0) + 1;
2355+ disabledElement.setAttribute("disabled", "");
2356+ });
2357+ return disabledElts;
2358+ }
2359+
2360+ function removeRequestIndicators(indicators, disabled) {
2361+ forEach(indicators, function (ic) {
2362+ var internalData = getInternalData(ic);
2363+ internalData.requestCount = (internalData.requestCount || 0) - 1;
2364+ if (internalData.requestCount === 0) {
2365+ ic.classList["remove"].call(ic.classList, htmx.config.requestClass);
2366+ }
2367+ });
2368+ forEach(disabled, function (disabledElement) {
2369+ var internalData = getInternalData(disabledElement);
2370+ internalData.requestCount = (internalData.requestCount || 0) - 1;
2371+ if (internalData.requestCount === 0) {
2372+ disabledElement.removeAttribute('disabled');
2373+ }
2374+ });
2375+ }
2376+
2377+ //====================================================================
2378+ // Input Value Processing
2379+ //====================================================================
2380+
2381+ function haveSeenNode(processed, elt) {
2382+ for (var i = 0; i < processed.length; i++) {
2383+ var node = processed[i];
2384+ if (node.isSameNode(elt)) {
2385+ return true;
2386+ }
2387+ }
2388+ return false;
2389+ }
2390+
2391+ function shouldInclude(elt) {
2392+ if(elt.name === "" || elt.name == null || elt.disabled) {
2393+ return false;
2394+ }
2395+ // ignore "submitter" types (see jQuery src/serialize.js)
2396+ if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
2397+ return false;
2398+ }
2399+ if (elt.type === "checkbox" || elt.type === "radio" ) {
2400+ return elt.checked;
2401+ }
2402+ return true;
2403+ }
2404+
2405+ function addValueToValues(name, value, values) {
2406+ // This is a little ugly because both the current value of the named value in the form
2407+ // and the new value could be arrays, so we have to handle all four cases :/
2408+ if (name != null && value != null) {
2409+ var current = values[name];
2410+ if (current === undefined) {
2411+ values[name] = value;
2412+ } else if (Array.isArray(current)) {
2413+ if (Array.isArray(value)) {
2414+ values[name] = current.concat(value);
2415+ } else {
2416+ current.push(value);
2417+ }
2418+ } else {
2419+ if (Array.isArray(value)) {
2420+ values[name] = [current].concat(value);
2421+ } else {
2422+ values[name] = [current, value];
2423+ }
2424+ }
2425+ }
2426+ }
2427+
2428+ function processInputValue(processed, values, errors, elt, validate) {
2429+ if (elt == null || haveSeenNode(processed, elt)) {
2430+ return;
2431+ } else {
2432+ processed.push(elt);
2433+ }
2434+ if (shouldInclude(elt)) {
2435+ var name = getRawAttribute(elt,"name");
2436+ var value = elt.value;
2437+ if (elt.multiple) {
2438+ value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
2439+ }
2440+ // include file inputs
2441+ if (elt.files) {
2442+ value = toArray(elt.files);
2443+ }
2444+ addValueToValues(name, value, values);
2445+ if (validate) {
2446+ validateElement(elt, errors);
2447+ }
2448+ }
2449+ if (matches(elt, 'form')) {
2450+ var inputs = elt.elements;
2451+ forEach(inputs, function(input) {
2452+ processInputValue(processed, values, errors, input, validate);
2453+ });
2454+ }
2455+ }
2456+
2457+ function validateElement(element, errors) {
2458+ if (element.willValidate) {
2459+ triggerEvent(element, "htmx:validation:validate")
2460+ if (!element.checkValidity()) {
2461+ errors.push({elt: element, message:element.validationMessage, validity:element.validity});
2462+ triggerEvent(element, "htmx:validation:failed", {message:element.validationMessage, validity:element.validity})
2463+ }
2464+ }
2465+ }
2466+
2467+ /**
2468+ * @param {HTMLElement} elt
2469+ * @param {string} verb
2470+ */
2471+ function getInputValues(elt, verb) {
2472+ var processed = [];
2473+ var values = {};
2474+ var formValues = {};
2475+ var errors = [];
2476+ var internalData = getInternalData(elt);
2477+
2478+ // only validate when form is directly submitted and novalidate or formnovalidate are not set
2479+ // or if the element has an explicit hx-validate="true" on it
2480+ var validate = (matches(elt, 'form') && elt.noValidate !== true) || getAttributeValue(elt, "hx-validate") === "true";
2481+ if (internalData.lastButtonClicked) {
2482+ validate = validate && internalData.lastButtonClicked.formNoValidate !== true;
2483+ }
2484+
2485+ // for a non-GET include the closest form
2486+ if (verb !== 'get') {
2487+ processInputValue(processed, formValues, errors, closest(elt, 'form'), validate);
2488+ }
2489+
2490+ // include the element itself
2491+ processInputValue(processed, values, errors, elt, validate);
2492+
2493+ // if a button or submit was clicked last, include its value
2494+ if (internalData.lastButtonClicked || elt.tagName === "BUTTON" ||
2495+ (elt.tagName === "INPUT" && getRawAttribute(elt, "type") === "submit")) {
2496+ var button = internalData.lastButtonClicked || elt
2497+ var name = getRawAttribute(button, "name")
2498+ addValueToValues(name, button.value, formValues)
2499+ }
2500+
2501+ // include any explicit includes
2502+ var includes = findAttributeTargets(elt, "hx-include");
2503+ forEach(includes, function(node) {
2504+ processInputValue(processed, values, errors, node, validate);
2505+ // if a non-form is included, include any input values within it
2506+ if (!matches(node, 'form')) {
2507+ forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {
2508+ processInputValue(processed, values, errors, descendant, validate);
2509+ })
2510+ }
2511+ });
2512+
2513+ // form values take precedence, overriding the regular values
2514+ values = mergeObjects(values, formValues);
2515+
2516+ return {errors:errors, values:values};
2517+ }
2518+
2519+ function appendParam(returnStr, name, realValue) {
2520+ if (returnStr !== "") {
2521+ returnStr += "&";
2522+ }
2523+ if (String(realValue) === "[object Object]") {
2524+ realValue = JSON.stringify(realValue);
2525+ }
2526+ var s = encodeURIComponent(realValue);
2527+ returnStr += encodeURIComponent(name) + "=" + s;
2528+ return returnStr;
2529+ }
2530+
2531+ function urlEncode(values) {
2532+ var returnStr = "";
2533+ for (var name in values) {
2534+ if (values.hasOwnProperty(name)) {
2535+ var value = values[name];
2536+ if (Array.isArray(value)) {
2537+ forEach(value, function(v) {
2538+ returnStr = appendParam(returnStr, name, v);
2539+ });
2540+ } else {
2541+ returnStr = appendParam(returnStr, name, value);
2542+ }
2543+ }
2544+ }
2545+ return returnStr;
2546+ }
2547+
2548+ function makeFormData(values) {
2549+ var formData = new FormData();
2550+ for (var name in values) {
2551+ if (values.hasOwnProperty(name)) {
2552+ var value = values[name];
2553+ if (Array.isArray(value)) {
2554+ forEach(value, function(v) {
2555+ formData.append(name, v);
2556+ });
2557+ } else {
2558+ formData.append(name, value);
2559+ }
2560+ }
2561+ }
2562+ return formData;
2563+ }
2564+
2565+ //====================================================================
2566+ // Ajax
2567+ //====================================================================
2568+
2569+ /**
2570+ * @param {HTMLElement} elt
2571+ * @param {HTMLElement} target
2572+ * @param {string} prompt
2573+ * @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification
2574+ */
2575+ function getHeaders(elt, target, prompt) {
2576+ var headers = {
2577+ "HX-Request" : "true",
2578+ "HX-Trigger" : getRawAttribute(elt, "id"),
2579+ "HX-Trigger-Name" : getRawAttribute(elt, "name"),
2580+ "HX-Target" : getAttributeValue(target, "id"),
2581+ "HX-Current-URL" : getDocument().location.href,
2582+ }
2583+ getValuesForElement(elt, "hx-headers", false, headers)
2584+ if (prompt !== undefined) {
2585+ headers["HX-Prompt"] = prompt;
2586+ }
2587+ if (getInternalData(elt).boosted) {
2588+ headers["HX-Boosted"] = "true";
2589+ }
2590+ return headers;
2591+ }
2592+
2593+ /**
2594+ * filterValues takes an object containing form input values
2595+ * and returns a new object that only contains keys that are
2596+ * specified by the closest "hx-params" attribute
2597+ * @param {Object} inputValues
2598+ * @param {HTMLElement} elt
2599+ * @returns {Object}
2600+ */
2601+ function filterValues(inputValues, elt) {
2602+ var paramsValue = getClosestAttributeValue(elt, "hx-params");
2603+ if (paramsValue) {
2604+ if (paramsValue === "none") {
2605+ return {};
2606+ } else if (paramsValue === "*") {
2607+ return inputValues;
2608+ } else if(paramsValue.indexOf("not ") === 0) {
2609+ forEach(paramsValue.substr(4).split(","), function (name) {
2610+ name = name.trim();
2611+ delete inputValues[name];
2612+ });
2613+ return inputValues;
2614+ } else {
2615+ var newValues = {}
2616+ forEach(paramsValue.split(","), function (name) {
2617+ name = name.trim();
2618+ newValues[name] = inputValues[name];
2619+ });
2620+ return newValues;
2621+ }
2622+ } else {
2623+ return inputValues;
2624+ }
2625+ }
2626+
2627+ function isAnchorLink(elt) {
2628+ return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf("#") >=0
2629+ }
2630+
2631+ /**
2632+ *
2633+ * @param {HTMLElement} elt
2634+ * @param {string} swapInfoOverride
2635+ * @returns {import("./htmx").HtmxSwapSpecification}
2636+ */
2637+ function getSwapSpecification(elt, swapInfoOverride) {
2638+ var swapInfo = swapInfoOverride ? swapInfoOverride : getClosestAttributeValue(elt, "hx-swap");
2639+ var swapSpec = {
2640+ "swapStyle" : getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
2641+ "swapDelay" : htmx.config.defaultSwapDelay,
2642+ "settleDelay" : htmx.config.defaultSettleDelay
2643+ }
2644+ if (getInternalData(elt).boosted && !isAnchorLink(elt)) {
2645+ swapSpec["show"] = "top"
2646+ }
2647+ if (swapInfo) {
2648+ var split = splitOnWhitespace(swapInfo);
2649+ if (split.length > 0) {
2650+ for (var i = 0; i < split.length; i++) {
2651+ var value = split[i];
2652+ if (value.indexOf("swap:") === 0) {
2653+ swapSpec["swapDelay"] = parseInterval(value.substr(5));
2654+ } else if (value.indexOf("settle:") === 0) {
2655+ swapSpec["settleDelay"] = parseInterval(value.substr(7));
2656+ } else if (value.indexOf("transition:") === 0) {
2657+ swapSpec["transition"] = value.substr(11) === "true";
2658+ } else if (value.indexOf("ignoreTitle:") === 0) {
2659+ swapSpec["ignoreTitle"] = value.substr(12) === "true";
2660+ } else if (value.indexOf("scroll:") === 0) {
2661+ var scrollSpec = value.substr(7);
2662+ var splitSpec = scrollSpec.split(":");
2663+ var scrollVal = splitSpec.pop();
2664+ var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
2665+ swapSpec["scroll"] = scrollVal;
2666+ swapSpec["scrollTarget"] = selectorVal;
2667+ } else if (value.indexOf("show:") === 0) {
2668+ var showSpec = value.substr(5);
2669+ var splitSpec = showSpec.split(":");
2670+ var showVal = splitSpec.pop();
2671+ var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
2672+ swapSpec["show"] = showVal;
2673+ swapSpec["showTarget"] = selectorVal;
2674+ } else if (value.indexOf("focus-scroll:") === 0) {
2675+ var focusScrollVal = value.substr("focus-scroll:".length);
2676+ swapSpec["focusScroll"] = focusScrollVal == "true";
2677+ } else if (i == 0) {
2678+ swapSpec["swapStyle"] = value;
2679+ } else {
2680+ logError('Unknown modifier in hx-swap: ' + value);
2681+ }
2682+ }
2683+ }
2684+ }
2685+ return swapSpec;
2686+ }
2687+
2688+ function usesFormData(elt) {
2689+ return getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data" ||
2690+ (matches(elt, "form") && getRawAttribute(elt, 'enctype') === "multipart/form-data");
2691+ }
2692+
2693+ function encodeParamsForBody(xhr, elt, filteredParameters) {
2694+ var encodedParameters = null;
2695+ withExtensions(elt, function (extension) {
2696+ if (encodedParameters == null) {
2697+ encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt);
2698+ }
2699+ });
2700+ if (encodedParameters != null) {
2701+ return encodedParameters;
2702+ } else {
2703+ if (usesFormData(elt)) {
2704+ return makeFormData(filteredParameters);
2705+ } else {
2706+ return urlEncode(filteredParameters);
2707+ }
2708+ }
2709+ }
2710+
2711+ /**
2712+ *
2713+ * @param {Element} target
2714+ * @returns {import("./htmx").HtmxSettleInfo}
2715+ */
2716+ function makeSettleInfo(target) {
2717+ return {tasks: [], elts: [target]};
2718+ }
2719+
2720+ function updateScrollState(content, swapSpec) {
2721+ var first = content[0];
2722+ var last = content[content.length - 1];
2723+ if (swapSpec.scroll) {
2724+ var target = null;
2725+ if (swapSpec.scrollTarget) {
2726+ target = querySelectorExt(first, swapSpec.scrollTarget);
2727+ }
2728+ if (swapSpec.scroll === "top" && (first || target)) {
2729+ target = target || first;
2730+ target.scrollTop = 0;
2731+ }
2732+ if (swapSpec.scroll === "bottom" && (last || target)) {
2733+ target = target || last;
2734+ target.scrollTop = target.scrollHeight;
2735+ }
2736+ }
2737+ if (swapSpec.show) {
2738+ var target = null;
2739+ if (swapSpec.showTarget) {
2740+ var targetStr = swapSpec.showTarget;
2741+ if (swapSpec.showTarget === "window") {
2742+ targetStr = "body";
2743+ }
2744+ target = querySelectorExt(first, targetStr);
2745+ }
2746+ if (swapSpec.show === "top" && (first || target)) {
2747+ target = target || first;
2748+ target.scrollIntoView({block:'start', behavior: htmx.config.scrollBehavior});
2749+ }
2750+ if (swapSpec.show === "bottom" && (last || target)) {
2751+ target = target || last;
2752+ target.scrollIntoView({block:'end', behavior: htmx.config.scrollBehavior});
2753+ }
2754+ }
2755+ }
2756+
2757+ /**
2758+ * @param {HTMLElement} elt
2759+ * @param {string} attr
2760+ * @param {boolean=} evalAsDefault
2761+ * @param {Object=} values
2762+ * @returns {Object}
2763+ */
2764+ function getValuesForElement(elt, attr, evalAsDefault, values) {
2765+ if (values == null) {
2766+ values = {};
2767+ }
2768+ if (elt == null) {
2769+ return values;
2770+ }
2771+ var attributeValue = getAttributeValue(elt, attr);
2772+ if (attributeValue) {
2773+ var str = attributeValue.trim();
2774+ var evaluateValue = evalAsDefault;
2775+ if (str === "unset") {
2776+ return null;
2777+ }
2778+ if (str.indexOf("javascript:") === 0) {
2779+ str = str.substr(11);
2780+ evaluateValue = true;
2781+ } else if (str.indexOf("js:") === 0) {
2782+ str = str.substr(3);
2783+ evaluateValue = true;
2784+ }
2785+ if (str.indexOf('{') !== 0) {
2786+ str = "{" + str + "}";
2787+ }
2788+ var varsValues;
2789+ if (evaluateValue) {
2790+ varsValues = maybeEval(elt,function () {return Function("return (" + str + ")")();}, {});
2791+ } else {
2792+ varsValues = parseJSON(str);
2793+ }
2794+ for (var key in varsValues) {
2795+ if (varsValues.hasOwnProperty(key)) {
2796+ if (values[key] == null) {
2797+ values[key] = varsValues[key];
2798+ }
2799+ }
2800+ }
2801+ }
2802+ return getValuesForElement(parentElt(elt), attr, evalAsDefault, values);
2803+ }
2804+
2805+ function maybeEval(elt, toEval, defaultVal) {
2806+ if (htmx.config.allowEval) {
2807+ return toEval();
2808+ } else {
2809+ triggerErrorEvent(elt, 'htmx:evalDisallowedError');
2810+ return defaultVal;
2811+ }
2812+ }
2813+
2814+ /**
2815+ * @param {HTMLElement} elt
2816+ * @param {*} expressionVars
2817+ * @returns
2818+ */
2819+ function getHXVarsForElement(elt, expressionVars) {
2820+ return getValuesForElement(elt, "hx-vars", true, expressionVars);
2821+ }
2822+
2823+ /**
2824+ * @param {HTMLElement} elt
2825+ * @param {*} expressionVars
2826+ * @returns
2827+ */
2828+ function getHXValsForElement(elt, expressionVars) {
2829+ return getValuesForElement(elt, "hx-vals", false, expressionVars);
2830+ }
2831+
2832+ /**
2833+ * @param {HTMLElement} elt
2834+ * @returns {Object}
2835+ */
2836+ function getExpressionVars(elt) {
2837+ return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt));
2838+ }
2839+
2840+ function safelySetHeaderValue(xhr, header, headerValue) {
2841+ if (headerValue !== null) {
2842+ try {
2843+ xhr.setRequestHeader(header, headerValue);
2844+ } catch (e) {
2845+ // On an exception, try to set the header URI encoded instead
2846+ xhr.setRequestHeader(header, encodeURIComponent(headerValue));
2847+ xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
2848+ }
2849+ }
2850+ }
2851+
2852+ function getPathFromResponse(xhr) {
2853+ // NB: IE11 does not support this stuff
2854+ if (xhr.responseURL && typeof(URL) !== "undefined") {
2855+ try {
2856+ var url = new URL(xhr.responseURL);
2857+ return url.pathname + url.search;
2858+ } catch (e) {
2859+ triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
2860+ }
2861+ }
2862+ }
2863+
2864+ function hasHeader(xhr, regexp) {
2865+ return xhr.getAllResponseHeaders().match(regexp);
2866+ }
2867+
2868+ function ajaxHelper(verb, path, context) {
2869+ verb = verb.toLowerCase();
2870+ if (context) {
2871+ if (context instanceof Element || isType(context, 'String')) {
2872+ return issueAjaxRequest(verb, path, null, null, {
2873+ targetOverride: resolveTarget(context),
2874+ returnPromise: true
2875+ });
2876+ } else {
2877+ return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event,
2878+ {
2879+ handler : context.handler,
2880+ headers : context.headers,
2881+ values : context.values,
2882+ targetOverride: resolveTarget(context.target),
2883+ swapOverride: context.swap,
2884+ returnPromise: true
2885+ });
2886+ }
2887+ } else {
2888+ return issueAjaxRequest(verb, path, null, null, {
2889+ returnPromise: true
2890+ });
2891+ }
2892+ }
2893+
2894+ function hierarchyForElt(elt) {
2895+ var arr = [];
2896+ while (elt) {
2897+ arr.push(elt);
2898+ elt = elt.parentElement;
2899+ }
2900+ return arr;
2901+ }
2902+
2903+ function verifyPath(elt, path, requestConfig) {
2904+ var sameHost
2905+ var url
2906+ if (typeof URL === "function") {
2907+ url = new URL(path, document.location.href);
2908+ var origin = document.location.origin;
2909+ sameHost = origin === url.origin;
2910+ } else {
2911+ // IE11 doesn't support URL
2912+ url = path
2913+ sameHost = startsWith(path, document.location.origin)
2914+ }
2915+
2916+ if (htmx.config.selfRequestsOnly) {
2917+ if (!sameHost) {
2918+ return false;
2919+ }
2920+ }
2921+ return triggerEvent(elt, "htmx:validateUrl", mergeObjects({url: url, sameHost: sameHost}, requestConfig));
2922+ }
2923+
2924+ function issueAjaxRequest(verb, path, elt, event, etc, confirmed) {
2925+ var resolve = null;
2926+ var reject = null;
2927+ etc = etc != null ? etc : {};
2928+ if(etc.returnPromise && typeof Promise !== "undefined"){
2929+ var promise = new Promise(function (_resolve, _reject) {
2930+ resolve = _resolve;
2931+ reject = _reject;
2932+ });
2933+ }
2934+ if(elt == null) {
2935+ elt = getDocument().body;
2936+ }
2937+ var responseHandler = etc.handler || handleAjaxResponse;
2938+
2939+ if (!bodyContains(elt)) {
2940+ // do not issue requests for elements removed from the DOM
2941+ maybeCall(resolve);
2942+ return promise;
2943+ }
2944+ var target = etc.targetOverride || getTarget(elt);
2945+ if (target == null || target == DUMMY_ELT) {
2946+ triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")});
2947+ maybeCall(reject);
2948+ return promise;
2949+ }
2950+
2951+ var eltData = getInternalData(elt);
2952+ var submitter = eltData.lastButtonClicked;
2953+
2954+ if (submitter) {
2955+ var buttonPath = getRawAttribute(submitter, "formaction");
2956+ if (buttonPath != null) {
2957+ path = buttonPath;
2958+ }
2959+
2960+ var buttonVerb = getRawAttribute(submitter, "formmethod")
2961+ if (buttonVerb != null) {
2962+ verb = buttonVerb;
2963+ }
2964+ }
2965+
2966+ // allow event-based confirmation w/ a callback
2967+ if (!confirmed) {
2968+ var issueRequest = function() {
2969+ return issueAjaxRequest(verb, path, elt, event, etc, true);
2970+ }
2971+ var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest};
2972+ if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) {
2973+ maybeCall(resolve);
2974+ return promise;
2975+ }
2976+ }
2977+
2978+ var syncElt = elt;
2979+ var syncStrategy = getClosestAttributeValue(elt, "hx-sync");
2980+ var queueStrategy = null;
2981+ var abortable = false;
2982+ if (syncStrategy) {
2983+ var syncStrings = syncStrategy.split(":");
2984+ var selector = syncStrings[0].trim();
2985+ if (selector === "this") {
2986+ syncElt = findThisElement(elt, 'hx-sync');
2987+ } else {
2988+ syncElt = querySelectorExt(elt, selector);
2989+ }
2990+ // default to the drop strategy
2991+ syncStrategy = (syncStrings[1] || 'drop').trim();
2992+ eltData = getInternalData(syncElt);
2993+ if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) {
2994+ maybeCall(resolve);
2995+ return promise;
2996+ } else if (syncStrategy === "abort") {
2997+ if (eltData.xhr) {
2998+ maybeCall(resolve);
2999+ return promise;
3000+ } else {
3001+ abortable = true;
3002+ }
3003+ } else if (syncStrategy === "replace") {
3004+ triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
3005+ } else if (syncStrategy.indexOf("queue") === 0) {
3006+ var queueStrArray = syncStrategy.split(" ");
3007+ queueStrategy = (queueStrArray[1] || "last").trim();
3008+ }
3009+ }
3010+
3011+ if (eltData.xhr) {
3012+ if (eltData.abortable) {
3013+ triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
3014+ } else {
3015+ if(queueStrategy == null){
3016+ if (event) {
3017+ var eventData = getInternalData(event);
3018+ if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) {
3019+ queueStrategy = eventData.triggerSpec.queue;
3020+ }
3021+ }
3022+ if (queueStrategy == null) {
3023+ queueStrategy = "last";
3024+ }
3025+ }
3026+ if (eltData.queuedRequests == null) {
3027+ eltData.queuedRequests = [];
3028+ }
3029+ if (queueStrategy === "first" && eltData.queuedRequests.length === 0) {
3030+ eltData.queuedRequests.push(function () {
3031+ issueAjaxRequest(verb, path, elt, event, etc)
3032+ });
3033+ } else if (queueStrategy === "all") {
3034+ eltData.queuedRequests.push(function () {
3035+ issueAjaxRequest(verb, path, elt, event, etc)
3036+ });
3037+ } else if (queueStrategy === "last") {
3038+ eltData.queuedRequests = []; // dump existing queue
3039+ eltData.queuedRequests.push(function () {
3040+ issueAjaxRequest(verb, path, elt, event, etc)
3041+ });
3042+ }
3043+ maybeCall(resolve);
3044+ return promise;
3045+ }
3046+ }
3047+
3048+ var xhr = new XMLHttpRequest();
3049+ eltData.xhr = xhr;
3050+ eltData.abortable = abortable;
3051+ var endRequestLock = function(){
3052+ eltData.xhr = null;
3053+ eltData.abortable = false;
3054+ if (eltData.queuedRequests != null &&
3055+ eltData.queuedRequests.length > 0) {
3056+ var queuedRequest = eltData.queuedRequests.shift();
3057+ queuedRequest();
3058+ }
3059+ }
3060+ var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
3061+ if (promptQuestion) {
3062+ var promptResponse = prompt(promptQuestion);
3063+ // prompt returns null if cancelled and empty string if accepted with no entry
3064+ if (promptResponse === null ||
3065+ !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) {
3066+ maybeCall(resolve);
3067+ endRequestLock();
3068+ return promise;
3069+ }
3070+ }
3071+
3072+ var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
3073+ if (confirmQuestion) {
3074+ if(!confirm(confirmQuestion)) {
3075+ maybeCall(resolve);
3076+ endRequestLock()
3077+ return promise;
3078+ }
3079+ }
3080+
3081+
3082+ var headers = getHeaders(elt, target, promptResponse);
3083+ if (etc.headers) {
3084+ headers = mergeObjects(headers, etc.headers);
3085+ }
3086+ var results = getInputValues(elt, verb);
3087+ var errors = results.errors;
3088+ var rawParameters = results.values;
3089+ if (etc.values) {
3090+ rawParameters = mergeObjects(rawParameters, etc.values);
3091+ }
3092+ var expressionVars = getExpressionVars(elt);
3093+ var allParameters = mergeObjects(rawParameters, expressionVars);
3094+ var filteredParameters = filterValues(allParameters, elt);
3095+
3096+ if (verb !== 'get' && !usesFormData(elt)) {
3097+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
3098+ }
3099+
3100+ if (htmx.config.getCacheBusterParam && verb === 'get') {
3101+ filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
3102+ }
3103+
3104+ // behavior of anchors w/ empty href is to use the current URL
3105+ if (path == null || path === "") {
3106+ path = getDocument().location.href;
3107+ }
3108+
3109+
3110+ var requestAttrValues = getValuesForElement(elt, 'hx-request');
3111+
3112+ var eltIsBoosted = getInternalData(elt).boosted;
3113+
3114+ var useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0
3115+
3116+ var requestConfig = {
3117+ boosted: eltIsBoosted,
3118+ useUrlParams: useUrlParams,
3119+ parameters: filteredParameters,
3120+ unfilteredParameters: allParameters,
3121+ headers:headers,
3122+ target:target,
3123+ verb:verb,
3124+ errors:errors,
3125+ withCredentials: etc.credentials || requestAttrValues.credentials || htmx.config.withCredentials,
3126+ timeout: etc.timeout || requestAttrValues.timeout || htmx.config.timeout,
3127+ path:path,
3128+ triggeringEvent:event
3129+ };
3130+
3131+ if(!triggerEvent(elt, 'htmx:configRequest', requestConfig)){
3132+ maybeCall(resolve);
3133+ endRequestLock();
3134+ return promise;
3135+ }
3136+
3137+ // copy out in case the object was overwritten
3138+ path = requestConfig.path;
3139+ verb = requestConfig.verb;
3140+ headers = requestConfig.headers;
3141+ filteredParameters = requestConfig.parameters;
3142+ errors = requestConfig.errors;
3143+ useUrlParams = requestConfig.useUrlParams;
3144+
3145+ if(errors && errors.length > 0){
3146+ triggerEvent(elt, 'htmx:validation:halted', requestConfig)
3147+ maybeCall(resolve);
3148+ endRequestLock();
3149+ return promise;
3150+ }
3151+
3152+ var splitPath = path.split("#");
3153+ var pathNoAnchor = splitPath[0];
3154+ var anchor = splitPath[1];
3155+
3156+ var finalPath = path
3157+ if (useUrlParams) {
3158+ finalPath = pathNoAnchor;
3159+ var values = Object.keys(filteredParameters).length !== 0;
3160+ if (values) {
3161+ if (finalPath.indexOf("?") < 0) {
3162+ finalPath += "?";
3163+ } else {
3164+ finalPath += "&";
3165+ }
3166+ finalPath += urlEncode(filteredParameters);
3167+ if (anchor) {
3168+ finalPath += "#" + anchor;
3169+ }
3170+ }
3171+ }
3172+
3173+ if (!verifyPath(elt, finalPath, requestConfig)) {
3174+ triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
3175+ maybeCall(reject);
3176+ return promise;
3177+ };
3178+
3179+ xhr.open(verb.toUpperCase(), finalPath, true);
3180+ xhr.overrideMimeType("text/html");
3181+ xhr.withCredentials = requestConfig.withCredentials;
3182+ xhr.timeout = requestConfig.timeout;
3183+
3184+ // request headers
3185+ if (requestAttrValues.noHeaders) {
3186+ // ignore all headers
3187+ } else {
3188+ for (var header in headers) {
3189+ if (headers.hasOwnProperty(header)) {
3190+ var headerValue = headers[header];
3191+ safelySetHeaderValue(xhr, header, headerValue);
3192+ }
3193+ }
3194+ }
3195+
3196+ var responseInfo = {
3197+ xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
3198+ pathInfo: {
3199+ requestPath: path,
3200+ finalRequestPath: finalPath,
3201+ anchor: anchor
3202+ }
3203+ };
3204+
3205+ xhr.onload = function () {
3206+ try {
3207+ var hierarchy = hierarchyForElt(elt);
3208+ responseInfo.pathInfo.responsePath = getPathFromResponse(xhr);
3209+ responseHandler(elt, responseInfo);
3210+ removeRequestIndicators(indicators, disableElts);
3211+ triggerEvent(elt, 'htmx:afterRequest', responseInfo);
3212+ triggerEvent(elt, 'htmx:afterOnLoad', responseInfo);
3213+ // if the body no longer contains the element, trigger the event on the closest parent
3214+ // remaining in the DOM
3215+ if (!bodyContains(elt)) {
3216+ var secondaryTriggerElt = null;
3217+ while (hierarchy.length > 0 && secondaryTriggerElt == null) {
3218+ var parentEltInHierarchy = hierarchy.shift();
3219+ if (bodyContains(parentEltInHierarchy)) {
3220+ secondaryTriggerElt = parentEltInHierarchy;
3221+ }
3222+ }
3223+ if (secondaryTriggerElt) {
3224+ triggerEvent(secondaryTriggerElt, 'htmx:afterRequest', responseInfo);
3225+ triggerEvent(secondaryTriggerElt, 'htmx:afterOnLoad', responseInfo);
3226+ }
3227+ }
3228+ maybeCall(resolve);
3229+ endRequestLock();
3230+ } catch (e) {
3231+ triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({error:e}, responseInfo));
3232+ throw e;
3233+ }
3234+ }
3235+ xhr.onerror = function () {
3236+ removeRequestIndicators(indicators, disableElts);
3237+ triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
3238+ triggerErrorEvent(elt, 'htmx:sendError', responseInfo);
3239+ maybeCall(reject);
3240+ endRequestLock();
3241+ }
3242+ xhr.onabort = function() {
3243+ removeRequestIndicators(indicators, disableElts);
3244+ triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
3245+ triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo);
3246+ maybeCall(reject);
3247+ endRequestLock();
3248+ }
3249+ xhr.ontimeout = function() {
3250+ removeRequestIndicators(indicators, disableElts);
3251+ triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
3252+ triggerErrorEvent(elt, 'htmx:timeout', responseInfo);
3253+ maybeCall(reject);
3254+ endRequestLock();
3255+ }
3256+ if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){
3257+ maybeCall(resolve);
3258+ endRequestLock()
3259+ return promise
3260+ }
3261+ var indicators = addRequestIndicatorClasses(elt);
3262+ var disableElts = disableElements(elt);
3263+
3264+ forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
3265+ forEach([xhr, xhr.upload], function (target) {
3266+ target.addEventListener(eventName, function(event){
3267+ triggerEvent(elt, "htmx:xhr:" + eventName, {
3268+ lengthComputable:event.lengthComputable,
3269+ loaded:event.loaded,
3270+ total:event.total
3271+ });
3272+ })
3273+ });
3274+ });
3275+ triggerEvent(elt, 'htmx:beforeSend', responseInfo);
3276+ var params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters)
3277+ xhr.send(params);
3278+ return promise;
3279+ }
3280+
3281+ function determineHistoryUpdates(elt, responseInfo) {
3282+
3283+ var xhr = responseInfo.xhr;
3284+
3285+ //===========================================
3286+ // First consult response headers
3287+ //===========================================
3288+ var pathFromHeaders = null;
3289+ var typeFromHeaders = null;
3290+ if (hasHeader(xhr,/HX-Push:/i)) {
3291+ pathFromHeaders = xhr.getResponseHeader("HX-Push");
3292+ typeFromHeaders = "push";
3293+ } else if (hasHeader(xhr,/HX-Push-Url:/i)) {
3294+ pathFromHeaders = xhr.getResponseHeader("HX-Push-Url");
3295+ typeFromHeaders = "push";
3296+ } else if (hasHeader(xhr,/HX-Replace-Url:/i)) {
3297+ pathFromHeaders = xhr.getResponseHeader("HX-Replace-Url");
3298+ typeFromHeaders = "replace";
3299+ }
3300+
3301+ // if there was a response header, that has priority
3302+ if (pathFromHeaders) {
3303+ if (pathFromHeaders === "false") {
3304+ return {}
3305+ } else {
3306+ return {
3307+ type: typeFromHeaders,
3308+ path : pathFromHeaders
3309+ }
3310+ }
3311+ }
3312+
3313+ //===========================================
3314+ // Next resolve via DOM values
3315+ //===========================================
3316+ var requestPath = responseInfo.pathInfo.finalRequestPath;
3317+ var responsePath = responseInfo.pathInfo.responsePath;
3318+
3319+ var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
3320+ var replaceUrl = getClosestAttributeValue(elt, "hx-replace-url");
3321+ var elementIsBoosted = getInternalData(elt).boosted;
3322+
3323+ var saveType = null;
3324+ var path = null;
3325+
3326+ if (pushUrl) {
3327+ saveType = "push";
3328+ path = pushUrl;
3329+ } else if (replaceUrl) {
3330+ saveType = "replace";
3331+ path = replaceUrl;
3332+ } else if (elementIsBoosted) {
3333+ saveType = "push";
3334+ path = responsePath || requestPath; // if there is no response path, go with the original request path
3335+ }
3336+
3337+ if (path) {
3338+ // false indicates no push, return empty object
3339+ if (path === "false") {
3340+ return {};
3341+ }
3342+
3343+ // true indicates we want to follow wherever the server ended up sending us
3344+ if (path === "true") {
3345+ path = responsePath || requestPath; // if there is no response path, go with the original request path
3346+ }
3347+
3348+ // restore any anchor associated with the request
3349+ if (responseInfo.pathInfo.anchor &&
3350+ path.indexOf("#") === -1) {
3351+ path = path + "#" + responseInfo.pathInfo.anchor;
3352+ }
3353+
3354+ return {
3355+ type:saveType,
3356+ path: path
3357+ }
3358+ } else {
3359+ return {};
3360+ }
3361+ }
3362+
3363+ function handleAjaxResponse(elt, responseInfo) {
3364+ var xhr = responseInfo.xhr;
3365+ var target = responseInfo.target;
3366+ var etc = responseInfo.etc;
3367+ var requestConfig = responseInfo.requestConfig;
3368+
3369+ if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
3370+
3371+ if (hasHeader(xhr, /HX-Trigger:/i)) {
3372+ handleTrigger(xhr, "HX-Trigger", elt);
3373+ }
3374+
3375+ if (hasHeader(xhr, /HX-Location:/i)) {
3376+ saveCurrentPageToHistory();
3377+ var redirectPath = xhr.getResponseHeader("HX-Location");
3378+ var swapSpec;
3379+ if (redirectPath.indexOf("{") === 0) {
3380+ swapSpec = parseJSON(redirectPath);
3381+ // what's the best way to throw an error if the user didn't include this
3382+ redirectPath = swapSpec['path'];
3383+ delete swapSpec['path'];
3384+ }
3385+ ajaxHelper('GET', redirectPath, swapSpec).then(function(){
3386+ pushUrlIntoHistory(redirectPath);
3387+ });
3388+ return;
3389+ }
3390+
3391+ var shouldRefresh = hasHeader(xhr, /HX-Refresh:/i) && "true" === xhr.getResponseHeader("HX-Refresh");
3392+
3393+ if (hasHeader(xhr, /HX-Redirect:/i)) {
3394+ location.href = xhr.getResponseHeader("HX-Redirect");
3395+ shouldRefresh && location.reload();
3396+ return;
3397+ }
3398+
3399+ if (shouldRefresh) {
3400+ location.reload();
3401+ return;
3402+ }
3403+
3404+ if (hasHeader(xhr,/HX-Retarget:/i)) {
3405+ responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
3406+ }
3407+
3408+ var historyUpdate = determineHistoryUpdates(elt, responseInfo);
3409+
3410+ // by default htmx only swaps on 200 return codes and does not swap
3411+ // on 204 'No Content'
3412+ // this can be ovverriden by responding to the htmx:beforeSwap event and
3413+ // overriding the detail.shouldSwap property
3414+ var shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204;
3415+ var serverResponse = xhr.response;
3416+ var isError = xhr.status >= 400;
3417+ var ignoreTitle = htmx.config.ignoreTitle
3418+ var beforeSwapDetails = mergeObjects({shouldSwap: shouldSwap, serverResponse:serverResponse, isError:isError, ignoreTitle:ignoreTitle }, responseInfo);
3419+ if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return;
3420+
3421+ target = beforeSwapDetails.target; // allow re-targeting
3422+ serverResponse = beforeSwapDetails.serverResponse; // allow updating content
3423+ isError = beforeSwapDetails.isError; // allow updating error
3424+ ignoreTitle = beforeSwapDetails.ignoreTitle; // allow updating ignoring title
3425+
3426+ responseInfo.target = target; // Make updated target available to response events
3427+ responseInfo.failed = isError; // Make failed property available to response events
3428+ responseInfo.successful = !isError; // Make successful property available to response events
3429+
3430+ if (beforeSwapDetails.shouldSwap) {
3431+ if (xhr.status === 286) {
3432+ cancelPolling(elt);
3433+ }
3434+
3435+ withExtensions(elt, function (extension) {
3436+ serverResponse = extension.transformResponse(serverResponse, xhr, elt);
3437+ });
3438+
3439+ // Save current page if there will be a history update
3440+ if (historyUpdate.type) {
3441+ saveCurrentPageToHistory();
3442+ }
3443+
3444+ var swapOverride = etc.swapOverride;
3445+ if (hasHeader(xhr,/HX-Reswap:/i)) {
3446+ swapOverride = xhr.getResponseHeader("HX-Reswap");
3447+ }
3448+ var swapSpec = getSwapSpecification(elt, swapOverride);
3449+
3450+ if (swapSpec.hasOwnProperty('ignoreTitle')) {
3451+ ignoreTitle = swapSpec.ignoreTitle;
3452+ }
3453+
3454+ target.classList.add(htmx.config.swappingClass);
3455+
3456+ // optional transition API promise callbacks
3457+ var settleResolve = null;
3458+ var settleReject = null;
3459+
3460+ var doSwap = function () {
3461+ try {
3462+ var activeElt = document.activeElement;
3463+ var selectionInfo = {};
3464+ try {
3465+ selectionInfo = {
3466+ elt: activeElt,
3467+ // @ts-ignore
3468+ start: activeElt ? activeElt.selectionStart : null,
3469+ // @ts-ignore
3470+ end: activeElt ? activeElt.selectionEnd : null
3471+ };
3472+ } catch (e) {
3473+ // safari issue - see https://github.com/microsoft/playwright/issues/5894
3474+ }
3475+
3476+ var selectOverride;
3477+ if (hasHeader(xhr, /HX-Reselect:/i)) {
3478+ selectOverride = xhr.getResponseHeader("HX-Reselect");
3479+ }
3480+
3481+ var settleInfo = makeSettleInfo(target);
3482+ selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
3483+
3484+ if (selectionInfo.elt &&
3485+ !bodyContains(selectionInfo.elt) &&
3486+ getRawAttribute(selectionInfo.elt, "id")) {
3487+ var newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, "id"));
3488+ var focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll };
3489+ if (newActiveElt) {
3490+ // @ts-ignore
3491+ if (selectionInfo.start && newActiveElt.setSelectionRange) {
3492+ // @ts-ignore
3493+ try {
3494+ newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
3495+ } catch (e) {
3496+ // the setSelectionRange method is present on fields that don't support it, so just let this fail
3497+ }
3498+ }
3499+ newActiveElt.focus(focusOptions);
3500+ }
3501+ }
3502+
3503+ target.classList.remove(htmx.config.swappingClass);
3504+ forEach(settleInfo.elts, function (elt) {
3505+ if (elt.classList) {
3506+ elt.classList.add(htmx.config.settlingClass);
3507+ }
3508+ triggerEvent(elt, 'htmx:afterSwap', responseInfo);
3509+ });
3510+
3511+ if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
3512+ var finalElt = elt;
3513+ if (!bodyContains(elt)) {
3514+ finalElt = getDocument().body;
3515+ }
3516+ handleTrigger(xhr, "HX-Trigger-After-Swap", finalElt);
3517+ }
3518+
3519+ var doSettle = function () {
3520+ forEach(settleInfo.tasks, function (task) {
3521+ task.call();
3522+ });
3523+ forEach(settleInfo.elts, function (elt) {
3524+ if (elt.classList) {
3525+ elt.classList.remove(htmx.config.settlingClass);
3526+ }
3527+ triggerEvent(elt, 'htmx:afterSettle', responseInfo);
3528+ });
3529+
3530+ // if we need to save history, do so
3531+ if (historyUpdate.type) {
3532+ if (historyUpdate.type === "push") {
3533+ pushUrlIntoHistory(historyUpdate.path);
3534+ triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
3535+ } else {
3536+ replaceUrlInHistory(historyUpdate.path);
3537+ triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
3538+ }
3539+ }
3540+ if (responseInfo.pathInfo.anchor) {
3541+ var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
3542+ if(anchorTarget) {
3543+ anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
3544+ }
3545+ }
3546+
3547+ if(settleInfo.title && !ignoreTitle) {
3548+ var titleElt = find("title");
3549+ if(titleElt) {
3550+ titleElt.innerHTML = settleInfo.title;
3551+ } else {
3552+ window.document.title = settleInfo.title;
3553+ }
3554+ }
3555+
3556+ updateScrollState(settleInfo.elts, swapSpec);
3557+
3558+ if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
3559+ var finalElt = elt;
3560+ if (!bodyContains(elt)) {
3561+ finalElt = getDocument().body;
3562+ }
3563+ handleTrigger(xhr, "HX-Trigger-After-Settle", finalElt);
3564+ }
3565+ maybeCall(settleResolve);
3566+ }
3567+
3568+ if (swapSpec.settleDelay > 0) {
3569+ setTimeout(doSettle, swapSpec.settleDelay)
3570+ } else {
3571+ doSettle();
3572+ }
3573+ } catch (e) {
3574+ triggerErrorEvent(elt, 'htmx:swapError', responseInfo);
3575+ maybeCall(settleReject);
3576+ throw e;
3577+ }
3578+ };
3579+
3580+ var shouldTransition = htmx.config.globalViewTransitions
3581+ if(swapSpec.hasOwnProperty('transition')){
3582+ shouldTransition = swapSpec.transition;
3583+ }
3584+
3585+ if(shouldTransition &&
3586+ triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
3587+ typeof Promise !== "undefined" && document.startViewTransition){
3588+ var settlePromise = new Promise(function (_resolve, _reject) {
3589+ settleResolve = _resolve;
3590+ settleReject = _reject;
3591+ });
3592+ // wrap the original doSwap() in a call to startViewTransition()
3593+ var innerDoSwap = doSwap;
3594+ doSwap = function() {
3595+ document.startViewTransition(function () {
3596+ innerDoSwap();
3597+ return settlePromise;
3598+ });
3599+ }
3600+ }
3601+
3602+
3603+ if (swapSpec.swapDelay > 0) {
3604+ setTimeout(doSwap, swapSpec.swapDelay)
3605+ } else {
3606+ doSwap();
3607+ }
3608+ }
3609+ if (isError) {
3610+ triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({error: "Response Status Error Code " + xhr.status + " from " + responseInfo.pathInfo.requestPath}, responseInfo));
3611+ }
3612+ }
3613+
3614+ //====================================================================
3615+ // Extensions API
3616+ //====================================================================
3617+
3618+ /** @type {Object<string, import("./htmx").HtmxExtension>} */
3619+ var extensions = {};
3620+
3621+ /**
3622+ * extensionBase defines the default functions for all extensions.
3623+ * @returns {import("./htmx").HtmxExtension}
3624+ */
3625+ function extensionBase() {
3626+ return {
3627+ init: function(api) {return null;},
3628+ onEvent : function(name, evt) {return true;},
3629+ transformResponse : function(text, xhr, elt) {return text;},
3630+ isInlineSwap : function(swapStyle) {return false;},
3631+ handleSwap : function(swapStyle, target, fragment, settleInfo) {return false;},
3632+ encodeParameters : function(xhr, parameters, elt) {return null;}
3633+ }
3634+ }
3635+
3636+ /**
3637+ * defineExtension initializes the extension and adds it to the htmx registry
3638+ *
3639+ * @param {string} name
3640+ * @param {import("./htmx").HtmxExtension} extension
3641+ */
3642+ function defineExtension(name, extension) {
3643+ if(extension.init) {
3644+ extension.init(internalAPI)
3645+ }
3646+ extensions[name] = mergeObjects(extensionBase(), extension);
3647+ }
3648+
3649+ /**
3650+ * removeExtension removes an extension from the htmx registry
3651+ *
3652+ * @param {string} name
3653+ */
3654+ function removeExtension(name) {
3655+ delete extensions[name];
3656+ }
3657+
3658+ /**
3659+ * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element
3660+ *
3661+ * @param {HTMLElement} elt
3662+ * @param {import("./htmx").HtmxExtension[]=} extensionsToReturn
3663+ * @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
3664+ */
3665+ function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
3666+
3667+ if (elt == undefined) {
3668+ return extensionsToReturn;
3669+ }
3670+ if (extensionsToReturn == undefined) {
3671+ extensionsToReturn = [];
3672+ }
3673+ if (extensionsToIgnore == undefined) {
3674+ extensionsToIgnore = [];
3675+ }
3676+ var extensionsForElement = getAttributeValue(elt, "hx-ext");
3677+ if (extensionsForElement) {
3678+ forEach(extensionsForElement.split(","), function(extensionName){
3679+ extensionName = extensionName.replace(/ /g, '');
3680+ if (extensionName.slice(0, 7) == "ignore:") {
3681+ extensionsToIgnore.push(extensionName.slice(7));
3682+ return;
3683+ }
3684+ if (extensionsToIgnore.indexOf(extensionName) < 0) {
3685+ var extension = extensions[extensionName];
3686+ if (extension && extensionsToReturn.indexOf(extension) < 0) {
3687+ extensionsToReturn.push(extension);
3688+ }
3689+ }
3690+ });
3691+ }
3692+ return getExtensions(parentElt(elt), extensionsToReturn, extensionsToIgnore);
3693+ }
3694+
3695+ //====================================================================
3696+ // Initialization
3697+ //====================================================================
3698+ var isReady = false
3699+ getDocument().addEventListener('DOMContentLoaded', function() {
3700+ isReady = true
3701+ })
3702+
3703+ /**
3704+ * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
3705+ *
3706+ * This function uses isReady because there is no realiable way to ask the browswer whether
3707+ * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
3708+ * firing and readystate=complete.
3709+ */
3710+ function ready(fn) {
3711+ // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
3712+ // some means other than the initial page load.
3713+ if (isReady || getDocument().readyState === 'complete') {
3714+ fn();
3715+ } else {
3716+ getDocument().addEventListener('DOMContentLoaded', fn);
3717+ }
3718+ }
3719+
3720+ function insertIndicatorStyles() {
3721+ if (htmx.config.includeIndicatorStyles !== false) {
3722+ getDocument().head.insertAdjacentHTML("beforeend",
3723+ "<style>\
3724+ ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
3725+ ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
3726+ ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
3727+ </style>");
3728+ }
3729+ }
3730+
3731+ function getMetaConfig() {
3732+ var element = getDocument().querySelector('meta[name="htmx-config"]');
3733+ if (element) {
3734+ // @ts-ignore
3735+ return parseJSON(element.content);
3736+ } else {
3737+ return null;
3738+ }
3739+ }
3740+
3741+ function mergeMetaConfig() {
3742+ var metaConfig = getMetaConfig();
3743+ if (metaConfig) {
3744+ htmx.config = mergeObjects(htmx.config , metaConfig)
3745+ }
3746+ }
3747+
3748+ // initialize the document
3749+ ready(function () {
3750+ mergeMetaConfig();
3751+ insertIndicatorStyles();
3752+ var body = getDocument().body;
3753+ processNode(body);
3754+ var restoredElts = getDocument().querySelectorAll(
3755+ "[hx-trigger='restored'],[data-hx-trigger='restored']"
3756+ );
3757+ body.addEventListener("htmx:abort", function (evt) {
3758+ var target = evt.target;
3759+ var internalData = getInternalData(target);
3760+ if (internalData && internalData.xhr) {
3761+ internalData.xhr.abort();
3762+ }
3763+ });
3764+ var originalPopstate = window.onpopstate;
3765+ window.onpopstate = function (event) {
3766+ if (event.state && event.state.htmx) {
3767+ restoreHistory();
3768+ forEach(restoredElts, function(elt){
3769+ triggerEvent(elt, 'htmx:restored', {
3770+ 'document': getDocument(),
3771+ 'triggerEvent': triggerEvent
3772+ });
3773+ });
3774+ } else {
3775+ if (originalPopstate) {
3776+ originalPopstate(event);
3777+ }
3778+ }
3779+ };
3780+ setTimeout(function () {
3781+ triggerEvent(body, 'htmx:load', {}); // give ready handlers a chance to load up before firing this event
3782+ body = null; // kill reference for gc
3783+ }, 0);
3784+ })
3785+
3786+ return htmx;
3787+ }
3788+)()
3789+}));
A internal/webui/public/libs/hyperscript.min.js +1 -0 generated file: collapsed
M tailwind.config.js +4 -4
@@ -2,7 +2,7 @@
22 module.exports = {
33 content: [
44 "./internal/webui/**/*.{gohtml,go}",
5- "./internal/templates/models/html/**/*.{gohtml, go}"
5+ "./internal/templating/models/html/**/*.{gohtml, go}",
66 ],
77 theme: {
88 extend: {
@@ -10,12 +10,12 @@ module.exports = {
1010 gordonmascot: {
1111 lblue: "#007BC0",
1212 mblue: "#005293",
13- hblue: '#011E5C',
13+ hblue: "#011E5C",
1414 lbeige: "#FFDCB3",
15- mbeige: '#D7B49F',
15+ mbeige: "#D7B49F",
1616 },
1717 },
1818 },
1919 },
2020 plugins: [require("@catppuccin/tailwindcss")],
21-}⏎̸
21+};

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close