├── .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("", 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("" + resp + "
", 1); 275 | case "col": 276 | return parseHTML("" + resp + "
", 2); 277 | case "tr": 278 | return parseHTML("" + resp + "
", 2); 279 | case "td": 280 | case "th": 281 | return parseHTML("" + resp + "
", 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 | 25 | 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 | ] --------------------------------------------------------------------------------