UNPKG

87.6 kBJavaScriptView Raw
1//AMD insanity
2(function (root, factory) {
3 if (typeof define === 'function' && define.amd) {
4 // AMD. Register as an anonymous module.
5 define([], factory);
6 } else {
7 // Browser globals
8 root.htmx = factory();
9 }
10}(typeof self !== 'undefined' ? self : this, function () {
11return (function () {
12 'use strict';
13
14 // Public API
15 var htmx = {
16 onLoad: onLoadHelper,
17 process: processNode,
18 on: addEventListenerImpl,
19 off: removeEventListenerImpl,
20 trigger : triggerEvent,
21 find : find,
22 findAll : findAll,
23 closest : closest,
24 remove : removeElement,
25 addClass : addClassToElement,
26 removeClass : removeClassFromElement,
27 toggleClass : toggleClassOnElement,
28 takeClass : takeClassForElement,
29 defineExtension : defineExtension,
30 removeExtension : removeExtension,
31 logAll : logAll,
32 logger : null,
33 config : {
34 historyEnabled:true,
35 historyCacheSize:10,
36 defaultSwapStyle:'innerHTML',
37 defaultSwapDelay:0,
38 defaultSettleDelay:100,
39 includeIndicatorStyles:true,
40 indicatorClass:'htmx-indicator',
41 requestClass:'htmx-request',
42 settlingClass:'htmx-settling',
43 swappingClass:'htmx-swapping',
44 attributesToSettle:["class", "style", "width", "height"]
45 },
46 parseInterval:parseInterval,
47 _:internalEval,
48 createEventSource: function(url){
49 return new EventSource(url, {withCredentials:true})
50 },
51 createWebSocket: function(url){
52 return new WebSocket(url, []);
53 }
54 };
55
56 var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
57 var VERB_SELECTOR = VERBS.map(function(verb){
58 return "[hx-" + verb + "], [data-hx-" + verb + "]"
59 }).join(", ");
60
61 //====================================================================
62 // Utilities
63 //====================================================================
64 function parseInterval(str) {
65 if (str == null || str === "null" || str === "false" || str === "") {
66 return null;
67 } else if (str.lastIndexOf("ms") === str.length - 2) {
68 return parseFloat(str.substr(0, str.length - 2));
69 } else if (str.lastIndexOf("s") === str.length - 1) {
70 return parseFloat(str.substr(0, str.length - 1)) * 1000;
71 } else {
72 return parseFloat(str);
73 }
74 }
75
76 function getRawAttribute(elt, name) {
77 return elt.getAttribute && elt.getAttribute(name);
78 }
79
80 // resolve with both hx and data-hx prefixes
81 function hasAttribute(elt, qualifiedName) {
82 return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
83 elt.hasAttribute("data-" + qualifiedName));
84 }
85
86 function getAttributeValue(elt, qualifiedName) {
87 return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
88 }
89
90 function parentElt(elt) {
91 return elt.parentElement;
92 }
93
94 function getDocument() {
95 return document;
96 }
97
98 function getClosestMatch(elt, condition) {
99 if (condition(elt)) {
100 return elt;
101 } else if (parentElt(elt)) {
102 return getClosestMatch(parentElt(elt), condition);
103 } else {
104 return null;
105 }
106 }
107
108 function getClosestAttributeValue(elt, attributeName) {
109 var closestAttr = null;
110 getClosestMatch(elt, function (e) {
111 return closestAttr = getAttributeValue(e, attributeName);
112 });
113 return closestAttr;
114 }
115
116 function matches(elt, selector) {
117 // noinspection JSUnresolvedVariable
118 var matchesFunction = elt.matches ||
119 elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
120 || elt.webkitMatchesSelector || elt.oMatchesSelector;
121 return matchesFunction && matchesFunction.call(elt, selector);
122 }
123
124 function getStartTag(str) {
125 var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
126 var match = tagMatcher.exec( str );
127 if (match) {
128 return match[1].toLowerCase();
129 } else {
130 return "";
131 }
132 }
133
134 function parseHTML(resp, depth) {
135 var parser = new DOMParser();
136 var responseDoc = parser.parseFromString(resp, "text/html");
137 var responseNode = responseDoc.body;
138 while (depth > 0) {
139 depth--;
140 responseNode = responseNode.firstChild;
141 }
142 if (responseNode == null) {
143 responseNode = getDocument().createDocumentFragment();
144 }
145 return responseNode;
146 }
147
148 function makeFragment(resp) {
149 var startTag = getStartTag(resp);
150 switch (startTag) {
151 case "thead":
152 case "tbody":
153 case "tfoot":
154 case "colgroup":
155 case "caption":
156 return parseHTML("<table>" + resp + "</table>", 1);
157 case "col":
158 return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
159 case "tr":
160 return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
161 case "td":
162 case "th":
163 return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
164 case "script":
165 return parseHTML("<div>" + resp + "</div>", 1);
166 default:
167 return parseHTML(resp, 0);
168 }
169 }
170
171 function isType(o, type) {
172 return Object.prototype.toString.call(o) === "[object " + type + "]";
173 }
174
175 function isFunction(o) {
176 return isType(o, "Function");
177 }
178
179 function isRawObject(o) {
180 return isType(o, "Object");
181 }
182
183 function getInternalData(elt) {
184 var dataProp = 'htmx-internal-data';
185 var data = elt[dataProp];
186 if (!data) {
187 data = elt[dataProp] = {};
188 }
189 return data;
190 }
191
192 function toArray(arr) {
193 var returnArr = [];
194 if (arr) {
195 for (var i = 0; i < arr.length; i++) {
196 returnArr.push(arr[i]);
197 }
198 }
199 return returnArr
200 }
201
202 function forEach(arr, func) {
203 if (arr) {
204 for (var i = 0; i < arr.length; i++) {
205 func(arr[i]);
206 }
207 }
208 }
209
210 function isScrolledIntoView(el) {
211 var rect = el.getBoundingClientRect();
212 var elemTop = rect.top;
213 var elemBottom = rect.bottom;
214 return elemTop < window.innerHeight && elemBottom >= 0;
215 }
216
217 function bodyContains(elt) {
218 return getDocument().body.contains(elt);
219 }
220
221 function splitOnWhitespace(trigger) {
222 return trigger.trim().split(/\s+/);
223 }
224
225 function mergeObjects(obj1, obj2) {
226 for (var key in obj2) {
227 if (obj2.hasOwnProperty(key)) {
228 obj1[key] = obj2[key];
229 }
230 }
231 return obj1;
232 }
233
234 function parseJSON(jString) {
235 try {
236 return JSON.parse(jString);
237 } catch(error) {
238 logError(error);
239 return null;
240 }
241 }
242
243 //==========================================================================================
244 // public API
245 //==========================================================================================
246
247 function internalEval(str){
248 return eval(str);
249 }
250
251 function onLoadHelper(callback) {
252 var value = htmx.on("htmx:load", function(evt) {
253 callback(evt.detail.elt);
254 });
255 return value;
256 }
257
258 function logAll(){
259 htmx.logger = function(elt, event, data) {
260 if(console) {
261 console.log(event, elt, data);
262 }
263 }
264 }
265
266 function find(eltOrSelector, selector) {
267 if (selector) {
268 return eltOrSelector.querySelector(selector);
269 } else {
270 return getDocument().body.querySelector(eltOrSelector);
271 }
272 }
273
274 function findAll(eltOrSelector, selector) {
275 if (selector) {
276 return eltOrSelector.querySelectorAll(selector);
277 } else {
278 return getDocument().body.querySelectorAll(eltOrSelector);
279 }
280 }
281
282 function removeElement(elt, delay) {
283 if (delay) {
284 setTimeout(function(){removeElement(elt);}, delay)
285 } else {
286 elt.parentElement.removeChild(elt);
287 }
288 }
289
290 function addClassToElement(elt, clazz, delay) {
291 if (delay) {
292 setTimeout(function(){addClassToElement(elt, clazz);}, delay)
293 } else {
294 elt.classList.add(clazz);
295 }
296 }
297
298 function removeClassFromElement(elt, clazz, delay) {
299 if (delay) {
300 setTimeout(function(){removeClassFromElement(elt, clazz);}, delay)
301 } else {
302 elt.classList.remove(clazz);
303 }
304 }
305
306 function toggleClassOnElement(elt, clazz) {
307 elt.classList.toggle(clazz);
308 }
309
310 function takeClassForElement(elt, clazz) {
311 forEach(elt.parentElement.children, function(child){
312 removeClassFromElement(child, clazz);
313 })
314 addClassToElement(elt, clazz);
315 }
316
317 function closest(elt, selector) {
318 do if (elt == null || matches(elt, selector)) return elt;
319 while (elt = elt && parentElt(elt));
320 }
321
322 function processEventArgs(arg1, arg2, arg3) {
323 if (isFunction(arg2)) {
324 return {
325 target: getDocument().body,
326 event: arg1,
327 listener: arg2
328 }
329 } else {
330 return {
331 target: arg1,
332 event: arg2,
333 listener: arg3
334 }
335 }
336
337 }
338
339 function addEventListenerImpl(arg1, arg2, arg3) {
340 ready(function(){
341 var eventArgs = processEventArgs(arg1, arg2, arg3);
342 eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener);
343 })
344 var b = isFunction(arg2);
345 return b ? arg2 : arg3;
346 }
347
348 function removeEventListenerImpl(arg1, arg2, arg3) {
349 ready(function(){
350 var eventArgs = processEventArgs(arg1, arg2, arg3);
351 eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener);
352 })
353 return isFunction(arg2) ? arg2 : arg3;
354 }
355
356 //====================================================================
357 // Node processing
358 //====================================================================
359
360 function getTarget(elt) {
361 var explicitTarget = getClosestMatch(elt, function(e){return getAttributeValue(e,"hx-target") !== null});
362 if (explicitTarget) {
363 var targetStr = getAttributeValue(explicitTarget, "hx-target");
364 if (targetStr === "this") {
365 return explicitTarget;
366 } else if (targetStr.indexOf("closest ") === 0) {
367 return closest(elt, targetStr.substr(8));
368 } else if (targetStr.indexOf("find ") === 0) {
369 return find(elt, targetStr.substr(5));
370 } else {
371 return getDocument().querySelector(targetStr);
372 }
373 } else {
374 var data = getInternalData(elt);
375 if (data.boosted) {
376 return getDocument().body;
377 } else {
378 return elt;
379 }
380 }
381 }
382
383 function shouldSettleAttribute(name) {
384 var attributesToSettle = htmx.config.attributesToSettle;
385 for (var i = 0; i < attributesToSettle.length; i++) {
386 if (name === attributesToSettle[i]) {
387 return true;
388 }
389 }
390 return false;
391 }
392
393 function cloneAttributes(mergeTo, mergeFrom) {
394 forEach(mergeTo.attributes, function (attr) {
395 if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
396 mergeTo.removeAttribute(attr.name)
397 }
398 });
399 forEach(mergeFrom.attributes, function (attr) {
400 if (shouldSettleAttribute(attr.name)) {
401 mergeTo.setAttribute(attr.name, attr.value);
402 }
403 });
404 }
405
406 function isInlineSwap(swapStyle, target) {
407 var extensions = getExtensions(target);
408 for (var i = 0; i < extensions.length; i++) {
409 var extension = extensions[i];
410 try {
411 if (extension.isInlineSwap(swapStyle)) {
412 return true;
413 }
414 } catch(e) {
415 logError(e);
416 }
417 }
418 return swapStyle === "outerHTML";
419 }
420
421 function oobSwap(oobValue, child, settleInfo) {
422 if (oobValue === "true") {
423 oobValue = "outerHTML"
424 }
425 var target = getDocument().getElementById(child.id);
426 if (target) {
427 var fragment;
428 fragment = getDocument().createDocumentFragment();
429 fragment.appendChild(child); // pulls the child out of the existing fragment
430 if (!isInlineSwap(oobValue, target)) {
431 fragment = child; // if this is not an inline swap, we use the content of the node, not the node itself
432 }
433 swap(oobValue, target, target, fragment, settleInfo);
434 } else {
435 child.parentNode.removeChild(child);
436 triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: child})
437 }
438 return oobValue;
439 }
440
441 function handleOutOfBandSwaps(fragment, settleInfo) {
442 forEach(toArray(fragment.children), function (child) {
443 var oobValue = getAttributeValue(child, "hx-swap-oob");
444 if (oobValue != null) {
445 oobSwap(oobValue, child, settleInfo);
446 }
447 });
448 }
449
450 function handleAttributes(parentNode, fragment, settleInfo) {
451 forEach(fragment.querySelectorAll("[id]"), function (newNode) {
452 if (newNode.id && newNode.id.length > 0) {
453 var oldNode = parentNode.querySelector(newNode.tagName + "[id='" + newNode.id + "']");
454 if (oldNode && oldNode !== parentNode) {
455 var newAttributes = newNode.cloneNode();
456 cloneAttributes(newNode, oldNode);
457 settleInfo.tasks.push(function () {
458 cloneAttributes(newNode, newAttributes);
459 });
460 }
461 }
462 });
463 }
464
465 function makeAjaxLoadTask(child) {
466 return function () {
467 processNode(child);
468 processScripts(child);
469 processFocus(child)
470 triggerEvent(child, 'htmx:load');
471 };
472 }
473
474 function processFocus(child) {
475 var autofocus = "[autofocus]";
476 var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
477 if (autoFocusedElt != null) {
478 autoFocusedElt.focus();
479 }
480 }
481
482 function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
483 handleAttributes(parentNode, fragment, settleInfo);
484 while(fragment.childNodes.length > 0){
485 var child = fragment.firstChild;
486 parentNode.insertBefore(child, insertBefore);
487 if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
488 settleInfo.tasks.push(makeAjaxLoadTask(child));
489 }
490 }
491 }
492
493 function closeConnections(target) {
494 var internalData = getInternalData(target);
495 if (internalData.webSocket) {
496 internalData.webSocket.close();
497 }
498 if (internalData.sseEventSource) {
499 internalData.sseEventSource.close();
500 }
501 if (target.children) { // IE
502 forEach(target.children, function(child) { closeConnections(child) });
503 }
504 }
505
506 function swapOuterHTML(target, fragment, settleInfo) {
507 if (target.tagName === "BODY") {
508 return swapInnerHTML(target, fragment);
509 } else {
510 var eltBeforeNewContent = target.previousSibling;
511 insertNodesBefore(parentElt(target), target, fragment, settleInfo);
512 if (eltBeforeNewContent == null) {
513 var newElt = parentElt(target).firstChild;
514 } else {
515 var newElt = eltBeforeNewContent.nextSibling;
516 }
517 getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
518 while(newElt && newElt !== target) {
519 if (newElt.nodeType === Node.ELEMENT_NODE) {
520 settleInfo.elts.push(newElt);
521 }
522 newElt = newElt.nextElementSibling;
523 }
524 closeConnections(target);
525 parentElt(target).removeChild(target);
526 }
527 }
528
529 function swapAfterBegin(target, fragment, settleInfo) {
530 return insertNodesBefore(target, target.firstChild, fragment, settleInfo);
531 }
532
533 function swapBeforeBegin(target, fragment, settleInfo) {
534 return insertNodesBefore(parentElt(target), target, fragment, settleInfo);
535 }
536
537 function swapBeforeEnd(target, fragment, settleInfo) {
538 return insertNodesBefore(target, null, fragment, settleInfo);
539 }
540
541 function swapAfterEnd(target, fragment, settleInfo) {
542 return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo);
543 }
544
545 function swapInnerHTML(target, fragment, settleInfo) {
546 var firstChild = target.firstChild;
547 insertNodesBefore(target, firstChild, fragment, settleInfo);
548 if (firstChild) {
549 while (firstChild.nextSibling) {
550 closeConnections(firstChild.nextSibling)
551 target.removeChild(firstChild.nextSibling);
552 }
553 closeConnections(firstChild)
554 target.removeChild(firstChild);
555 }
556 }
557
558 function maybeSelectFromResponse(elt, fragment) {
559 var selector = getClosestAttributeValue(elt, "hx-select");
560 if (selector) {
561 var newFragment = getDocument().createDocumentFragment();
562 forEach(fragment.querySelectorAll(selector), function (node) {
563 newFragment.appendChild(node);
564 });
565 fragment = newFragment;
566 }
567 return fragment;
568 }
569
570 function swap(swapStyle, elt, target, fragment, settleInfo) {
571 switch (swapStyle) {
572 case "none":
573 return;
574 case "outerHTML":
575 swapOuterHTML(target, fragment, settleInfo);
576 return;
577 case "afterbegin":
578 swapAfterBegin(target, fragment, settleInfo);
579 return;
580 case "beforebegin":
581 swapBeforeBegin(target, fragment, settleInfo);
582 return;
583 case "beforeend":
584 swapBeforeEnd(target, fragment, settleInfo);
585 return;
586 case "afterend":
587 swapAfterEnd(target, fragment, settleInfo);
588 return;
589 default:
590 var extensions = getExtensions(elt);
591 for (var i = 0; i < extensions.length; i++) {
592 var ext = extensions[i];
593 try {
594 var newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo);
595 if (newElements) {
596 if (typeof newElements.length !== 'undefined') {
597 // if handleSwap returns an array (like) of elements, we handle them
598 for (var j = 0; j < newElements.length; j++) {
599 var child = newElements[j];
600 if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
601 settleInfo.tasks.push(makeAjaxLoadTask(child));
602 }
603 }
604 }
605 return;
606 }
607 } catch (e) {
608 logError(e);
609 }
610 }
611 swapInnerHTML(target, fragment, settleInfo);
612 }
613 }
614
615 var TITLE_FINDER = /<title>([\s\S]+?)<\/title>/im;
616 function findTitle(content) {
617 var result = TITLE_FINDER.exec(content);
618 if (result) {
619 return result[1];
620 }
621 }
622
623 function selectAndSwap(swapStyle, target, elt, responseText, settleInfo) {
624 var title = findTitle(responseText);
625 if(title) {
626 window.document.title = title;
627 }
628 var fragment = makeFragment(responseText);
629 if (fragment) {
630 handleOutOfBandSwaps(fragment, settleInfo);
631 fragment = maybeSelectFromResponse(elt, fragment);
632 return swap(swapStyle, elt, target, fragment, settleInfo);
633 }
634 }
635
636 function handleTrigger(xhr, header, elt) {
637 var triggerBody = xhr.getResponseHeader(header);
638 if (triggerBody.indexOf("{") === 0) {
639 var triggers = parseJSON(triggerBody);
640 for (var eventName in triggers) {
641 if (triggers.hasOwnProperty(eventName)) {
642 var detail = triggers[eventName];
643 if (!isRawObject(detail)) {
644 detail = {"value": detail}
645 }
646 triggerEvent(elt, eventName, detail);
647 }
648 }
649 } else {
650 triggerEvent(elt, triggerBody, []);
651 }
652 }
653
654 var WHITESPACE = /\s/;
655 var SYMBOL_START = /[_$a-zA-Z]/;
656 var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
657 var STRINGISH_START = ['"', "'", "/"];
658 var NOT_WHITESPACE = /[^\s]/;
659 function tokenizeString(str) {
660 var tokens = [];
661 var position = 0;
662 while (position < str.length) {
663 if(SYMBOL_START.exec(str.charAt(position))) {
664 var startPosition = position;
665 position++;
666 while (SYMBOL_CONT.exec(str.charAt(position + 1))) {
667 position++;
668 }
669 tokens.push(str.substr(startPosition, position - startPosition + 1));
670 } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) {
671 var startChar = str.charAt(position);
672 var startPosition = position;
673 position++;
674 while (position < str.length && str.charAt(position) !== startChar ) {
675 if (str.charAt(position) === "\\") {
676 position++;
677 }
678 position++;
679 }
680 tokens.push(str.substr(startPosition, position - startPosition + 1));
681 } else {
682 var symbol = str.charAt(position);
683 tokens.push(symbol);
684 }
685 position++;
686 }
687 return tokens;
688 }
689
690 function isPossibleRelativeReference(token, last, paramName) {
691 return SYMBOL_START.exec(token.charAt(0)) &&
692 token !== "true" &&
693 token !== "false" &&
694 token !== "this" &&
695 token !== paramName &&
696 last !== ".";
697 }
698
699 function maybeGenerateConditional(tokens, paramName) {
700 if (tokens[0] === '[') {
701 tokens.shift();
702 var bracketCount = 1;
703 var conditionalSource = " return (function(" + paramName + "){ return (";
704 var last = null;
705 while (tokens.length > 0) {
706 var token = tokens[0];
707 if (token === "]") {
708 bracketCount--;
709 if (bracketCount === 0) {
710 if (last === null) {
711 conditionalSource = conditionalSource + "true";
712 }
713 tokens.shift();
714 conditionalSource += ")})";
715 try {
716 var conditionFunction = Function(conditionalSource)();
717 conditionFunction.source = conditionalSource;
718 return conditionFunction;
719 } catch (e) {
720 triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource})
721 return null;
722 }
723 }
724 } else if (token === "[") {
725 bracketCount++;
726 }
727 if (isPossibleRelativeReference(token, last, paramName)) {
728 conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
729 } else {
730 conditionalSource = conditionalSource + token;
731 }
732 last = tokens.shift();
733 }
734 }
735 }
736
737 function consumeUntil(tokens, match) {
738 var result = "";
739 while (tokens.length > 0 && !tokens[0].match(match)) {
740 result += tokens.shift();
741 }
742 return result;
743 }
744
745 function getTriggerSpecs(elt) {
746 var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
747 var triggerSpecs = [];
748 if (explicitTrigger) {
749 var tokens = tokenizeString(explicitTrigger);
750 do {
751 consumeUntil(tokens, NOT_WHITESPACE);
752 var initialLength = tokens.length;
753 var trigger = consumeUntil(tokens, /[,\[\s]/);
754 if (trigger !== "") {
755 if (trigger === "every") {
756 var every = {trigger: 'every'};
757 consumeUntil(tokens, NOT_WHITESPACE);
758 every.pollInterval = parseInterval(consumeUntil(tokens, WHITESPACE));
759 triggerSpecs.push(every);
760 } else if (trigger.indexOf("sse:") === 0) {
761 triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
762 } else {
763 var triggerSpec = {trigger: trigger};
764 var eventFilter = maybeGenerateConditional(tokens, "event");
765 if (eventFilter) {
766 triggerSpec.eventFilter = eventFilter;
767 }
768 while (tokens.length > 0 && tokens[0] !== ",") {
769 consumeUntil(tokens, NOT_WHITESPACE)
770 var token = tokens.shift();
771 if (token === "changed") {
772 triggerSpec.changed = true;
773 } else if (token === "once") {
774 triggerSpec.once = true;
775 } else if (token === "delay" && tokens[0] === ":") {
776 tokens.shift();
777 triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE));
778 } else if (token === "throttle" && tokens[0] === ":") {
779 tokens.shift();
780 triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE));
781 } else {
782 triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
783 }
784 }
785 triggerSpecs.push(triggerSpec);
786 }
787 }
788 if (tokens.length === initialLength) {
789 triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
790 }
791 consumeUntil(tokens, NOT_WHITESPACE);
792 } while (tokens[0] === "," && tokens.shift())
793 }
794
795 if (triggerSpecs.length > 0) {
796 return triggerSpecs;
797 } else if (matches(elt, 'form')) {
798 return [{trigger: 'submit'}];
799 } else if (matches(elt, 'input, textarea, select')) {
800 return [{trigger: 'change'}];
801 } else {
802 return [{trigger: 'click'}];
803 }
804 }
805
806 function cancelPolling(elt) {
807 getInternalData(elt).cancelled = true;
808 }
809
810 function processPolling(elt, verb, path, interval) {
811 var nodeData = getInternalData(elt);
812 nodeData.timeout = setTimeout(function () {
813 if (bodyContains(elt) && nodeData.cancelled !== true) {
814 issueAjaxRequest(elt, verb, path);
815 processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb), interval);
816 }
817 }, interval);
818 }
819
820 function isLocalLink(elt) {
821 return location.hostname === elt.hostname &&
822 getRawAttribute(elt,'href') &&
823 getRawAttribute(elt,'href').indexOf("#") !== 0;
824 }
825
826 function boostElement(elt, nodeData, triggerSpecs) {
827 if ((elt.tagName === "A" && isLocalLink(elt)) || elt.tagName === "FORM") {
828 nodeData.boosted = true;
829 var verb, path;
830 if (elt.tagName === "A") {
831 verb = "get";
832 path = getRawAttribute(elt, 'href');
833 } else {
834 var rawAttribute = getRawAttribute(elt, "method");
835 verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
836 path = getRawAttribute(elt, 'action');
837 }
838 triggerSpecs.forEach(function(triggerSpec) {
839 addEventListener(elt, verb, path, nodeData, triggerSpec, true);
840 });
841 }
842 }
843
844 function shouldCancel(elt) {
845 return elt.tagName === "FORM" ||
846 (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) ||
847 (elt.tagName === "A" && elt.href && elt.href.indexOf('#') !== 0);
848 }
849
850 function ignoreBoostedAnchorCtrlClick(elt, evt) {
851 return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && evt.ctrlKey;
852 }
853
854 function maybeFilterEvent(triggerSpec, evt) {
855 var eventFilter = triggerSpec.eventFilter;
856 if(eventFilter){
857 try {
858 return eventFilter(evt) !== true;
859 } catch(e) {
860 triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source});
861 return true;
862 }
863 }
864 return false;
865 }
866
867 function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) {
868 var eventListener = function (evt) {
869 if (maybeFilterEvent(triggerSpec, evt)) {
870 return;
871 }
872 if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
873 return;
874 }
875 if(explicitCancel || shouldCancel(elt)){
876 evt.preventDefault();
877 }
878 var eventData = getInternalData(evt);
879 var elementData = getInternalData(elt);
880 if (!eventData.handled) {
881 eventData.handled = true;
882 if (triggerSpec.once) {
883 if (elementData.triggeredOnce) {
884 return;
885 } else {
886 elementData.triggeredOnce = true;
887 }
888 }
889 if (triggerSpec.changed) {
890 if (elementData.lastValue === elt.value) {
891 return;
892 } else {
893 elementData.lastValue = elt.value;
894 }
895 }
896 if (elementData.delayed) {
897 clearTimeout(elementData.delayed);
898 }
899 if (elementData.throttle) {
900 return;
901 }
902
903 if (triggerSpec.throttle) {
904 elementData.throttle = setTimeout(function(){
905 issueAjaxRequest(elt, verb, path, evt.target, evt);
906 elementData.throttle = null;
907 }, triggerSpec.throttle);
908 } else if (triggerSpec.delay) {
909 elementData.delayed = setTimeout(function(){
910 issueAjaxRequest(elt, verb, path, evt.target, evt);
911 }, triggerSpec.delay);
912 } else {
913 issueAjaxRequest(elt, verb, path, evt.target, evt);
914 }
915 }
916 };
917 nodeData.trigger = triggerSpec.trigger;
918 nodeData.eventListener = eventListener;
919 elt.addEventListener(triggerSpec.trigger, eventListener);
920 }
921
922 var windowIsScrolling = false // used by initScrollHandler
923 var scrollHandler = null;
924 function initScrollHandler() {
925 if (!scrollHandler) {
926 scrollHandler = function() {
927 windowIsScrolling = true
928 };
929 window.addEventListener("scroll", scrollHandler)
930 setInterval(function() {
931 if (windowIsScrolling) {
932 windowIsScrolling = false;
933 forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
934 maybeReveal(elt);
935 })
936 }
937 }, 200);
938 }
939 }
940
941 function maybeReveal(elt) {
942 var nodeData = getInternalData(elt);
943 if (!nodeData.revealed && isScrolledIntoView(elt)) {
944 nodeData.revealed = true;
945 issueAjaxRequest(elt, nodeData.verb, nodeData.path);
946 }
947 }
948
949 function processWebSocketInfo(elt, nodeData, info) {
950 var values = splitOnWhitespace(info);
951 for (var i = 0; i < values.length; i++) {
952 var value = values[i].split(/:(.+)/);
953 if (value[0] === "connect") {
954 processWebSocketSource(elt, value[1]);
955 }
956 if (value[0] === "send") {
957 processWebSocketSend(elt);
958 }
959 }
960 }
961
962 function processWebSocketSource(elt, wssSource) {
963 if (wssSource.indexOf("ws:") !== 0 && wssSource.indexOf("wss:") !== 0) {
964 wssSource = "wss:" + wssSource;
965 }
966 var socket = htmx.createWebSocket(wssSource);
967 socket.onerror = function (e) {
968 triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
969 maybeCloseWebSocketSource(elt);
970 };
971 getInternalData(elt).webSocket = socket;
972 socket.addEventListener('message', function (event) {
973 if (maybeCloseWebSocketSource(elt)) {
974 return;
975 }
976
977 var response = event.data;
978 withExtensions(elt, function(extension){
979 response = extension.transformResponse(response, null, elt);
980 });
981
982 var settleInfo = makeSettleInfo(elt);
983 var fragment = makeFragment(response);
984 var children = toArray(fragment.children);
985 for (var i = 0; i < children.length; i++) {
986 var child = children[i];
987 oobSwap(getAttributeValue(child, "hx-swap-oob") || "true", child, settleInfo);
988 }
989
990 settleImmediately(settleInfo.tasks);
991 });
992 }
993
994 function maybeCloseWebSocketSource(elt) {
995 if (!bodyContains(elt)) {
996 getInternalData(elt).webSocket.close();
997 return true;
998 }
999 }
1000
1001 function processWebSocketSend(elt) {
1002 var webSocketSourceElt = getClosestMatch(elt, function (parent) {
1003 return getInternalData(parent).webSocket != null;
1004 });
1005 if (webSocketSourceElt) {
1006 var webSocket = getInternalData(webSocketSourceElt).webSocket;
1007 elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) {
1008 var headers = getHeaders(elt, webSocketSourceElt, null, elt);
1009 var results = getInputValues(elt, 'post');
1010 var errors = results.errors;
1011 var rawParameters = results.values;
1012 var expressionVars = getExpressionVars(elt);
1013 var allParameters = mergeObjects(rawParameters, expressionVars);
1014 var filteredParameters = filterValues(allParameters, elt);
1015 filteredParameters['HEADERS'] = headers;
1016 if (errors && errors.length > 0) {
1017 triggerEvent(elt, 'htmx:validation:halted', errors);
1018 return;
1019 }
1020 webSocket.send(JSON.stringify(filteredParameters));
1021 if(shouldCancel(elt)){
1022 evt.preventDefault();
1023 }
1024 });
1025 } else {
1026 triggerErrorEvent(elt, "htmx:noWebSocketSourceError");
1027 }
1028 }
1029
1030 //====================================================================
1031 // Server Sent Events
1032 //====================================================================
1033
1034 function processSSEInfo(elt, nodeData, info) {
1035 var values = splitOnWhitespace(info);
1036 for (var i = 0; i < values.length; i++) {
1037 var value = values[i].split(/:(.+)/);
1038 if (value[0] === "connect") {
1039 processSSESource(elt, value[1]);
1040 }
1041
1042 if ((value[0] === "swap")) {
1043 processSSESwap(elt, value[1])
1044 }
1045 }
1046 }
1047
1048 function processSSESource(elt, sseSrc) {
1049 var source = htmx.createEventSource(sseSrc);
1050 source.onerror = function (e) {
1051 triggerErrorEvent(elt, "htmx:sseError", {error:e, source:source});
1052 maybeCloseSSESource(elt);
1053 };
1054 getInternalData(elt).sseEventSource = source;
1055 }
1056
1057 function processSSESwap(elt, sseEventName) {
1058 var sseSourceElt = getClosestMatch(elt, hasEventSource);
1059 if (sseSourceElt) {
1060 var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
1061 var sseListener = function (event) {
1062 if (maybeCloseSSESource(sseSourceElt)) {
1063 sseEventSource.removeEventListener(sseEventName, sseListener);
1064 return;
1065 }
1066
1067 ///////////////////////////
1068 // TODO: merge this code with AJAX and WebSockets code in the future.
1069
1070 var response = event.data;
1071 withExtensions(elt, function(extension){
1072 response = extension.transformResponse(response, null, elt);
1073 });
1074
1075 var swapSpec = getSwapSpecification(elt)
1076 var target = getTarget(elt)
1077 var settleInfo = makeSettleInfo(elt);
1078
1079 selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
1080 triggerEvent(elt, "htmx:sseMessage", event)
1081 };
1082
1083 getInternalData(elt).sseListener = sseListener;
1084 sseEventSource.addEventListener(sseEventName, sseListener);
1085 } else {
1086 triggerErrorEvent(elt, "htmx:noSSESourceError");
1087 }
1088 }
1089
1090 function processSSETrigger(elt, verb, path, sseEventName) {
1091 var sseSourceElt = getClosestMatch(elt, hasEventSource);
1092 if (sseSourceElt) {
1093 var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
1094 var sseListener = function () {
1095 if (!maybeCloseSSESource(sseSourceElt)) {
1096 if (bodyContains(elt)) {
1097 issueAjaxRequest(elt, verb, path);
1098 } else {
1099 sseEventSource.removeEventListener(sseEventName, sseListener);
1100 }
1101 }
1102 };
1103 getInternalData(elt).sseListener = sseListener;
1104 sseEventSource.addEventListener(sseEventName, sseListener);
1105 } else {
1106 triggerErrorEvent(elt, "htmx:noSSESourceError");
1107 }
1108 }
1109
1110 function maybeCloseSSESource(elt) {
1111 if (!bodyContains(elt)) {
1112 getInternalData(elt).sseEventSource.close();
1113 return true;
1114 }
1115 }
1116
1117 function hasEventSource(node) {
1118 return getInternalData(node).sseEventSource != null;
1119 }
1120
1121 //====================================================================
1122
1123 function loadImmediately(elt, verb, path, nodeData, delay) {
1124 var load = function(){
1125 if (!nodeData.loaded) {
1126 nodeData.loaded = true;
1127 issueAjaxRequest(elt, verb, path);
1128 }
1129 }
1130 if (delay) {
1131 setTimeout(load, delay);
1132 } else {
1133 load();
1134 }
1135 }
1136
1137 function processVerbs(elt, nodeData, triggerSpecs) {
1138 var explicitAction = false;
1139 forEach(VERBS, function (verb) {
1140 if (hasAttribute(elt,'hx-' + verb)) {
1141 var path = getAttributeValue(elt, 'hx-' + verb);
1142 explicitAction = true;
1143 nodeData.path = path;
1144 nodeData.verb = verb;
1145 triggerSpecs.forEach(function(triggerSpec) {
1146 if (triggerSpec.sseEvent) {
1147 processSSETrigger(elt, verb, path, triggerSpec.sseEvent);
1148 } else if (triggerSpec.trigger === "revealed") {
1149 initScrollHandler();
1150 maybeReveal(elt);
1151 } else if (triggerSpec.trigger === "load") {
1152 loadImmediately(elt, verb, path, nodeData, triggerSpec.delay);
1153 } else if (triggerSpec.pollInterval) {
1154 nodeData.polling = true;
1155 processPolling(elt, verb, path, triggerSpec.pollInterval);
1156 } else {
1157 addEventListener(elt, verb, path, nodeData, triggerSpec);
1158 }
1159 });
1160 }
1161 });
1162 return explicitAction;
1163 }
1164
1165 function evalScript(script) {
1166 if (script.type === "text/javascript" || script.type === "") {
1167 try {
1168 Function(script.innerText)();
1169 } catch (e) {
1170 logError(e);
1171 }
1172 }
1173 }
1174
1175 function processScripts(elt) {
1176 if (matches(elt, "script")) {
1177 evalScript(elt);
1178 }
1179 forEach(findAll(elt, "script"), function (script) {
1180 evalScript(script);
1181 });
1182 }
1183
1184 function isBoosted() {
1185 return document.querySelector("[hx-boost], [data-hx-boost]");
1186 }
1187
1188 function findElementsToProcess(elt) {
1189 if (elt.querySelectorAll) {
1190 var boostedElts = isBoosted() ? ", a, form" : "";
1191 var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-sse], [data-hx-sse], [hx-ws]," +
1192 " [data-hx-ws]");
1193 return results;
1194 } else {
1195 return [];
1196 }
1197 }
1198
1199 function initNode(elt) {
1200 var nodeData = getInternalData(elt);
1201 if (!nodeData.initialized) {
1202 nodeData.initialized = true;
1203
1204 if (elt.value) {
1205 nodeData.lastValue = elt.value;
1206 }
1207
1208 var triggerSpecs = getTriggerSpecs(elt);
1209 var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
1210
1211 if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
1212 boostElement(elt, nodeData, triggerSpecs);
1213 }
1214
1215 var sseInfo = getAttributeValue(elt, 'hx-sse');
1216 if (sseInfo) {
1217 processSSEInfo(elt, nodeData, sseInfo);
1218 }
1219
1220 var wsInfo = getAttributeValue(elt, 'hx-ws');
1221 if (wsInfo) {
1222 processWebSocketInfo(elt, nodeData, wsInfo);
1223 }
1224 triggerEvent(elt, "htmx:processedNode");
1225 }
1226 }
1227
1228 function processNode(elt) {
1229 initNode(elt);
1230 forEach(findElementsToProcess(elt), function(child) { initNode(child) });
1231 }
1232
1233 //====================================================================
1234 // Event/Log Support
1235 //====================================================================
1236
1237 function kebabEventName(str) {
1238 return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
1239 }
1240
1241 function makeEvent(eventName, detail) {
1242 var evt;
1243 if (window.CustomEvent && typeof window.CustomEvent === 'function') {
1244 evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail});
1245 } else {
1246 evt = getDocument().createEvent('CustomEvent');
1247 evt.initCustomEvent(eventName, true, true, detail);
1248 }
1249 return evt;
1250 }
1251
1252 function triggerErrorEvent(elt, eventName, detail) {
1253 triggerEvent(elt, eventName, mergeObjects({error:eventName}, detail));
1254 }
1255
1256 function ignoreEventForLogging(eventName) {
1257 return eventName === "htmx:processedNode"
1258 }
1259
1260 function withExtensions(elt, toDo) {
1261 forEach(getExtensions(elt), function(extension){
1262 try {
1263 toDo(extension);
1264 } catch (e) {
1265 logError(e);
1266 }
1267 });
1268 }
1269
1270 function logError(msg) {
1271 if(console.error) {
1272 console.error(msg);
1273 } else if (console.log) {
1274 console.log("ERROR: ", msg);
1275 }
1276 }
1277
1278 function triggerEvent(elt, eventName, detail) {
1279 if (detail == null) {
1280 detail = {};
1281 }
1282 detail["elt"] = elt;
1283 var event = makeEvent(eventName, detail);
1284 if (htmx.logger && !ignoreEventForLogging(eventName)) {
1285 htmx.logger(elt, eventName, detail);
1286 }
1287 if (detail.error) {
1288 logError(detail.error);
1289 triggerEvent(elt, "htmx:error", {errorInfo:detail})
1290 }
1291 var eventResult = elt.dispatchEvent(event);
1292 var kebabName = kebabEventName(eventName);
1293 if (eventResult && kebabName !== eventName) {
1294 var kebabedEvent = makeEvent(kebabName, event.detail);
1295 eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
1296 }
1297 withExtensions(elt, function (extension) {
1298 eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
1299 });
1300 return eventResult;
1301 }
1302
1303 //====================================================================
1304 // History Support
1305 //====================================================================
1306 var currentPathForHistory = null;
1307
1308 function getHistoryElement() {
1309 var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]');
1310 return historyElt || getDocument().body;
1311 }
1312
1313 function saveToHistoryCache(url, content, title, scroll) {
1314 var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
1315 for (var i = 0; i < historyCache.length; i++) {
1316 if (historyCache[i].url === url) {
1317 historyCache = historyCache.slice(i, 1);
1318 break;
1319 }
1320 }
1321 historyCache.push({url:url, content: content, title:title, scroll:scroll})
1322 while (historyCache.length > htmx.config.historyCacheSize) {
1323 historyCache.shift();
1324 }
1325 try {
1326 localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
1327 } catch (e) {
1328 triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e})
1329 }
1330 }
1331
1332 function getCachedHistory(url) {
1333 var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
1334 for (var i = 0; i < historyCache.length; i++) {
1335 if (historyCache[i].url === url) {
1336 return historyCache[i];
1337 }
1338 }
1339 return null;
1340 }
1341
1342 function cleanInnerHtmlForHistory(elt) {
1343 var className = htmx.config.requestClass;
1344 var clone = elt.cloneNode(true);
1345 forEach(findAll(clone, "." + className), function(child){
1346 removeClassFromElement(child, className);
1347 });
1348 return clone.innerHTML;
1349 }
1350
1351 function saveHistory() {
1352 var elt = getHistoryElement();
1353 var path = currentPathForHistory || location.pathname+location.search;
1354 triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path:path, historyElt:elt});
1355 if(htmx.config.historyEnabled) history.replaceState({htmx:true}, getDocument().title, window.location.href);
1356 saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY);
1357 }
1358
1359 function pushUrlIntoHistory(path) {
1360 if(htmx.config.historyEnabled) history.pushState({htmx:true}, "", path);
1361 currentPathForHistory = path;
1362 }
1363
1364 function settleImmediately(tasks) {
1365 forEach(tasks, function (task) {
1366 task.call();
1367 });
1368 }
1369
1370 function loadHistoryFromServer(path) {
1371 var request = new XMLHttpRequest();
1372 var details = {path: path, xhr:request};
1373 triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
1374 request.open('GET', path, true);
1375 request.onload = function () {
1376 if (this.status >= 200 && this.status < 400) {
1377 triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
1378 var fragment = makeFragment(this.response);
1379 fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment;
1380 var historyElement = getHistoryElement();
1381 var settleInfo = makeSettleInfo(historyElement);
1382 swapInnerHTML(historyElement, fragment, settleInfo)
1383 settleImmediately(settleInfo.tasks);
1384 currentPathForHistory = path;
1385 } else {
1386 triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details);
1387 }
1388 };
1389 request.send();
1390 }
1391
1392 function restoreHistory(path) {
1393 saveHistory(currentPathForHistory);
1394 path = path || location.pathname+location.search;
1395 triggerEvent(getDocument().body, "htmx:historyRestore", {path:path});
1396 var cached = getCachedHistory(path);
1397 if (cached) {
1398 var fragment = makeFragment(cached.content);
1399 var historyElement = getHistoryElement();
1400 var settleInfo = makeSettleInfo(historyElement);
1401 swapInnerHTML(historyElement, fragment, settleInfo)
1402 settleImmediately(settleInfo.tasks);
1403 document.title = cached.title;
1404 window.scrollTo(0, cached.scroll);
1405 currentPathForHistory = path;
1406 } else {
1407 loadHistoryFromServer(path);
1408 }
1409 }
1410
1411 function shouldPush(elt) {
1412 var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
1413 return (pushUrl && pushUrl !== "false") ||
1414 (elt.tagName === "A" && getInternalData(elt).boosted);
1415 }
1416
1417 function getPushUrl(elt) {
1418 var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
1419 return (pushUrl === "true" || pushUrl === "false") ? null : pushUrl;
1420 }
1421
1422 function addRequestIndicatorClasses(elt) {
1423 mutateRequestIndicatorClasses(elt, "add");
1424 }
1425
1426 function removeRequestIndicatorClasses(elt) {
1427 mutateRequestIndicatorClasses(elt, "remove");
1428 }
1429
1430 function mutateRequestIndicatorClasses(elt, action) {
1431 var indicator = getClosestAttributeValue(elt, 'hx-indicator');
1432 if (indicator) {
1433 var indicators = getDocument().querySelectorAll(indicator);
1434 } else {
1435 indicators = [elt];
1436 }
1437 forEach(indicators, function(ic) {
1438 ic.classList[action].call(ic.classList, htmx.config.requestClass);
1439 });
1440 }
1441
1442 //====================================================================
1443 // Input Value Processing
1444 //====================================================================
1445
1446 function haveSeenNode(processed, elt) {
1447 for (var i = 0; i < processed.length; i++) {
1448 var node = processed[i];
1449 if (node.isSameNode(elt)) {
1450 return true;
1451 }
1452 }
1453 return false;
1454 }
1455
1456 function shouldInclude(elt) {
1457 if(elt.name === "" || elt.name == null || elt.disabled) {
1458 return false;
1459 }
1460 // ignore "submitter" types (see jQuery src/serialize.js)
1461 if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
1462 return false;
1463 }
1464 if (elt.type === "checkbox" || elt.type === "radio" ) {
1465 return elt.checked;
1466 }
1467 return true;
1468 }
1469
1470 function processInputValue(processed, values, errors, elt) {
1471 if (elt == null || haveSeenNode(processed, elt)) {
1472 return;
1473 } else {
1474 processed.push(elt);
1475 }
1476 if (shouldInclude(elt)) {
1477 var name = getRawAttribute(elt,"name");
1478 var value = elt.value;
1479 if (!!getRawAttribute(elt, 'multiple')) {
1480 value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
1481 }
1482 // include file inputs
1483 if (elt.files) {
1484 value = toArray(elt.files);
1485 }
1486 // This is a little ugly because both the current value of the named value in the form
1487 // and the new value could be arrays, so we have to handle all four cases :/
1488 if (name != null && value != null) {
1489 var current = values[name];
1490 if(current) {
1491 if (Array.isArray(current)) {
1492 if (Array.isArray(value)) {
1493 values[name] = current.concat(value);
1494 } else {
1495 current.push(value);
1496 }
1497 } else {
1498 if (Array.isArray(value)) {
1499 values[name] = [current].concat(value);
1500 } else {
1501 values[name] = [current, value];
1502 }
1503 }
1504 } else {
1505 values[name] = value;
1506 }
1507 }
1508 validateElement(elt, errors);
1509 }
1510 if (matches(elt, 'form')) {
1511 var inputs = elt.elements;
1512 forEach(inputs, function(input) {
1513 processInputValue(processed, values, errors, input);
1514 });
1515 }
1516 }
1517
1518 function validateElement(element, errors) {
1519 if (element.willValidate) {
1520 triggerEvent(element, "htmx:validation:validate")
1521 if (!element.checkValidity()) {
1522 errors.push({elt: element, message:element.validationMessage, validity:element.validity});
1523 triggerEvent(element, "htmx:validation:failed", {message:element.validationMessage, validity:element.validity})
1524 }
1525 }
1526 }
1527
1528 function getInputValues(elt, verb) {
1529 var processed = [];
1530 var values = {};
1531 var errors = [];
1532
1533 // for a non-GET include the closest form
1534 if (verb !== 'get') {
1535 processInputValue(processed, values, errors, closest(elt, 'form'));
1536 }
1537
1538 // include the element itself
1539 processInputValue(processed, values, errors, elt);
1540
1541 // include any explicit includes
1542 var includes = getClosestAttributeValue(elt, "hx-include");
1543 if (includes) {
1544 var nodes = getDocument().querySelectorAll(includes);
1545 forEach(nodes, function(node) {
1546 processInputValue(processed, values, errors, node);
1547 });
1548 }
1549
1550
1551 return {errors:errors, values:values};
1552 }
1553
1554 function appendParam(returnStr, name, realValue) {
1555 if (returnStr !== "") {
1556 returnStr += "&";
1557 }
1558 returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue);
1559 return returnStr;
1560 }
1561
1562 function urlEncode(values) {
1563 var returnStr = "";
1564 for (var name in values) {
1565 if (values.hasOwnProperty(name)) {
1566 var value = values[name];
1567 if (Array.isArray(value)) {
1568 forEach(value, function(v) {
1569 returnStr = appendParam(returnStr, name, v);
1570 });
1571 } else {
1572 returnStr = appendParam(returnStr, name, value);
1573 }
1574 }
1575 }
1576 return returnStr;
1577 }
1578
1579 function makeFormData(values) {
1580 var formData = new FormData();
1581 for (var name in values) {
1582 if (values.hasOwnProperty(name)) {
1583 var value = values[name];
1584 if (Array.isArray(value)) {
1585 forEach(value, function(v) {
1586 formData.append(name, v);
1587 });
1588 } else {
1589 formData.append(name, value);
1590 }
1591 }
1592 }
1593 return formData;
1594 }
1595
1596 //====================================================================
1597 // Ajax
1598 //====================================================================
1599
1600 function getHeaders(elt, target, prompt, eventTarget) {
1601 var headers = {
1602 "HX-Request" : "true",
1603 "HX-Trigger" : getRawAttribute(elt, "id"),
1604 "HX-Trigger-Name" : getRawAttribute(elt, "name"),
1605 "HX-Target" : getAttributeValue(target, "id"),
1606 "HX-Current-URL" : getDocument().location.href,
1607 }
1608 if (prompt !== undefined) {
1609 headers["HX-Prompt"] = prompt;
1610 }
1611 if (eventTarget) {
1612 headers["HX-Event-Target"] = getRawAttribute(eventTarget, "id");
1613 }
1614 if (getDocument().activeElement) {
1615 headers["HX-Active-Element"] = getRawAttribute(getDocument().activeElement, "id");
1616 headers["HX-Active-Element-Name"] = getRawAttribute(getDocument().activeElement, "name");
1617 if (getDocument().activeElement.value) {
1618 headers["HX-Active-Element-Value"] = getRawAttribute(getDocument().activeElement, "value");
1619 }
1620 }
1621 return headers;
1622 }
1623
1624 function filterValues(inputValues, elt) {
1625 var paramsValue = getClosestAttributeValue(elt, "hx-params");
1626 if (paramsValue) {
1627 if (paramsValue === "none") {
1628 return {};
1629 } else if (paramsValue === "*") {
1630 return inputValues;
1631 } else if(paramsValue.indexOf("not ") === 0) {
1632 forEach(paramsValue.substr(4).split(","), function (name) {
1633 name = name.trim();
1634 delete inputValues[name];
1635 });
1636 return inputValues;
1637 } else {
1638 var newValues = {}
1639 forEach(paramsValue.split(","), function (name) {
1640 name = name.trim();
1641 newValues[name] = inputValues[name];
1642 });
1643 return newValues;
1644 }
1645 } else {
1646 return inputValues;
1647 }
1648 }
1649
1650 function getSwapSpecification(elt) {
1651 var swapInfo = getClosestAttributeValue(elt, "hx-swap");
1652 var swapSpec = {
1653 "swapStyle" : htmx.config.defaultSwapStyle,
1654 "swapDelay" : htmx.config.defaultSwapDelay,
1655 "settleDelay" : htmx.config.defaultSettleDelay
1656 }
1657 if (swapInfo) {
1658 var split = splitOnWhitespace(swapInfo);
1659 if (split.length > 0) {
1660 swapSpec["swapStyle"] = split[0];
1661 for (var i = 1; i < split.length; i++) {
1662 var modifier = split[i];
1663 if (modifier.indexOf("swap:") === 0) {
1664 swapSpec["swapDelay"] = parseInterval(modifier.substr(5));
1665 }
1666 if (modifier.indexOf("settle:") === 0) {
1667 swapSpec["settleDelay"] = parseInterval(modifier.substr(7));
1668 }
1669 if (modifier.indexOf("scroll:") === 0) {
1670 swapSpec["scroll"] = modifier.substr(7);
1671 }
1672 if (modifier.indexOf("show:") === 0) {
1673 swapSpec["show"] = modifier.substr(5);
1674 }
1675 }
1676 }
1677 }
1678 return swapSpec;
1679 }
1680
1681 function encodeParamsForBody(xhr, elt, filteredParameters) {
1682 var encodedParameters = null;
1683 withExtensions(elt, function (extension) {
1684 if (encodedParameters == null) {
1685 encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt);
1686 }
1687 });
1688 if (encodedParameters != null) {
1689 return encodedParameters;
1690 } else {
1691 if (getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data") {
1692 return makeFormData(filteredParameters);
1693 } else {
1694 return urlEncode(filteredParameters);
1695 }
1696 }
1697 }
1698
1699 function makeSettleInfo(target) {
1700 return {tasks: [], elts: [target]};
1701 }
1702
1703 function updateScrollState(target, altContent, swapSpec) {
1704 if (swapSpec.scroll) {
1705 if (swapSpec.scroll === "top") {
1706 target.scrollTop = 0;
1707 }
1708 if (swapSpec.scroll === "bottom") {
1709 target.scrollTop = target.scrollHeight;
1710 }
1711 }
1712 if (swapSpec.show) {
1713 if (swapSpec.show === "top") {
1714 target.scrollIntoView(true);
1715 }
1716 if (swapSpec.show === "bottom") {
1717 target.scrollIntoView(false);
1718 }
1719 }
1720 }
1721
1722 function getValuesForElement(elt, attr, strToValues, expressionVars) {
1723 if (expressionVars == null) {
1724 expressionVars = {};
1725 }
1726 if (elt == null) {
1727 return expressionVars;
1728 }
1729 var attributeValue = getAttributeValue(elt, attr);
1730 if (attributeValue) {
1731 var str = attributeValue.trim();
1732 if (str.indexOf('{') !== 0) {
1733 str = "{" + str + "}";
1734 }
1735 var varsValues = strToValues(str);
1736 for (var key in varsValues) {
1737 if (varsValues.hasOwnProperty(key)) {
1738 if (expressionVars[key] == null) {
1739 expressionVars[key] = varsValues[key];
1740 }
1741 }
1742 }
1743 }
1744 return getValuesForElement(parentElt(elt), attr, strToValues, expressionVars);
1745 }
1746
1747 function getHXVarsForElement(elt, expressionVars) {
1748 return getValuesForElement(elt, "hx-vars", function(valueStr){
1749 return Function("return (" + valueStr + ")")()
1750 }, expressionVars);
1751 }
1752
1753 function getHXValsForElement(elt, expressionVars) {
1754 return getValuesForElement(elt, "hx-vals", function(valueStr){
1755 return parseJSON(valueStr);
1756 }, expressionVars);
1757 }
1758
1759 function getExpressionVars(elt) {
1760 return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt));
1761 }
1762
1763 function safelySetHeaderValue(xhr, header, headerValue) {
1764 if (headerValue !== null) {
1765 try {
1766 xhr.setRequestHeader(header, headerValue);
1767 } catch (e) {
1768 // On an exception, try to set the header URI encoded instead
1769 xhr.setRequestHeader(header, encodeURIComponent(headerValue));
1770 xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
1771 }
1772 }
1773 }
1774
1775 function getResponseURL(xhr) {
1776 // NB: IE11 does not support this stuff
1777 if (xhr.responseURL && typeof(URL) !== "undefined") {
1778 try {
1779 var url = new URL(xhr.responseURL);
1780 return url.pathname + url.search;
1781 } catch (e) {
1782 triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
1783 }
1784 }
1785 }
1786
1787 function hasHeader(xhr, regexp) {
1788 return xhr.getAllResponseHeaders().match(regexp);
1789 }
1790
1791 function issueAjaxRequest(elt, verb, path, eventTarget, triggeringEvent) {
1792 if (!bodyContains(elt)) {
1793 console.log("Body does not contain", elt);
1794 return; // do not issue requests for elements removed from the DOM
1795 }
1796 var target = getTarget(elt);
1797 if (target == null) {
1798 triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")});
1799 return;
1800 }
1801 var eltData = getInternalData(elt);
1802 if (eltData.requestInFlight) {
1803 eltData.queuedRequest = function(){issueAjaxRequest(elt, verb, path, eventTarget, triggeringEvent)};
1804 return;
1805 } else {
1806 eltData.requestInFlight = true;
1807 }
1808 var endRequestLock = function(){
1809 eltData.requestInFlight = false
1810 var queuedRequest = eltData.queuedRequest;
1811 eltData.queuedRequest = null;
1812 if (queuedRequest) {
1813 queuedRequest();
1814 }
1815 }
1816 var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
1817 if (promptQuestion) {
1818 var promptResponse = prompt(promptQuestion);
1819 // prompt returns null if cancelled and empty string if accepted with no entry
1820 if (promptResponse === null ||
1821 !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target}))
1822 return endRequestLock();
1823 }
1824
1825 var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
1826 if (confirmQuestion) {
1827 if(!confirm(confirmQuestion)) return endRequestLock();
1828 }
1829
1830 var xhr = new XMLHttpRequest();
1831
1832 var headers = getHeaders(elt, target, promptResponse, eventTarget);
1833 var results = getInputValues(elt, verb);
1834 var errors = results.errors;
1835 var rawParameters = results.values;
1836 var expressionVars = getExpressionVars(elt);
1837 var allParameters = mergeObjects(rawParameters, expressionVars);
1838 var filteredParameters = filterValues(allParameters, elt);
1839
1840 if (verb !== 'get' && getClosestAttributeValue(elt, "hx-encoding") == null) {
1841 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
1842 }
1843
1844 // behavior of anchors w/ empty href is to use the current URL
1845 if (path == null || path === "") {
1846 path = getDocument().location.href;
1847 }
1848
1849 var requestConfig = {
1850 parameters: filteredParameters,
1851 unfilteredParameters: allParameters,
1852 headers:headers,
1853 target:target,
1854 verb:verb,
1855 errors:errors,
1856 path:path,
1857 triggeringEvent:triggeringEvent
1858 };
1859
1860 if(!triggerEvent(elt, 'htmx:configRequest', requestConfig)) return endRequestLock();
1861 // copy out in case the object was overwritten
1862 path = requestConfig.path;
1863 verb = requestConfig.verb;
1864 headers = requestConfig.headers;
1865 filteredParameters = requestConfig.parameters;
1866 errors = requestConfig.errors;
1867
1868 if(errors && errors.length > 0){
1869 triggerEvent(elt, 'htmx:validation:halted', requestConfig)
1870 return endRequestLock();
1871 }
1872
1873 var splitPath = path.split("#");
1874 var pathNoAnchor = splitPath[0];
1875 var anchor = splitPath[1];
1876 if (verb === 'get') {
1877 var finalPathForGet = pathNoAnchor;
1878 var values = Object.keys(filteredParameters).length !== 0;
1879 if (values) {
1880 if (finalPathForGet.indexOf("?") < 0) {
1881 finalPathForGet += "?";
1882 } else {
1883 finalPathForGet += "&";
1884 }
1885 finalPathForGet += urlEncode(filteredParameters);
1886 if (anchor) {
1887 finalPathForGet += "#" + anchor;
1888 }
1889 }
1890 xhr.open('GET', finalPathForGet, true);
1891 } else {
1892 xhr.open(verb.toUpperCase(), path, true);
1893 }
1894
1895 xhr.overrideMimeType("text/html");
1896
1897 // request headers
1898 for (var header in headers) {
1899 if (headers.hasOwnProperty(header)) {
1900 var headerValue = headers[header];
1901 safelySetHeaderValue(xhr, header, headerValue);
1902 }
1903 }
1904
1905 var eventDetail = {xhr: xhr, target: target, requestConfig: requestConfig};
1906 xhr.onload = function () {
1907 try {
1908 if (!triggerEvent(elt, 'htmx:beforeOnLoad', eventDetail)) return;
1909
1910 if (hasHeader(xhr, /HX-Trigger:/i)) {
1911 handleTrigger(xhr, "HX-Trigger", elt);
1912 }
1913
1914 if (hasHeader(xhr,/HX-Push:/i)) {
1915 var pushedUrl = xhr.getResponseHeader("HX-Push");
1916 }
1917
1918 if (hasHeader(xhr, /HX-Redirect:/i)) {
1919 window.location.href = xhr.getResponseHeader("HX-Redirect");
1920 return;
1921 }
1922
1923 if (hasHeader(xhr,/HX-Refresh:/i)) {
1924 if ("true" === xhr.getResponseHeader("HX-Refresh")) {
1925 location.reload();
1926 return;
1927 }
1928 }
1929
1930 var shouldSaveHistory = shouldPush(elt) || pushedUrl;
1931
1932 if (this.status >= 200 && this.status < 400) {
1933 if (this.status === 286) {
1934 cancelPolling(elt);
1935 }
1936 // don't process 'No Content'
1937 if (this.status !== 204) {
1938 if (!triggerEvent(target, 'htmx:beforeSwap', eventDetail)) return;
1939
1940 var serverResponse = this.response;
1941 withExtensions(elt, function(extension){
1942 serverResponse = extension.transformResponse(serverResponse, xhr, elt);
1943 });
1944
1945 // Save current page
1946 if (shouldSaveHistory) {
1947 saveHistory();
1948 }
1949
1950 var swapSpec = getSwapSpecification(elt);
1951
1952 target.classList.add(htmx.config.swappingClass);
1953 var doSwap = function () {
1954 try {
1955
1956 var activeElt = document.activeElement;
1957 var selectionInfo = {
1958 elt: activeElt,
1959 start: activeElt ? activeElt.selectionStart : null,
1960 end: activeElt ? activeElt.selectionEnd : null
1961 };
1962
1963 var settleInfo = makeSettleInfo(target);
1964 selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo);
1965
1966 if (selectionInfo.elt &&
1967 !bodyContains(selectionInfo.elt) &&
1968 selectionInfo.elt.id) {
1969 var newActiveElt = document.getElementById(selectionInfo.elt.id);
1970 if (newActiveElt) {
1971 if (selectionInfo.start && newActiveElt.setSelectionRange) {
1972 newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
1973 }
1974 newActiveElt.focus();
1975 }
1976 }
1977
1978 target.classList.remove(htmx.config.swappingClass);
1979 forEach(settleInfo.elts, function (elt) {
1980 if (elt.classList) {
1981 elt.classList.add(htmx.config.settlingClass);
1982 }
1983 triggerEvent(elt, 'htmx:afterSwap', eventDetail);
1984 });
1985 if (anchor) {
1986 location.hash = anchor;
1987 }
1988
1989 if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
1990 handleTrigger(xhr, "HX-Trigger-After-Swap", elt);
1991 }
1992
1993 var doSettle = function(){
1994 forEach(settleInfo.tasks, function (task) {
1995 task.call();
1996 });
1997 forEach(settleInfo.elts, function (elt) {
1998 if (elt.classList) {
1999 elt.classList.remove(htmx.config.settlingClass);
2000 }
2001 triggerEvent(elt, 'htmx:afterSettle', eventDetail);
2002 });
2003 // push URL and save new page
2004 if (shouldSaveHistory) {
2005 var pathToPush = pushedUrl || getPushUrl(elt) || getResponseURL(xhr) || finalPathForGet || path;
2006 pushUrlIntoHistory(pathToPush);
2007 triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path:pathToPush});
2008 }
2009 updateScrollState(target, settleInfo.elts, swapSpec);
2010
2011 if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
2012 handleTrigger(xhr, "HX-Trigger-After-Settle", elt);
2013 }
2014 }
2015
2016 if (swapSpec.settleDelay > 0) {
2017 setTimeout(doSettle, swapSpec.settleDelay)
2018 } else {
2019 doSettle();
2020 }
2021 } catch (e) {
2022 triggerErrorEvent(elt, 'htmx:swapError', eventDetail);
2023 throw e;
2024 }
2025 };
2026
2027 if (swapSpec.swapDelay > 0) {
2028 setTimeout(doSwap, swapSpec.swapDelay)
2029 } else {
2030 doSwap();
2031 }
2032 }
2033 } else {
2034 triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({error: "Response Status Error Code " + this.status + " from " + path}, eventDetail));
2035 }
2036 } catch (e) {
2037 triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({error:e}, eventDetail));
2038 throw e;
2039 } finally {
2040 removeRequestIndicatorClasses(elt);
2041 var finalElt = getInternalData(elt).replacedWith || elt;
2042 triggerEvent(finalElt, 'htmx:afterRequest', eventDetail);
2043 triggerEvent(finalElt, 'htmx:afterOnLoad', eventDetail);
2044 endRequestLock();
2045 }
2046 }
2047 xhr.onerror = function () {
2048 removeRequestIndicatorClasses(elt);
2049 triggerErrorEvent(elt, 'htmx:afterRequest', eventDetail);
2050 triggerErrorEvent(elt, 'htmx:sendError', eventDetail);
2051 endRequestLock();
2052 }
2053 xhr.onabort = function() {
2054 removeRequestIndicatorClasses(elt);
2055 endRequestLock();
2056 }
2057 if(!triggerEvent(elt, 'htmx:beforeRequest', eventDetail)) return endRequestLock();
2058 addRequestIndicatorClasses(elt);
2059
2060 forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
2061 xhr.addEventListener(eventName, function(event){
2062 triggerEvent(elt, "htmx:xhr:" + eventName, mergeObjects({}, event.detail));
2063 })
2064 });
2065 xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
2066 }
2067
2068 //====================================================================
2069 // Extensions API
2070 //====================================================================
2071 var extensions = {};
2072 function extensionBase() {
2073 return {
2074 onEvent : function(name, evt) {return true;},
2075 transformResponse : function(text, xhr, elt) {return text;},
2076 isInlineSwap : function(swapStyle) {return false;},
2077 handleSwap : function(swapStyle, target, fragment, settleInfo) {return false;},
2078 encodeParameters : function(xhr, parameters, elt) {return null;}
2079 }
2080 }
2081
2082 function defineExtension(name, extension) {
2083 extensions[name] = mergeObjects(extensionBase(), extension);
2084 }
2085
2086 function removeExtension(name) {
2087 delete extensions[name];
2088 }
2089
2090 function getExtensions(elt, extensionsToReturn) {
2091 if (elt == null) {
2092 return extensionsToReturn;
2093 }
2094 if (extensionsToReturn == null) {
2095 extensionsToReturn = [];
2096 }
2097 var extensionsForElement = getAttributeValue(elt, "hx-ext");
2098 if (extensionsForElement) {
2099 forEach(extensionsForElement.split(","), function(extensionName){
2100 extensionName = extensionName.replace(/ /g, '');
2101 var extension = extensions[extensionName];
2102 if (extension && extensionsToReturn.indexOf(extension) < 0) {
2103 extensionsToReturn.push(extension);
2104 }
2105 });
2106 }
2107 return getExtensions(parentElt(elt), extensionsToReturn);
2108 }
2109
2110 //====================================================================
2111 // Initialization
2112 //====================================================================
2113
2114 function ready(fn) {
2115 if (getDocument().readyState !== 'loading') {
2116 fn();
2117 } else {
2118 getDocument().addEventListener('DOMContentLoaded', fn);
2119 }
2120 }
2121
2122 function insertIndicatorStyles() {
2123 if (htmx.config.includeIndicatorStyles !== false) {
2124 getDocument().head.insertAdjacentHTML("beforeend",
2125 "<style>\
2126 ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
2127 ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
2128 ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
2129 </style>");
2130 }
2131 }
2132
2133 function getMetaConfig() {
2134 var element = getDocument().querySelector('meta[name="htmx-config"]');
2135 if (element) {
2136 return parseJSON(element.content);
2137 } else {
2138 return null;
2139 }
2140 }
2141
2142 function mergeMetaConfig() {
2143 var metaConfig = getMetaConfig();
2144 if (metaConfig) {
2145 htmx.config = mergeObjects(htmx.config , metaConfig)
2146 }
2147 }
2148
2149 // initialize the document
2150 ready(function () {
2151 mergeMetaConfig();
2152 insertIndicatorStyles();
2153 var body = getDocument().body;
2154 processNode(body);
2155 triggerEvent(body, 'htmx:load', {});
2156 window.onpopstate = function (event) {
2157 if (event.state && event.state.htmx) {
2158 restoreHistory();
2159 }
2160 };
2161 })
2162
2163 return htmx;
2164 }
2165)()
2166}));