UNPKG

51.6 kBJavaScriptView Raw
1// noinspection JSUnusedAssignment
2var htmx = htmx || (function () {
3 'use strict';
4
5 var VERBS = ['get', 'post', 'put', 'delete', 'patch']
6
7 //====================================================================
8 // Utilities
9 //====================================================================
10
11 function parseInterval(str) {
12 if (str === "null" || str === "false" || str === "") {
13 return null;
14 } else if (str.lastIndexOf("ms") === str.length - 2) {
15 return parseFloat(str.substr(0, str.length - 2));
16 } else if (str.lastIndexOf("s") === str.length - 1) {
17 return parseFloat(str.substr(0, str.length - 1)) * 1000;
18 } else {
19 return parseFloat(str);
20 }
21 }
22
23 function getRawAttribute(elt, name) {
24 return elt.getAttribute && elt.getAttribute(name);
25 }
26
27 // resolve with both kt and data-kt prefixes
28 function getAttributeValue(elt, qualifiedName) {
29 return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
30 }
31
32 function parentElt(elt) {
33 return elt.parentElement;
34 }
35
36 function getDocument() {
37 return document;
38 }
39
40 function getClosestMatch(elt, condition) {
41 if (condition(elt)) {
42 return elt;
43 } else if (parentElt(elt)) {
44 return getClosestMatch(parentElt(elt), condition);
45 } else {
46 return null;
47 }
48 }
49
50 function getClosestAttributeValue(elt, attributeName) {
51 var closestAttr = null;
52 getClosestMatch(elt, function (e) {
53 return closestAttr = getRawAttribute(e, attributeName);
54 });
55 return closestAttr;
56 }
57
58 function matches(elt, selector) {
59 // noinspection JSUnresolvedVariable
60 var matchesFunction = elt.matches ||
61 elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
62 || elt.webkitMatchesSelector || elt.oMatchesSelector;
63 return matchesFunction && matchesFunction.call(elt, selector);
64 }
65
66 function getStartTag(str) {
67 var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
68 var match = tagMatcher.exec( str );
69 if (match) {
70 return match[1].toLowerCase();
71 } else {
72 return "";
73 }
74 }
75
76 function parseHTML(resp, depth) {
77 var parser = new DOMParser();
78 var responseDoc = parser.parseFromString(resp, "text/html");
79 var responseNode = responseDoc.body;
80 while (depth > 0) {
81 depth--;
82 responseNode = responseNode.firstChild;
83 }
84 return responseNode;
85 }
86
87 function makeFragment(resp) {
88 var startTag = getStartTag(resp);
89 switch (startTag) {
90 case "thead":
91 case "tbody":
92 case "tfoot":
93 case "colgroup":
94 case "caption":
95 return parseHTML("<table>" + resp + "</table>", 1);
96 case "col":
97 return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
98 case "tr":
99 return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
100 case "td":
101 case "th":
102 return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
103 default:
104 return parseHTML(resp, 0);
105 }
106 }
107
108 function isType(o, type) {
109 return Object.prototype.toString.call(o) === "[object " + type + "]";
110 }
111
112 function isFunction(o) {
113 return isType(o, "Function");
114 }
115
116 function isRawObject(o) {
117 return isType(o, "Object");
118 }
119
120 function getInternalData(elt) {
121 var dataProp = 'htmx-internal-data';
122 var data = elt[dataProp];
123 if (!data) {
124 data = elt[dataProp] = {};
125 }
126 return data;
127 }
128
129 function toArray(arr) {
130 var returnArr = [];
131 if (arr) {
132 for (var i = 0; i < arr.length; i++) {
133 returnArr.push(arr[i]);
134 }
135 }
136 return returnArr
137 }
138
139 function forEach(arr, func) {
140 if (arr) {
141 for (var i = 0; i < arr.length; i++) {
142 func(arr[i]);
143 }
144 }
145 }
146
147 function isScrolledIntoView(el) {
148 var rect = el.getBoundingClientRect();
149 var elemTop = rect.top;
150 var elemBottom = rect.bottom;
151 return elemTop < window.innerHeight && elemBottom >= 0;
152 }
153
154 function bodyContains(elt) {
155 return getDocument().body.contains(elt);
156 }
157
158 function concat(arr1, arr2) {
159 return arr1.concat(arr2);
160 }
161
162 function splitOnWhitespace(trigger) {
163 return trigger.split(/\s+/);
164 }
165
166 function mergeObjects(obj1, obj2) {
167 for (var key in obj2) {
168 if (obj2.hasOwnProperty(key)) {
169 obj1[key] = obj2[key];
170 }
171 }
172 return obj1;
173 }
174
175 //==========================================================================================
176 // public API
177 //==========================================================================================
178
179 function internalEval(str){
180 return eval(str);
181 }
182
183 function onLoadHelper(callback) {
184 var value = htmx.on("load.htmx", function(evt) {
185 callback(evt.detail.elt);
186 });
187 return value;
188 }
189
190 function logAll(){
191 htmx.logger = function(elt, event, data) {
192 if(console) {
193 console.log(event, elt, data);
194 }
195 }
196 }
197
198 function find(eltOrSelector, selector) {
199 if (selector) {
200 return eltOrSelector.querySelector(selector);
201 } else {
202 return getDocument().body.querySelector(eltOrSelector);
203 }
204 }
205
206 function findAll(eltOrSelector, selector) {
207 if (selector) {
208 return eltOrSelector.querySelectorAll(selector);
209 } else {
210 return getDocument().body.querySelectorAll(eltOrSelector);
211 }
212 }
213
214 function removeElement(elt, delay) {
215 if (delay) {
216 setTimeout(function(){removeElement(elt);}, delay)
217 } else {
218 elt.parentElement.removeChild(elt);
219 }
220 }
221
222 function addClassToElement(elt, clazz, delay) {
223 if (delay) {
224 setTimeout(function(){addClassToElement(elt, clazz);}, delay)
225 } else {
226 elt.classList.add(clazz);
227 }
228 }
229
230 function removeClassFromElement(elt, clazz, delay) {
231 if (delay) {
232 setTimeout(function(){removeClassFromElement(elt, clazz);}, delay)
233 } else {
234 elt.classList.remove(clazz);
235 }
236 }
237
238 function toggleClassOnElement(elt, clazz) {
239 elt.classList.toggle(clazz);
240 }
241
242 function takeClassForElement(elt, clazz) {
243 forEach(elt.parentElement.children, function(child){
244 removeClassFromElement(child, clazz);
245 })
246 addClassToElement(elt, clazz);
247 }
248
249 function closest(elt, selector) {
250 do if (elt == null || matches(elt, selector)) return elt;
251 while (elt = elt && parentElt(elt));
252 }
253
254 function processEventArgs(arg1, arg2, arg3) {
255 if (isFunction(arg2)) {
256 return {
257 target: getDocument().body,
258 event: arg1,
259 listener: arg2
260 }
261 } else {
262 return {
263 target: arg1,
264 event: arg2,
265 listener: arg3
266 }
267 }
268
269 }
270
271 function addEventListenerImpl(arg1, arg2, arg3) {
272 ready(function(){
273 var eventArgs = processEventArgs(arg1, arg2, arg3);
274 eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener);
275 })
276 var b = isFunction(arg2);
277 return b ? arg2 : arg3;
278 }
279
280 function removeEventListenerImpl(arg1, arg2, arg3) {
281 ready(function(){
282 var eventArgs = processEventArgs(arg1, arg2, arg3);
283 eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener);
284 })
285 return isFunction(arg2) ? arg2 : arg3;
286 }
287
288 //====================================================================
289 // Node processing
290 //====================================================================
291
292 function getTarget(elt) {
293 var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
294 if (explicitTarget) {
295 var targetStr = getRawAttribute(explicitTarget, "hx-target");
296 if (targetStr === "this") {
297 return explicitTarget;
298 } else if (targetStr.indexOf("closest ") === 0) {
299 return closest(elt, targetStr.substr(8));
300 } else {
301 return getDocument().querySelector(targetStr);
302 }
303 } else {
304 var data = getInternalData(elt);
305 if (data.boosted) {
306 return getDocument().body;
307 } else {
308 return elt;
309 }
310 }
311 }
312
313 function cloneAttributes(mergeTo, mergeFrom) {
314 forEach(mergeTo.attributes, function (attr) {
315 if (!mergeFrom.hasAttribute(attr.name)) {
316 mergeTo.removeAttribute(attr.name)
317 }
318 });
319 forEach(mergeFrom.attributes, function (attr) {
320 mergeTo.setAttribute(attr.name, attr.value);
321 });
322 }
323
324 function handleOutOfBandSwaps(fragment) {
325 var settleTasks = [];
326 forEach(toArray(fragment.children), function (child) {
327 if (getAttributeValue(child, "hx-swap-oob") === "true") {
328 var target = getDocument().getElementById(child.id);
329 if (target) {
330 var fragment = getDocument().createDocumentFragment();
331 fragment.appendChild(child);
332 settleTasks = settleTasks.concat(swapOuterHTML(target, fragment));
333 } else {
334 child.parentNode.removeChild(child);
335 triggerErrorEvent(getDocument().body, "oobErrorNoTarget.htmx", {content: child})
336 }
337 }
338 });
339 return settleTasks;
340 }
341
342 function handleAttributes(parentNode, fragment) {
343 var attributeSwaps = [];
344 forEach(fragment.querySelectorAll("[id]"), function (newNode) {
345 var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
346 if (oldNode) {
347 var newAttributes = newNode.cloneNode();
348 cloneAttributes(newNode, oldNode);
349 attributeSwaps.push(function () {
350 cloneAttributes(newNode, newAttributes);
351 });
352 }
353 });
354 return attributeSwaps;
355 }
356
357 function insertNodesBefore(parentNode, insertBefore, fragment) {
358 var settleTasks = handleAttributes(parentNode, fragment);
359 while(fragment.childNodes.length > 0){
360 var child = fragment.firstChild;
361 parentNode.insertBefore(child, insertBefore);
362 if (child.nodeType !== Node.TEXT_NODE) {
363 triggerEvent(child, 'load.htmx', {});
364 processNode(child);
365 }
366 }
367 return settleTasks;
368 }
369
370 function swapOuterHTML(target, fragment) {
371 if (target.tagName === "BODY") {
372 return swapInnerHTML(target, fragment);
373 } else {
374 var settleTasks = insertNodesBefore(parentElt(target), target, fragment);
375 parentElt(target).removeChild(target);
376 return settleTasks;
377 }
378 }
379
380 function swapAfterBegin(target, fragment) {
381 return insertNodesBefore(target, target.firstChild, fragment);
382 }
383
384 function swapBeforeBegin(target, fragment) {
385 return insertNodesBefore(parentElt(target), target, fragment);
386 }
387
388 function swapBeforeEnd(target, fragment) {
389 return insertNodesBefore(target, null, fragment);
390 }
391
392 function swapAfterEnd(target, fragment) {
393 return insertNodesBefore(parentElt(target), target.nextSibling, fragment);
394 }
395
396 function swapInnerHTML(target, fragment) {
397 var firstChild = target.firstChild;
398 var settleTasks = insertNodesBefore(target, firstChild, fragment);
399 if (firstChild) {
400 while (firstChild.nextSibling) {
401 target.removeChild(firstChild.nextSibling);
402 }
403 target.removeChild(firstChild);
404 }
405 return settleTasks;
406 }
407
408 function maybeSelectFromResponse(elt, fragment) {
409 var selector = getClosestAttributeValue(elt, "hx-select");
410 if (selector) {
411 var newFragment = getDocument().createDocumentFragment();
412 forEach(fragment.querySelectorAll(selector), function (node) {
413 newFragment.appendChild(node);
414 });
415 fragment = newFragment;
416 }
417 return fragment;
418 }
419
420 function swapResponse(swapStyle, target, elt, responseText) {
421 var fragment = makeFragment(responseText);
422 if (fragment) {
423 var settleTasks = handleOutOfBandSwaps(fragment);
424
425 fragment = maybeSelectFromResponse(elt, fragment);
426
427 switch(swapStyle) {
428 case "outerHTML": return concat(settleTasks, swapOuterHTML(target, fragment));
429 case "afterbegin": return concat(settleTasks, swapAfterBegin(target, fragment));
430 case "beforebegin": return concat(settleTasks, swapBeforeBegin(target, fragment));
431 case "beforeend": return concat(settleTasks, swapBeforeEnd(target, fragment));
432 case "afterend": return concat(settleTasks, swapAfterEnd(target, fragment));
433 default: return concat(settleTasks, swapInnerHTML(target, fragment));
434 }
435 }
436 }
437
438 function handleTrigger(elt, trigger) {
439 if (trigger) {
440 if (trigger.indexOf("{") === 0) {
441 var triggers = JSON.parse(trigger);
442 for (var eventName in triggers) {
443 if (triggers.hasOwnProperty(eventName)) {
444 var detail = triggers[eventName];
445 if (!isRawObject(detail)) {
446 detail = {"value": detail}
447 }
448 triggerEvent(elt, eventName, detail);
449 }
450 }
451 } else {
452 triggerEvent(elt, trigger, []);
453 }
454 }
455 }
456
457 function getTriggerSpec(elt) {
458
459 var triggerSpec = {
460 "trigger" : "click"
461 }
462 var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
463 if (explicitTrigger) {
464 var tokens = splitOnWhitespace(explicitTrigger);
465 if (tokens.length > 0) {
466 var trigger = tokens[0];
467 if (trigger === "every") {
468 triggerSpec.pollInterval = parseInterval(tokens[1]);
469 } else if (trigger.indexOf("sse:") === 0) {
470 triggerSpec.sseEvent = trigger.substr(4);
471 } else {
472 triggerSpec['trigger'] = trigger;
473 for (var i = 1; i < tokens.length; i++) {
474 var token = tokens[i].trim();
475 if (token === "changed") {
476 triggerSpec.changed = true;
477 }
478 if (token === "once") {
479 triggerSpec.once = true;
480 }
481 if (token.indexOf("delay:") === 0) {
482 triggerSpec.delay = parseInterval(token.substr(6));
483 }
484 }
485 }
486 }
487 } else {
488 if (matches(elt, 'form')) {
489 triggerSpec['trigger'] = 'submit';
490 } else if (matches(elt, 'input, textarea, select')) {
491 triggerSpec['trigger'] = 'change';
492 }
493 }
494 return triggerSpec;
495 }
496
497 function parseClassOperation(trimmedValue) {
498 var split = splitOnWhitespace(trimmedValue);
499 if (split.length > 1) {
500 var operation = split[0];
501 var classDef = split[1].trim();
502 var cssClass;
503 var delay;
504 if (classDef.indexOf(":") > 0) {
505 var splitCssClass = classDef.split(':');
506 cssClass = splitCssClass[0];
507 delay = parseInterval(splitCssClass[1]);
508 } else {
509 cssClass = classDef;
510 delay = 100;
511 }
512 return {
513 operation:operation,
514 cssClass:cssClass,
515 delay:delay
516 }
517 } else {
518 return null;
519 }
520 }
521
522 function processClassList(elt, classList) {
523 forEach(classList.split("&"), function (run) {
524 var currentRunTime = 0;
525 forEach(run.split(","), function(value){
526 var trimmedValue = value.trim();
527 var classOperation = parseClassOperation(trimmedValue);
528 if (classOperation) {
529 if (classOperation.operation === "toggle") {
530 setTimeout(function () {
531 setInterval(function () {
532 elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
533 }, classOperation.delay);
534 }, currentRunTime);
535 currentRunTime = currentRunTime + classOperation.delay;
536 } else {
537 currentRunTime = currentRunTime + classOperation.delay;
538 setTimeout(function () {
539 elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
540 }, currentRunTime);
541 }
542 }
543 });
544 });
545 }
546
547 function cancelPolling(elt) {
548 getInternalData(elt).cancelled = true;
549 }
550
551 function processPolling(elt, verb, path, interval) {
552 var nodeData = getInternalData(elt);
553 nodeData.timeout = setTimeout(function () {
554 if (bodyContains(elt) && nodeData.cancelled !== true) {
555 issueAjaxRequest(elt, verb, path);
556 processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb), interval);
557 }
558 }, interval);
559 }
560
561 function isLocalLink(elt) {
562 return location.hostname === elt.hostname &&
563 getRawAttribute(elt,'href') &&
564 getRawAttribute(elt,'href').indexOf("#") !== 0;
565 }
566
567 function boostElement(elt, nodeData, triggerSpec) {
568 if ((elt.tagName === "A" && isLocalLink(elt)) || elt.tagName === "FORM") {
569 nodeData.boosted = true;
570 var verb, path;
571 if (elt.tagName === "A") {
572 verb = "get";
573 path = getRawAttribute(elt, 'href');
574 } else {
575 var rawAttribute = getRawAttribute(elt, "method");
576 verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
577 path = getRawAttribute(elt, 'action');
578 }
579 addEventListener(elt, verb, path, nodeData, triggerSpec, true);
580 }
581 }
582
583 function shouldCancel(elt) {
584 return elt.tagName === "FORM" ||
585 (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) ||
586 (elt.tagName === "A" && elt.href && elt.href.indexOf('#') !== 0);
587 }
588
589 function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) {
590 var eventListener = function (evt) {
591 if(explicitCancel || shouldCancel(elt)) evt.preventDefault();
592 var eventData = getInternalData(evt);
593 var elementData = getInternalData(elt);
594 if (!eventData.handled) {
595 eventData.handled = true;
596 if (triggerSpec.once) {
597 if (elementData.triggeredOnce) {
598 return;
599 } else {
600 elementData.triggeredOnce = true;
601 }
602 }
603 if (triggerSpec.changed) {
604 if (elementData.lastValue === elt.value) {
605 return;
606 } else {
607 elementData.lastValue = elt.value;
608 }
609 }
610 if (elementData.delayed) {
611 clearTimeout(elementData.delayed);
612 }
613 var issueRequest = function(){
614 issueAjaxRequest(elt, verb, path, evt.target);
615 }
616 if (triggerSpec.delay) {
617 elementData.delayed = setTimeout(issueRequest, triggerSpec.delay);
618 } else {
619 issueRequest();
620 }
621 }
622 };
623 nodeData.trigger = triggerSpec.trigger;
624 nodeData.eventListener = eventListener;
625 elt.addEventListener(triggerSpec.trigger, eventListener);
626 }
627
628 function initScrollHandler() {
629 if (!window['htmxScrollHandler']) {
630 var scrollHandler = function() {
631 forEach(getDocument().querySelectorAll("[hx-trigger='revealed']"), function (elt) {
632 maybeReveal(elt);
633 });
634 };
635 window['htmxScrollHandler'] = scrollHandler;
636 window.addEventListener("scroll", scrollHandler)
637 }
638 }
639
640 function maybeReveal(elt) {
641 var nodeData = getInternalData(elt);
642 if (!nodeData.revealed && isScrolledIntoView(elt)) {
643 nodeData.revealed = true;
644 issueAjaxRequest(elt, nodeData.verb, nodeData.path);
645 }
646 }
647
648 function maybeCloseSSESource(elt) {
649 if (!bodyContains(elt)) {
650 elt.sseSource.close();
651 return true;
652 }
653 }
654
655 function initSSESource(elt, sseSrc) {
656 var detail = {
657 config:{withCredentials: true}
658 };
659 triggerEvent(elt, "initSSE.htmx", detail);
660 var source = new EventSource(sseSrc, detail.config);
661 source.onerror = function (e) {
662 triggerErrorEvent(elt, "sseError.htmx", {error:e, source:source});
663 maybeCloseSSESource(elt);
664 };
665 getInternalData(elt).sseSource = source;
666 }
667
668 function processSSETrigger(elt, verb, path, sseEventName) {
669 var sseSource = getClosestMatch(elt, function (parent) {
670 return parent.sseSource;
671 });
672 if (sseSource) {
673 var sseListener = function () {
674 if (!maybeCloseSSESource(sseSource)) {
675 if (bodyContains(elt)) {
676 issueAjaxRequest(elt, verb, path);
677 } else {
678 sseSource.sseSource.removeEventListener(sseEventName, sseListener);
679 }
680 }
681 };
682 sseSource.sseSource.addEventListener(sseEventName, sseListener);
683 } else {
684 triggerErrorEvent(elt, "noSSESourceError.htmx")
685 }
686 }
687
688 function loadImmediately(elt, verb, path, nodeData, delay) {
689 var load = function(){
690 if (!nodeData.loaded) {
691 nodeData.loaded = true;
692 issueAjaxRequest(elt, verb, path);
693 }
694 }
695 if (delay) {
696 setTimeout(load, delay);
697 } else {
698 load();
699 }
700 }
701
702 function processVerbs(elt, nodeData, triggerSpec) {
703 var explicitAction = false;
704 forEach(VERBS, function (verb) {
705 var path = getAttributeValue(elt, 'hx-' + verb);
706 if (path) {
707 explicitAction = true;
708 nodeData.path = path;
709 nodeData.verb = verb;
710 if (triggerSpec.sseEvent) {
711 processSSETrigger(elt, verb, path, triggerSpec.sseEvent);
712 } else if (triggerSpec.trigger === "revealed") {
713 initScrollHandler();
714 maybeReveal(elt);
715 } else if (triggerSpec.trigger === "load") {
716 loadImmediately(elt, verb, path, nodeData, triggerSpec.delay);
717 } else if (triggerSpec.pollInterval) {
718 nodeData.polling = true;
719 processPolling(elt, verb, path, triggerSpec.pollInterval);
720 } else {
721 addEventListener(elt, verb, path, nodeData, triggerSpec);
722 }
723 }
724 });
725 return explicitAction;
726 }
727
728 function processNode(elt) {
729 var nodeData = getInternalData(elt);
730 if (!nodeData.processed) {
731 nodeData.processed = true;
732
733 var triggerSpec = getTriggerSpec(elt);
734 var explicitAction = processVerbs(elt, nodeData, triggerSpec);
735
736 if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
737 boostElement(elt, nodeData, triggerSpec);
738 }
739 var sseSrc = getAttributeValue(elt, 'hx-sse-source');
740 if (sseSrc) {
741 initSSESource(elt, sseSrc);
742 }
743 var addClass = getAttributeValue(elt, 'hx-classes');
744 if (addClass) {
745 processClassList(elt, addClass);
746 }
747 }
748 if (elt.children) { // IE
749 forEach(elt.children, function(child) { processNode(child) });
750 }
751 }
752
753 //====================================================================
754 // Event/Log Support
755 //====================================================================
756
757 function sendError(elt, eventName, detail) {
758 var errorURL = getClosestAttributeValue(elt, "hx-error-url");
759 if (errorURL) {
760 var xhr = new XMLHttpRequest();
761 xhr.open("POST", errorURL);
762 xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
763 xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "detail" : detail }));
764 }
765 }
766
767 function makeEvent(eventName, detail) {
768 var evt;
769 if (window.CustomEvent && typeof window.CustomEvent === 'function') {
770 evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail});
771 } else {
772 evt = getDocument().createEvent('CustomEvent');
773 evt.initCustomEvent(eventName, true, true, detail);
774 }
775 return evt;
776 }
777
778 function triggerErrorEvent(elt, eventName, detail) {
779 triggerEvent(elt, eventName, mergeObjects({isError:true}, detail));
780 }
781
782 function triggerEvent(elt, eventName, detail) {
783 detail["elt"] = elt;
784 var event = makeEvent(eventName, detail);
785 if (htmx.logger) {
786 htmx.logger(elt, eventName, detail);
787 if (detail.isError) {
788 sendError(elt, eventName, detail);
789 }
790 }
791 var eventResult = elt.dispatchEvent(event);
792 return eventResult;
793 }
794
795 //====================================================================
796 // History Support
797 //====================================================================
798 var currentPathForHistory = null;
799
800 function getHistoryElement() {
801 var historyElt = getDocument().querySelector('[hx-history-elt]');
802 return historyElt || getDocument().body;
803 }
804
805 function saveToHistoryCache(url, content, title, scroll) {
806 var historyCache = JSON.parse(localStorage.getItem("htmx-history-cache")) || [];
807 for (var i = 0; i < historyCache.length; i++) {
808 if (historyCache[i].url === url) {
809 historyCache = historyCache.slice(i, 1);
810 break;
811 }
812 }
813 historyCache.push({url:url, content: content, title:title, scroll:scroll})
814 while (historyCache.length > htmx.config.historyCacheSize) {
815 historyCache.shift();
816 }
817 localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
818 }
819
820 function getCachedHistory(url) {
821 var historyCache = JSON.parse(localStorage.getItem("htmx-history-cache")) || [];
822 for (var i = 0; i < historyCache.length; i++) {
823 if (historyCache[i].url === url) {
824 return historyCache[i];
825 }
826 }
827 return null;
828 }
829
830 function saveHistory() {
831 var elt = getHistoryElement();
832 var path = currentPathForHistory || location.pathname+location.search;
833 triggerEvent(getDocument().body, "beforeHistorySave.htmx", {path:path, historyElt:elt});
834 if(htmx.config.historyEnabled) history.replaceState({}, getDocument().title, window.location.href);
835 saveToHistoryCache(path, elt.innerHTML, getDocument().title, window.scrollY);
836 }
837
838 function pushUrlIntoHistory(path) {
839 if(htmx.config.historyEnabled) history.pushState({}, "", path);
840 currentPathForHistory = path;
841 }
842
843 function settleImmediately(settleTasks) {
844 forEach(settleTasks, function (task) {
845 task.call();
846 });
847 }
848
849 function loadHistoryFromServer(path) {
850 var request = new XMLHttpRequest();
851 var details = {path: path, xhr:request};
852 triggerEvent(getDocument().body, "historyCacheMiss.htmx", details);
853 request.open('GET', path, true);
854 request.onload = function () {
855 if (this.status >= 200 && this.status < 400) {
856 triggerEvent(getDocument().body, "historyCacheMissLoad.htmx", details);
857 var fragment = makeFragment(this.response);
858 fragment = fragment.querySelector('[hx-history-elt]') || fragment;
859 settleImmediately(swapInnerHTML(getHistoryElement(), fragment));
860 currentPathForHistory = path;
861 } else {
862 triggerErrorEvent(getDocument().body, "historyCacheMissLoadError.htmx", details);
863 }
864 };
865 request.send();
866 }
867
868 function restoreHistory(path) {
869 saveHistory(currentPathForHistory);
870 path = path || location.pathname+location.search;
871 triggerEvent(getDocument().body, "historyRestore.htmx", {path:path});
872 var cached = getCachedHistory(path);
873 if (cached) {
874 settleImmediately(swapInnerHTML(getHistoryElement(), makeFragment(cached.content)));
875 document.title = cached.title;
876 window.scrollTo(0, cached.scroll);
877 currentPathForHistory = path;
878 } else {
879 loadHistoryFromServer(path);
880 }
881 }
882
883 function shouldPush(elt) {
884 return getClosestAttributeValue(elt, "hx-push-url") === "true" ||
885 (elt.tagName === "A" && getInternalData(elt).boosted);
886 }
887
888 function addRequestIndicatorClasses(elt) {
889 mutateRequestIndicatorClasses(elt, "add");
890 }
891
892 function removeRequestIndicatorClasses(elt) {
893 mutateRequestIndicatorClasses(elt, "remove");
894 }
895
896 function mutateRequestIndicatorClasses(elt, action) {
897 var indicator = getClosestAttributeValue(elt, 'hx-indicator');
898 if (indicator) {
899 var indicators = getDocument().querySelectorAll(indicator);
900 } else {
901 indicators = [elt];
902 }
903 forEach(indicators, function(ic) {
904 ic.classList[action].call(ic.classList, "htmx-request");
905 });
906 }
907
908 //====================================================================
909 // Input Value Processing
910 //====================================================================
911
912 function haveSeenNode(processed, elt) {
913 for (var i = 0; i < processed.length; i++) {
914 var node = processed[i];
915 if (node.isSameNode(elt)) {
916 return true;
917 }
918 }
919 return false;
920 }
921
922 function shouldInclude(elt) {
923 if(elt.name === "" || elt.name == null || elt.disabled) {
924 return false;
925 }
926 // ignore "submitter" types (see jQuery src/serialize.js)
927 if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
928 return false;
929 }
930 if (elt.type === "checkbox" || elt.type === "radio" ) {
931 return elt.checked;
932 }
933 return true;
934 }
935
936 function processInputValue(processed, values, elt) {
937 if (elt == null || haveSeenNode(processed, elt)) {
938 return;
939 } else {
940 processed.push(elt);
941 }
942 if (shouldInclude(elt)) {
943 var name = getRawAttribute(elt,"name");
944 var value = elt.value;
945 if (name && value) {
946 var current = values[name];
947 if(current) {
948 if (Array.isArray(current)) {
949 current.push(value);
950 } else {
951 values[name] = [current, value];
952 }
953 } else {
954 values[name] = value;
955 }
956 }
957 }
958 if (matches(elt, 'form')) {
959 var inputs = elt.elements;
960 forEach(inputs, function(input) {
961 processInputValue(processed, values, input);
962 });
963 }
964 }
965
966 function getInputValues(elt, verb) {
967 var processed = [];
968 var values = {};
969 // include the element itself
970 processInputValue(processed, values, elt);
971
972 // include any explicit includes
973 var includes = getClosestAttributeValue(elt, "hx-include");
974 if (includes) {
975 var nodes = getDocument().querySelectorAll(includes);
976 forEach(nodes, function(node) {
977 processInputValue(processed, values, node);
978 });
979 }
980
981 // for a non-GET include the closest form
982 if (verb !== 'get') {
983 processInputValue(processed, values, closest(elt, 'form'));
984 }
985 return values;
986 }
987
988 function appendParam(returnStr, name, realValue) {
989 if (returnStr !== "") {
990 returnStr += "&";
991 }
992 returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue);
993 return returnStr;
994 }
995
996 function urlEncode(values) {
997 var returnStr = "";
998 for (var name in values) {
999 if (values.hasOwnProperty(name)) {
1000 var value = values[name];
1001 if (Array.isArray(value)) {
1002 forEach(value, function(v) {
1003 returnStr = appendParam(returnStr, name, v);
1004 });
1005 } else {
1006 returnStr = appendParam(returnStr, name, value);
1007 }
1008 }
1009 }
1010 return returnStr;
1011 }
1012
1013 //====================================================================
1014 // Ajax
1015 //====================================================================
1016
1017 function getHeaders(elt, target, prompt, eventTarget) {
1018 var headers = {
1019 "X-HX-Request" : "true",
1020 "X-HX-Trigger" : getRawAttribute(elt, "id"),
1021 "X-HX-Trigger-Name" : getRawAttribute(elt, "name"),
1022 "X-HX-Target" : getRawAttribute(target, "id"),
1023 "Current-URL" : getDocument().location.href,
1024 }
1025 if (prompt) {
1026 headers["X-HX-Prompt"] = prompt;
1027 }
1028 if (eventTarget) {
1029 headers["X-HX-Event-Target"] = getRawAttribute(eventTarget, "id");
1030 }
1031 if (getDocument().activeElement) {
1032 headers["X-HX-Active-Element"] = getRawAttribute(getDocument().activeElement, "id");
1033 headers["X-HX-Active-Element-Name"] = getRawAttribute(getDocument().activeElement, "name");
1034 if (getDocument().activeElement.value) {
1035 headers["X-HX-Active-Element-Value"] = getRawAttribute(getDocument().activeElement, "value");
1036 }
1037 }
1038 return headers;
1039 }
1040
1041 function filterValues(inputValues, elt, verb) {
1042 var paramsValue = getClosestAttributeValue(elt, "hx-params");
1043 if (paramsValue) {
1044 if (paramsValue === "none") {
1045 return {};
1046 } else if (paramsValue === "*") {
1047 return inputValues;
1048 } else if(paramsValue.indexOf("not ") === 0) {
1049 forEach(paramsValue.substr(4).split(","), function (name) {
1050 name = name.trim();
1051 delete inputValues[name];
1052 });
1053 return inputValues;
1054 } else {
1055 var newValues = {}
1056 forEach(paramsValue.split(","), function (name) {
1057 name = name.trim();
1058 newValues[name] = inputValues[name];
1059 });
1060 return newValues;
1061 }
1062 } else {
1063 return inputValues;
1064 }
1065 }
1066
1067 function getSwapSpecification(elt) {
1068 var swapInfo = getClosestAttributeValue(elt, "hx-swap");
1069 var swapSpec = {
1070 "swapStyle" : htmx.config.defaultSwapStyle,
1071 "swapDelay" : htmx.config.defaultSwapDelay,
1072 "settleDelay" : htmx.config.defaultSettleDelay
1073 }
1074 if (swapInfo) {
1075 var split = splitOnWhitespace(swapInfo);
1076 if (split.length > 0) {
1077 swapSpec["swapStyle"] = split[0];
1078 for (var i = 1; i < split.length; i++) {
1079 var modifier = split[i];
1080 if (modifier.indexOf("swap:") === 0) {
1081 swapSpec["swapDelay"] = parseInterval(modifier.substr(5));
1082 }
1083 if (modifier.indexOf("settle:") === 0) {
1084 swapSpec["settleDelay"] = parseInterval(modifier.substr(7));
1085 }
1086 }
1087 }
1088 }
1089 return swapSpec;
1090 }
1091
1092 function issueAjaxRequest(elt, verb, path, eventTarget) {
1093 var target = getTarget(elt);
1094 if (target == null) {
1095 triggerErrorEvent(elt, 'targetError.htmx', {target: getRawAttribute(elt, "hx-target")});
1096 return;
1097 }
1098 var eltData = getInternalData(elt);
1099 if (eltData.requestInFlight) {
1100 return;
1101 } else {
1102 eltData.requestInFlight = true;
1103 }
1104 var endRequestLock = function(){
1105 eltData.requestInFlight = false
1106 }
1107 var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
1108 if (promptQuestion) {
1109 var promptResponse = prompt(promptQuestion);
1110 if(!triggerEvent(elt, 'prompt.htmx', {prompt: promptResponse, target:target})) return endRequestLock();
1111 }
1112
1113 var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
1114 if (confirmQuestion) {
1115 if(!confirm(confirmQuestion)) return endRequestLock();
1116 }
1117
1118 var xhr = new XMLHttpRequest();
1119
1120 var headers = getHeaders(elt, target, promptResponse, eventTarget);
1121 var rawParameters = getInputValues(elt, verb);
1122 var filteredParameters = filterValues(rawParameters, elt, verb);
1123
1124 if (verb !== 'get') {
1125 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
1126 if (verb !== 'post') {
1127 headers['X-HTTP-Method-Override'] = verb.toUpperCase();
1128 }
1129 }
1130
1131 var requestConfig = {
1132 parameters: filteredParameters,
1133 unfilteredParameters:rawParameters,
1134 headers:headers,
1135 target:target,
1136 verb:verb
1137 };
1138 if(!triggerEvent(elt, 'configRequest.htmx', requestConfig)) return endRequestLock();
1139
1140 // request type
1141 var requestURL;
1142 if (verb === 'get') {
1143 var noValues = Object.keys(filteredParameters).length === 0;
1144 requestURL = path + (noValues ? "" : "?" + urlEncode(filteredParameters));
1145 xhr.open('GET', requestURL, true);
1146 } else {
1147 requestURL = path;
1148 xhr.open('POST', requestURL, true);
1149 }
1150
1151 xhr.overrideMimeType("text/html");
1152
1153 // request headers
1154 for (var header in headers) {
1155 if (headers.hasOwnProperty(header)) {
1156 if(headers[header]) xhr.setRequestHeader(header, headers[header]);
1157 }
1158 }
1159
1160 var eventDetail = {xhr: xhr, target: target};
1161 xhr.onload = function () {
1162 try {
1163 if (!triggerEvent(elt, 'beforeOnLoad.htmx', eventDetail)) return;
1164
1165 handleTrigger(elt, this.getResponseHeader("X-HX-Trigger"));
1166 var pushedUrl = this.getResponseHeader("X-HX-Push");
1167
1168 var shouldSaveHistory = shouldPush(elt) || pushedUrl;
1169
1170 if (this.status >= 200 && this.status < 400) {
1171 if (this.status === 286) {
1172 cancelPolling(elt);
1173 }
1174 // don't process 'No Content' response
1175 if (this.status !== 204) {
1176 if (!triggerEvent(elt, 'beforeSwap.htmx', eventDetail)) return;
1177
1178 var resp = this.response;
1179
1180 // Save current page
1181 if (shouldSaveHistory) {
1182 saveHistory();
1183 }
1184
1185 var swapSpec = getSwapSpecification(elt);
1186
1187 target.classList.add("htmx-swapping");
1188 var doSwap = function () {
1189 try {
1190 var settleTasks = swapResponse(swapSpec.swapStyle, target, elt, resp);
1191 target.classList.remove("htmx-swapping");
1192 target.classList.add("htmx-settling");
1193 triggerEvent(elt, 'afterSwap.htmx', eventDetail);
1194
1195 var doSettle = function(){
1196 forEach(settleTasks, function (settleTask) {
1197 settleTask.call();
1198 });
1199 target.classList.remove("htmx-settling");
1200 // push URL and save new page
1201 if (shouldSaveHistory) {
1202 pushUrlIntoHistory(pushedUrl || requestURL );
1203 }
1204 triggerEvent(elt, 'afterSettle.htmx', eventDetail);
1205 }
1206
1207 if (swapSpec.settleDelay > 0) {
1208 setTimeout(doSettle, swapSpec.settleDelay)
1209 } else {
1210 doSettle();
1211 }
1212 } catch (e) {
1213 triggerErrorEvent(elt, 'swapError.htmx', eventDetail);
1214 throw e;
1215 }
1216 };
1217
1218 if (swapSpec.swapDelay > 0) {
1219 setTimeout(doSwap, swapSpec.swapDelay)
1220 } else {
1221 doSwap();
1222 }
1223 }
1224 } else {
1225 triggerErrorEvent(elt, 'responseError.htmx', eventDetail);
1226 }
1227 } catch (e) {
1228 eventDetail['exception'] = e;
1229 triggerErrorEvent(elt, 'onLoadError.htmx', eventDetail);
1230 throw e;
1231 } finally {
1232 removeRequestIndicatorClasses(elt);
1233 endRequestLock();
1234 triggerEvent(elt, 'afterOnLoad.htmx', eventDetail);
1235 }
1236 }
1237 xhr.onerror = function () {
1238 removeRequestIndicatorClasses(elt);
1239 triggerErrorEvent(elt, 'sendError.htmx', eventDetail);
1240 endRequestLock();
1241 }
1242 if(!triggerEvent(elt, 'beforeRequest.htmx', eventDetail)) return endRequestLock();
1243 addRequestIndicatorClasses(elt);
1244 xhr.send(verb === 'get' ? null : urlEncode(filteredParameters));
1245 }
1246
1247 //====================================================================
1248 // Initialization
1249 //====================================================================
1250
1251 function ready(fn) {
1252 if (getDocument().readyState !== 'loading') {
1253 fn();
1254 } else {
1255 getDocument().addEventListener('DOMContentLoaded', fn);
1256 }
1257 }
1258
1259 // insert htmx-indicator css rules immediate, if not configured otherwise
1260 (function() {
1261 var metaConfig = getMetaConfig();
1262 if (metaConfig === null || metaConfig.includeIndicatorStyles !== false) {
1263 getDocument().head.insertAdjacentHTML("beforeend",
1264 "<style>\
1265 .htmx-indicator{opacity:0;transition: opacity 200ms ease-in;}\
1266 .htmx-request .htmx-indicator{opacity:1}\
1267 .htmx-request.htmx-indicator{opacity:1}\
1268 </style>");
1269 }
1270 })();
1271
1272 function getMetaConfig() {
1273 var element = getDocument().querySelector('meta[name="htmx-config"]');
1274 if (element) {
1275 return JSON.parse(element.content);
1276 } else {
1277 return null;
1278 }
1279 }
1280
1281 function mergeMetaConfig() {
1282 var metaConfig = getMetaConfig();
1283 if (metaConfig) {
1284 htmx.config = mergeObjects(htmx.config , metaConfig)
1285 }
1286 }
1287
1288 // initialize the document
1289 ready(function () {
1290 mergeMetaConfig();
1291 var body = getDocument().body;
1292 processNode(body);
1293 triggerEvent(body, 'load.htmx', {});
1294 window.onpopstate = function () {
1295 restoreHistory();
1296 };
1297 })
1298
1299 // Public API
1300 return {
1301 onLoad: onLoadHelper,
1302 process: processNode,
1303 on: addEventListenerImpl,
1304 off: removeEventListenerImpl,
1305 trigger : triggerEvent,
1306 find : find,
1307 findAll : findAll,
1308 closest : closest,
1309 remove : removeElement,
1310 addClass : addClassToElement,
1311 removeClass : removeClassFromElement,
1312 toggleClass : toggleClassOnElement,
1313 takeClass : takeClassForElement,
1314 logAll : logAll,
1315 logger : null,
1316 config : {
1317 historyEnabled:true,
1318 historyCacheSize:10,
1319 defaultSwapStyle:'innerHTML',
1320 defaultSwapDelay:0,
1321 defaultSettleDelay:100,
1322 includeIndicatorStyles:true
1323 },
1324 version: "0.0.2",
1325 _:internalEval
1326 }
1327 }
1328)();
\No newline at end of file