UNPKG

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