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