ocaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

First cut at HTMX app

authored by

Kento Okura and committed by
Jon Sterling
92a817df 4d3bf4d5

+6763 -289
+10
LICENSES/0BSD.txt
··· 1 + Permission to use, copy, modify, and/or distribute this software for 2 + any purpose with or without fee is hereby granted. 3 + 4 + THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 5 + WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 6 + OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 7 + FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 8 + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 9 + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 10 + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+27
bin/forester/App.ml
··· 13 13 14 14 let katex = Jv.get Jv.global "katex" 15 15 16 + type _katex_config = { 17 + displayMode: Jv.t; 18 + output: Jstr.t; 19 + leqno: Jv.t; 20 + fleqn: Jv.t; 21 + throwOnError: Jv.t; 22 + errorColor: Jstr.t; 23 + minRuleThickness: Jv.t; 24 + colorIsTextColor: Jv.t; 25 + maxSize: Jv.t; 26 + maxExpand: Jv.t; 27 + strict: Jv.t; 28 + trust: Jv.t; (* Boolean or function*) 29 + globalGroup: Jv.t 30 + } 31 + 32 + let trust_config = 33 + Jv.obj 34 + [| 35 + "trust", 36 + Jv.of_bool true 37 + |] 38 + 16 39 let render o = Jv.call o "render" 17 40 18 41 let () = ··· 30 53 [| 31 54 Jv.get (El.to_jv elt) "textContent"; 32 55 El.to_jv elt; 56 + trust_config; 33 57 |] 34 58 ) 35 59 (Jstr.v ".math") ··· 38 62 Jv.repr f 39 63 |] 40 64 65 + (* let () = ignore @@ Jv.call htmx "logAll" [||] *) 66 + 41 67 let () = 42 68 ignore @@ 43 69 Ev.listen ··· 47 73 Ev.( 48 74 if Keyboard.ctrl_key ev && Keyboard.key ev = Jstr.v "k" then 49 75 begin 76 + Console.log ["hello"]; 50 77 prevent_default e; 51 78 ignore @@ 52 79 ajax
+6
bin/forester/theme/REUSE.toml
··· 21 21 precedence = "override" 22 22 SPDX-FileCopyrightText = "2024 The Forester Project Contributors" 23 23 SPDX-License-Identifier = "CC0-1.0" 24 + 25 + [[annotations]] 26 + path = ["htmx.js"] 27 + precedence = "override" 28 + SPDX-FileCopyrightText = "2020, Big Sky Software" 29 + SPDX-License-Identifier = "0BSD"
+5829
bin/forester/theme/htmx.js
··· 1 + var htmx = (function () { 2 + "use strict"; 3 + 4 + // Public API 5 + const htmx = { 6 + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine 7 + /* Event processing */ 8 + /** @type {typeof onLoadHelper} */ 9 + onLoad: null, 10 + /** @type {typeof processNode} */ 11 + process: null, 12 + /** @type {typeof addEventListenerImpl} */ 13 + on: null, 14 + /** @type {typeof removeEventListenerImpl} */ 15 + off: null, 16 + /** @type {typeof triggerEvent} */ 17 + trigger: null, 18 + /** @type {typeof ajaxHelper} */ 19 + ajax: null, 20 + /* DOM querying helpers */ 21 + /** @type {typeof find} */ 22 + find: null, 23 + /** @type {typeof findAll} */ 24 + findAll: null, 25 + /** @type {typeof closest} */ 26 + closest: null, 27 + /** 28 + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism 29 + * 30 + * @see https://htmx.org/api/#values 31 + * 32 + * @param {Element} elt the element to resolve values on 33 + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** 34 + * @returns {Object} 35 + */ 36 + values: function (elt, type) { 37 + const inputValues = getInputValues(elt, type || "post"); 38 + return inputValues.values; 39 + }, 40 + /* DOM manipulation helpers */ 41 + /** @type {typeof removeElement} */ 42 + remove: null, 43 + /** @type {typeof addClassToElement} */ 44 + addClass: null, 45 + /** @type {typeof removeClassFromElement} */ 46 + removeClass: null, 47 + /** @type {typeof toggleClassOnElement} */ 48 + toggleClass: null, 49 + /** @type {typeof takeClassForElement} */ 50 + takeClass: null, 51 + /** @type {typeof swap} */ 52 + swap: null, 53 + /* Extension entrypoints */ 54 + /** @type {typeof defineExtension} */ 55 + defineExtension: null, 56 + /** @type {typeof removeExtension} */ 57 + removeExtension: null, 58 + /* Debugging */ 59 + /** @type {typeof logAll} */ 60 + logAll: null, 61 + /** @type {typeof logNone} */ 62 + logNone: null, 63 + /* Debugging */ 64 + /** 65 + * The logger htmx uses to log with 66 + * 67 + * @see https://htmx.org/api/#logger 68 + */ 69 + logger: null, 70 + /** 71 + * A property holding the configuration htmx uses at runtime. 72 + * 73 + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. 74 + * 75 + * @see https://htmx.org/api/#config 76 + */ 77 + config: { 78 + /** 79 + * Whether to use history. 80 + * @type boolean 81 + * @default true 82 + */ 83 + historyEnabled: true, 84 + /** 85 + * The number of pages to keep in **localStorage** for history support. 86 + * @type number 87 + * @default 10 88 + */ 89 + historyCacheSize: 10, 90 + /** 91 + * @type boolean 92 + * @default false 93 + */ 94 + refreshOnHistoryMiss: false, 95 + /** 96 + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. 97 + * @type HtmxSwapStyle 98 + * @default 'innerHTML' 99 + */ 100 + defaultSwapStyle: "innerHTML", 101 + /** 102 + * The default delay between receiving a response from the server and doing the swap. 103 + * @type number 104 + * @default 0 105 + */ 106 + defaultSwapDelay: 0, 107 + /** 108 + * The default delay between completing the content swap and settling attributes. 109 + * @type number 110 + * @default 20 111 + */ 112 + defaultSettleDelay: 20, 113 + /** 114 + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. 115 + * @type boolean 116 + * @default true 117 + */ 118 + includeIndicatorStyles: true, 119 + /** 120 + * The class to place on indicators when a request is in flight. 121 + * @type string 122 + * @default 'htmx-indicator' 123 + */ 124 + indicatorClass: "htmx-indicator", 125 + /** 126 + * The class to place on triggering elements when a request is in flight. 127 + * @type string 128 + * @default 'htmx-request' 129 + */ 130 + requestClass: "htmx-request", 131 + /** 132 + * The class to temporarily place on elements that htmx has added to the DOM. 133 + * @type string 134 + * @default 'htmx-added' 135 + */ 136 + addedClass: "htmx-added", 137 + /** 138 + * The class to place on target elements when htmx is in the settling phase. 139 + * @type string 140 + * @default 'htmx-settling' 141 + */ 142 + settlingClass: "htmx-settling", 143 + /** 144 + * The class to place on target elements when htmx is in the swapping phase. 145 + * @type string 146 + * @default 'htmx-swapping' 147 + */ 148 + swappingClass: "htmx-swapping", 149 + /** 150 + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. 151 + * @type boolean 152 + * @default true 153 + */ 154 + allowEval: true, 155 + /** 156 + * If set to false, disables the interpretation of script tags. 157 + * @type boolean 158 + * @default true 159 + */ 160 + allowScriptTags: true, 161 + /** 162 + * If set, the nonce will be added to inline scripts. 163 + * @type string 164 + * @default '' 165 + */ 166 + inlineScriptNonce: "", 167 + /** 168 + * If set, the nonce will be added to inline styles. 169 + * @type string 170 + * @default '' 171 + */ 172 + inlineStyleNonce: "", 173 + /** 174 + * The attributes to settle during the settling phase. 175 + * @type string[] 176 + * @default ['class', 'style', 'width', 'height'] 177 + */ 178 + attributesToSettle: ["class", "style", "width", "height"], 179 + /** 180 + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. 181 + * @type boolean 182 + * @default false 183 + */ 184 + withCredentials: false, 185 + /** 186 + * @type number 187 + * @default 0 188 + */ 189 + timeout: 0, 190 + /** 191 + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. 192 + * @type {'full-jitter' | ((retryCount:number) => number)} 193 + * @default "full-jitter" 194 + */ 195 + wsReconnectDelay: "full-jitter", 196 + /** 197 + * The type of binary data being received over the WebSocket connection 198 + * @type BinaryType 199 + * @default 'blob' 200 + */ 201 + wsBinaryType: "blob", 202 + /** 203 + * @type string 204 + * @default '[hx-disable], [data-hx-disable]' 205 + */ 206 + disableSelector: "[hx-disable], [data-hx-disable]", 207 + /** 208 + * @type {'auto' | 'instant' | 'smooth'} 209 + * @default 'instant' 210 + */ 211 + scrollBehavior: "instant", 212 + /** 213 + * If the focused element should be scrolled into view. 214 + * @type boolean 215 + * @default false 216 + */ 217 + defaultFocusScroll: false, 218 + /** 219 + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser 220 + * @type boolean 221 + * @default false 222 + */ 223 + getCacheBusterParam: false, 224 + /** 225 + * If set to true, htmx will use the View Transition API when swapping in new content. 226 + * @type boolean 227 + * @default false 228 + */ 229 + globalViewTransitions: false, 230 + /** 231 + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body 232 + * @type {(HttpVerb)[]} 233 + * @default ['get', 'delete'] 234 + */ 235 + methodsThatUseUrlParams: ["get", "delete"], 236 + /** 237 + * If set to true, disables htmx-based requests to non-origin hosts. 238 + * @type boolean 239 + * @default false 240 + */ 241 + selfRequestsOnly: true, 242 + /** 243 + * If set to true htmx will not update the title of the document when a title tag is found in new content 244 + * @type boolean 245 + * @default false 246 + */ 247 + ignoreTitle: false, 248 + /** 249 + * Whether the target of a boosted element is scrolled into the viewport. 250 + * @type boolean 251 + * @default true 252 + */ 253 + scrollIntoViewOnBoost: true, 254 + /** 255 + * The cache to store evaluated trigger specifications into. 256 + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 257 + * @type {Object|null} 258 + * @default null 259 + */ 260 + triggerSpecsCache: null, 261 + /** @type boolean */ 262 + disableInheritance: false, 263 + /** @type HtmxResponseHandlingConfig[] */ 264 + responseHandling: [ 265 + { code: "204", swap: false }, 266 + { code: "[23]..", swap: true }, 267 + { code: "[45]..", swap: false, error: true }, 268 + ], 269 + /** 270 + * Whether to process OOB swaps on elements that are nested within the main response element. 271 + * @type boolean 272 + * @default true 273 + */ 274 + allowNestedOobSwaps: true, 275 + }, 276 + /** @type {typeof parseInterval} */ 277 + parseInterval: null, 278 + /** @type {typeof internalEval} */ 279 + _: null, 280 + version: "2.0.4", 281 + }; 282 + // Tsc madness part 2 283 + htmx.onLoad = onLoadHelper; 284 + htmx.process = processNode; 285 + htmx.on = addEventListenerImpl; 286 + htmx.off = removeEventListenerImpl; 287 + htmx.trigger = triggerEvent; 288 + htmx.ajax = ajaxHelper; 289 + htmx.find = find; 290 + htmx.findAll = findAll; 291 + htmx.closest = closest; 292 + htmx.remove = removeElement; 293 + htmx.addClass = addClassToElement; 294 + htmx.removeClass = removeClassFromElement; 295 + htmx.toggleClass = toggleClassOnElement; 296 + htmx.takeClass = takeClassForElement; 297 + htmx.swap = swap; 298 + htmx.defineExtension = defineExtension; 299 + htmx.removeExtension = removeExtension; 300 + htmx.logAll = logAll; 301 + htmx.logNone = logNone; 302 + htmx.parseInterval = parseInterval; 303 + htmx._ = internalEval; 304 + 305 + const internalAPI = { 306 + addTriggerHandler, 307 + bodyContains, 308 + canAccessLocalStorage, 309 + findThisElement, 310 + filterValues, 311 + swap, 312 + hasAttribute, 313 + getAttributeValue, 314 + getClosestAttributeValue, 315 + getClosestMatch, 316 + getExpressionVars, 317 + getHeaders, 318 + getInputValues, 319 + getInternalData, 320 + getSwapSpecification, 321 + getTriggerSpecs, 322 + getTarget, 323 + makeFragment, 324 + mergeObjects, 325 + makeSettleInfo, 326 + oobSwap, 327 + querySelectorExt, 328 + settleImmediately, 329 + shouldCancel, 330 + triggerEvent, 331 + triggerErrorEvent, 332 + withExtensions, 333 + }; 334 + 335 + const VERBS = ["get", "post", "put", "delete", "patch"]; 336 + const VERB_SELECTOR = VERBS.map(function (verb) { 337 + return "[hx-" + verb + "], [data-hx-" + verb + "]"; 338 + }).join(", "); 339 + 340 + //= =================================================================== 341 + // Utilities 342 + //= =================================================================== 343 + 344 + /** 345 + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. 346 + * 347 + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** 348 + * 349 + * @see https://htmx.org/api/#parseInterval 350 + * 351 + * @param {string} str timing string 352 + * @returns {number|undefined} 353 + */ 354 + function parseInterval(str) { 355 + if (str == undefined) { 356 + return undefined; 357 + } 358 + 359 + let interval = NaN; 360 + if (str.slice(-2) == "ms") { 361 + interval = parseFloat(str.slice(0, -2)); 362 + } else if (str.slice(-1) == "s") { 363 + interval = parseFloat(str.slice(0, -1)) * 1000; 364 + } else if (str.slice(-1) == "m") { 365 + interval = parseFloat(str.slice(0, -1)) * 1000 * 60; 366 + } else { 367 + interval = parseFloat(str); 368 + } 369 + return isNaN(interval) ? undefined : interval; 370 + } 371 + 372 + /** 373 + * @param {Node} elt 374 + * @param {string} name 375 + * @returns {(string | null)} 376 + */ 377 + function getRawAttribute(elt, name) { 378 + return elt instanceof Element && elt.getAttribute(name); 379 + } 380 + 381 + /** 382 + * @param {Element} elt 383 + * @param {string} qualifiedName 384 + * @returns {boolean} 385 + */ 386 + // resolve with both hx and data-hx prefixes 387 + function hasAttribute(elt, qualifiedName) { 388 + return ( 389 + !!elt.hasAttribute && 390 + (elt.hasAttribute(qualifiedName) || 391 + elt.hasAttribute("data-" + qualifiedName)) 392 + ); 393 + } 394 + 395 + /** 396 + * 397 + * @param {Node} elt 398 + * @param {string} qualifiedName 399 + * @returns {(string | null)} 400 + */ 401 + function getAttributeValue(elt, qualifiedName) { 402 + return ( 403 + getRawAttribute(elt, qualifiedName) || 404 + getRawAttribute(elt, "data-" + qualifiedName) 405 + ); 406 + } 407 + 408 + /** 409 + * @param {Node} elt 410 + * @returns {Node | null} 411 + */ 412 + function parentElt(elt) { 413 + const parent = elt.parentElement; 414 + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode; 415 + return parent; 416 + } 417 + 418 + /** 419 + * @returns {Document} 420 + */ 421 + function getDocument() { 422 + return document; 423 + } 424 + 425 + /** 426 + * @param {Node} elt 427 + * @param {boolean} global 428 + * @returns {Node|Document} 429 + */ 430 + function getRootNode(elt, global) { 431 + return elt.getRootNode 432 + ? elt.getRootNode({ composed: global }) 433 + : getDocument(); 434 + } 435 + 436 + /** 437 + * @param {Node} elt 438 + * @param {(e:Node) => boolean} condition 439 + * @returns {Node | null} 440 + */ 441 + function getClosestMatch(elt, condition) { 442 + while (elt && !condition(elt)) { 443 + elt = parentElt(elt); 444 + } 445 + 446 + return elt || null; 447 + } 448 + 449 + /** 450 + * @param {Element} initialElement 451 + * @param {Element} ancestor 452 + * @param {string} attributeName 453 + * @returns {string|null} 454 + */ 455 + function getAttributeValueWithDisinheritance( 456 + initialElement, 457 + ancestor, 458 + attributeName, 459 + ) { 460 + const attributeValue = getAttributeValue(ancestor, attributeName); 461 + const disinherit = getAttributeValue(ancestor, "hx-disinherit"); 462 + var inherit = getAttributeValue(ancestor, "hx-inherit"); 463 + if (initialElement !== ancestor) { 464 + if (htmx.config.disableInheritance) { 465 + if ( 466 + inherit && 467 + (inherit === "*" || inherit.split(" ").indexOf(attributeName) >= 0) 468 + ) { 469 + return attributeValue; 470 + } else { 471 + return null; 472 + } 473 + } 474 + if ( 475 + disinherit && 476 + (disinherit === "*" || 477 + disinherit.split(" ").indexOf(attributeName) >= 0) 478 + ) { 479 + return "unset"; 480 + } 481 + } 482 + return attributeValue; 483 + } 484 + 485 + /** 486 + * @param {Element} elt 487 + * @param {string} attributeName 488 + * @returns {string | null} 489 + */ 490 + function getClosestAttributeValue(elt, attributeName) { 491 + let closestAttr = null; 492 + getClosestMatch(elt, function (e) { 493 + return !!(closestAttr = getAttributeValueWithDisinheritance( 494 + elt, 495 + asElement(e), 496 + attributeName, 497 + )); 498 + }); 499 + if (closestAttr !== "unset") { 500 + return closestAttr; 501 + } 502 + } 503 + 504 + /** 505 + * @param {Node} elt 506 + * @param {string} selector 507 + * @returns {boolean} 508 + */ 509 + function matches(elt, selector) { 510 + // @ts-ignore: non-standard properties for browser compatibility 511 + // noinspection JSUnresolvedVariable 512 + const matchesFunction = 513 + elt instanceof Element && 514 + (elt.matches || 515 + elt.matchesSelector || 516 + elt.msMatchesSelector || 517 + elt.mozMatchesSelector || 518 + elt.webkitMatchesSelector || 519 + elt.oMatchesSelector); 520 + return !!matchesFunction && matchesFunction.call(elt, selector); 521 + } 522 + 523 + /** 524 + * @param {string} str 525 + * @returns {string} 526 + */ 527 + function getStartTag(str) { 528 + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; 529 + const match = tagMatcher.exec(str); 530 + if (match) { 531 + return match[1].toLowerCase(); 532 + } else { 533 + return ""; 534 + } 535 + } 536 + 537 + /** 538 + * @param {string} resp 539 + * @returns {Document} 540 + */ 541 + function parseHTML(resp) { 542 + const parser = new DOMParser(); 543 + return parser.parseFromString(resp, "text/html"); 544 + } 545 + 546 + /** 547 + * @param {DocumentFragment} fragment 548 + * @param {Node} elt 549 + */ 550 + function takeChildrenFor(fragment, elt) { 551 + while (elt.childNodes.length > 0) { 552 + fragment.append(elt.childNodes[0]); 553 + } 554 + } 555 + 556 + /** 557 + * @param {HTMLScriptElement} script 558 + * @returns {HTMLScriptElement} 559 + */ 560 + function duplicateScript(script) { 561 + const newScript = getDocument().createElement("script"); 562 + forEach(script.attributes, function (attr) { 563 + newScript.setAttribute(attr.name, attr.value); 564 + }); 565 + newScript.textContent = script.textContent; 566 + newScript.async = false; 567 + if (htmx.config.inlineScriptNonce) { 568 + newScript.nonce = htmx.config.inlineScriptNonce; 569 + } 570 + return newScript; 571 + } 572 + 573 + /** 574 + * @param {HTMLScriptElement} script 575 + * @returns {boolean} 576 + */ 577 + function isJavaScriptScriptNode(script) { 578 + return ( 579 + script.matches("script") && 580 + (script.type === "text/javascript" || 581 + script.type === "module" || 582 + script.type === "") 583 + ); 584 + } 585 + 586 + /** 587 + * we have to make new copies of script tags that we are going to insert because 588 + * SOME browsers (not saying who, but it involves an element and an animal) don't 589 + * execute scripts created in <template> tags when they are inserted into the DOM 590 + * and all the others do lmao 591 + * @param {DocumentFragment} fragment 592 + */ 593 + function normalizeScriptTags(fragment) { 594 + Array.from(fragment.querySelectorAll("script")).forEach( 595 + /** @param {HTMLScriptElement} script */ (script) => { 596 + if (isJavaScriptScriptNode(script)) { 597 + const newScript = duplicateScript(script); 598 + const parent = script.parentNode; 599 + try { 600 + parent.insertBefore(newScript, script); 601 + } catch (e) { 602 + logError(e); 603 + } finally { 604 + script.remove(); 605 + } 606 + } 607 + }, 608 + ); 609 + } 610 + 611 + /** 612 + * @typedef {DocumentFragment & {title?: string}} DocumentFragmentWithTitle 613 + * @description a document fragment representing the response HTML, including 614 + * a `title` property for any title information found 615 + */ 616 + 617 + /** 618 + * @param {string} response HTML 619 + * @returns {DocumentFragmentWithTitle} 620 + */ 621 + function makeFragment(response) { 622 + // strip head tag to determine shape of response we are dealing with 623 + const responseWithNoHead = response.replace( 624 + /<head(\s[^>]*)?>[\s\S]*?<\/head>/i, 625 + "", 626 + ); 627 + const startTag = getStartTag(responseWithNoHead); 628 + /** @type DocumentFragmentWithTitle */ 629 + let fragment; 630 + if (startTag === "html") { 631 + // if it is a full document, parse it and return the body 632 + fragment = /** @type DocumentFragmentWithTitle */ ( 633 + new DocumentFragment() 634 + ); 635 + const doc = parseHTML(response); 636 + takeChildrenFor(fragment, doc.body); 637 + fragment.title = doc.title; 638 + } else if (startTag === "body") { 639 + // parse body w/o wrapping in template 640 + fragment = /** @type DocumentFragmentWithTitle */ ( 641 + new DocumentFragment() 642 + ); 643 + const doc = parseHTML(responseWithNoHead); 644 + takeChildrenFor(fragment, doc.body); 645 + fragment.title = doc.title; 646 + } else { 647 + // otherwise we have non-body partial HTML content, so wrap it in a template to maximize parsing flexibility 648 + const doc = parseHTML( 649 + '<body><template class="internal-htmx-wrapper">' + 650 + responseWithNoHead + 651 + "</template></body>", 652 + ); 653 + fragment = /** @type DocumentFragmentWithTitle */ ( 654 + doc.querySelector("template").content 655 + ); 656 + // extract title into fragment for later processing 657 + fragment.title = doc.title; 658 + 659 + // for legacy reasons we support a title tag at the root level of non-body responses, so we need to handle it 660 + var titleElement = fragment.querySelector("title"); 661 + if (titleElement && titleElement.parentNode === fragment) { 662 + titleElement.remove(); 663 + fragment.title = titleElement.innerText; 664 + } 665 + } 666 + if (fragment) { 667 + if (htmx.config.allowScriptTags) { 668 + normalizeScriptTags(fragment); 669 + } else { 670 + // remove all script tags if scripts are disabled 671 + fragment 672 + .querySelectorAll("script") 673 + .forEach((script) => script.remove()); 674 + } 675 + } 676 + return fragment; 677 + } 678 + 679 + /** 680 + * @param {Function} func 681 + */ 682 + function maybeCall(func) { 683 + if (func) { 684 + func(); 685 + } 686 + } 687 + 688 + /** 689 + * @param {any} o 690 + * @param {string} type 691 + * @returns 692 + */ 693 + function isType(o, type) { 694 + return Object.prototype.toString.call(o) === "[object " + type + "]"; 695 + } 696 + 697 + /** 698 + * @param {*} o 699 + * @returns {o is Function} 700 + */ 701 + function isFunction(o) { 702 + return typeof o === "function"; 703 + } 704 + 705 + /** 706 + * @param {*} o 707 + * @returns {o is Object} 708 + */ 709 + function isRawObject(o) { 710 + return isType(o, "Object"); 711 + } 712 + 713 + /** 714 + * @typedef {Object} OnHandler 715 + * @property {(keyof HTMLElementEventMap)|string} event 716 + * @property {EventListener} listener 717 + */ 718 + 719 + /** 720 + * @typedef {Object} ListenerInfo 721 + * @property {string} trigger 722 + * @property {EventListener} listener 723 + * @property {EventTarget} on 724 + */ 725 + 726 + /** 727 + * @typedef {Object} HtmxNodeInternalData 728 + * Element data 729 + * @property {number} [initHash] 730 + * @property {boolean} [boosted] 731 + * @property {OnHandler[]} [onHandlers] 732 + * @property {number} [timeout] 733 + * @property {ListenerInfo[]} [listenerInfos] 734 + * @property {boolean} [cancelled] 735 + * @property {boolean} [triggeredOnce] 736 + * @property {number} [delayed] 737 + * @property {number|null} [throttle] 738 + * @property {WeakMap<HtmxTriggerSpecification,WeakMap<EventTarget,string>>} [lastValue] 739 + * @property {boolean} [loaded] 740 + * @property {string} [path] 741 + * @property {string} [verb] 742 + * @property {boolean} [polling] 743 + * @property {HTMLButtonElement|HTMLInputElement|null} [lastButtonClicked] 744 + * @property {number} [requestCount] 745 + * @property {XMLHttpRequest} [xhr] 746 + * @property {(() => void)[]} [queuedRequests] 747 + * @property {boolean} [abortable] 748 + * @property {boolean} [firstInitCompleted] 749 + * 750 + * Event data 751 + * @property {HtmxTriggerSpecification} [triggerSpec] 752 + * @property {EventTarget[]} [handledFor] 753 + */ 754 + 755 + /** 756 + * getInternalData retrieves "private" data stored by htmx within an element 757 + * @param {EventTarget|Event} elt 758 + * @returns {HtmxNodeInternalData} 759 + */ 760 + function getInternalData(elt) { 761 + const dataProp = "htmx-internal-data"; 762 + let data = elt[dataProp]; 763 + if (!data) { 764 + data = elt[dataProp] = {}; 765 + } 766 + return data; 767 + } 768 + 769 + /** 770 + * toArray converts an ArrayLike object into a real array. 771 + * @template T 772 + * @param {ArrayLike<T>} arr 773 + * @returns {T[]} 774 + */ 775 + function toArray(arr) { 776 + const returnArr = []; 777 + if (arr) { 778 + for (let i = 0; i < arr.length; i++) { 779 + returnArr.push(arr[i]); 780 + } 781 + } 782 + return returnArr; 783 + } 784 + 785 + /** 786 + * @template T 787 + * @param {T[]|NamedNodeMap|HTMLCollection|HTMLFormControlsCollection|ArrayLike<T>} arr 788 + * @param {(T) => void} func 789 + */ 790 + function forEach(arr, func) { 791 + if (arr) { 792 + for (let i = 0; i < arr.length; i++) { 793 + func(arr[i]); 794 + } 795 + } 796 + } 797 + 798 + /** 799 + * @param {Element} el 800 + * @returns {boolean} 801 + */ 802 + function isScrolledIntoView(el) { 803 + const rect = el.getBoundingClientRect(); 804 + const elemTop = rect.top; 805 + const elemBottom = rect.bottom; 806 + return elemTop < window.innerHeight && elemBottom >= 0; 807 + } 808 + 809 + /** 810 + * Checks whether the element is in the document (includes shadow roots). 811 + * This function this is a slight misnomer; it will return true even for elements in the head. 812 + * 813 + * @param {Node} elt 814 + * @returns {boolean} 815 + */ 816 + function bodyContains(elt) { 817 + return elt.getRootNode({ composed: true }) === document; 818 + } 819 + 820 + /** 821 + * @param {string} trigger 822 + * @returns {string[]} 823 + */ 824 + function splitOnWhitespace(trigger) { 825 + return trigger.trim().split(/\s+/); 826 + } 827 + 828 + /** 829 + * mergeObjects takes all the keys from 830 + * obj2 and duplicates them into obj1 831 + * @template T1 832 + * @template T2 833 + * @param {T1} obj1 834 + * @param {T2} obj2 835 + * @returns {T1 & T2} 836 + */ 837 + function mergeObjects(obj1, obj2) { 838 + for (const key in obj2) { 839 + if (obj2.hasOwnProperty(key)) { 840 + // @ts-ignore tsc doesn't seem to properly handle types merging 841 + obj1[key] = obj2[key]; 842 + } 843 + } 844 + // @ts-ignore tsc doesn't seem to properly handle types merging 845 + return obj1; 846 + } 847 + 848 + /** 849 + * @param {string} jString 850 + * @returns {any|null} 851 + */ 852 + function parseJSON(jString) { 853 + try { 854 + return JSON.parse(jString); 855 + } catch (error) { 856 + logError(error); 857 + return null; 858 + } 859 + } 860 + 861 + /** 862 + * @returns {boolean} 863 + */ 864 + function canAccessLocalStorage() { 865 + const test = "htmx:localStorageTest"; 866 + try { 867 + localStorage.setItem(test, test); 868 + localStorage.removeItem(test); 869 + return true; 870 + } catch (e) { 871 + return false; 872 + } 873 + } 874 + 875 + /** 876 + * @param {string} path 877 + * @returns {string} 878 + */ 879 + function normalizePath(path) { 880 + try { 881 + const url = new URL(path); 882 + if (url) { 883 + path = url.pathname + url.search; 884 + } 885 + // remove trailing slash, unless index page 886 + if (!/^\/$/.test(path)) { 887 + path = path.replace(/\/+$/, ""); 888 + } 889 + return path; 890 + } catch (e) { 891 + // be kind to IE11, which doesn't support URL() 892 + return path; 893 + } 894 + } 895 + 896 + //= ========================================================================================= 897 + // public API 898 + //= ========================================================================================= 899 + 900 + /** 901 + * @param {string} str 902 + * @returns {any} 903 + */ 904 + function internalEval(str) { 905 + return maybeEval(getDocument().body, function () { 906 + return eval(str); 907 + }); 908 + } 909 + 910 + /** 911 + * Adds a callback for the **htmx:load** event. This can be used to process new content, for example initializing the content with a javascript library 912 + * 913 + * @see https://htmx.org/api/#onLoad 914 + * 915 + * @param {(elt: Node) => void} callback the callback to call on newly loaded content 916 + * @returns {EventListener} 917 + */ 918 + function onLoadHelper(callback) { 919 + const value = htmx.on( 920 + "htmx:load", 921 + /** @param {CustomEvent} evt */ function (evt) { 922 + callback(evt.detail.elt); 923 + }, 924 + ); 925 + return value; 926 + } 927 + 928 + /** 929 + * Log all htmx events, useful for debugging. 930 + * 931 + * @see https://htmx.org/api/#logAll 932 + */ 933 + function logAll() { 934 + htmx.logger = function (elt, event, data) { 935 + if (console) { 936 + console.log(event, elt, data); 937 + } 938 + }; 939 + } 940 + 941 + function logNone() { 942 + htmx.logger = null; 943 + } 944 + 945 + /** 946 + * Finds an element matching the selector 947 + * 948 + * @see https://htmx.org/api/#find 949 + * 950 + * @param {ParentNode|string} eltOrSelector the root element to find the matching element in, inclusive | the selector to match 951 + * @param {string} [selector] the selector to match 952 + * @returns {Element|null} 953 + */ 954 + function find(eltOrSelector, selector) { 955 + if (typeof eltOrSelector !== "string") { 956 + return eltOrSelector.querySelector(selector); 957 + } else { 958 + return find(getDocument(), eltOrSelector); 959 + } 960 + } 961 + 962 + /** 963 + * Finds all elements matching the selector 964 + * 965 + * @see https://htmx.org/api/#findAll 966 + * 967 + * @param {ParentNode|string} eltOrSelector the root element to find the matching elements in, inclusive | the selector to match 968 + * @param {string} [selector] the selector to match 969 + * @returns {NodeListOf<Element>} 970 + */ 971 + function findAll(eltOrSelector, selector) { 972 + if (typeof eltOrSelector !== "string") { 973 + return eltOrSelector.querySelectorAll(selector); 974 + } else { 975 + return findAll(getDocument(), eltOrSelector); 976 + } 977 + } 978 + 979 + /** 980 + * @returns Window 981 + */ 982 + function getWindow() { 983 + return window; 984 + } 985 + 986 + /** 987 + * Removes an element from the DOM 988 + * 989 + * @see https://htmx.org/api/#remove 990 + * 991 + * @param {Node} elt 992 + * @param {number} [delay] 993 + */ 994 + function removeElement(elt, delay) { 995 + elt = resolveTarget(elt); 996 + if (delay) { 997 + getWindow().setTimeout(function () { 998 + removeElement(elt); 999 + elt = null; 1000 + }, delay); 1001 + } else { 1002 + parentElt(elt).removeChild(elt); 1003 + } 1004 + } 1005 + 1006 + /** 1007 + * @param {any} elt 1008 + * @return {Element|null} 1009 + */ 1010 + function asElement(elt) { 1011 + return elt instanceof Element ? elt : null; 1012 + } 1013 + 1014 + /** 1015 + * @param {any} elt 1016 + * @return {HTMLElement|null} 1017 + */ 1018 + function asHtmlElement(elt) { 1019 + return elt instanceof HTMLElement ? elt : null; 1020 + } 1021 + 1022 + /** 1023 + * @param {any} value 1024 + * @return {string|null} 1025 + */ 1026 + function asString(value) { 1027 + return typeof value === "string" ? value : null; 1028 + } 1029 + 1030 + /** 1031 + * @param {EventTarget} elt 1032 + * @return {ParentNode|null} 1033 + */ 1034 + function asParentNode(elt) { 1035 + return elt instanceof Element || 1036 + elt instanceof Document || 1037 + elt instanceof DocumentFragment 1038 + ? elt 1039 + : null; 1040 + } 1041 + 1042 + /** 1043 + * This method adds a class to the given element. 1044 + * 1045 + * @see https://htmx.org/api/#addClass 1046 + * 1047 + * @param {Element|string} elt the element to add the class to 1048 + * @param {string} clazz the class to add 1049 + * @param {number} [delay] the delay (in milliseconds) before class is added 1050 + */ 1051 + function addClassToElement(elt, clazz, delay) { 1052 + elt = asElement(resolveTarget(elt)); 1053 + if (!elt) { 1054 + return; 1055 + } 1056 + if (delay) { 1057 + getWindow().setTimeout(function () { 1058 + addClassToElement(elt, clazz); 1059 + elt = null; 1060 + }, delay); 1061 + } else { 1062 + elt.classList && elt.classList.add(clazz); 1063 + } 1064 + } 1065 + 1066 + /** 1067 + * Removes a class from the given element 1068 + * 1069 + * @see https://htmx.org/api/#removeClass 1070 + * 1071 + * @param {Node|string} node element to remove the class from 1072 + * @param {string} clazz the class to remove 1073 + * @param {number} [delay] the delay (in milliseconds before class is removed) 1074 + */ 1075 + function removeClassFromElement(node, clazz, delay) { 1076 + let elt = asElement(resolveTarget(node)); 1077 + if (!elt) { 1078 + return; 1079 + } 1080 + if (delay) { 1081 + getWindow().setTimeout(function () { 1082 + removeClassFromElement(elt, clazz); 1083 + elt = null; 1084 + }, delay); 1085 + } else { 1086 + if (elt.classList) { 1087 + elt.classList.remove(clazz); 1088 + // if there are no classes left, remove the class attribute 1089 + if (elt.classList.length === 0) { 1090 + elt.removeAttribute("class"); 1091 + } 1092 + } 1093 + } 1094 + } 1095 + 1096 + /** 1097 + * Toggles the given class on an element 1098 + * 1099 + * @see https://htmx.org/api/#toggleClass 1100 + * 1101 + * @param {Element|string} elt the element to toggle the class on 1102 + * @param {string} clazz the class to toggle 1103 + */ 1104 + function toggleClassOnElement(elt, clazz) { 1105 + elt = resolveTarget(elt); 1106 + elt.classList.toggle(clazz); 1107 + } 1108 + 1109 + /** 1110 + * Takes the given class from its siblings, so that among its siblings, only the given element will have the class. 1111 + * 1112 + * @see https://htmx.org/api/#takeClass 1113 + * 1114 + * @param {Node|string} elt the element that will take the class 1115 + * @param {string} clazz the class to take 1116 + */ 1117 + function takeClassForElement(elt, clazz) { 1118 + elt = resolveTarget(elt); 1119 + forEach(elt.parentElement.children, function (child) { 1120 + removeClassFromElement(child, clazz); 1121 + }); 1122 + addClassToElement(asElement(elt), clazz); 1123 + } 1124 + 1125 + /** 1126 + * Finds the closest matching element in the given elements parentage, inclusive of the element 1127 + * 1128 + * @see https://htmx.org/api/#closest 1129 + * 1130 + * @param {Element|string} elt the element to find the selector from 1131 + * @param {string} selector the selector to find 1132 + * @returns {Element|null} 1133 + */ 1134 + function closest(elt, selector) { 1135 + elt = asElement(resolveTarget(elt)); 1136 + if (elt && elt.closest) { 1137 + return elt.closest(selector); 1138 + } else { 1139 + // TODO remove when IE goes away 1140 + do { 1141 + if (elt == null || matches(elt, selector)) { 1142 + return elt; 1143 + } 1144 + } while ((elt = elt && asElement(parentElt(elt)))); 1145 + return null; 1146 + } 1147 + } 1148 + 1149 + /** 1150 + * @param {string} str 1151 + * @param {string} prefix 1152 + * @returns {boolean} 1153 + */ 1154 + function startsWith(str, prefix) { 1155 + return str.substring(0, prefix.length) === prefix; 1156 + } 1157 + 1158 + /** 1159 + * @param {string} str 1160 + * @param {string} suffix 1161 + * @returns {boolean} 1162 + */ 1163 + function endsWith(str, suffix) { 1164 + return str.substring(str.length - suffix.length) === suffix; 1165 + } 1166 + 1167 + /** 1168 + * @param {string} selector 1169 + * @returns {string} 1170 + */ 1171 + function normalizeSelector(selector) { 1172 + const trimmedSelector = selector.trim(); 1173 + if (startsWith(trimmedSelector, "<") && endsWith(trimmedSelector, "/>")) { 1174 + return trimmedSelector.substring(1, trimmedSelector.length - 2); 1175 + } else { 1176 + return trimmedSelector; 1177 + } 1178 + } 1179 + 1180 + /** 1181 + * @param {Node|Element|Document|string} elt 1182 + * @param {string} selector 1183 + * @param {boolean=} global 1184 + * @returns {(Node|Window)[]} 1185 + */ 1186 + function querySelectorAllExt(elt, selector, global) { 1187 + if (selector.indexOf("global ") === 0) { 1188 + return querySelectorAllExt(elt, selector.slice(7), true); 1189 + } 1190 + 1191 + elt = resolveTarget(elt); 1192 + 1193 + const parts = []; 1194 + { 1195 + let chevronsCount = 0; 1196 + let offset = 0; 1197 + for (let i = 0; i < selector.length; i++) { 1198 + const char = selector[i]; 1199 + if (char === "," && chevronsCount === 0) { 1200 + parts.push(selector.substring(offset, i)); 1201 + offset = i + 1; 1202 + continue; 1203 + } 1204 + if (char === "<") { 1205 + chevronsCount++; 1206 + } else if ( 1207 + char === "/" && 1208 + i < selector.length - 1 && 1209 + selector[i + 1] === ">" 1210 + ) { 1211 + chevronsCount--; 1212 + } 1213 + } 1214 + if (offset < selector.length) { 1215 + parts.push(selector.substring(offset)); 1216 + } 1217 + } 1218 + 1219 + const result = []; 1220 + const unprocessedParts = []; 1221 + while (parts.length > 0) { 1222 + const selector = normalizeSelector(parts.shift()); 1223 + let item; 1224 + if (selector.indexOf("closest ") === 0) { 1225 + item = closest(asElement(elt), normalizeSelector(selector.substr(8))); 1226 + } else if (selector.indexOf("find ") === 0) { 1227 + item = find(asParentNode(elt), normalizeSelector(selector.substr(5))); 1228 + } else if (selector === "next" || selector === "nextElementSibling") { 1229 + item = asElement(elt).nextElementSibling; 1230 + } else if (selector.indexOf("next ") === 0) { 1231 + item = scanForwardQuery( 1232 + elt, 1233 + normalizeSelector(selector.substr(5)), 1234 + !!global, 1235 + ); 1236 + } else if ( 1237 + selector === "previous" || 1238 + selector === "previousElementSibling" 1239 + ) { 1240 + item = asElement(elt).previousElementSibling; 1241 + } else if (selector.indexOf("previous ") === 0) { 1242 + item = scanBackwardsQuery( 1243 + elt, 1244 + normalizeSelector(selector.substr(9)), 1245 + !!global, 1246 + ); 1247 + } else if (selector === "document") { 1248 + item = document; 1249 + } else if (selector === "window") { 1250 + item = window; 1251 + } else if (selector === "body") { 1252 + item = document.body; 1253 + } else if (selector === "root") { 1254 + item = getRootNode(elt, !!global); 1255 + } else if (selector === "host") { 1256 + item = /** @type ShadowRoot */ (elt.getRootNode()).host; 1257 + } else { 1258 + unprocessedParts.push(selector); 1259 + } 1260 + 1261 + if (item) { 1262 + result.push(item); 1263 + } 1264 + } 1265 + 1266 + if (unprocessedParts.length > 0) { 1267 + const standardSelector = unprocessedParts.join(","); 1268 + const rootNode = asParentNode(getRootNode(elt, !!global)); 1269 + result.push(...toArray(rootNode.querySelectorAll(standardSelector))); 1270 + } 1271 + 1272 + return result; 1273 + } 1274 + 1275 + /** 1276 + * @param {Node} start 1277 + * @param {string} match 1278 + * @param {boolean} global 1279 + * @returns {Element} 1280 + */ 1281 + var scanForwardQuery = function (start, match, global) { 1282 + const results = asParentNode(getRootNode(start, global)).querySelectorAll( 1283 + match, 1284 + ); 1285 + for (let i = 0; i < results.length; i++) { 1286 + const elt = results[i]; 1287 + if ( 1288 + elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING 1289 + ) { 1290 + return elt; 1291 + } 1292 + } 1293 + }; 1294 + 1295 + /** 1296 + * @param {Node} start 1297 + * @param {string} match 1298 + * @param {boolean} global 1299 + * @returns {Element} 1300 + */ 1301 + var scanBackwardsQuery = function (start, match, global) { 1302 + const results = asParentNode(getRootNode(start, global)).querySelectorAll( 1303 + match, 1304 + ); 1305 + for (let i = results.length - 1; i >= 0; i--) { 1306 + const elt = results[i]; 1307 + if ( 1308 + elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING 1309 + ) { 1310 + return elt; 1311 + } 1312 + } 1313 + }; 1314 + 1315 + /** 1316 + * @param {Node|string} eltOrSelector 1317 + * @param {string=} selector 1318 + * @returns {Node|Window} 1319 + */ 1320 + function querySelectorExt(eltOrSelector, selector) { 1321 + if (typeof eltOrSelector !== "string") { 1322 + return querySelectorAllExt(eltOrSelector, selector)[0]; 1323 + } else { 1324 + return querySelectorAllExt(getDocument().body, eltOrSelector)[0]; 1325 + } 1326 + } 1327 + 1328 + /** 1329 + * @template {EventTarget} T 1330 + * @param {T|string} eltOrSelector 1331 + * @param {T} [context] 1332 + * @returns {Element|T|null} 1333 + */ 1334 + function resolveTarget(eltOrSelector, context) { 1335 + if (typeof eltOrSelector === "string") { 1336 + return find(asParentNode(context) || document, eltOrSelector); 1337 + } else { 1338 + return eltOrSelector; 1339 + } 1340 + } 1341 + 1342 + /** 1343 + * @typedef {keyof HTMLElementEventMap|string} AnyEventName 1344 + */ 1345 + 1346 + /** 1347 + * @typedef {Object} EventArgs 1348 + * @property {EventTarget} target 1349 + * @property {AnyEventName} event 1350 + * @property {EventListener} listener 1351 + * @property {Object|boolean} options 1352 + */ 1353 + 1354 + /** 1355 + * @param {EventTarget|AnyEventName} arg1 1356 + * @param {AnyEventName|EventListener} arg2 1357 + * @param {EventListener|Object|boolean} [arg3] 1358 + * @param {Object|boolean} [arg4] 1359 + * @returns {EventArgs} 1360 + */ 1361 + function processEventArgs(arg1, arg2, arg3, arg4) { 1362 + if (isFunction(arg2)) { 1363 + return { 1364 + target: getDocument().body, 1365 + event: asString(arg1), 1366 + listener: arg2, 1367 + options: arg3, 1368 + }; 1369 + } else { 1370 + return { 1371 + target: resolveTarget(arg1), 1372 + event: asString(arg2), 1373 + listener: arg3, 1374 + options: arg4, 1375 + }; 1376 + } 1377 + } 1378 + 1379 + /** 1380 + * Adds an event listener to an element 1381 + * 1382 + * @see https://htmx.org/api/#on 1383 + * 1384 + * @param {EventTarget|string} arg1 the element to add the listener to | the event name to add the listener for 1385 + * @param {string|EventListener} arg2 the event name to add the listener for | the listener to add 1386 + * @param {EventListener|Object|boolean} [arg3] the listener to add | options to add 1387 + * @param {Object|boolean} [arg4] options to add 1388 + * @returns {EventListener} 1389 + */ 1390 + function addEventListenerImpl(arg1, arg2, arg3, arg4) { 1391 + ready(function () { 1392 + const eventArgs = processEventArgs(arg1, arg2, arg3, arg4); 1393 + eventArgs.target.addEventListener( 1394 + eventArgs.event, 1395 + eventArgs.listener, 1396 + eventArgs.options, 1397 + ); 1398 + }); 1399 + const b = isFunction(arg2); 1400 + return b ? arg2 : arg3; 1401 + } 1402 + 1403 + /** 1404 + * Removes an event listener from an element 1405 + * 1406 + * @see https://htmx.org/api/#off 1407 + * 1408 + * @param {EventTarget|string} arg1 the element to remove the listener from | the event name to remove the listener from 1409 + * @param {string|EventListener} arg2 the event name to remove the listener from | the listener to remove 1410 + * @param {EventListener} [arg3] the listener to remove 1411 + * @returns {EventListener} 1412 + */ 1413 + function removeEventListenerImpl(arg1, arg2, arg3) { 1414 + ready(function () { 1415 + const eventArgs = processEventArgs(arg1, arg2, arg3); 1416 + eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener); 1417 + }); 1418 + return isFunction(arg2) ? arg2 : arg3; 1419 + } 1420 + 1421 + //= =================================================================== 1422 + // Node processing 1423 + //= =================================================================== 1424 + 1425 + const DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors 1426 + /** 1427 + * @param {Element} elt 1428 + * @param {string} attrName 1429 + * @returns {(Node|Window)[]} 1430 + */ 1431 + function findAttributeTargets(elt, attrName) { 1432 + const attrTarget = getClosestAttributeValue(elt, attrName); 1433 + if (attrTarget) { 1434 + if (attrTarget === "this") { 1435 + return [findThisElement(elt, attrName)]; 1436 + } else { 1437 + const result = querySelectorAllExt(elt, attrTarget); 1438 + if (result.length === 0) { 1439 + logError( 1440 + 'The selector "' + 1441 + attrTarget + 1442 + '" on ' + 1443 + attrName + 1444 + " returned no matches!", 1445 + ); 1446 + return [DUMMY_ELT]; 1447 + } else { 1448 + return result; 1449 + } 1450 + } 1451 + } 1452 + } 1453 + 1454 + /** 1455 + * @param {Element} elt 1456 + * @param {string} attribute 1457 + * @returns {Element|null} 1458 + */ 1459 + function findThisElement(elt, attribute) { 1460 + return asElement( 1461 + getClosestMatch(elt, function (elt) { 1462 + return getAttributeValue(asElement(elt), attribute) != null; 1463 + }), 1464 + ); 1465 + } 1466 + 1467 + /** 1468 + * @param {Element} elt 1469 + * @returns {Node|Window|null} 1470 + */ 1471 + function getTarget(elt) { 1472 + const targetStr = getClosestAttributeValue(elt, "hx-target"); 1473 + if (targetStr) { 1474 + if (targetStr === "this") { 1475 + return findThisElement(elt, "hx-target"); 1476 + } else { 1477 + return querySelectorExt(elt, targetStr); 1478 + } 1479 + } else { 1480 + const data = getInternalData(elt); 1481 + if (data.boosted) { 1482 + return getDocument().body; 1483 + } else { 1484 + return elt; 1485 + } 1486 + } 1487 + } 1488 + 1489 + /** 1490 + * @param {string} name 1491 + * @returns {boolean} 1492 + */ 1493 + function shouldSettleAttribute(name) { 1494 + const attributesToSettle = htmx.config.attributesToSettle; 1495 + for (let i = 0; i < attributesToSettle.length; i++) { 1496 + if (name === attributesToSettle[i]) { 1497 + return true; 1498 + } 1499 + } 1500 + return false; 1501 + } 1502 + 1503 + /** 1504 + * @param {Element} mergeTo 1505 + * @param {Element} mergeFrom 1506 + */ 1507 + function cloneAttributes(mergeTo, mergeFrom) { 1508 + forEach(mergeTo.attributes, function (attr) { 1509 + if ( 1510 + !mergeFrom.hasAttribute(attr.name) && 1511 + shouldSettleAttribute(attr.name) 1512 + ) { 1513 + mergeTo.removeAttribute(attr.name); 1514 + } 1515 + }); 1516 + forEach(mergeFrom.attributes, function (attr) { 1517 + if (shouldSettleAttribute(attr.name)) { 1518 + mergeTo.setAttribute(attr.name, attr.value); 1519 + } 1520 + }); 1521 + } 1522 + 1523 + /** 1524 + * @param {HtmxSwapStyle} swapStyle 1525 + * @param {Element} target 1526 + * @returns {boolean} 1527 + */ 1528 + function isInlineSwap(swapStyle, target) { 1529 + const extensions = getExtensions(target); 1530 + for (let i = 0; i < extensions.length; i++) { 1531 + const extension = extensions[i]; 1532 + try { 1533 + if (extension.isInlineSwap(swapStyle)) { 1534 + return true; 1535 + } 1536 + } catch (e) { 1537 + logError(e); 1538 + } 1539 + } 1540 + return swapStyle === "outerHTML"; 1541 + } 1542 + 1543 + /** 1544 + * @param {string} oobValue 1545 + * @param {Element} oobElement 1546 + * @param {HtmxSettleInfo} settleInfo 1547 + * @param {Node|Document} [rootNode] 1548 + * @returns 1549 + */ 1550 + function oobSwap(oobValue, oobElement, settleInfo, rootNode) { 1551 + rootNode = rootNode || getDocument(); 1552 + let selector = "#" + getRawAttribute(oobElement, "id"); 1553 + /** @type HtmxSwapStyle */ 1554 + let swapStyle = "outerHTML"; 1555 + if (oobValue === "true") { 1556 + // do nothing 1557 + } else if (oobValue.indexOf(":") > 0) { 1558 + swapStyle = oobValue.substring(0, oobValue.indexOf(":")); 1559 + selector = oobValue.substring(oobValue.indexOf(":") + 1); 1560 + } else { 1561 + swapStyle = oobValue; 1562 + } 1563 + oobElement.removeAttribute("hx-swap-oob"); 1564 + oobElement.removeAttribute("data-hx-swap-oob"); 1565 + 1566 + const targets = querySelectorAllExt(rootNode, selector, false); 1567 + if (targets) { 1568 + forEach(targets, function (target) { 1569 + let fragment; 1570 + const oobElementClone = oobElement.cloneNode(true); 1571 + fragment = getDocument().createDocumentFragment(); 1572 + fragment.appendChild(oobElementClone); 1573 + if (!isInlineSwap(swapStyle, target)) { 1574 + fragment = asParentNode(oobElementClone); // if this is not an inline swap, we use the content of the node, not the node itself 1575 + } 1576 + 1577 + const beforeSwapDetails = { shouldSwap: true, target, fragment }; 1578 + if (!triggerEvent(target, "htmx:oobBeforeSwap", beforeSwapDetails)) 1579 + return; 1580 + 1581 + target = beforeSwapDetails.target; // allow re-targeting 1582 + if (beforeSwapDetails.shouldSwap) { 1583 + handlePreservedElements(fragment); 1584 + swapWithStyle(swapStyle, target, target, fragment, settleInfo); 1585 + restorePreservedElements(); 1586 + } 1587 + forEach(settleInfo.elts, function (elt) { 1588 + triggerEvent(elt, "htmx:oobAfterSwap", beforeSwapDetails); 1589 + }); 1590 + }); 1591 + oobElement.parentNode.removeChild(oobElement); 1592 + } else { 1593 + oobElement.parentNode.removeChild(oobElement); 1594 + triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", { 1595 + content: oobElement, 1596 + }); 1597 + } 1598 + return oobValue; 1599 + } 1600 + 1601 + function restorePreservedElements() { 1602 + const pantry = find("#--htmx-preserve-pantry--"); 1603 + if (pantry) { 1604 + for (const preservedElt of [...pantry.children]) { 1605 + const existingElement = find("#" + preservedElt.id); 1606 + // @ts-ignore - use proposed moveBefore feature 1607 + existingElement.parentNode.moveBefore(preservedElt, existingElement); 1608 + existingElement.remove(); 1609 + } 1610 + pantry.remove(); 1611 + } 1612 + } 1613 + 1614 + /** 1615 + * @param {DocumentFragment|ParentNode} fragment 1616 + */ 1617 + function handlePreservedElements(fragment) { 1618 + forEach( 1619 + findAll(fragment, "[hx-preserve], [data-hx-preserve]"), 1620 + function (preservedElt) { 1621 + const id = getAttributeValue(preservedElt, "id"); 1622 + const existingElement = getDocument().getElementById(id); 1623 + if (existingElement != null) { 1624 + if (preservedElt.moveBefore) { 1625 + // if the moveBefore API exists, use it 1626 + // get or create a storage spot for stuff 1627 + let pantry = find("#--htmx-preserve-pantry--"); 1628 + if (pantry == null) { 1629 + getDocument().body.insertAdjacentHTML( 1630 + "afterend", 1631 + "<div id='--htmx-preserve-pantry--'></div>", 1632 + ); 1633 + pantry = find("#--htmx-preserve-pantry--"); 1634 + } 1635 + // @ts-ignore - use proposed moveBefore feature 1636 + pantry.moveBefore(existingElement, null); 1637 + } else { 1638 + preservedElt.parentNode.replaceChild(existingElement, preservedElt); 1639 + } 1640 + } 1641 + }, 1642 + ); 1643 + } 1644 + 1645 + /** 1646 + * @param {Node} parentNode 1647 + * @param {ParentNode} fragment 1648 + * @param {HtmxSettleInfo} settleInfo 1649 + */ 1650 + function handleAttributes(parentNode, fragment, settleInfo) { 1651 + forEach(fragment.querySelectorAll("[id]"), function (newNode) { 1652 + const id = getRawAttribute(newNode, "id"); 1653 + if (id && id.length > 0) { 1654 + const normalizedId = id.replace("'", "\\'"); 1655 + const normalizedTag = newNode.tagName.replace(":", "\\:"); 1656 + const parentElt = asParentNode(parentNode); 1657 + const oldNode = 1658 + parentElt && 1659 + parentElt.querySelector( 1660 + normalizedTag + "[id='" + normalizedId + "']", 1661 + ); 1662 + if (oldNode && oldNode !== parentElt) { 1663 + const newAttributes = newNode.cloneNode(); 1664 + cloneAttributes(newNode, oldNode); 1665 + settleInfo.tasks.push(function () { 1666 + cloneAttributes(newNode, newAttributes); 1667 + }); 1668 + } 1669 + } 1670 + }); 1671 + } 1672 + 1673 + /** 1674 + * @param {Node} child 1675 + * @returns {HtmxSettleTask} 1676 + */ 1677 + function makeAjaxLoadTask(child) { 1678 + return function () { 1679 + removeClassFromElement(child, htmx.config.addedClass); 1680 + processNode(asElement(child)); 1681 + processFocus(asParentNode(child)); 1682 + triggerEvent(child, "htmx:load"); 1683 + }; 1684 + } 1685 + 1686 + /** 1687 + * @param {ParentNode} child 1688 + */ 1689 + function processFocus(child) { 1690 + const autofocus = "[autofocus]"; 1691 + const autoFocusedElt = asHtmlElement( 1692 + matches(child, autofocus) ? child : child.querySelector(autofocus), 1693 + ); 1694 + if (autoFocusedElt != null) { 1695 + autoFocusedElt.focus(); 1696 + } 1697 + } 1698 + 1699 + /** 1700 + * @param {Node} parentNode 1701 + * @param {Node} insertBefore 1702 + * @param {ParentNode} fragment 1703 + * @param {HtmxSettleInfo} settleInfo 1704 + */ 1705 + function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) { 1706 + handleAttributes(parentNode, fragment, settleInfo); 1707 + while (fragment.childNodes.length > 0) { 1708 + const child = fragment.firstChild; 1709 + addClassToElement(asElement(child), htmx.config.addedClass); 1710 + parentNode.insertBefore(child, insertBefore); 1711 + if ( 1712 + child.nodeType !== Node.TEXT_NODE && 1713 + child.nodeType !== Node.COMMENT_NODE 1714 + ) { 1715 + settleInfo.tasks.push(makeAjaxLoadTask(child)); 1716 + } 1717 + } 1718 + } 1719 + 1720 + /** 1721 + * based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0, 1722 + * derived from Java's string hashcode implementation 1723 + * @param {string} string 1724 + * @param {number} hash 1725 + * @returns {number} 1726 + */ 1727 + function stringHash(string, hash) { 1728 + let char = 0; 1729 + while (char < string.length) { 1730 + hash = ((hash << 5) - hash + string.charCodeAt(char++)) | 0; // bitwise or ensures we have a 32-bit int 1731 + } 1732 + return hash; 1733 + } 1734 + 1735 + /** 1736 + * @param {Element} elt 1737 + * @returns {number} 1738 + */ 1739 + function attributeHash(elt) { 1740 + let hash = 0; 1741 + // IE fix 1742 + if (elt.attributes) { 1743 + for (let i = 0; i < elt.attributes.length; i++) { 1744 + const attribute = elt.attributes[i]; 1745 + if (attribute.value) { 1746 + // only include attributes w/ actual values (empty is same as non-existent) 1747 + hash = stringHash(attribute.name, hash); 1748 + hash = stringHash(attribute.value, hash); 1749 + } 1750 + } 1751 + } 1752 + return hash; 1753 + } 1754 + 1755 + /** 1756 + * @param {EventTarget} elt 1757 + */ 1758 + function deInitOnHandlers(elt) { 1759 + const internalData = getInternalData(elt); 1760 + if (internalData.onHandlers) { 1761 + for (let i = 0; i < internalData.onHandlers.length; i++) { 1762 + const handlerInfo = internalData.onHandlers[i]; 1763 + removeEventListenerImpl(elt, handlerInfo.event, handlerInfo.listener); 1764 + } 1765 + delete internalData.onHandlers; 1766 + } 1767 + } 1768 + 1769 + /** 1770 + * @param {Node} element 1771 + */ 1772 + function deInitNode(element) { 1773 + const internalData = getInternalData(element); 1774 + if (internalData.timeout) { 1775 + clearTimeout(internalData.timeout); 1776 + } 1777 + if (internalData.listenerInfos) { 1778 + forEach(internalData.listenerInfos, function (info) { 1779 + if (info.on) { 1780 + removeEventListenerImpl(info.on, info.trigger, info.listener); 1781 + } 1782 + }); 1783 + } 1784 + deInitOnHandlers(element); 1785 + forEach(Object.keys(internalData), function (key) { 1786 + if (key !== "firstInitCompleted") delete internalData[key]; 1787 + }); 1788 + } 1789 + 1790 + /** 1791 + * @param {Node} element 1792 + */ 1793 + function cleanUpElement(element) { 1794 + triggerEvent(element, "htmx:beforeCleanupElement"); 1795 + deInitNode(element); 1796 + // @ts-ignore IE11 code 1797 + // noinspection JSUnresolvedReference 1798 + if (element.children) { 1799 + // IE 1800 + // @ts-ignore 1801 + forEach(element.children, function (child) { 1802 + cleanUpElement(child); 1803 + }); 1804 + } 1805 + } 1806 + 1807 + /** 1808 + * @param {Node} target 1809 + * @param {ParentNode} fragment 1810 + * @param {HtmxSettleInfo} settleInfo 1811 + */ 1812 + function swapOuterHTML(target, fragment, settleInfo) { 1813 + if (target instanceof Element && target.tagName === "BODY") { 1814 + // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately 1815 + return swapInnerHTML(target, fragment, settleInfo); 1816 + } 1817 + /** @type {Node} */ 1818 + let newElt; 1819 + const eltBeforeNewContent = target.previousSibling; 1820 + const parentNode = parentElt(target); 1821 + if (!parentNode) { 1822 + // when parent node disappears, we can't do anything 1823 + return; 1824 + } 1825 + insertNodesBefore(parentNode, target, fragment, settleInfo); 1826 + if (eltBeforeNewContent == null) { 1827 + newElt = parentNode.firstChild; 1828 + } else { 1829 + newElt = eltBeforeNewContent.nextSibling; 1830 + } 1831 + settleInfo.elts = settleInfo.elts.filter(function (e) { 1832 + return e !== target; 1833 + }); 1834 + // scan through all newly added content and add all elements to the settle info so we trigger 1835 + // events properly on them 1836 + while (newElt && newElt !== target) { 1837 + if (newElt instanceof Element) { 1838 + settleInfo.elts.push(newElt); 1839 + } 1840 + newElt = newElt.nextSibling; 1841 + } 1842 + cleanUpElement(target); 1843 + if (target instanceof Element) { 1844 + target.remove(); 1845 + } else { 1846 + target.parentNode.removeChild(target); 1847 + } 1848 + } 1849 + 1850 + /** 1851 + * @param {Node} target 1852 + * @param {ParentNode} fragment 1853 + * @param {HtmxSettleInfo} settleInfo 1854 + */ 1855 + function swapAfterBegin(target, fragment, settleInfo) { 1856 + return insertNodesBefore(target, target.firstChild, fragment, settleInfo); 1857 + } 1858 + 1859 + /** 1860 + * @param {Node} target 1861 + * @param {ParentNode} fragment 1862 + * @param {HtmxSettleInfo} settleInfo 1863 + */ 1864 + function swapBeforeBegin(target, fragment, settleInfo) { 1865 + return insertNodesBefore(parentElt(target), target, fragment, settleInfo); 1866 + } 1867 + 1868 + /** 1869 + * @param {Node} target 1870 + * @param {ParentNode} fragment 1871 + * @param {HtmxSettleInfo} settleInfo 1872 + */ 1873 + function swapBeforeEnd(target, fragment, settleInfo) { 1874 + return insertNodesBefore(target, null, fragment, settleInfo); 1875 + } 1876 + 1877 + /** 1878 + * @param {Node} target 1879 + * @param {ParentNode} fragment 1880 + * @param {HtmxSettleInfo} settleInfo 1881 + */ 1882 + function swapAfterEnd(target, fragment, settleInfo) { 1883 + return insertNodesBefore( 1884 + parentElt(target), 1885 + target.nextSibling, 1886 + fragment, 1887 + settleInfo, 1888 + ); 1889 + } 1890 + 1891 + /** 1892 + * @param {Node} target 1893 + */ 1894 + function swapDelete(target) { 1895 + cleanUpElement(target); 1896 + const parent = parentElt(target); 1897 + if (parent) { 1898 + return parent.removeChild(target); 1899 + } 1900 + } 1901 + 1902 + /** 1903 + * @param {Node} target 1904 + * @param {ParentNode} fragment 1905 + * @param {HtmxSettleInfo} settleInfo 1906 + */ 1907 + function swapInnerHTML(target, fragment, settleInfo) { 1908 + const firstChild = target.firstChild; 1909 + insertNodesBefore(target, firstChild, fragment, settleInfo); 1910 + if (firstChild) { 1911 + while (firstChild.nextSibling) { 1912 + cleanUpElement(firstChild.nextSibling); 1913 + target.removeChild(firstChild.nextSibling); 1914 + } 1915 + cleanUpElement(firstChild); 1916 + target.removeChild(firstChild); 1917 + } 1918 + } 1919 + 1920 + /** 1921 + * @param {HtmxSwapStyle} swapStyle 1922 + * @param {Element} elt 1923 + * @param {Node} target 1924 + * @param {ParentNode} fragment 1925 + * @param {HtmxSettleInfo} settleInfo 1926 + */ 1927 + function swapWithStyle(swapStyle, elt, target, fragment, settleInfo) { 1928 + switch (swapStyle) { 1929 + case "none": 1930 + return; 1931 + case "outerHTML": 1932 + swapOuterHTML(target, fragment, settleInfo); 1933 + return; 1934 + case "afterbegin": 1935 + swapAfterBegin(target, fragment, settleInfo); 1936 + return; 1937 + case "beforebegin": 1938 + swapBeforeBegin(target, fragment, settleInfo); 1939 + return; 1940 + case "beforeend": 1941 + swapBeforeEnd(target, fragment, settleInfo); 1942 + return; 1943 + case "afterend": 1944 + swapAfterEnd(target, fragment, settleInfo); 1945 + return; 1946 + case "delete": 1947 + swapDelete(target); 1948 + return; 1949 + default: 1950 + var extensions = getExtensions(elt); 1951 + for (let i = 0; i < extensions.length; i++) { 1952 + const ext = extensions[i]; 1953 + try { 1954 + const newElements = ext.handleSwap( 1955 + swapStyle, 1956 + target, 1957 + fragment, 1958 + settleInfo, 1959 + ); 1960 + if (newElements) { 1961 + if (Array.isArray(newElements)) { 1962 + // if handleSwap returns an array (like) of elements, we handle them 1963 + for (let j = 0; j < newElements.length; j++) { 1964 + const child = newElements[j]; 1965 + if ( 1966 + child.nodeType !== Node.TEXT_NODE && 1967 + child.nodeType !== Node.COMMENT_NODE 1968 + ) { 1969 + settleInfo.tasks.push(makeAjaxLoadTask(child)); 1970 + } 1971 + } 1972 + } 1973 + return; 1974 + } 1975 + } catch (e) { 1976 + logError(e); 1977 + } 1978 + } 1979 + if (swapStyle === "innerHTML") { 1980 + swapInnerHTML(target, fragment, settleInfo); 1981 + } else { 1982 + swapWithStyle( 1983 + htmx.config.defaultSwapStyle, 1984 + elt, 1985 + target, 1986 + fragment, 1987 + settleInfo, 1988 + ); 1989 + } 1990 + } 1991 + } 1992 + 1993 + /** 1994 + * @param {DocumentFragment} fragment 1995 + * @param {HtmxSettleInfo} settleInfo 1996 + * @param {Node|Document} [rootNode] 1997 + */ 1998 + function findAndSwapOobElements(fragment, settleInfo, rootNode) { 1999 + var oobElts = findAll(fragment, "[hx-swap-oob], [data-hx-swap-oob]"); 2000 + forEach(oobElts, function (oobElement) { 2001 + if ( 2002 + htmx.config.allowNestedOobSwaps || 2003 + oobElement.parentElement === null 2004 + ) { 2005 + const oobValue = getAttributeValue(oobElement, "hx-swap-oob"); 2006 + if (oobValue != null) { 2007 + oobSwap(oobValue, oobElement, settleInfo, rootNode); 2008 + } 2009 + } else { 2010 + oobElement.removeAttribute("hx-swap-oob"); 2011 + oobElement.removeAttribute("data-hx-swap-oob"); 2012 + } 2013 + }); 2014 + return oobElts.length > 0; 2015 + } 2016 + 2017 + /** 2018 + * Implements complete swapping pipeline, including: focus and selection preservation, 2019 + * title updates, scroll, OOB swapping, normal swapping and settling 2020 + * @param {string|Element} target 2021 + * @param {string} content 2022 + * @param {HtmxSwapSpecification} swapSpec 2023 + * @param {SwapOptions} [swapOptions] 2024 + */ 2025 + function swap(target, content, swapSpec, swapOptions) { 2026 + if (!swapOptions) { 2027 + swapOptions = {}; 2028 + } 2029 + 2030 + target = resolveTarget(target); 2031 + const rootNode = swapOptions.contextElement 2032 + ? getRootNode(swapOptions.contextElement, false) 2033 + : getDocument(); 2034 + 2035 + // preserve focus and selection 2036 + const activeElt = document.activeElement; 2037 + let selectionInfo = {}; 2038 + try { 2039 + selectionInfo = { 2040 + elt: activeElt, 2041 + // @ts-ignore 2042 + start: activeElt ? activeElt.selectionStart : null, 2043 + // @ts-ignore 2044 + end: activeElt ? activeElt.selectionEnd : null, 2045 + }; 2046 + } catch (e) { 2047 + // safari issue - see https://github.com/microsoft/playwright/issues/5894 2048 + } 2049 + const settleInfo = makeSettleInfo(target); 2050 + 2051 + // For text content swaps, don't parse the response as HTML, just insert it 2052 + if (swapSpec.swapStyle === "textContent") { 2053 + target.textContent = content; 2054 + // Otherwise, make the fragment and process it 2055 + } else { 2056 + let fragment = makeFragment(content); 2057 + 2058 + settleInfo.title = fragment.title; 2059 + 2060 + // select-oob swaps 2061 + if (swapOptions.selectOOB) { 2062 + const oobSelectValues = swapOptions.selectOOB.split(","); 2063 + for (let i = 0; i < oobSelectValues.length; i++) { 2064 + const oobSelectValue = oobSelectValues[i].split(":", 2); 2065 + let id = oobSelectValue[0].trim(); 2066 + if (id.indexOf("#") === 0) { 2067 + id = id.substring(1); 2068 + } 2069 + const oobValue = oobSelectValue[1] || "true"; 2070 + const oobElement = fragment.querySelector("#" + id); 2071 + if (oobElement) { 2072 + oobSwap(oobValue, oobElement, settleInfo, rootNode); 2073 + } 2074 + } 2075 + } 2076 + // oob swaps 2077 + findAndSwapOobElements(fragment, settleInfo, rootNode); 2078 + forEach( 2079 + findAll(fragment, "template"), 2080 + /** @param {HTMLTemplateElement} template */ function (template) { 2081 + if ( 2082 + template.content && 2083 + findAndSwapOobElements(template.content, settleInfo, rootNode) 2084 + ) { 2085 + // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap 2086 + template.remove(); 2087 + } 2088 + }, 2089 + ); 2090 + 2091 + // normal swap 2092 + if (swapOptions.select) { 2093 + const newFragment = getDocument().createDocumentFragment(); 2094 + forEach(fragment.querySelectorAll(swapOptions.select), function (node) { 2095 + newFragment.appendChild(node); 2096 + }); 2097 + fragment = newFragment; 2098 + } 2099 + handlePreservedElements(fragment); 2100 + swapWithStyle( 2101 + swapSpec.swapStyle, 2102 + swapOptions.contextElement, 2103 + target, 2104 + fragment, 2105 + settleInfo, 2106 + ); 2107 + restorePreservedElements(); 2108 + } 2109 + 2110 + // apply saved focus and selection information to swapped content 2111 + if ( 2112 + selectionInfo.elt && 2113 + !bodyContains(selectionInfo.elt) && 2114 + getRawAttribute(selectionInfo.elt, "id") 2115 + ) { 2116 + const newActiveElt = document.getElementById( 2117 + getRawAttribute(selectionInfo.elt, "id"), 2118 + ); 2119 + const focusOptions = { 2120 + preventScroll: 2121 + swapSpec.focusScroll !== undefined 2122 + ? !swapSpec.focusScroll 2123 + : !htmx.config.defaultFocusScroll, 2124 + }; 2125 + if (newActiveElt) { 2126 + // @ts-ignore 2127 + if (selectionInfo.start && newActiveElt.setSelectionRange) { 2128 + try { 2129 + // @ts-ignore 2130 + newActiveElt.setSelectionRange( 2131 + selectionInfo.start, 2132 + selectionInfo.end, 2133 + ); 2134 + } catch (e) { 2135 + // the setSelectionRange method is present on fields that don't support it, so just let this fail 2136 + } 2137 + } 2138 + newActiveElt.focus(focusOptions); 2139 + } 2140 + } 2141 + 2142 + target.classList.remove(htmx.config.swappingClass); 2143 + forEach(settleInfo.elts, function (elt) { 2144 + if (elt.classList) { 2145 + elt.classList.add(htmx.config.settlingClass); 2146 + } 2147 + triggerEvent(elt, "htmx:afterSwap", swapOptions.eventInfo); 2148 + }); 2149 + if (swapOptions.afterSwapCallback) { 2150 + swapOptions.afterSwapCallback(); 2151 + } 2152 + 2153 + // merge in new title after swap but before settle 2154 + if (!swapSpec.ignoreTitle) { 2155 + handleTitle(settleInfo.title); 2156 + } 2157 + 2158 + // settle 2159 + const doSettle = function () { 2160 + forEach(settleInfo.tasks, function (task) { 2161 + task.call(); 2162 + }); 2163 + forEach(settleInfo.elts, function (elt) { 2164 + if (elt.classList) { 2165 + elt.classList.remove(htmx.config.settlingClass); 2166 + } 2167 + triggerEvent(elt, "htmx:afterSettle", swapOptions.eventInfo); 2168 + }); 2169 + 2170 + if (swapOptions.anchor) { 2171 + const anchorTarget = asElement(resolveTarget("#" + swapOptions.anchor)); 2172 + if (anchorTarget) { 2173 + anchorTarget.scrollIntoView({ block: "start", behavior: "auto" }); 2174 + } 2175 + } 2176 + 2177 + updateScrollState(settleInfo.elts, swapSpec); 2178 + if (swapOptions.afterSettleCallback) { 2179 + swapOptions.afterSettleCallback(); 2180 + } 2181 + }; 2182 + 2183 + if (swapSpec.settleDelay > 0) { 2184 + getWindow().setTimeout(doSettle, swapSpec.settleDelay); 2185 + } else { 2186 + doSettle(); 2187 + } 2188 + } 2189 + 2190 + /** 2191 + * @param {XMLHttpRequest} xhr 2192 + * @param {string} header 2193 + * @param {EventTarget} elt 2194 + */ 2195 + function handleTriggerHeader(xhr, header, elt) { 2196 + const triggerBody = xhr.getResponseHeader(header); 2197 + if (triggerBody.indexOf("{") === 0) { 2198 + const triggers = parseJSON(triggerBody); 2199 + for (const eventName in triggers) { 2200 + if (triggers.hasOwnProperty(eventName)) { 2201 + let detail = triggers[eventName]; 2202 + if (isRawObject(detail)) { 2203 + // @ts-ignore 2204 + elt = detail.target !== undefined ? detail.target : elt; 2205 + } else { 2206 + detail = { value: detail }; 2207 + } 2208 + triggerEvent(elt, eventName, detail); 2209 + } 2210 + } 2211 + } else { 2212 + const eventNames = triggerBody.split(","); 2213 + for (let i = 0; i < eventNames.length; i++) { 2214 + triggerEvent(elt, eventNames[i].trim(), []); 2215 + } 2216 + } 2217 + } 2218 + 2219 + const WHITESPACE = /\s/; 2220 + const WHITESPACE_OR_COMMA = /[\s,]/; 2221 + const SYMBOL_START = /[_$a-zA-Z]/; 2222 + const SYMBOL_CONT = /[_$a-zA-Z0-9]/; 2223 + const STRINGISH_START = ['"', "'", "/"]; 2224 + const NOT_WHITESPACE = /[^\s]/; 2225 + const COMBINED_SELECTOR_START = /[{(]/; 2226 + const COMBINED_SELECTOR_END = /[})]/; 2227 + 2228 + /** 2229 + * @param {string} str 2230 + * @returns {string[]} 2231 + */ 2232 + function tokenizeString(str) { 2233 + /** @type string[] */ 2234 + const tokens = []; 2235 + let position = 0; 2236 + while (position < str.length) { 2237 + if (SYMBOL_START.exec(str.charAt(position))) { 2238 + var startPosition = position; 2239 + while (SYMBOL_CONT.exec(str.charAt(position + 1))) { 2240 + position++; 2241 + } 2242 + tokens.push(str.substring(startPosition, position + 1)); 2243 + } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) { 2244 + const startChar = str.charAt(position); 2245 + var startPosition = position; 2246 + position++; 2247 + while (position < str.length && str.charAt(position) !== startChar) { 2248 + if (str.charAt(position) === "\\") { 2249 + position++; 2250 + } 2251 + position++; 2252 + } 2253 + tokens.push(str.substring(startPosition, position + 1)); 2254 + } else { 2255 + const symbol = str.charAt(position); 2256 + tokens.push(symbol); 2257 + } 2258 + position++; 2259 + } 2260 + return tokens; 2261 + } 2262 + 2263 + /** 2264 + * @param {string} token 2265 + * @param {string|null} last 2266 + * @param {string} paramName 2267 + * @returns {boolean} 2268 + */ 2269 + function isPossibleRelativeReference(token, last, paramName) { 2270 + return ( 2271 + SYMBOL_START.exec(token.charAt(0)) && 2272 + token !== "true" && 2273 + token !== "false" && 2274 + token !== "this" && 2275 + token !== paramName && 2276 + last !== "." 2277 + ); 2278 + } 2279 + 2280 + /** 2281 + * @param {EventTarget|string} elt 2282 + * @param {string[]} tokens 2283 + * @param {string} paramName 2284 + * @returns {ConditionalFunction|null} 2285 + */ 2286 + function maybeGenerateConditional(elt, tokens, paramName) { 2287 + if (tokens[0] === "[") { 2288 + tokens.shift(); 2289 + let bracketCount = 1; 2290 + let conditionalSource = " return (function(" + paramName + "){ return ("; 2291 + let last = null; 2292 + while (tokens.length > 0) { 2293 + const token = tokens[0]; 2294 + // @ts-ignore For some reason tsc doesn't understand the shift call, and thinks we're comparing the same value here, i.e. '[' vs ']' 2295 + if (token === "]") { 2296 + bracketCount--; 2297 + if (bracketCount === 0) { 2298 + if (last === null) { 2299 + conditionalSource = conditionalSource + "true"; 2300 + } 2301 + tokens.shift(); 2302 + conditionalSource += ")})"; 2303 + try { 2304 + const conditionFunction = maybeEval( 2305 + elt, 2306 + function () { 2307 + return Function(conditionalSource)(); 2308 + }, 2309 + function () { 2310 + return true; 2311 + }, 2312 + ); 2313 + conditionFunction.source = conditionalSource; 2314 + return conditionFunction; 2315 + } catch (e) { 2316 + triggerErrorEvent(getDocument().body, "htmx:syntax:error", { 2317 + error: e, 2318 + source: conditionalSource, 2319 + }); 2320 + return null; 2321 + } 2322 + } 2323 + } else if (token === "[") { 2324 + bracketCount++; 2325 + } 2326 + if (isPossibleRelativeReference(token, last, paramName)) { 2327 + conditionalSource += 2328 + "((" + 2329 + paramName + 2330 + "." + 2331 + token + 2332 + ") ? (" + 2333 + paramName + 2334 + "." + 2335 + token + 2336 + ") : (window." + 2337 + token + 2338 + "))"; 2339 + } else { 2340 + conditionalSource = conditionalSource + token; 2341 + } 2342 + last = tokens.shift(); 2343 + } 2344 + } 2345 + } 2346 + 2347 + /** 2348 + * @param {string[]} tokens 2349 + * @param {RegExp} match 2350 + * @returns {string} 2351 + */ 2352 + function consumeUntil(tokens, match) { 2353 + let result = ""; 2354 + while (tokens.length > 0 && !match.test(tokens[0])) { 2355 + result += tokens.shift(); 2356 + } 2357 + return result; 2358 + } 2359 + 2360 + /** 2361 + * @param {string[]} tokens 2362 + * @returns {string} 2363 + */ 2364 + function consumeCSSSelector(tokens) { 2365 + let result; 2366 + if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) { 2367 + tokens.shift(); 2368 + result = consumeUntil(tokens, COMBINED_SELECTOR_END).trim(); 2369 + tokens.shift(); 2370 + } else { 2371 + result = consumeUntil(tokens, WHITESPACE_OR_COMMA); 2372 + } 2373 + return result; 2374 + } 2375 + 2376 + const INPUT_SELECTOR = "input, textarea, select"; 2377 + 2378 + /** 2379 + * @param {Element} elt 2380 + * @param {string} explicitTrigger 2381 + * @param {Object} cache for trigger specs 2382 + * @returns {HtmxTriggerSpecification[]} 2383 + */ 2384 + function parseAndCacheTrigger(elt, explicitTrigger, cache) { 2385 + /** @type HtmxTriggerSpecification[] */ 2386 + const triggerSpecs = []; 2387 + const tokens = tokenizeString(explicitTrigger); 2388 + do { 2389 + consumeUntil(tokens, NOT_WHITESPACE); 2390 + const initialLength = tokens.length; 2391 + const trigger = consumeUntil(tokens, /[,\[\s]/); 2392 + if (trigger !== "") { 2393 + if (trigger === "every") { 2394 + /** @type HtmxTriggerSpecification */ 2395 + const every = { trigger: "every" }; 2396 + consumeUntil(tokens, NOT_WHITESPACE); 2397 + every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/)); 2398 + consumeUntil(tokens, NOT_WHITESPACE); 2399 + var eventFilter = maybeGenerateConditional(elt, tokens, "event"); 2400 + if (eventFilter) { 2401 + every.eventFilter = eventFilter; 2402 + } 2403 + triggerSpecs.push(every); 2404 + } else { 2405 + /** @type HtmxTriggerSpecification */ 2406 + const triggerSpec = { trigger }; 2407 + var eventFilter = maybeGenerateConditional(elt, tokens, "event"); 2408 + if (eventFilter) { 2409 + triggerSpec.eventFilter = eventFilter; 2410 + } 2411 + consumeUntil(tokens, NOT_WHITESPACE); 2412 + while (tokens.length > 0 && tokens[0] !== ",") { 2413 + const token = tokens.shift(); 2414 + if (token === "changed") { 2415 + triggerSpec.changed = true; 2416 + } else if (token === "once") { 2417 + triggerSpec.once = true; 2418 + } else if (token === "consume") { 2419 + triggerSpec.consume = true; 2420 + } else if (token === "delay" && tokens[0] === ":") { 2421 + tokens.shift(); 2422 + triggerSpec.delay = parseInterval( 2423 + consumeUntil(tokens, WHITESPACE_OR_COMMA), 2424 + ); 2425 + } else if (token === "from" && tokens[0] === ":") { 2426 + tokens.shift(); 2427 + if (COMBINED_SELECTOR_START.test(tokens[0])) { 2428 + var from_arg = consumeCSSSelector(tokens); 2429 + } else { 2430 + var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); 2431 + if ( 2432 + from_arg === "closest" || 2433 + from_arg === "find" || 2434 + from_arg === "next" || 2435 + from_arg === "previous" 2436 + ) { 2437 + tokens.shift(); 2438 + const selector = consumeCSSSelector(tokens); 2439 + // `next` and `previous` allow a selector-less syntax 2440 + if (selector.length > 0) { 2441 + from_arg += " " + selector; 2442 + } 2443 + } 2444 + } 2445 + triggerSpec.from = from_arg; 2446 + } else if (token === "target" && tokens[0] === ":") { 2447 + tokens.shift(); 2448 + triggerSpec.target = consumeCSSSelector(tokens); 2449 + } else if (token === "throttle" && tokens[0] === ":") { 2450 + tokens.shift(); 2451 + triggerSpec.throttle = parseInterval( 2452 + consumeUntil(tokens, WHITESPACE_OR_COMMA), 2453 + ); 2454 + } else if (token === "queue" && tokens[0] === ":") { 2455 + tokens.shift(); 2456 + triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA); 2457 + } else if (token === "root" && tokens[0] === ":") { 2458 + tokens.shift(); 2459 + triggerSpec[token] = consumeCSSSelector(tokens); 2460 + } else if (token === "threshold" && tokens[0] === ":") { 2461 + tokens.shift(); 2462 + triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA); 2463 + } else { 2464 + triggerErrorEvent(elt, "htmx:syntax:error", { 2465 + token: tokens.shift(), 2466 + }); 2467 + } 2468 + consumeUntil(tokens, NOT_WHITESPACE); 2469 + } 2470 + triggerSpecs.push(triggerSpec); 2471 + } 2472 + } 2473 + if (tokens.length === initialLength) { 2474 + triggerErrorEvent(elt, "htmx:syntax:error", { token: tokens.shift() }); 2475 + } 2476 + consumeUntil(tokens, NOT_WHITESPACE); 2477 + } while (tokens[0] === "," && tokens.shift()); 2478 + if (cache) { 2479 + cache[explicitTrigger] = triggerSpecs; 2480 + } 2481 + return triggerSpecs; 2482 + } 2483 + 2484 + /** 2485 + * @param {Element} elt 2486 + * @returns {HtmxTriggerSpecification[]} 2487 + */ 2488 + function getTriggerSpecs(elt) { 2489 + const explicitTrigger = getAttributeValue(elt, "hx-trigger"); 2490 + let triggerSpecs = []; 2491 + if (explicitTrigger) { 2492 + const cache = htmx.config.triggerSpecsCache; 2493 + triggerSpecs = 2494 + (cache && cache[explicitTrigger]) || 2495 + parseAndCacheTrigger(elt, explicitTrigger, cache); 2496 + } 2497 + 2498 + if (triggerSpecs.length > 0) { 2499 + return triggerSpecs; 2500 + } else if (matches(elt, "form")) { 2501 + return [{ trigger: "submit" }]; 2502 + } else if (matches(elt, 'input[type="button"], input[type="submit"]')) { 2503 + return [{ trigger: "click" }]; 2504 + } else if (matches(elt, INPUT_SELECTOR)) { 2505 + return [{ trigger: "change" }]; 2506 + } else { 2507 + return [{ trigger: "click" }]; 2508 + } 2509 + } 2510 + 2511 + /** 2512 + * @param {Element} elt 2513 + */ 2514 + function cancelPolling(elt) { 2515 + getInternalData(elt).cancelled = true; 2516 + } 2517 + 2518 + /** 2519 + * @param {Element} elt 2520 + * @param {TriggerHandler} handler 2521 + * @param {HtmxTriggerSpecification} spec 2522 + */ 2523 + function processPolling(elt, handler, spec) { 2524 + const nodeData = getInternalData(elt); 2525 + nodeData.timeout = getWindow().setTimeout(function () { 2526 + if (bodyContains(elt) && nodeData.cancelled !== true) { 2527 + if ( 2528 + !maybeFilterEvent( 2529 + spec, 2530 + elt, 2531 + makeEvent("hx:poll:trigger", { 2532 + triggerSpec: spec, 2533 + target: elt, 2534 + }), 2535 + ) 2536 + ) { 2537 + handler(elt); 2538 + } 2539 + processPolling(elt, handler, spec); 2540 + } 2541 + }, spec.pollInterval); 2542 + } 2543 + 2544 + /** 2545 + * @param {HTMLAnchorElement} elt 2546 + * @returns {boolean} 2547 + */ 2548 + function isLocalLink(elt) { 2549 + return ( 2550 + location.hostname === elt.hostname && 2551 + getRawAttribute(elt, "href") && 2552 + getRawAttribute(elt, "href").indexOf("#") !== 0 2553 + ); 2554 + } 2555 + 2556 + /** 2557 + * @param {Element} elt 2558 + */ 2559 + function eltIsDisabled(elt) { 2560 + return closest(elt, htmx.config.disableSelector); 2561 + } 2562 + 2563 + /** 2564 + * @param {Element} elt 2565 + * @param {HtmxNodeInternalData} nodeData 2566 + * @param {HtmxTriggerSpecification[]} triggerSpecs 2567 + */ 2568 + function boostElement(elt, nodeData, triggerSpecs) { 2569 + if ( 2570 + (elt instanceof HTMLAnchorElement && 2571 + isLocalLink(elt) && 2572 + (elt.target === "" || elt.target === "_self")) || 2573 + (elt.tagName === "FORM" && 2574 + String(getRawAttribute(elt, "method")).toLowerCase() !== "dialog") 2575 + ) { 2576 + nodeData.boosted = true; 2577 + let verb, path; 2578 + if (elt.tagName === "A") { 2579 + verb = /** @type HttpVerb */ ("get"); 2580 + path = getRawAttribute(elt, "href"); 2581 + } else { 2582 + const rawAttribute = getRawAttribute(elt, "method"); 2583 + verb = /** @type HttpVerb */ ( 2584 + rawAttribute ? rawAttribute.toLowerCase() : "get" 2585 + ); 2586 + path = getRawAttribute(elt, "action"); 2587 + if (path == null || path === "") { 2588 + // if there is no action attribute on the form set path to current href before the 2589 + // following logic to properly clear parameters on a GET (not on a POST!) 2590 + path = getDocument().location.href; 2591 + } 2592 + if (verb === "get" && path.includes("?")) { 2593 + path = path.replace(/\?[^#]+/, ""); 2594 + } 2595 + } 2596 + triggerSpecs.forEach(function (triggerSpec) { 2597 + addEventListener( 2598 + elt, 2599 + function (node, evt) { 2600 + const elt = asElement(node); 2601 + if (eltIsDisabled(elt)) { 2602 + cleanUpElement(elt); 2603 + return; 2604 + } 2605 + issueAjaxRequest(verb, path, elt, evt); 2606 + }, 2607 + nodeData, 2608 + triggerSpec, 2609 + true, 2610 + ); 2611 + }); 2612 + } 2613 + } 2614 + 2615 + /** 2616 + * @param {Event} evt 2617 + * @param {Node} node 2618 + * @returns {boolean} 2619 + */ 2620 + function shouldCancel(evt, node) { 2621 + const elt = asElement(node); 2622 + if (!elt) { 2623 + return false; 2624 + } 2625 + if (evt.type === "submit" || evt.type === "click") { 2626 + if (elt.tagName === "FORM") { 2627 + return true; 2628 + } 2629 + if ( 2630 + matches(elt, 'input[type="submit"], button') && 2631 + (matches(elt, "[form]") || closest(elt, "form") !== null) 2632 + ) { 2633 + return true; 2634 + } 2635 + if ( 2636 + elt instanceof HTMLAnchorElement && 2637 + elt.href && 2638 + (elt.getAttribute("href") === "#" || 2639 + elt.getAttribute("href").indexOf("#") !== 0) 2640 + ) { 2641 + return true; 2642 + } 2643 + } 2644 + return false; 2645 + } 2646 + 2647 + /** 2648 + * @param {Node} elt 2649 + * @param {Event|MouseEvent|KeyboardEvent|TouchEvent} evt 2650 + * @returns {boolean} 2651 + */ 2652 + function ignoreBoostedAnchorCtrlClick(elt, evt) { 2653 + return ( 2654 + getInternalData(elt).boosted && 2655 + elt instanceof HTMLAnchorElement && 2656 + evt.type === "click" && 2657 + // @ts-ignore this will resolve to undefined for events that don't define those properties, which is fine 2658 + (evt.ctrlKey || evt.metaKey) 2659 + ); 2660 + } 2661 + 2662 + /** 2663 + * @param {HtmxTriggerSpecification} triggerSpec 2664 + * @param {Node} elt 2665 + * @param {Event} evt 2666 + * @returns {boolean} 2667 + */ 2668 + function maybeFilterEvent(triggerSpec, elt, evt) { 2669 + const eventFilter = triggerSpec.eventFilter; 2670 + if (eventFilter) { 2671 + try { 2672 + return eventFilter.call(elt, evt) !== true; 2673 + } catch (e) { 2674 + const source = eventFilter.source; 2675 + triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", { 2676 + error: e, 2677 + source, 2678 + }); 2679 + return true; 2680 + } 2681 + } 2682 + return false; 2683 + } 2684 + 2685 + /** 2686 + * @param {Node} elt 2687 + * @param {TriggerHandler} handler 2688 + * @param {HtmxNodeInternalData} nodeData 2689 + * @param {HtmxTriggerSpecification} triggerSpec 2690 + * @param {boolean} [explicitCancel] 2691 + */ 2692 + function addEventListener( 2693 + elt, 2694 + handler, 2695 + nodeData, 2696 + triggerSpec, 2697 + explicitCancel, 2698 + ) { 2699 + const elementData = getInternalData(elt); 2700 + /** @type {(Node|Window)[]} */ 2701 + let eltsToListenOn; 2702 + if (triggerSpec.from) { 2703 + eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from); 2704 + } else { 2705 + eltsToListenOn = [elt]; 2706 + } 2707 + // store the initial values of the elements, so we can tell if they change 2708 + if (triggerSpec.changed) { 2709 + if (!("lastValue" in elementData)) { 2710 + elementData.lastValue = new WeakMap(); 2711 + } 2712 + eltsToListenOn.forEach(function (eltToListenOn) { 2713 + if (!elementData.lastValue.has(triggerSpec)) { 2714 + elementData.lastValue.set(triggerSpec, new WeakMap()); 2715 + } 2716 + // @ts-ignore value will be undefined for non-input elements, which is fine 2717 + elementData.lastValue 2718 + .get(triggerSpec) 2719 + .set(eltToListenOn, eltToListenOn.value); 2720 + }); 2721 + } 2722 + forEach(eltsToListenOn, function (eltToListenOn) { 2723 + /** @type EventListener */ 2724 + const eventListener = function (evt) { 2725 + if (!bodyContains(elt)) { 2726 + eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener); 2727 + return; 2728 + } 2729 + if (ignoreBoostedAnchorCtrlClick(elt, evt)) { 2730 + return; 2731 + } 2732 + if (explicitCancel || shouldCancel(evt, elt)) { 2733 + evt.preventDefault(); 2734 + } 2735 + if (maybeFilterEvent(triggerSpec, elt, evt)) { 2736 + return; 2737 + } 2738 + const eventData = getInternalData(evt); 2739 + eventData.triggerSpec = triggerSpec; 2740 + if (eventData.handledFor == null) { 2741 + eventData.handledFor = []; 2742 + } 2743 + if (eventData.handledFor.indexOf(elt) < 0) { 2744 + eventData.handledFor.push(elt); 2745 + if (triggerSpec.consume) { 2746 + evt.stopPropagation(); 2747 + } 2748 + if (triggerSpec.target && evt.target) { 2749 + if (!matches(asElement(evt.target), triggerSpec.target)) { 2750 + return; 2751 + } 2752 + } 2753 + if (triggerSpec.once) { 2754 + if (elementData.triggeredOnce) { 2755 + return; 2756 + } else { 2757 + elementData.triggeredOnce = true; 2758 + } 2759 + } 2760 + if (triggerSpec.changed) { 2761 + const node = event.target; 2762 + // @ts-ignore value will be undefined for non-input elements, which is fine 2763 + const value = node.value; 2764 + const lastValue = elementData.lastValue.get(triggerSpec); 2765 + if (lastValue.has(node) && lastValue.get(node) === value) { 2766 + return; 2767 + } 2768 + lastValue.set(node, value); 2769 + } 2770 + if (elementData.delayed) { 2771 + clearTimeout(elementData.delayed); 2772 + } 2773 + if (elementData.throttle) { 2774 + return; 2775 + } 2776 + 2777 + if (triggerSpec.throttle > 0) { 2778 + if (!elementData.throttle) { 2779 + triggerEvent(elt, "htmx:trigger"); 2780 + handler(elt, evt); 2781 + elementData.throttle = getWindow().setTimeout(function () { 2782 + elementData.throttle = null; 2783 + }, triggerSpec.throttle); 2784 + } 2785 + } else if (triggerSpec.delay > 0) { 2786 + elementData.delayed = getWindow().setTimeout(function () { 2787 + triggerEvent(elt, "htmx:trigger"); 2788 + handler(elt, evt); 2789 + }, triggerSpec.delay); 2790 + } else { 2791 + triggerEvent(elt, "htmx:trigger"); 2792 + handler(elt, evt); 2793 + } 2794 + } 2795 + }; 2796 + if (nodeData.listenerInfos == null) { 2797 + nodeData.listenerInfos = []; 2798 + } 2799 + nodeData.listenerInfos.push({ 2800 + trigger: triggerSpec.trigger, 2801 + listener: eventListener, 2802 + on: eltToListenOn, 2803 + }); 2804 + eltToListenOn.addEventListener(triggerSpec.trigger, eventListener); 2805 + }); 2806 + } 2807 + 2808 + let windowIsScrolling = false; // used by initScrollHandler 2809 + let scrollHandler = null; 2810 + function initScrollHandler() { 2811 + if (!scrollHandler) { 2812 + scrollHandler = function () { 2813 + windowIsScrolling = true; 2814 + }; 2815 + window.addEventListener("scroll", scrollHandler); 2816 + window.addEventListener("resize", scrollHandler); 2817 + setInterval(function () { 2818 + if (windowIsScrolling) { 2819 + windowIsScrolling = false; 2820 + forEach( 2821 + getDocument().querySelectorAll( 2822 + "[hx-trigger*='revealed'],[data-hx-trigger*='revealed']", 2823 + ), 2824 + function (elt) { 2825 + maybeReveal(elt); 2826 + }, 2827 + ); 2828 + } 2829 + }, 200); 2830 + } 2831 + } 2832 + 2833 + /** 2834 + * @param {Element} elt 2835 + */ 2836 + function maybeReveal(elt) { 2837 + if (!hasAttribute(elt, "data-hx-revealed") && isScrolledIntoView(elt)) { 2838 + elt.setAttribute("data-hx-revealed", "true"); 2839 + const nodeData = getInternalData(elt); 2840 + if (nodeData.initHash) { 2841 + triggerEvent(elt, "revealed"); 2842 + } else { 2843 + // if the node isn't initialized, wait for it before triggering the request 2844 + elt.addEventListener( 2845 + "htmx:afterProcessNode", 2846 + function () { 2847 + triggerEvent(elt, "revealed"); 2848 + }, 2849 + { once: true }, 2850 + ); 2851 + } 2852 + } 2853 + } 2854 + 2855 + //= =================================================================== 2856 + 2857 + /** 2858 + * @param {Element} elt 2859 + * @param {TriggerHandler} handler 2860 + * @param {HtmxNodeInternalData} nodeData 2861 + * @param {number} delay 2862 + */ 2863 + function loadImmediately(elt, handler, nodeData, delay) { 2864 + const load = function () { 2865 + if (!nodeData.loaded) { 2866 + nodeData.loaded = true; 2867 + triggerEvent(elt, "htmx:trigger"); 2868 + handler(elt); 2869 + } 2870 + }; 2871 + if (delay > 0) { 2872 + getWindow().setTimeout(load, delay); 2873 + } else { 2874 + load(); 2875 + } 2876 + } 2877 + 2878 + /** 2879 + * @param {Element} elt 2880 + * @param {HtmxNodeInternalData} nodeData 2881 + * @param {HtmxTriggerSpecification[]} triggerSpecs 2882 + * @returns {boolean} 2883 + */ 2884 + function processVerbs(elt, nodeData, triggerSpecs) { 2885 + let explicitAction = false; 2886 + forEach(VERBS, function (verb) { 2887 + if (hasAttribute(elt, "hx-" + verb)) { 2888 + const path = getAttributeValue(elt, "hx-" + verb); 2889 + explicitAction = true; 2890 + nodeData.path = path; 2891 + nodeData.verb = verb; 2892 + triggerSpecs.forEach(function (triggerSpec) { 2893 + addTriggerHandler(elt, triggerSpec, nodeData, function (node, evt) { 2894 + const elt = asElement(node); 2895 + if (closest(elt, htmx.config.disableSelector)) { 2896 + cleanUpElement(elt); 2897 + return; 2898 + } 2899 + issueAjaxRequest(verb, path, elt, evt); 2900 + }); 2901 + }); 2902 + } 2903 + }); 2904 + return explicitAction; 2905 + } 2906 + 2907 + /** 2908 + * @callback TriggerHandler 2909 + * @param {Node} elt 2910 + * @param {Event} [evt] 2911 + */ 2912 + 2913 + /** 2914 + * @param {Node} elt 2915 + * @param {HtmxTriggerSpecification} triggerSpec 2916 + * @param {HtmxNodeInternalData} nodeData 2917 + * @param {TriggerHandler} handler 2918 + */ 2919 + function addTriggerHandler(elt, triggerSpec, nodeData, handler) { 2920 + if (triggerSpec.trigger === "revealed") { 2921 + initScrollHandler(); 2922 + addEventListener(elt, handler, nodeData, triggerSpec); 2923 + maybeReveal(asElement(elt)); 2924 + } else if (triggerSpec.trigger === "intersect") { 2925 + const observerOptions = {}; 2926 + if (triggerSpec.root) { 2927 + observerOptions.root = querySelectorExt(elt, triggerSpec.root); 2928 + } 2929 + if (triggerSpec.threshold) { 2930 + observerOptions.threshold = parseFloat(triggerSpec.threshold); 2931 + } 2932 + const observer = new IntersectionObserver(function (entries) { 2933 + for (let i = 0; i < entries.length; i++) { 2934 + const entry = entries[i]; 2935 + if (entry.isIntersecting) { 2936 + triggerEvent(elt, "intersect"); 2937 + break; 2938 + } 2939 + } 2940 + }, observerOptions); 2941 + observer.observe(asElement(elt)); 2942 + addEventListener(asElement(elt), handler, nodeData, triggerSpec); 2943 + } else if (!nodeData.firstInitCompleted && triggerSpec.trigger === "load") { 2944 + if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", { elt }))) { 2945 + loadImmediately(asElement(elt), handler, nodeData, triggerSpec.delay); 2946 + } 2947 + } else if (triggerSpec.pollInterval > 0) { 2948 + nodeData.polling = true; 2949 + processPolling(asElement(elt), handler, triggerSpec); 2950 + } else { 2951 + addEventListener(elt, handler, nodeData, triggerSpec); 2952 + } 2953 + } 2954 + 2955 + /** 2956 + * @param {Node} node 2957 + * @returns {boolean} 2958 + */ 2959 + function shouldProcessHxOn(node) { 2960 + const elt = asElement(node); 2961 + if (!elt) { 2962 + return false; 2963 + } 2964 + const attributes = elt.attributes; 2965 + for (let j = 0; j < attributes.length; j++) { 2966 + const attrName = attributes[j].name; 2967 + if ( 2968 + startsWith(attrName, "hx-on:") || 2969 + startsWith(attrName, "data-hx-on:") || 2970 + startsWith(attrName, "hx-on-") || 2971 + startsWith(attrName, "data-hx-on-") 2972 + ) { 2973 + return true; 2974 + } 2975 + } 2976 + return false; 2977 + } 2978 + 2979 + /** 2980 + * @param {Node} elt 2981 + * @returns {Element[]} 2982 + */ 2983 + const HX_ON_QUERY = new XPathEvaluator().createExpression( 2984 + './/*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + 2985 + ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', 2986 + ); 2987 + 2988 + function processHXOnRoot(elt, elements) { 2989 + if (shouldProcessHxOn(elt)) { 2990 + elements.push(asElement(elt)); 2991 + } 2992 + const iter = HX_ON_QUERY.evaluate(elt); 2993 + let node = null; 2994 + while ((node = iter.iterateNext())) elements.push(asElement(node)); 2995 + } 2996 + 2997 + function findHxOnWildcardElements(elt) { 2998 + /** @type {Element[]} */ 2999 + const elements = []; 3000 + if (elt instanceof DocumentFragment) { 3001 + for (const child of elt.childNodes) { 3002 + processHXOnRoot(child, elements); 3003 + } 3004 + } else { 3005 + processHXOnRoot(elt, elements); 3006 + } 3007 + return elements; 3008 + } 3009 + 3010 + /** 3011 + * @param {Element} elt 3012 + * @returns {NodeListOf<Element>|[]} 3013 + */ 3014 + function findElementsToProcess(elt) { 3015 + if (elt.querySelectorAll) { 3016 + const boostedSelector = 3017 + ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]"; 3018 + 3019 + const extensionSelectors = []; 3020 + for (const e in extensions) { 3021 + const extension = extensions[e]; 3022 + if (extension.getSelectors) { 3023 + var selectors = extension.getSelectors(); 3024 + if (selectors) { 3025 + extensionSelectors.push(selectors); 3026 + } 3027 + } 3028 + } 3029 + 3030 + const results = elt.querySelectorAll( 3031 + VERB_SELECTOR + 3032 + boostedSelector + 3033 + ", form, [type='submit']," + 3034 + " [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]" + 3035 + extensionSelectors 3036 + .flat() 3037 + .map((s) => ", " + s) 3038 + .join(""), 3039 + ); 3040 + 3041 + return results; 3042 + } else { 3043 + return []; 3044 + } 3045 + } 3046 + 3047 + /** 3048 + * Handle submit buttons/inputs that have the form attribute set 3049 + * see https://developer.mozilla.org/docs/Web/HTML/Element/button 3050 + * @param {Event} evt 3051 + */ 3052 + function maybeSetLastButtonClicked(evt) { 3053 + const elt = /** @type {HTMLButtonElement|HTMLInputElement} */ ( 3054 + closest(asElement(evt.target), "button, input[type='submit']") 3055 + ); 3056 + const internalData = getRelatedFormData(evt); 3057 + if (internalData) { 3058 + internalData.lastButtonClicked = elt; 3059 + } 3060 + } 3061 + 3062 + /** 3063 + * @param {Event} evt 3064 + */ 3065 + function maybeUnsetLastButtonClicked(evt) { 3066 + const internalData = getRelatedFormData(evt); 3067 + if (internalData) { 3068 + internalData.lastButtonClicked = null; 3069 + } 3070 + } 3071 + 3072 + /** 3073 + * @param {Event} evt 3074 + * @returns {HtmxNodeInternalData|undefined} 3075 + */ 3076 + function getRelatedFormData(evt) { 3077 + const elt = closest(asElement(evt.target), "button, input[type='submit']"); 3078 + if (!elt) { 3079 + return; 3080 + } 3081 + const form = 3082 + resolveTarget("#" + getRawAttribute(elt, "form"), elt.getRootNode()) || 3083 + closest(elt, "form"); 3084 + if (!form) { 3085 + return; 3086 + } 3087 + return getInternalData(form); 3088 + } 3089 + 3090 + /** 3091 + * @param {EventTarget} elt 3092 + */ 3093 + function initButtonTracking(elt) { 3094 + // need to handle both click and focus in: 3095 + // focusin - in case someone tabs in to a button and hits the space bar 3096 + // click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724 3097 + elt.addEventListener("click", maybeSetLastButtonClicked); 3098 + elt.addEventListener("focusin", maybeSetLastButtonClicked); 3099 + elt.addEventListener("focusout", maybeUnsetLastButtonClicked); 3100 + } 3101 + 3102 + /** 3103 + * @param {Element} elt 3104 + * @param {string} eventName 3105 + * @param {string} code 3106 + */ 3107 + function addHxOnEventHandler(elt, eventName, code) { 3108 + const nodeData = getInternalData(elt); 3109 + if (!Array.isArray(nodeData.onHandlers)) { 3110 + nodeData.onHandlers = []; 3111 + } 3112 + let func; 3113 + /** @type EventListener */ 3114 + const listener = function (e) { 3115 + maybeEval(elt, function () { 3116 + if (eltIsDisabled(elt)) { 3117 + return; 3118 + } 3119 + if (!func) { 3120 + func = new Function("event", code); 3121 + } 3122 + func.call(elt, e); 3123 + }); 3124 + }; 3125 + elt.addEventListener(eventName, listener); 3126 + nodeData.onHandlers.push({ event: eventName, listener }); 3127 + } 3128 + 3129 + /** 3130 + * @param {Element} elt 3131 + */ 3132 + function processHxOnWildcard(elt) { 3133 + // wipe any previous on handlers so that this function takes precedence 3134 + deInitOnHandlers(elt); 3135 + 3136 + for (let i = 0; i < elt.attributes.length; i++) { 3137 + const name = elt.attributes[i].name; 3138 + const value = elt.attributes[i].value; 3139 + if (startsWith(name, "hx-on") || startsWith(name, "data-hx-on")) { 3140 + const afterOnPosition = name.indexOf("-on") + 3; 3141 + const nextChar = name.slice(afterOnPosition, afterOnPosition + 1); 3142 + if (nextChar === "-" || nextChar === ":") { 3143 + let eventName = name.slice(afterOnPosition + 1); 3144 + // if the eventName starts with a colon or dash, prepend "htmx" for shorthand support 3145 + if (startsWith(eventName, ":")) { 3146 + eventName = "htmx" + eventName; 3147 + } else if (startsWith(eventName, "-")) { 3148 + eventName = "htmx:" + eventName.slice(1); 3149 + } else if (startsWith(eventName, "htmx-")) { 3150 + eventName = "htmx:" + eventName.slice(5); 3151 + } 3152 + 3153 + addHxOnEventHandler(elt, eventName, value); 3154 + } 3155 + } 3156 + } 3157 + } 3158 + 3159 + /** 3160 + * @param {Element|HTMLInputElement} elt 3161 + */ 3162 + function initNode(elt) { 3163 + if (closest(elt, htmx.config.disableSelector)) { 3164 + cleanUpElement(elt); 3165 + return; 3166 + } 3167 + const nodeData = getInternalData(elt); 3168 + const attrHash = attributeHash(elt); 3169 + if (nodeData.initHash !== attrHash) { 3170 + // clean up any previously processed info 3171 + deInitNode(elt); 3172 + 3173 + nodeData.initHash = attrHash; 3174 + 3175 + triggerEvent(elt, "htmx:beforeProcessNode"); 3176 + 3177 + const triggerSpecs = getTriggerSpecs(elt); 3178 + const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs); 3179 + 3180 + if (!hasExplicitHttpAction) { 3181 + if (getClosestAttributeValue(elt, "hx-boost") === "true") { 3182 + boostElement(elt, nodeData, triggerSpecs); 3183 + } else if (hasAttribute(elt, "hx-trigger")) { 3184 + triggerSpecs.forEach(function (triggerSpec) { 3185 + // For "naked" triggers, don't do anything at all 3186 + addTriggerHandler(elt, triggerSpec, nodeData, function () {}); 3187 + }); 3188 + } 3189 + } 3190 + 3191 + // Handle submit buttons/inputs that have the form attribute set 3192 + // see https://developer.mozilla.org/docs/Web/HTML/Element/button 3193 + if ( 3194 + elt.tagName === "FORM" || 3195 + (getRawAttribute(elt, "type") === "submit" && hasAttribute(elt, "form")) 3196 + ) { 3197 + initButtonTracking(elt); 3198 + } 3199 + 3200 + nodeData.firstInitCompleted = true; 3201 + triggerEvent(elt, "htmx:afterProcessNode"); 3202 + } 3203 + } 3204 + 3205 + /** 3206 + * Processes new content, enabling htmx behavior. This can be useful if you have content that is added to the DOM outside of the normal htmx request cycle but still want htmx attributes to work. 3207 + * 3208 + * @see https://htmx.org/api/#process 3209 + * 3210 + * @param {Element|string} elt element to process 3211 + */ 3212 + function processNode(elt) { 3213 + elt = resolveTarget(elt); 3214 + if (closest(elt, htmx.config.disableSelector)) { 3215 + cleanUpElement(elt); 3216 + return; 3217 + } 3218 + initNode(elt); 3219 + forEach(findElementsToProcess(elt), function (child) { 3220 + initNode(child); 3221 + }); 3222 + forEach(findHxOnWildcardElements(elt), processHxOnWildcard); 3223 + } 3224 + 3225 + //= =================================================================== 3226 + // Event/Log Support 3227 + //= =================================================================== 3228 + 3229 + /** 3230 + * @param {string} str 3231 + * @returns {string} 3232 + */ 3233 + function kebabEventName(str) { 3234 + return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); 3235 + } 3236 + 3237 + /** 3238 + * @param {string} eventName 3239 + * @param {any} detail 3240 + * @returns {CustomEvent} 3241 + */ 3242 + function makeEvent(eventName, detail) { 3243 + let evt; 3244 + if (window.CustomEvent && typeof window.CustomEvent === "function") { 3245 + // TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM 3246 + // This breaks expected encapsulation but needs to be here until decided otherwise by core devs 3247 + evt = new CustomEvent(eventName, { 3248 + bubbles: true, 3249 + cancelable: true, 3250 + composed: true, 3251 + detail, 3252 + }); 3253 + } else { 3254 + evt = getDocument().createEvent("CustomEvent"); 3255 + evt.initCustomEvent(eventName, true, true, detail); 3256 + } 3257 + return evt; 3258 + } 3259 + 3260 + /** 3261 + * @param {EventTarget|string} elt 3262 + * @param {string} eventName 3263 + * @param {any=} detail 3264 + */ 3265 + function triggerErrorEvent(elt, eventName, detail) { 3266 + triggerEvent(elt, eventName, mergeObjects({ error: eventName }, detail)); 3267 + } 3268 + 3269 + /** 3270 + * @param {string} eventName 3271 + * @returns {boolean} 3272 + */ 3273 + function ignoreEventForLogging(eventName) { 3274 + return eventName === "htmx:afterProcessNode"; 3275 + } 3276 + 3277 + /** 3278 + * `withExtensions` locates all active extensions for a provided element, then 3279 + * executes the provided function using each of the active extensions. It should 3280 + * be called internally at every extendable execution point in htmx. 3281 + * 3282 + * @param {Element} elt 3283 + * @param {(extension:HtmxExtension) => void} toDo 3284 + * @returns void 3285 + */ 3286 + function withExtensions(elt, toDo) { 3287 + forEach(getExtensions(elt), function (extension) { 3288 + try { 3289 + toDo(extension); 3290 + } catch (e) { 3291 + logError(e); 3292 + } 3293 + }); 3294 + } 3295 + 3296 + function logError(msg) { 3297 + if (console.error) { 3298 + console.error(msg); 3299 + } else if (console.log) { 3300 + console.log("ERROR: ", msg); 3301 + } 3302 + } 3303 + 3304 + /** 3305 + * Triggers a given event on an element 3306 + * 3307 + * @see https://htmx.org/api/#trigger 3308 + * 3309 + * @param {EventTarget|string} elt the element to trigger the event on 3310 + * @param {string} eventName the name of the event to trigger 3311 + * @param {any=} detail details for the event 3312 + * @returns {boolean} 3313 + */ 3314 + function triggerEvent(elt, eventName, detail) { 3315 + elt = resolveTarget(elt); 3316 + if (detail == null) { 3317 + detail = {}; 3318 + } 3319 + detail.elt = elt; 3320 + const event = makeEvent(eventName, detail); 3321 + if (htmx.logger && !ignoreEventForLogging(eventName)) { 3322 + htmx.logger(elt, eventName, detail); 3323 + } 3324 + if (detail.error) { 3325 + logError(detail.error); 3326 + triggerEvent(elt, "htmx:error", { errorInfo: detail }); 3327 + } 3328 + let eventResult = elt.dispatchEvent(event); 3329 + const kebabName = kebabEventName(eventName); 3330 + if (eventResult && kebabName !== eventName) { 3331 + const kebabedEvent = makeEvent(kebabName, event.detail); 3332 + eventResult = eventResult && elt.dispatchEvent(kebabedEvent); 3333 + } 3334 + withExtensions(asElement(elt), function (extension) { 3335 + eventResult = 3336 + eventResult && 3337 + extension.onEvent(eventName, event) !== false && 3338 + !event.defaultPrevented; 3339 + }); 3340 + return eventResult; 3341 + } 3342 + 3343 + //= =================================================================== 3344 + // History Support 3345 + //= =================================================================== 3346 + let currentPathForHistory = location.pathname + location.search; 3347 + 3348 + /** 3349 + * @returns {Element} 3350 + */ 3351 + function getHistoryElement() { 3352 + const historyElt = getDocument().querySelector( 3353 + "[hx-history-elt],[data-hx-history-elt]", 3354 + ); 3355 + return historyElt || getDocument().body; 3356 + } 3357 + 3358 + /** 3359 + * @param {string} url 3360 + * @param {Element} rootElt 3361 + */ 3362 + function saveToHistoryCache(url, rootElt) { 3363 + if (!canAccessLocalStorage()) { 3364 + return; 3365 + } 3366 + 3367 + // get state to save 3368 + const innerHTML = cleanInnerHtmlForHistory(rootElt); 3369 + const title = getDocument().title; 3370 + const scroll = window.scrollY; 3371 + 3372 + if (htmx.config.historyCacheSize <= 0) { 3373 + // make sure that an eventually already existing cache is purged 3374 + localStorage.removeItem("htmx-history-cache"); 3375 + return; 3376 + } 3377 + 3378 + url = normalizePath(url); 3379 + 3380 + const historyCache = 3381 + parseJSON(localStorage.getItem("htmx-history-cache")) || []; 3382 + for (let i = 0; i < historyCache.length; i++) { 3383 + if (historyCache[i].url === url) { 3384 + historyCache.splice(i, 1); 3385 + break; 3386 + } 3387 + } 3388 + 3389 + /** @type HtmxHistoryItem */ 3390 + const newHistoryItem = { url, content: innerHTML, title, scroll }; 3391 + 3392 + triggerEvent(getDocument().body, "htmx:historyItemCreated", { 3393 + item: newHistoryItem, 3394 + cache: historyCache, 3395 + }); 3396 + 3397 + historyCache.push(newHistoryItem); 3398 + while (historyCache.length > htmx.config.historyCacheSize) { 3399 + historyCache.shift(); 3400 + } 3401 + 3402 + // keep trying to save the cache until it succeeds or is empty 3403 + while (historyCache.length > 0) { 3404 + try { 3405 + localStorage.setItem( 3406 + "htmx-history-cache", 3407 + JSON.stringify(historyCache), 3408 + ); 3409 + break; 3410 + } catch (e) { 3411 + triggerErrorEvent(getDocument().body, "htmx:historyCacheError", { 3412 + cause: e, 3413 + cache: historyCache, 3414 + }); 3415 + historyCache.shift(); // shrink the cache and retry 3416 + } 3417 + } 3418 + } 3419 + 3420 + /** 3421 + * @typedef {Object} HtmxHistoryItem 3422 + * @property {string} url 3423 + * @property {string} content 3424 + * @property {string} title 3425 + * @property {number} scroll 3426 + */ 3427 + 3428 + /** 3429 + * @param {string} url 3430 + * @returns {HtmxHistoryItem|null} 3431 + */ 3432 + function getCachedHistory(url) { 3433 + if (!canAccessLocalStorage()) { 3434 + return null; 3435 + } 3436 + 3437 + url = normalizePath(url); 3438 + 3439 + const historyCache = 3440 + parseJSON(localStorage.getItem("htmx-history-cache")) || []; 3441 + for (let i = 0; i < historyCache.length; i++) { 3442 + if (historyCache[i].url === url) { 3443 + return historyCache[i]; 3444 + } 3445 + } 3446 + return null; 3447 + } 3448 + 3449 + /** 3450 + * @param {Element} elt 3451 + * @returns {string} 3452 + */ 3453 + function cleanInnerHtmlForHistory(elt) { 3454 + const className = htmx.config.requestClass; 3455 + const clone = /** @type Element */ (elt.cloneNode(true)); 3456 + forEach(findAll(clone, "." + className), function (child) { 3457 + removeClassFromElement(child, className); 3458 + }); 3459 + // remove the disabled attribute for any element disabled due to an htmx request 3460 + forEach(findAll(clone, "[data-disabled-by-htmx]"), function (child) { 3461 + child.removeAttribute("disabled"); 3462 + }); 3463 + return clone.innerHTML; 3464 + } 3465 + 3466 + function saveCurrentPageToHistory() { 3467 + const elt = getHistoryElement(); 3468 + const path = currentPathForHistory || location.pathname + location.search; 3469 + 3470 + // Allow history snapshot feature to be disabled where hx-history="false" 3471 + // is present *anywhere* in the current document we're about to save, 3472 + // so we can prevent privileged data entering the cache. 3473 + // The page will still be reachable as a history entry, but htmx will fetch it 3474 + // live from the server onpopstate rather than look in the localStorage cache 3475 + let disableHistoryCache; 3476 + try { 3477 + disableHistoryCache = getDocument().querySelector( 3478 + '[hx-history="false" i],[data-hx-history="false" i]', 3479 + ); 3480 + } catch (e) { 3481 + // IE11: insensitive modifier not supported so fallback to case sensitive selector 3482 + disableHistoryCache = getDocument().querySelector( 3483 + '[hx-history="false"],[data-hx-history="false"]', 3484 + ); 3485 + } 3486 + if (!disableHistoryCache) { 3487 + triggerEvent(getDocument().body, "htmx:beforeHistorySave", { 3488 + path, 3489 + historyElt: elt, 3490 + }); 3491 + saveToHistoryCache(path, elt); 3492 + } 3493 + 3494 + if (htmx.config.historyEnabled) 3495 + history.replaceState( 3496 + { htmx: true }, 3497 + getDocument().title, 3498 + window.location.href, 3499 + ); 3500 + } 3501 + 3502 + /** 3503 + * @param {string} path 3504 + */ 3505 + function pushUrlIntoHistory(path) { 3506 + // remove the cache buster parameter, if any 3507 + if (htmx.config.getCacheBusterParam) { 3508 + path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, ""); 3509 + if (endsWith(path, "&") || endsWith(path, "?")) { 3510 + path = path.slice(0, -1); 3511 + } 3512 + } 3513 + if (htmx.config.historyEnabled) { 3514 + history.pushState({ htmx: true }, "", path); 3515 + } 3516 + currentPathForHistory = path; 3517 + } 3518 + 3519 + /** 3520 + * @param {string} path 3521 + */ 3522 + function replaceUrlInHistory(path) { 3523 + if (htmx.config.historyEnabled) 3524 + history.replaceState({ htmx: true }, "", path); 3525 + currentPathForHistory = path; 3526 + } 3527 + 3528 + /** 3529 + * @param {HtmxSettleTask[]} tasks 3530 + */ 3531 + function settleImmediately(tasks) { 3532 + forEach(tasks, function (task) { 3533 + task.call(undefined); 3534 + }); 3535 + } 3536 + 3537 + /** 3538 + * @param {string} path 3539 + */ 3540 + function loadHistoryFromServer(path) { 3541 + const request = new XMLHttpRequest(); 3542 + const details = { path, xhr: request }; 3543 + triggerEvent(getDocument().body, "htmx:historyCacheMiss", details); 3544 + request.open("GET", path, true); 3545 + request.setRequestHeader("HX-Request", "true"); 3546 + request.setRequestHeader("HX-History-Restore-Request", "true"); 3547 + request.setRequestHeader("HX-Current-URL", getDocument().location.href); 3548 + request.onload = function () { 3549 + if (this.status >= 200 && this.status < 400) { 3550 + triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details); 3551 + const fragment = makeFragment(this.response); 3552 + /** @type ParentNode */ 3553 + const content = 3554 + fragment.querySelector("[hx-history-elt],[data-hx-history-elt]") || 3555 + fragment; 3556 + const historyElement = getHistoryElement(); 3557 + const settleInfo = makeSettleInfo(historyElement); 3558 + handleTitle(fragment.title); 3559 + 3560 + handlePreservedElements(fragment); 3561 + swapInnerHTML(historyElement, content, settleInfo); 3562 + restorePreservedElements(); 3563 + settleImmediately(settleInfo.tasks); 3564 + currentPathForHistory = path; 3565 + triggerEvent(getDocument().body, "htmx:historyRestore", { 3566 + path, 3567 + cacheMiss: true, 3568 + serverResponse: this.response, 3569 + }); 3570 + } else { 3571 + triggerErrorEvent( 3572 + getDocument().body, 3573 + "htmx:historyCacheMissLoadError", 3574 + details, 3575 + ); 3576 + } 3577 + }; 3578 + request.send(); 3579 + } 3580 + 3581 + /** 3582 + * @param {string} [path] 3583 + */ 3584 + function restoreHistory(path) { 3585 + saveCurrentPageToHistory(); 3586 + path = path || location.pathname + location.search; 3587 + const cached = getCachedHistory(path); 3588 + if (cached) { 3589 + const fragment = makeFragment(cached.content); 3590 + const historyElement = getHistoryElement(); 3591 + const settleInfo = makeSettleInfo(historyElement); 3592 + handleTitle(cached.title); 3593 + handlePreservedElements(fragment); 3594 + swapInnerHTML(historyElement, fragment, settleInfo); 3595 + restorePreservedElements(); 3596 + settleImmediately(settleInfo.tasks); 3597 + getWindow().setTimeout(function () { 3598 + window.scrollTo(0, cached.scroll); 3599 + }, 0); // next 'tick', so browser has time to render layout 3600 + currentPathForHistory = path; 3601 + triggerEvent(getDocument().body, "htmx:historyRestore", { 3602 + path, 3603 + item: cached, 3604 + }); 3605 + } else { 3606 + if (htmx.config.refreshOnHistoryMiss) { 3607 + // @ts-ignore: optional parameter in reload() function throws error 3608 + // noinspection JSUnresolvedReference 3609 + window.location.reload(true); 3610 + } else { 3611 + loadHistoryFromServer(path); 3612 + } 3613 + } 3614 + } 3615 + 3616 + /** 3617 + * @param {Element} elt 3618 + * @returns {Element[]} 3619 + */ 3620 + function addRequestIndicatorClasses(elt) { 3621 + let indicators = /** @type Element[] */ ( 3622 + findAttributeTargets(elt, "hx-indicator") 3623 + ); 3624 + if (indicators == null) { 3625 + indicators = [elt]; 3626 + } 3627 + forEach(indicators, function (ic) { 3628 + const internalData = getInternalData(ic); 3629 + internalData.requestCount = (internalData.requestCount || 0) + 1; 3630 + ic.classList.add.call(ic.classList, htmx.config.requestClass); 3631 + }); 3632 + return indicators; 3633 + } 3634 + 3635 + /** 3636 + * @param {Element} elt 3637 + * @returns {Element[]} 3638 + */ 3639 + function disableElements(elt) { 3640 + let disabledElts = /** @type Element[] */ ( 3641 + findAttributeTargets(elt, "hx-disabled-elt") 3642 + ); 3643 + if (disabledElts == null) { 3644 + disabledElts = []; 3645 + } 3646 + forEach(disabledElts, function (disabledElement) { 3647 + const internalData = getInternalData(disabledElement); 3648 + internalData.requestCount = (internalData.requestCount || 0) + 1; 3649 + disabledElement.setAttribute("disabled", ""); 3650 + disabledElement.setAttribute("data-disabled-by-htmx", ""); 3651 + }); 3652 + return disabledElts; 3653 + } 3654 + 3655 + /** 3656 + * @param {Element[]} indicators 3657 + * @param {Element[]} disabled 3658 + */ 3659 + function removeRequestIndicators(indicators, disabled) { 3660 + forEach(indicators.concat(disabled), function (ele) { 3661 + const internalData = getInternalData(ele); 3662 + internalData.requestCount = (internalData.requestCount || 1) - 1; 3663 + }); 3664 + forEach(indicators, function (ic) { 3665 + const internalData = getInternalData(ic); 3666 + if (internalData.requestCount === 0) { 3667 + ic.classList.remove.call(ic.classList, htmx.config.requestClass); 3668 + } 3669 + }); 3670 + forEach(disabled, function (disabledElement) { 3671 + const internalData = getInternalData(disabledElement); 3672 + if (internalData.requestCount === 0) { 3673 + disabledElement.removeAttribute("disabled"); 3674 + disabledElement.removeAttribute("data-disabled-by-htmx"); 3675 + } 3676 + }); 3677 + } 3678 + 3679 + //= =================================================================== 3680 + // Input Value Processing 3681 + //= =================================================================== 3682 + 3683 + /** 3684 + * @param {Element[]} processed 3685 + * @param {Element} elt 3686 + * @returns {boolean} 3687 + */ 3688 + function haveSeenNode(processed, elt) { 3689 + for (let i = 0; i < processed.length; i++) { 3690 + const node = processed[i]; 3691 + if (node.isSameNode(elt)) { 3692 + return true; 3693 + } 3694 + } 3695 + return false; 3696 + } 3697 + 3698 + /** 3699 + * @param {Element} element 3700 + * @return {boolean} 3701 + */ 3702 + function shouldInclude(element) { 3703 + // Cast to trick tsc, undefined values will work fine here 3704 + const elt = /** @type {HTMLInputElement} */ (element); 3705 + if ( 3706 + elt.name === "" || 3707 + elt.name == null || 3708 + elt.disabled || 3709 + closest(elt, "fieldset[disabled]") 3710 + ) { 3711 + return false; 3712 + } 3713 + // ignore "submitter" types (see jQuery src/serialize.js) 3714 + if ( 3715 + elt.type === "button" || 3716 + elt.type === "submit" || 3717 + elt.tagName === "image" || 3718 + elt.tagName === "reset" || 3719 + elt.tagName === "file" 3720 + ) { 3721 + return false; 3722 + } 3723 + if (elt.type === "checkbox" || elt.type === "radio") { 3724 + return elt.checked; 3725 + } 3726 + return true; 3727 + } 3728 + 3729 + /** @param {string} name 3730 + * @param {string|Array|FormDataEntryValue} value 3731 + * @param {FormData} formData */ 3732 + function addValueToFormData(name, value, formData) { 3733 + if (name != null && value != null) { 3734 + if (Array.isArray(value)) { 3735 + value.forEach(function (v) { 3736 + formData.append(name, v); 3737 + }); 3738 + } else { 3739 + formData.append(name, value); 3740 + } 3741 + } 3742 + } 3743 + 3744 + /** @param {string} name 3745 + * @param {string|Array} value 3746 + * @param {FormData} formData */ 3747 + function removeValueFromFormData(name, value, formData) { 3748 + if (name != null && value != null) { 3749 + let values = formData.getAll(name); 3750 + if (Array.isArray(value)) { 3751 + values = values.filter((v) => value.indexOf(v) < 0); 3752 + } else { 3753 + values = values.filter((v) => v !== value); 3754 + } 3755 + formData.delete(name); 3756 + forEach(values, (v) => formData.append(name, v)); 3757 + } 3758 + } 3759 + 3760 + /** 3761 + * @param {Element[]} processed 3762 + * @param {FormData} formData 3763 + * @param {HtmxElementValidationError[]} errors 3764 + * @param {Element|HTMLInputElement|HTMLSelectElement|HTMLFormElement} elt 3765 + * @param {boolean} validate 3766 + */ 3767 + function processInputValue(processed, formData, errors, elt, validate) { 3768 + if (elt == null || haveSeenNode(processed, elt)) { 3769 + return; 3770 + } else { 3771 + processed.push(elt); 3772 + } 3773 + if (shouldInclude(elt)) { 3774 + const name = getRawAttribute(elt, "name"); 3775 + // @ts-ignore value will be undefined for non-input elements, which is fine 3776 + let value = elt.value; 3777 + if (elt instanceof HTMLSelectElement && elt.multiple) { 3778 + value = toArray(elt.querySelectorAll("option:checked")).map( 3779 + function (e) { 3780 + return /** @type HTMLOptionElement */ (e).value; 3781 + }, 3782 + ); 3783 + } 3784 + // include file inputs 3785 + if (elt instanceof HTMLInputElement && elt.files) { 3786 + value = toArray(elt.files); 3787 + } 3788 + addValueToFormData(name, value, formData); 3789 + if (validate) { 3790 + validateElement(elt, errors); 3791 + } 3792 + } 3793 + if (elt instanceof HTMLFormElement) { 3794 + forEach(elt.elements, function (input) { 3795 + if (processed.indexOf(input) >= 0) { 3796 + // The input has already been processed and added to the values, but the FormData that will be 3797 + // constructed right after on the form, will include it once again. So remove that input's value 3798 + // now to avoid duplicates 3799 + removeValueFromFormData(input.name, input.value, formData); 3800 + } else { 3801 + processed.push(input); 3802 + } 3803 + if (validate) { 3804 + validateElement(input, errors); 3805 + } 3806 + }); 3807 + new FormData(elt).forEach(function (value, name) { 3808 + if (value instanceof File && value.name === "") { 3809 + return; // ignore no-name files 3810 + } 3811 + addValueToFormData(name, value, formData); 3812 + }); 3813 + } 3814 + } 3815 + 3816 + /** 3817 + * 3818 + * @param {Element} elt 3819 + * @param {HtmxElementValidationError[]} errors 3820 + */ 3821 + function validateElement(elt, errors) { 3822 + const element = /** @type {HTMLElement & ElementInternals} */ (elt); 3823 + if (element.willValidate) { 3824 + triggerEvent(element, "htmx:validation:validate"); 3825 + if (!element.checkValidity()) { 3826 + errors.push({ 3827 + elt: element, 3828 + message: element.validationMessage, 3829 + validity: element.validity, 3830 + }); 3831 + triggerEvent(element, "htmx:validation:failed", { 3832 + message: element.validationMessage, 3833 + validity: element.validity, 3834 + }); 3835 + } 3836 + } 3837 + } 3838 + 3839 + /** 3840 + * Override values in the one FormData with those from another. 3841 + * @param {FormData} receiver the formdata that will be mutated 3842 + * @param {FormData} donor the formdata that will provide the overriding values 3843 + * @returns {FormData} the {@linkcode receiver} 3844 + */ 3845 + function overrideFormData(receiver, donor) { 3846 + for (const key of donor.keys()) { 3847 + receiver.delete(key); 3848 + } 3849 + donor.forEach(function (value, key) { 3850 + receiver.append(key, value); 3851 + }); 3852 + return receiver; 3853 + } 3854 + 3855 + /** 3856 + * @param {Element|HTMLFormElement} elt 3857 + * @param {HttpVerb} verb 3858 + * @returns {{errors: HtmxElementValidationError[], formData: FormData, values: Object}} 3859 + */ 3860 + function getInputValues(elt, verb) { 3861 + /** @type Element[] */ 3862 + const processed = []; 3863 + const formData = new FormData(); 3864 + const priorityFormData = new FormData(); 3865 + /** @type HtmxElementValidationError[] */ 3866 + const errors = []; 3867 + const internalData = getInternalData(elt); 3868 + if ( 3869 + internalData.lastButtonClicked && 3870 + !bodyContains(internalData.lastButtonClicked) 3871 + ) { 3872 + internalData.lastButtonClicked = null; 3873 + } 3874 + 3875 + // only validate when form is directly submitted and novalidate or formnovalidate are not set 3876 + // or if the element has an explicit hx-validate="true" on it 3877 + let validate = 3878 + (elt instanceof HTMLFormElement && elt.noValidate !== true) || 3879 + getAttributeValue(elt, "hx-validate") === "true"; 3880 + if (internalData.lastButtonClicked) { 3881 + validate = 3882 + validate && internalData.lastButtonClicked.formNoValidate !== true; 3883 + } 3884 + 3885 + // for a non-GET include the closest form 3886 + if (verb !== "get") { 3887 + processInputValue( 3888 + processed, 3889 + priorityFormData, 3890 + errors, 3891 + closest(elt, "form"), 3892 + validate, 3893 + ); 3894 + } 3895 + 3896 + // include the element itself 3897 + processInputValue(processed, formData, errors, elt, validate); 3898 + 3899 + // if a button or submit was clicked last, include its value 3900 + if ( 3901 + internalData.lastButtonClicked || 3902 + elt.tagName === "BUTTON" || 3903 + (elt.tagName === "INPUT" && getRawAttribute(elt, "type") === "submit") 3904 + ) { 3905 + const button = 3906 + internalData.lastButtonClicked || 3907 + /** @type HTMLInputElement|HTMLButtonElement */ (elt); 3908 + const name = getRawAttribute(button, "name"); 3909 + addValueToFormData(name, button.value, priorityFormData); 3910 + } 3911 + 3912 + // include any explicit includes 3913 + const includes = findAttributeTargets(elt, "hx-include"); 3914 + forEach(includes, function (node) { 3915 + processInputValue(processed, formData, errors, asElement(node), validate); 3916 + // if a non-form is included, include any input values within it 3917 + if (!matches(node, "form")) { 3918 + forEach( 3919 + asParentNode(node).querySelectorAll(INPUT_SELECTOR), 3920 + function (descendant) { 3921 + processInputValue( 3922 + processed, 3923 + formData, 3924 + errors, 3925 + descendant, 3926 + validate, 3927 + ); 3928 + }, 3929 + ); 3930 + } 3931 + }); 3932 + 3933 + // values from a <form> take precedence, overriding the regular values 3934 + overrideFormData(formData, priorityFormData); 3935 + 3936 + return { errors, formData, values: formDataProxy(formData) }; 3937 + } 3938 + 3939 + /** 3940 + * @param {string} returnStr 3941 + * @param {string} name 3942 + * @param {any} realValue 3943 + * @returns {string} 3944 + */ 3945 + function appendParam(returnStr, name, realValue) { 3946 + if (returnStr !== "") { 3947 + returnStr += "&"; 3948 + } 3949 + if (String(realValue) === "[object Object]") { 3950 + realValue = JSON.stringify(realValue); 3951 + } 3952 + const s = encodeURIComponent(realValue); 3953 + returnStr += encodeURIComponent(name) + "=" + s; 3954 + return returnStr; 3955 + } 3956 + 3957 + /** 3958 + * @param {FormData|Object} values 3959 + * @returns string 3960 + */ 3961 + function urlEncode(values) { 3962 + values = formDataFromObject(values); 3963 + let returnStr = ""; 3964 + values.forEach(function (value, key) { 3965 + returnStr = appendParam(returnStr, key, value); 3966 + }); 3967 + return returnStr; 3968 + } 3969 + 3970 + //= =================================================================== 3971 + // Ajax 3972 + //= =================================================================== 3973 + 3974 + /** 3975 + * @param {Element} elt 3976 + * @param {Element} target 3977 + * @param {string} prompt 3978 + * @returns {HtmxHeaderSpecification} 3979 + */ 3980 + function getHeaders(elt, target, prompt) { 3981 + /** @type HtmxHeaderSpecification */ 3982 + const headers = { 3983 + "HX-Request": "true", 3984 + "HX-Trigger": getRawAttribute(elt, "id"), 3985 + "HX-Trigger-Name": getRawAttribute(elt, "name"), 3986 + "HX-Target": getAttributeValue(target, "id"), 3987 + "HX-Current-URL": getDocument().location.href, 3988 + }; 3989 + getValuesForElement(elt, "hx-headers", false, headers); 3990 + if (prompt !== undefined) { 3991 + headers["HX-Prompt"] = prompt; 3992 + } 3993 + if (getInternalData(elt).boosted) { 3994 + headers["HX-Boosted"] = "true"; 3995 + } 3996 + return headers; 3997 + } 3998 + 3999 + /** 4000 + * filterValues takes an object containing form input values 4001 + * and returns a new object that only contains keys that are 4002 + * specified by the closest "hx-params" attribute 4003 + * @param {FormData} inputValues 4004 + * @param {Element} elt 4005 + * @returns {FormData} 4006 + */ 4007 + function filterValues(inputValues, elt) { 4008 + const paramsValue = getClosestAttributeValue(elt, "hx-params"); 4009 + if (paramsValue) { 4010 + if (paramsValue === "none") { 4011 + return new FormData(); 4012 + } else if (paramsValue === "*") { 4013 + return inputValues; 4014 + } else if (paramsValue.indexOf("not ") === 0) { 4015 + forEach(paramsValue.slice(4).split(","), function (name) { 4016 + name = name.trim(); 4017 + inputValues.delete(name); 4018 + }); 4019 + return inputValues; 4020 + } else { 4021 + const newValues = new FormData(); 4022 + forEach(paramsValue.split(","), function (name) { 4023 + name = name.trim(); 4024 + if (inputValues.has(name)) { 4025 + inputValues.getAll(name).forEach(function (value) { 4026 + newValues.append(name, value); 4027 + }); 4028 + } 4029 + }); 4030 + return newValues; 4031 + } 4032 + } else { 4033 + return inputValues; 4034 + } 4035 + } 4036 + 4037 + /** 4038 + * @param {Element} elt 4039 + * @return {boolean} 4040 + */ 4041 + function isAnchorLink(elt) { 4042 + return ( 4043 + !!getRawAttribute(elt, "href") && 4044 + getRawAttribute(elt, "href").indexOf("#") >= 0 4045 + ); 4046 + } 4047 + 4048 + /** 4049 + * @param {Element} elt 4050 + * @param {HtmxSwapStyle} [swapInfoOverride] 4051 + * @returns {HtmxSwapSpecification} 4052 + */ 4053 + function getSwapSpecification(elt, swapInfoOverride) { 4054 + const swapInfo = 4055 + swapInfoOverride || getClosestAttributeValue(elt, "hx-swap"); 4056 + /** @type HtmxSwapSpecification */ 4057 + const swapSpec = { 4058 + swapStyle: getInternalData(elt).boosted 4059 + ? "innerHTML" 4060 + : htmx.config.defaultSwapStyle, 4061 + swapDelay: htmx.config.defaultSwapDelay, 4062 + settleDelay: htmx.config.defaultSettleDelay, 4063 + }; 4064 + if ( 4065 + htmx.config.scrollIntoViewOnBoost && 4066 + getInternalData(elt).boosted && 4067 + !isAnchorLink(elt) 4068 + ) { 4069 + swapSpec.show = "top"; 4070 + } 4071 + if (swapInfo) { 4072 + const split = splitOnWhitespace(swapInfo); 4073 + if (split.length > 0) { 4074 + for (let i = 0; i < split.length; i++) { 4075 + const value = split[i]; 4076 + if (value.indexOf("swap:") === 0) { 4077 + swapSpec.swapDelay = parseInterval(value.slice(5)); 4078 + } else if (value.indexOf("settle:") === 0) { 4079 + swapSpec.settleDelay = parseInterval(value.slice(7)); 4080 + } else if (value.indexOf("transition:") === 0) { 4081 + swapSpec.transition = value.slice(11) === "true"; 4082 + } else if (value.indexOf("ignoreTitle:") === 0) { 4083 + swapSpec.ignoreTitle = value.slice(12) === "true"; 4084 + } else if (value.indexOf("scroll:") === 0) { 4085 + const scrollSpec = value.slice(7); 4086 + var splitSpec = scrollSpec.split(":"); 4087 + const scrollVal = splitSpec.pop(); 4088 + var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null; 4089 + // @ts-ignore 4090 + swapSpec.scroll = scrollVal; 4091 + swapSpec.scrollTarget = selectorVal; 4092 + } else if (value.indexOf("show:") === 0) { 4093 + const showSpec = value.slice(5); 4094 + var splitSpec = showSpec.split(":"); 4095 + const showVal = splitSpec.pop(); 4096 + var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null; 4097 + swapSpec.show = showVal; 4098 + swapSpec.showTarget = selectorVal; 4099 + } else if (value.indexOf("focus-scroll:") === 0) { 4100 + const focusScrollVal = value.slice("focus-scroll:".length); 4101 + swapSpec.focusScroll = focusScrollVal == "true"; 4102 + } else if (i == 0) { 4103 + swapSpec.swapStyle = value; 4104 + } else { 4105 + logError("Unknown modifier in hx-swap: " + value); 4106 + } 4107 + } 4108 + } 4109 + } 4110 + return swapSpec; 4111 + } 4112 + 4113 + /** 4114 + * @param {Element} elt 4115 + * @return {boolean} 4116 + */ 4117 + function usesFormData(elt) { 4118 + return ( 4119 + getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data" || 4120 + (matches(elt, "form") && 4121 + getRawAttribute(elt, "enctype") === "multipart/form-data") 4122 + ); 4123 + } 4124 + 4125 + /** 4126 + * @param {XMLHttpRequest} xhr 4127 + * @param {Element} elt 4128 + * @param {FormData} filteredParameters 4129 + * @returns {*|string|null} 4130 + */ 4131 + function encodeParamsForBody(xhr, elt, filteredParameters) { 4132 + let encodedParameters = null; 4133 + withExtensions(elt, function (extension) { 4134 + if (encodedParameters == null) { 4135 + encodedParameters = extension.encodeParameters( 4136 + xhr, 4137 + filteredParameters, 4138 + elt, 4139 + ); 4140 + } 4141 + }); 4142 + if (encodedParameters != null) { 4143 + return encodedParameters; 4144 + } else { 4145 + if (usesFormData(elt)) { 4146 + // Force conversion to an actual FormData object in case filteredParameters is a formDataProxy 4147 + // See https://github.com/bigskysoftware/htmx/issues/2317 4148 + return overrideFormData( 4149 + new FormData(), 4150 + formDataFromObject(filteredParameters), 4151 + ); 4152 + } else { 4153 + return urlEncode(filteredParameters); 4154 + } 4155 + } 4156 + } 4157 + 4158 + /** 4159 + * 4160 + * @param {Element} target 4161 + * @returns {HtmxSettleInfo} 4162 + */ 4163 + function makeSettleInfo(target) { 4164 + return { tasks: [], elts: [target] }; 4165 + } 4166 + 4167 + /** 4168 + * @param {Element[]} content 4169 + * @param {HtmxSwapSpecification} swapSpec 4170 + */ 4171 + function updateScrollState(content, swapSpec) { 4172 + const first = content[0]; 4173 + const last = content[content.length - 1]; 4174 + if (swapSpec.scroll) { 4175 + var target = null; 4176 + if (swapSpec.scrollTarget) { 4177 + target = asElement(querySelectorExt(first, swapSpec.scrollTarget)); 4178 + } 4179 + if (swapSpec.scroll === "top" && (first || target)) { 4180 + target = target || first; 4181 + target.scrollTop = 0; 4182 + } 4183 + if (swapSpec.scroll === "bottom" && (last || target)) { 4184 + target = target || last; 4185 + target.scrollTop = target.scrollHeight; 4186 + } 4187 + } 4188 + if (swapSpec.show) { 4189 + var target = null; 4190 + if (swapSpec.showTarget) { 4191 + let targetStr = swapSpec.showTarget; 4192 + if (swapSpec.showTarget === "window") { 4193 + targetStr = "body"; 4194 + } 4195 + target = asElement(querySelectorExt(first, targetStr)); 4196 + } 4197 + if (swapSpec.show === "top" && (first || target)) { 4198 + target = target || first; 4199 + // @ts-ignore For some reason tsc doesn't recognize "instant" as a valid option for now 4200 + target.scrollIntoView({ 4201 + block: "start", 4202 + behavior: htmx.config.scrollBehavior, 4203 + }); 4204 + } 4205 + if (swapSpec.show === "bottom" && (last || target)) { 4206 + target = target || last; 4207 + // @ts-ignore For some reason tsc doesn't recognize "instant" as a valid option for now 4208 + target.scrollIntoView({ 4209 + block: "end", 4210 + behavior: htmx.config.scrollBehavior, 4211 + }); 4212 + } 4213 + } 4214 + } 4215 + 4216 + /** 4217 + * @param {Element} elt 4218 + * @param {string} attr 4219 + * @param {boolean=} evalAsDefault 4220 + * @param {Object=} values 4221 + * @returns {Object} 4222 + */ 4223 + function getValuesForElement(elt, attr, evalAsDefault, values) { 4224 + if (values == null) { 4225 + values = {}; 4226 + } 4227 + if (elt == null) { 4228 + return values; 4229 + } 4230 + const attributeValue = getAttributeValue(elt, attr); 4231 + if (attributeValue) { 4232 + let str = attributeValue.trim(); 4233 + let evaluateValue = evalAsDefault; 4234 + if (str === "unset") { 4235 + return null; 4236 + } 4237 + if (str.indexOf("javascript:") === 0) { 4238 + str = str.slice(11); 4239 + evaluateValue = true; 4240 + } else if (str.indexOf("js:") === 0) { 4241 + str = str.slice(3); 4242 + evaluateValue = true; 4243 + } 4244 + if (str.indexOf("{") !== 0) { 4245 + str = "{" + str + "}"; 4246 + } 4247 + let varsValues; 4248 + if (evaluateValue) { 4249 + varsValues = maybeEval( 4250 + elt, 4251 + function () { 4252 + return Function("return (" + str + ")")(); 4253 + }, 4254 + {}, 4255 + ); 4256 + } else { 4257 + varsValues = parseJSON(str); 4258 + } 4259 + for (const key in varsValues) { 4260 + if (varsValues.hasOwnProperty(key)) { 4261 + if (values[key] == null) { 4262 + values[key] = varsValues[key]; 4263 + } 4264 + } 4265 + } 4266 + } 4267 + return getValuesForElement( 4268 + asElement(parentElt(elt)), 4269 + attr, 4270 + evalAsDefault, 4271 + values, 4272 + ); 4273 + } 4274 + 4275 + /** 4276 + * @param {EventTarget|string} elt 4277 + * @param {() => any} toEval 4278 + * @param {any=} defaultVal 4279 + * @returns {any} 4280 + */ 4281 + function maybeEval(elt, toEval, defaultVal) { 4282 + if (htmx.config.allowEval) { 4283 + return toEval(); 4284 + } else { 4285 + triggerErrorEvent(elt, "htmx:evalDisallowedError"); 4286 + return defaultVal; 4287 + } 4288 + } 4289 + 4290 + /** 4291 + * @param {Element} elt 4292 + * @param {*?} expressionVars 4293 + * @returns 4294 + */ 4295 + function getHXVarsForElement(elt, expressionVars) { 4296 + return getValuesForElement(elt, "hx-vars", true, expressionVars); 4297 + } 4298 + 4299 + /** 4300 + * @param {Element} elt 4301 + * @param {*?} expressionVars 4302 + * @returns 4303 + */ 4304 + function getHXValsForElement(elt, expressionVars) { 4305 + return getValuesForElement(elt, "hx-vals", false, expressionVars); 4306 + } 4307 + 4308 + /** 4309 + * @param {Element} elt 4310 + * @returns {FormData} 4311 + */ 4312 + function getExpressionVars(elt) { 4313 + return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt)); 4314 + } 4315 + 4316 + /** 4317 + * @param {XMLHttpRequest} xhr 4318 + * @param {string} header 4319 + * @param {string|null} headerValue 4320 + */ 4321 + function safelySetHeaderValue(xhr, header, headerValue) { 4322 + if (headerValue !== null) { 4323 + try { 4324 + xhr.setRequestHeader(header, headerValue); 4325 + } catch (e) { 4326 + // On an exception, try to set the header URI encoded instead 4327 + xhr.setRequestHeader(header, encodeURIComponent(headerValue)); 4328 + xhr.setRequestHeader(header + "-URI-AutoEncoded", "true"); 4329 + } 4330 + } 4331 + } 4332 + 4333 + /** 4334 + * @param {XMLHttpRequest} xhr 4335 + * @return {string} 4336 + */ 4337 + function getPathFromResponse(xhr) { 4338 + // NB: IE11 does not support this stuff 4339 + if (xhr.responseURL && typeof URL !== "undefined") { 4340 + try { 4341 + const url = new URL(xhr.responseURL); 4342 + return url.pathname + url.search; 4343 + } catch (e) { 4344 + triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", { 4345 + url: xhr.responseURL, 4346 + }); 4347 + } 4348 + } 4349 + } 4350 + 4351 + /** 4352 + * @param {XMLHttpRequest} xhr 4353 + * @param {RegExp} regexp 4354 + * @return {boolean} 4355 + */ 4356 + function hasHeader(xhr, regexp) { 4357 + return regexp.test(xhr.getAllResponseHeaders()); 4358 + } 4359 + 4360 + /** 4361 + * Issues an htmx-style AJAX request 4362 + * 4363 + * @see https://htmx.org/api/#ajax 4364 + * 4365 + * @param {HttpVerb} verb 4366 + * @param {string} path the URL path to make the AJAX 4367 + * @param {Element|string|HtmxAjaxHelperContext} context the element to target (defaults to the **body**) | a selector for the target | a context object that contains any of the following 4368 + * @return {Promise<void>} Promise that resolves immediately if no request is sent, or when the request is complete 4369 + */ 4370 + function ajaxHelper(verb, path, context) { 4371 + verb = /** @type HttpVerb */ (verb.toLowerCase()); 4372 + if (context) { 4373 + if (context instanceof Element || typeof context === "string") { 4374 + return issueAjaxRequest(verb, path, null, null, { 4375 + targetOverride: resolveTarget(context) || DUMMY_ELT, 4376 + returnPromise: true, 4377 + }); 4378 + } else { 4379 + let resolvedTarget = resolveTarget(context.target); 4380 + // If target is supplied but can't resolve OR source is supplied but both target and source can't be resolved 4381 + // then use DUMMY_ELT to abort the request with htmx:targetError to avoid it replacing body by mistake 4382 + if ( 4383 + (context.target && !resolvedTarget) || 4384 + (context.source && !resolvedTarget && !resolveTarget(context.source)) 4385 + ) { 4386 + resolvedTarget = DUMMY_ELT; 4387 + } 4388 + return issueAjaxRequest( 4389 + verb, 4390 + path, 4391 + resolveTarget(context.source), 4392 + context.event, 4393 + { 4394 + handler: context.handler, 4395 + headers: context.headers, 4396 + values: context.values, 4397 + targetOverride: resolvedTarget, 4398 + swapOverride: context.swap, 4399 + select: context.select, 4400 + returnPromise: true, 4401 + }, 4402 + ); 4403 + } 4404 + } else { 4405 + return issueAjaxRequest(verb, path, null, null, { 4406 + returnPromise: true, 4407 + }); 4408 + } 4409 + } 4410 + 4411 + /** 4412 + * @param {Element} elt 4413 + * @return {Element[]} 4414 + */ 4415 + function hierarchyForElt(elt) { 4416 + const arr = []; 4417 + while (elt) { 4418 + arr.push(elt); 4419 + elt = elt.parentElement; 4420 + } 4421 + return arr; 4422 + } 4423 + 4424 + /** 4425 + * @param {Element} elt 4426 + * @param {string} path 4427 + * @param {HtmxRequestConfig} requestConfig 4428 + * @return {boolean} 4429 + */ 4430 + function verifyPath(elt, path, requestConfig) { 4431 + let sameHost; 4432 + let url; 4433 + if (typeof URL === "function") { 4434 + url = new URL(path, document.location.href); 4435 + const origin = document.location.origin; 4436 + sameHost = origin === url.origin; 4437 + } else { 4438 + // IE11 doesn't support URL 4439 + url = path; 4440 + sameHost = startsWith(path, document.location.origin); 4441 + } 4442 + 4443 + if (htmx.config.selfRequestsOnly) { 4444 + if (!sameHost) { 4445 + return false; 4446 + } 4447 + } 4448 + return triggerEvent( 4449 + elt, 4450 + "htmx:validateUrl", 4451 + mergeObjects({ url, sameHost }, requestConfig), 4452 + ); 4453 + } 4454 + 4455 + /** 4456 + * @param {Object|FormData} obj 4457 + * @return {FormData} 4458 + */ 4459 + function formDataFromObject(obj) { 4460 + if (obj instanceof FormData) return obj; 4461 + const formData = new FormData(); 4462 + for (const key in obj) { 4463 + if (obj.hasOwnProperty(key)) { 4464 + if (obj[key] && typeof obj[key].forEach === "function") { 4465 + obj[key].forEach(function (v) { 4466 + formData.append(key, v); 4467 + }); 4468 + } else if ( 4469 + typeof obj[key] === "object" && 4470 + !(obj[key] instanceof Blob) 4471 + ) { 4472 + formData.append(key, JSON.stringify(obj[key])); 4473 + } else { 4474 + formData.append(key, obj[key]); 4475 + } 4476 + } 4477 + } 4478 + return formData; 4479 + } 4480 + 4481 + /** 4482 + * @param {FormData} formData 4483 + * @param {string} name 4484 + * @param {Array} array 4485 + * @returns {Array} 4486 + */ 4487 + function formDataArrayProxy(formData, name, array) { 4488 + // mutating the array should mutate the underlying form data 4489 + return new Proxy(array, { 4490 + get: function (target, key) { 4491 + if (typeof key === "number") return target[key]; 4492 + if (key === "length") return target.length; 4493 + if (key === "push") { 4494 + return function (value) { 4495 + target.push(value); 4496 + formData.append(name, value); 4497 + }; 4498 + } 4499 + if (typeof target[key] === "function") { 4500 + return function () { 4501 + target[key].apply(target, arguments); 4502 + formData.delete(name); 4503 + target.forEach(function (v) { 4504 + formData.append(name, v); 4505 + }); 4506 + }; 4507 + } 4508 + 4509 + if (target[key] && target[key].length === 1) { 4510 + return target[key][0]; 4511 + } else { 4512 + return target[key]; 4513 + } 4514 + }, 4515 + set: function (target, index, value) { 4516 + target[index] = value; 4517 + formData.delete(name); 4518 + target.forEach(function (v) { 4519 + formData.append(name, v); 4520 + }); 4521 + return true; 4522 + }, 4523 + }); 4524 + } 4525 + 4526 + /** 4527 + * @param {FormData} formData 4528 + * @returns {Object} 4529 + */ 4530 + function formDataProxy(formData) { 4531 + return new Proxy(formData, { 4532 + get: function (target, name) { 4533 + if (typeof name === "symbol") { 4534 + // Forward symbol calls to the FormData itself directly 4535 + const result = Reflect.get(target, name); 4536 + // Wrap in function with apply to correctly bind the FormData context, as a direct call would result in an illegal invocation error 4537 + if (typeof result === "function") { 4538 + return function () { 4539 + return result.apply(formData, arguments); 4540 + }; 4541 + } else { 4542 + return result; 4543 + } 4544 + } 4545 + if (name === "toJSON") { 4546 + // Support JSON.stringify call on proxy 4547 + return () => Object.fromEntries(formData); 4548 + } 4549 + if (name in target) { 4550 + // Wrap in function with apply to correctly bind the FormData context, as a direct call would result in an illegal invocation error 4551 + if (typeof target[name] === "function") { 4552 + return function () { 4553 + return formData[name].apply(formData, arguments); 4554 + }; 4555 + } else { 4556 + return target[name]; 4557 + } 4558 + } 4559 + const array = formData.getAll(name); 4560 + // Those 2 undefined & single value returns are for retro-compatibility as we weren't using FormData before 4561 + if (array.length === 0) { 4562 + return undefined; 4563 + } else if (array.length === 1) { 4564 + return array[0]; 4565 + } else { 4566 + return formDataArrayProxy(target, name, array); 4567 + } 4568 + }, 4569 + set: function (target, name, value) { 4570 + if (typeof name !== "string") { 4571 + return false; 4572 + } 4573 + target.delete(name); 4574 + if (value && typeof value.forEach === "function") { 4575 + value.forEach(function (v) { 4576 + target.append(name, v); 4577 + }); 4578 + } else if (typeof value === "object" && !(value instanceof Blob)) { 4579 + target.append(name, JSON.stringify(value)); 4580 + } else { 4581 + target.append(name, value); 4582 + } 4583 + return true; 4584 + }, 4585 + deleteProperty: function (target, name) { 4586 + if (typeof name === "string") { 4587 + target.delete(name); 4588 + } 4589 + return true; 4590 + }, 4591 + // Support Object.assign call from proxy 4592 + ownKeys: function (target) { 4593 + return Reflect.ownKeys(Object.fromEntries(target)); 4594 + }, 4595 + getOwnPropertyDescriptor: function (target, prop) { 4596 + return Reflect.getOwnPropertyDescriptor( 4597 + Object.fromEntries(target), 4598 + prop, 4599 + ); 4600 + }, 4601 + }); 4602 + } 4603 + 4604 + /** 4605 + * @param {HttpVerb} verb 4606 + * @param {string} path 4607 + * @param {Element} elt 4608 + * @param {Event} event 4609 + * @param {HtmxAjaxEtc} [etc] 4610 + * @param {boolean} [confirmed] 4611 + * @return {Promise<void>} 4612 + */ 4613 + function issueAjaxRequest(verb, path, elt, event, etc, confirmed) { 4614 + let resolve = null; 4615 + let reject = null; 4616 + etc = etc != null ? etc : {}; 4617 + if (etc.returnPromise && typeof Promise !== "undefined") { 4618 + var promise = new Promise(function (_resolve, _reject) { 4619 + resolve = _resolve; 4620 + reject = _reject; 4621 + }); 4622 + } 4623 + if (elt == null) { 4624 + elt = getDocument().body; 4625 + } 4626 + const responseHandler = etc.handler || handleAjaxResponse; 4627 + const select = etc.select || null; 4628 + 4629 + if (!bodyContains(elt)) { 4630 + // do not issue requests for elements removed from the DOM 4631 + maybeCall(resolve); 4632 + return promise; 4633 + } 4634 + const target = etc.targetOverride || asElement(getTarget(elt)); 4635 + if (target == null || target == DUMMY_ELT) { 4636 + triggerErrorEvent(elt, "htmx:targetError", { 4637 + target: getAttributeValue(elt, "hx-target"), 4638 + }); 4639 + maybeCall(reject); 4640 + return promise; 4641 + } 4642 + 4643 + let eltData = getInternalData(elt); 4644 + const submitter = eltData.lastButtonClicked; 4645 + 4646 + if (submitter) { 4647 + const buttonPath = getRawAttribute(submitter, "formaction"); 4648 + if (buttonPath != null) { 4649 + path = buttonPath; 4650 + } 4651 + 4652 + const buttonVerb = getRawAttribute(submitter, "formmethod"); 4653 + if (buttonVerb != null) { 4654 + // ignore buttons with formmethod="dialog" 4655 + if (buttonVerb.toLowerCase() !== "dialog") { 4656 + verb = /** @type HttpVerb */ (buttonVerb); 4657 + } 4658 + } 4659 + } 4660 + 4661 + const confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); 4662 + // allow event-based confirmation w/ a callback 4663 + if (confirmed === undefined) { 4664 + const issueRequest = function (skipConfirmation) { 4665 + return issueAjaxRequest( 4666 + verb, 4667 + path, 4668 + elt, 4669 + event, 4670 + etc, 4671 + !!skipConfirmation, 4672 + ); 4673 + }; 4674 + const confirmDetails = { 4675 + target, 4676 + elt, 4677 + path, 4678 + verb, 4679 + triggeringEvent: event, 4680 + etc, 4681 + issueRequest, 4682 + question: confirmQuestion, 4683 + }; 4684 + if (triggerEvent(elt, "htmx:confirm", confirmDetails) === false) { 4685 + maybeCall(resolve); 4686 + return promise; 4687 + } 4688 + } 4689 + 4690 + let syncElt = elt; 4691 + let syncStrategy = getClosestAttributeValue(elt, "hx-sync"); 4692 + let queueStrategy = null; 4693 + let abortable = false; 4694 + if (syncStrategy) { 4695 + const syncStrings = syncStrategy.split(":"); 4696 + const selector = syncStrings[0].trim(); 4697 + if (selector === "this") { 4698 + syncElt = findThisElement(elt, "hx-sync"); 4699 + } else { 4700 + syncElt = asElement(querySelectorExt(elt, selector)); 4701 + } 4702 + // default to the drop strategy 4703 + syncStrategy = (syncStrings[1] || "drop").trim(); 4704 + eltData = getInternalData(syncElt); 4705 + if ( 4706 + syncStrategy === "drop" && 4707 + eltData.xhr && 4708 + eltData.abortable !== true 4709 + ) { 4710 + maybeCall(resolve); 4711 + return promise; 4712 + } else if (syncStrategy === "abort") { 4713 + if (eltData.xhr) { 4714 + maybeCall(resolve); 4715 + return promise; 4716 + } else { 4717 + abortable = true; 4718 + } 4719 + } else if (syncStrategy === "replace") { 4720 + triggerEvent(syncElt, "htmx:abort"); // abort the current request and continue 4721 + } else if (syncStrategy.indexOf("queue") === 0) { 4722 + const queueStrArray = syncStrategy.split(" "); 4723 + queueStrategy = (queueStrArray[1] || "last").trim(); 4724 + } 4725 + } 4726 + 4727 + if (eltData.xhr) { 4728 + if (eltData.abortable) { 4729 + triggerEvent(syncElt, "htmx:abort"); // abort the current request and continue 4730 + } else { 4731 + if (queueStrategy == null) { 4732 + if (event) { 4733 + const eventData = getInternalData(event); 4734 + if ( 4735 + eventData && 4736 + eventData.triggerSpec && 4737 + eventData.triggerSpec.queue 4738 + ) { 4739 + queueStrategy = eventData.triggerSpec.queue; 4740 + } 4741 + } 4742 + if (queueStrategy == null) { 4743 + queueStrategy = "last"; 4744 + } 4745 + } 4746 + if (eltData.queuedRequests == null) { 4747 + eltData.queuedRequests = []; 4748 + } 4749 + if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { 4750 + eltData.queuedRequests.push(function () { 4751 + issueAjaxRequest(verb, path, elt, event, etc); 4752 + }); 4753 + } else if (queueStrategy === "all") { 4754 + eltData.queuedRequests.push(function () { 4755 + issueAjaxRequest(verb, path, elt, event, etc); 4756 + }); 4757 + } else if (queueStrategy === "last") { 4758 + eltData.queuedRequests = []; // dump existing queue 4759 + eltData.queuedRequests.push(function () { 4760 + issueAjaxRequest(verb, path, elt, event, etc); 4761 + }); 4762 + } 4763 + maybeCall(resolve); 4764 + return promise; 4765 + } 4766 + } 4767 + 4768 + const xhr = new XMLHttpRequest(); 4769 + eltData.xhr = xhr; 4770 + eltData.abortable = abortable; 4771 + const endRequestLock = function () { 4772 + eltData.xhr = null; 4773 + eltData.abortable = false; 4774 + if (eltData.queuedRequests != null && eltData.queuedRequests.length > 0) { 4775 + const queuedRequest = eltData.queuedRequests.shift(); 4776 + queuedRequest(); 4777 + } 4778 + }; 4779 + const promptQuestion = getClosestAttributeValue(elt, "hx-prompt"); 4780 + if (promptQuestion) { 4781 + var promptResponse = prompt(promptQuestion); 4782 + // prompt returns null if cancelled and empty string if accepted with no entry 4783 + if ( 4784 + promptResponse === null || 4785 + !triggerEvent(elt, "htmx:prompt", { prompt: promptResponse, target }) 4786 + ) { 4787 + maybeCall(resolve); 4788 + endRequestLock(); 4789 + return promise; 4790 + } 4791 + } 4792 + 4793 + if (confirmQuestion && !confirmed) { 4794 + if (!confirm(confirmQuestion)) { 4795 + maybeCall(resolve); 4796 + endRequestLock(); 4797 + return promise; 4798 + } 4799 + } 4800 + 4801 + let headers = getHeaders(elt, target, promptResponse); 4802 + 4803 + if (verb !== "get" && !usesFormData(elt)) { 4804 + headers["Content-Type"] = "application/x-www-form-urlencoded"; 4805 + } 4806 + 4807 + if (etc.headers) { 4808 + headers = mergeObjects(headers, etc.headers); 4809 + } 4810 + const results = getInputValues(elt, verb); 4811 + let errors = results.errors; 4812 + const rawFormData = results.formData; 4813 + if (etc.values) { 4814 + overrideFormData(rawFormData, formDataFromObject(etc.values)); 4815 + } 4816 + const expressionVars = formDataFromObject(getExpressionVars(elt)); 4817 + const allFormData = overrideFormData(rawFormData, expressionVars); 4818 + let filteredFormData = filterValues(allFormData, elt); 4819 + 4820 + if (htmx.config.getCacheBusterParam && verb === "get") { 4821 + filteredFormData.set( 4822 + "org.htmx.cache-buster", 4823 + getRawAttribute(target, "id") || "true", 4824 + ); 4825 + } 4826 + 4827 + // behavior of anchors w/ empty href is to use the current URL 4828 + if (path == null || path === "") { 4829 + path = getDocument().location.href; 4830 + } 4831 + 4832 + /** 4833 + * @type {Object} 4834 + * @property {boolean} [credentials] 4835 + * @property {number} [timeout] 4836 + * @property {boolean} [noHeaders] 4837 + */ 4838 + const requestAttrValues = getValuesForElement(elt, "hx-request"); 4839 + 4840 + const eltIsBoosted = getInternalData(elt).boosted; 4841 + 4842 + let useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0; 4843 + 4844 + /** @type HtmxRequestConfig */ 4845 + const requestConfig = { 4846 + boosted: eltIsBoosted, 4847 + useUrlParams, 4848 + formData: filteredFormData, 4849 + parameters: formDataProxy(filteredFormData), 4850 + unfilteredFormData: allFormData, 4851 + unfilteredParameters: formDataProxy(allFormData), 4852 + headers, 4853 + target, 4854 + verb, 4855 + errors, 4856 + withCredentials: 4857 + etc.credentials || 4858 + requestAttrValues.credentials || 4859 + htmx.config.withCredentials, 4860 + timeout: etc.timeout || requestAttrValues.timeout || htmx.config.timeout, 4861 + path, 4862 + triggeringEvent: event, 4863 + }; 4864 + 4865 + if (!triggerEvent(elt, "htmx:configRequest", requestConfig)) { 4866 + maybeCall(resolve); 4867 + endRequestLock(); 4868 + return promise; 4869 + } 4870 + 4871 + // copy out in case the object was overwritten 4872 + path = requestConfig.path; 4873 + verb = requestConfig.verb; 4874 + headers = requestConfig.headers; 4875 + filteredFormData = formDataFromObject(requestConfig.parameters); 4876 + errors = requestConfig.errors; 4877 + useUrlParams = requestConfig.useUrlParams; 4878 + 4879 + if (errors && errors.length > 0) { 4880 + triggerEvent(elt, "htmx:validation:halted", requestConfig); 4881 + maybeCall(resolve); 4882 + endRequestLock(); 4883 + return promise; 4884 + } 4885 + 4886 + const splitPath = path.split("#"); 4887 + const pathNoAnchor = splitPath[0]; 4888 + const anchor = splitPath[1]; 4889 + 4890 + let finalPath = path; 4891 + if (useUrlParams) { 4892 + finalPath = pathNoAnchor; 4893 + const hasValues = !filteredFormData.keys().next().done; 4894 + if (hasValues) { 4895 + if (finalPath.indexOf("?") < 0) { 4896 + finalPath += "?"; 4897 + } else { 4898 + finalPath += "&"; 4899 + } 4900 + finalPath += urlEncode(filteredFormData); 4901 + if (anchor) { 4902 + finalPath += "#" + anchor; 4903 + } 4904 + } 4905 + } 4906 + 4907 + if (!verifyPath(elt, finalPath, requestConfig)) { 4908 + triggerErrorEvent(elt, "htmx:invalidPath", requestConfig); 4909 + maybeCall(reject); 4910 + return promise; 4911 + } 4912 + 4913 + xhr.open(verb.toUpperCase(), finalPath, true); 4914 + xhr.overrideMimeType("text/html"); 4915 + xhr.withCredentials = requestConfig.withCredentials; 4916 + xhr.timeout = requestConfig.timeout; 4917 + 4918 + // request headers 4919 + if (requestAttrValues.noHeaders) { 4920 + // ignore all headers 4921 + } else { 4922 + for (const header in headers) { 4923 + if (headers.hasOwnProperty(header)) { 4924 + const headerValue = headers[header]; 4925 + safelySetHeaderValue(xhr, header, headerValue); 4926 + } 4927 + } 4928 + } 4929 + 4930 + /** @type {HtmxResponseInfo} */ 4931 + const responseInfo = { 4932 + xhr, 4933 + target, 4934 + requestConfig, 4935 + etc, 4936 + boosted: eltIsBoosted, 4937 + select, 4938 + pathInfo: { 4939 + requestPath: path, 4940 + finalRequestPath: finalPath, 4941 + responsePath: null, 4942 + anchor, 4943 + }, 4944 + }; 4945 + 4946 + xhr.onload = function () { 4947 + try { 4948 + const hierarchy = hierarchyForElt(elt); 4949 + responseInfo.pathInfo.responsePath = getPathFromResponse(xhr); 4950 + responseHandler(elt, responseInfo); 4951 + if (responseInfo.keepIndicators !== true) { 4952 + removeRequestIndicators(indicators, disableElts); 4953 + } 4954 + triggerEvent(elt, "htmx:afterRequest", responseInfo); 4955 + triggerEvent(elt, "htmx:afterOnLoad", responseInfo); 4956 + // if the body no longer contains the element, trigger the event on the closest parent 4957 + // remaining in the DOM 4958 + if (!bodyContains(elt)) { 4959 + let secondaryTriggerElt = null; 4960 + while (hierarchy.length > 0 && secondaryTriggerElt == null) { 4961 + const parentEltInHierarchy = hierarchy.shift(); 4962 + if (bodyContains(parentEltInHierarchy)) { 4963 + secondaryTriggerElt = parentEltInHierarchy; 4964 + } 4965 + } 4966 + if (secondaryTriggerElt) { 4967 + triggerEvent( 4968 + secondaryTriggerElt, 4969 + "htmx:afterRequest", 4970 + responseInfo, 4971 + ); 4972 + triggerEvent(secondaryTriggerElt, "htmx:afterOnLoad", responseInfo); 4973 + } 4974 + } 4975 + maybeCall(resolve); 4976 + endRequestLock(); 4977 + } catch (e) { 4978 + triggerErrorEvent( 4979 + elt, 4980 + "htmx:onLoadError", 4981 + mergeObjects({ error: e }, responseInfo), 4982 + ); 4983 + throw e; 4984 + } 4985 + }; 4986 + xhr.onerror = function () { 4987 + removeRequestIndicators(indicators, disableElts); 4988 + triggerErrorEvent(elt, "htmx:afterRequest", responseInfo); 4989 + triggerErrorEvent(elt, "htmx:sendError", responseInfo); 4990 + maybeCall(reject); 4991 + endRequestLock(); 4992 + }; 4993 + xhr.onabort = function () { 4994 + removeRequestIndicators(indicators, disableElts); 4995 + triggerErrorEvent(elt, "htmx:afterRequest", responseInfo); 4996 + triggerErrorEvent(elt, "htmx:sendAbort", responseInfo); 4997 + maybeCall(reject); 4998 + endRequestLock(); 4999 + }; 5000 + xhr.ontimeout = function () { 5001 + removeRequestIndicators(indicators, disableElts); 5002 + triggerErrorEvent(elt, "htmx:afterRequest", responseInfo); 5003 + triggerErrorEvent(elt, "htmx:timeout", responseInfo); 5004 + maybeCall(reject); 5005 + endRequestLock(); 5006 + }; 5007 + if (!triggerEvent(elt, "htmx:beforeRequest", responseInfo)) { 5008 + maybeCall(resolve); 5009 + endRequestLock(); 5010 + return promise; 5011 + } 5012 + var indicators = addRequestIndicatorClasses(elt); 5013 + var disableElts = disableElements(elt); 5014 + 5015 + forEach( 5016 + ["loadstart", "loadend", "progress", "abort"], 5017 + function (eventName) { 5018 + forEach([xhr, xhr.upload], function (target) { 5019 + target.addEventListener(eventName, function (event) { 5020 + triggerEvent(elt, "htmx:xhr:" + eventName, { 5021 + lengthComputable: event.lengthComputable, 5022 + loaded: event.loaded, 5023 + total: event.total, 5024 + }); 5025 + }); 5026 + }); 5027 + }, 5028 + ); 5029 + triggerEvent(elt, "htmx:beforeSend", responseInfo); 5030 + const params = useUrlParams 5031 + ? null 5032 + : encodeParamsForBody(xhr, elt, filteredFormData); 5033 + xhr.send(params); 5034 + return promise; 5035 + } 5036 + 5037 + /** 5038 + * @typedef {Object} HtmxHistoryUpdate 5039 + * @property {string|null} [type] 5040 + * @property {string|null} [path] 5041 + */ 5042 + 5043 + /** 5044 + * @param {Element} elt 5045 + * @param {HtmxResponseInfo} responseInfo 5046 + * @return {HtmxHistoryUpdate} 5047 + */ 5048 + function determineHistoryUpdates(elt, responseInfo) { 5049 + const xhr = responseInfo.xhr; 5050 + 5051 + //= ========================================== 5052 + // First consult response headers 5053 + //= ========================================== 5054 + let pathFromHeaders = null; 5055 + let typeFromHeaders = null; 5056 + if (hasHeader(xhr, /HX-Push:/i)) { 5057 + pathFromHeaders = xhr.getResponseHeader("HX-Push"); 5058 + typeFromHeaders = "push"; 5059 + } else if (hasHeader(xhr, /HX-Push-Url:/i)) { 5060 + pathFromHeaders = xhr.getResponseHeader("HX-Push-Url"); 5061 + typeFromHeaders = "push"; 5062 + } else if (hasHeader(xhr, /HX-Replace-Url:/i)) { 5063 + pathFromHeaders = xhr.getResponseHeader("HX-Replace-Url"); 5064 + typeFromHeaders = "replace"; 5065 + } 5066 + 5067 + // if there was a response header, that has priority 5068 + if (pathFromHeaders) { 5069 + if (pathFromHeaders === "false") { 5070 + return {}; 5071 + } else { 5072 + return { 5073 + type: typeFromHeaders, 5074 + path: pathFromHeaders, 5075 + }; 5076 + } 5077 + } 5078 + 5079 + //= ========================================== 5080 + // Next resolve via DOM values 5081 + //= ========================================== 5082 + const requestPath = responseInfo.pathInfo.finalRequestPath; 5083 + const responsePath = responseInfo.pathInfo.responsePath; 5084 + 5085 + const pushUrl = getClosestAttributeValue(elt, "hx-push-url"); 5086 + const replaceUrl = getClosestAttributeValue(elt, "hx-replace-url"); 5087 + const elementIsBoosted = getInternalData(elt).boosted; 5088 + 5089 + let saveType = null; 5090 + let path = null; 5091 + 5092 + if (pushUrl) { 5093 + saveType = "push"; 5094 + path = pushUrl; 5095 + } else if (replaceUrl) { 5096 + saveType = "replace"; 5097 + path = replaceUrl; 5098 + } else if (elementIsBoosted) { 5099 + saveType = "push"; 5100 + path = responsePath || requestPath; // if there is no response path, go with the original request path 5101 + } 5102 + 5103 + if (path) { 5104 + // false indicates no push, return empty object 5105 + if (path === "false") { 5106 + return {}; 5107 + } 5108 + 5109 + // true indicates we want to follow wherever the server ended up sending us 5110 + if (path === "true") { 5111 + path = responsePath || requestPath; // if there is no response path, go with the original request path 5112 + } 5113 + 5114 + // restore any anchor associated with the request 5115 + if (responseInfo.pathInfo.anchor && path.indexOf("#") === -1) { 5116 + path = path + "#" + responseInfo.pathInfo.anchor; 5117 + } 5118 + 5119 + return { 5120 + type: saveType, 5121 + path, 5122 + }; 5123 + } else { 5124 + return {}; 5125 + } 5126 + } 5127 + 5128 + /** 5129 + * @param {HtmxResponseHandlingConfig} responseHandlingConfig 5130 + * @param {number} status 5131 + * @return {boolean} 5132 + */ 5133 + function codeMatches(responseHandlingConfig, status) { 5134 + var regExp = new RegExp(responseHandlingConfig.code); 5135 + return regExp.test(status.toString(10)); 5136 + } 5137 + 5138 + /** 5139 + * @param {XMLHttpRequest} xhr 5140 + * @return {HtmxResponseHandlingConfig} 5141 + */ 5142 + function resolveResponseHandling(xhr) { 5143 + for (var i = 0; i < htmx.config.responseHandling.length; i++) { 5144 + /** @type HtmxResponseHandlingConfig */ 5145 + var responseHandlingElement = htmx.config.responseHandling[i]; 5146 + if (codeMatches(responseHandlingElement, xhr.status)) { 5147 + return responseHandlingElement; 5148 + } 5149 + } 5150 + // no matches, return no swap 5151 + return { 5152 + swap: false, 5153 + }; 5154 + } 5155 + 5156 + /** 5157 + * @param {string} title 5158 + */ 5159 + function handleTitle(title) { 5160 + if (title) { 5161 + const titleElt = find("title"); 5162 + if (titleElt) { 5163 + titleElt.innerHTML = title; 5164 + } else { 5165 + window.document.title = title; 5166 + } 5167 + } 5168 + } 5169 + 5170 + /** 5171 + * @param {Element} elt 5172 + * @param {HtmxResponseInfo} responseInfo 5173 + */ 5174 + function handleAjaxResponse(elt, responseInfo) { 5175 + const xhr = responseInfo.xhr; 5176 + let target = responseInfo.target; 5177 + const etc = responseInfo.etc; 5178 + const responseInfoSelect = responseInfo.select; 5179 + 5180 + if (!triggerEvent(elt, "htmx:beforeOnLoad", responseInfo)) return; 5181 + 5182 + if (hasHeader(xhr, /HX-Trigger:/i)) { 5183 + handleTriggerHeader(xhr, "HX-Trigger", elt); 5184 + } 5185 + 5186 + if (hasHeader(xhr, /HX-Location:/i)) { 5187 + saveCurrentPageToHistory(); 5188 + let redirectPath = xhr.getResponseHeader("HX-Location"); 5189 + /** @type {HtmxAjaxHelperContext&{path:string}} */ 5190 + var redirectSwapSpec; 5191 + if (redirectPath.indexOf("{") === 0) { 5192 + redirectSwapSpec = parseJSON(redirectPath); 5193 + // what's the best way to throw an error if the user didn't include this 5194 + redirectPath = redirectSwapSpec.path; 5195 + delete redirectSwapSpec.path; 5196 + } 5197 + ajaxHelper("get", redirectPath, redirectSwapSpec).then(function () { 5198 + pushUrlIntoHistory(redirectPath); 5199 + }); 5200 + return; 5201 + } 5202 + 5203 + const shouldRefresh = 5204 + hasHeader(xhr, /HX-Refresh:/i) && 5205 + xhr.getResponseHeader("HX-Refresh") === "true"; 5206 + 5207 + if (hasHeader(xhr, /HX-Redirect:/i)) { 5208 + responseInfo.keepIndicators = true; 5209 + location.href = xhr.getResponseHeader("HX-Redirect"); 5210 + shouldRefresh && location.reload(); 5211 + return; 5212 + } 5213 + 5214 + if (shouldRefresh) { 5215 + responseInfo.keepIndicators = true; 5216 + location.reload(); 5217 + return; 5218 + } 5219 + 5220 + if (hasHeader(xhr, /HX-Retarget:/i)) { 5221 + if (xhr.getResponseHeader("HX-Retarget") === "this") { 5222 + responseInfo.target = elt; 5223 + } else { 5224 + responseInfo.target = asElement( 5225 + querySelectorExt(elt, xhr.getResponseHeader("HX-Retarget")), 5226 + ); 5227 + } 5228 + } 5229 + 5230 + const historyUpdate = determineHistoryUpdates(elt, responseInfo); 5231 + 5232 + const responseHandling = resolveResponseHandling(xhr); 5233 + const shouldSwap = responseHandling.swap; 5234 + let isError = !!responseHandling.error; 5235 + let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle; 5236 + let selectOverride = responseHandling.select; 5237 + if (responseHandling.target) { 5238 + responseInfo.target = asElement( 5239 + querySelectorExt(elt, responseHandling.target), 5240 + ); 5241 + } 5242 + var swapOverride = etc.swapOverride; 5243 + if (swapOverride == null && responseHandling.swapOverride) { 5244 + swapOverride = responseHandling.swapOverride; 5245 + } 5246 + 5247 + // response headers override response handling config 5248 + if (hasHeader(xhr, /HX-Retarget:/i)) { 5249 + if (xhr.getResponseHeader("HX-Retarget") === "this") { 5250 + responseInfo.target = elt; 5251 + } else { 5252 + responseInfo.target = asElement( 5253 + querySelectorExt(elt, xhr.getResponseHeader("HX-Retarget")), 5254 + ); 5255 + } 5256 + } 5257 + if (hasHeader(xhr, /HX-Reswap:/i)) { 5258 + swapOverride = xhr.getResponseHeader("HX-Reswap"); 5259 + } 5260 + 5261 + var serverResponse = xhr.response; 5262 + /** @type HtmxBeforeSwapDetails */ 5263 + var beforeSwapDetails = mergeObjects( 5264 + { 5265 + shouldSwap, 5266 + serverResponse, 5267 + isError, 5268 + ignoreTitle, 5269 + selectOverride, 5270 + swapOverride, 5271 + }, 5272 + responseInfo, 5273 + ); 5274 + 5275 + if ( 5276 + responseHandling.event && 5277 + !triggerEvent(target, responseHandling.event, beforeSwapDetails) 5278 + ) 5279 + return; 5280 + 5281 + if (!triggerEvent(target, "htmx:beforeSwap", beforeSwapDetails)) return; 5282 + 5283 + target = beforeSwapDetails.target; // allow re-targeting 5284 + serverResponse = beforeSwapDetails.serverResponse; // allow updating content 5285 + isError = beforeSwapDetails.isError; // allow updating error 5286 + ignoreTitle = beforeSwapDetails.ignoreTitle; // allow updating ignoring title 5287 + selectOverride = beforeSwapDetails.selectOverride; // allow updating select override 5288 + swapOverride = beforeSwapDetails.swapOverride; // allow updating swap override 5289 + 5290 + responseInfo.target = target; // Make updated target available to response events 5291 + responseInfo.failed = isError; // Make failed property available to response events 5292 + responseInfo.successful = !isError; // Make successful property available to response events 5293 + 5294 + if (beforeSwapDetails.shouldSwap) { 5295 + if (xhr.status === 286) { 5296 + cancelPolling(elt); 5297 + } 5298 + 5299 + withExtensions(elt, function (extension) { 5300 + serverResponse = extension.transformResponse(serverResponse, xhr, elt); 5301 + }); 5302 + 5303 + // Save current page if there will be a history update 5304 + if (historyUpdate.type) { 5305 + saveCurrentPageToHistory(); 5306 + } 5307 + 5308 + var swapSpec = getSwapSpecification(elt, swapOverride); 5309 + 5310 + if (!swapSpec.hasOwnProperty("ignoreTitle")) { 5311 + swapSpec.ignoreTitle = ignoreTitle; 5312 + } 5313 + 5314 + target.classList.add(htmx.config.swappingClass); 5315 + 5316 + // optional transition API promise callbacks 5317 + let settleResolve = null; 5318 + let settleReject = null; 5319 + 5320 + if (responseInfoSelect) { 5321 + selectOverride = responseInfoSelect; 5322 + } 5323 + 5324 + if (hasHeader(xhr, /HX-Reselect:/i)) { 5325 + selectOverride = xhr.getResponseHeader("HX-Reselect"); 5326 + } 5327 + 5328 + const selectOOB = getClosestAttributeValue(elt, "hx-select-oob"); 5329 + const select = getClosestAttributeValue(elt, "hx-select"); 5330 + 5331 + let doSwap = function () { 5332 + try { 5333 + // if we need to save history, do so, before swapping so that relative resources have the correct base URL 5334 + if (historyUpdate.type) { 5335 + triggerEvent( 5336 + getDocument().body, 5337 + "htmx:beforeHistoryUpdate", 5338 + mergeObjects({ history: historyUpdate }, responseInfo), 5339 + ); 5340 + if (historyUpdate.type === "push") { 5341 + pushUrlIntoHistory(historyUpdate.path); 5342 + triggerEvent(getDocument().body, "htmx:pushedIntoHistory", { 5343 + path: historyUpdate.path, 5344 + }); 5345 + } else { 5346 + replaceUrlInHistory(historyUpdate.path); 5347 + triggerEvent(getDocument().body, "htmx:replacedInHistory", { 5348 + path: historyUpdate.path, 5349 + }); 5350 + } 5351 + } 5352 + 5353 + swap(target, serverResponse, swapSpec, { 5354 + select: selectOverride || select, 5355 + selectOOB, 5356 + eventInfo: responseInfo, 5357 + anchor: responseInfo.pathInfo.anchor, 5358 + contextElement: elt, 5359 + afterSwapCallback: function () { 5360 + if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) { 5361 + let finalElt = elt; 5362 + if (!bodyContains(elt)) { 5363 + finalElt = getDocument().body; 5364 + } 5365 + handleTriggerHeader(xhr, "HX-Trigger-After-Swap", finalElt); 5366 + } 5367 + }, 5368 + afterSettleCallback: function () { 5369 + if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) { 5370 + let finalElt = elt; 5371 + if (!bodyContains(elt)) { 5372 + finalElt = getDocument().body; 5373 + } 5374 + handleTriggerHeader(xhr, "HX-Trigger-After-Settle", finalElt); 5375 + } 5376 + maybeCall(settleResolve); 5377 + }, 5378 + }); 5379 + } catch (e) { 5380 + triggerErrorEvent(elt, "htmx:swapError", responseInfo); 5381 + maybeCall(settleReject); 5382 + throw e; 5383 + } 5384 + }; 5385 + 5386 + let shouldTransition = htmx.config.globalViewTransitions; 5387 + if (swapSpec.hasOwnProperty("transition")) { 5388 + shouldTransition = swapSpec.transition; 5389 + } 5390 + 5391 + if ( 5392 + shouldTransition && 5393 + triggerEvent(elt, "htmx:beforeTransition", responseInfo) && 5394 + typeof Promise !== "undefined" && 5395 + // @ts-ignore experimental feature atm 5396 + document.startViewTransition 5397 + ) { 5398 + const settlePromise = new Promise(function (_resolve, _reject) { 5399 + settleResolve = _resolve; 5400 + settleReject = _reject; 5401 + }); 5402 + // wrap the original doSwap() in a call to startViewTransition() 5403 + const innerDoSwap = doSwap; 5404 + doSwap = function () { 5405 + // @ts-ignore experimental feature atm 5406 + document.startViewTransition(function () { 5407 + innerDoSwap(); 5408 + return settlePromise; 5409 + }); 5410 + }; 5411 + } 5412 + 5413 + if (swapSpec.swapDelay > 0) { 5414 + getWindow().setTimeout(doSwap, swapSpec.swapDelay); 5415 + } else { 5416 + doSwap(); 5417 + } 5418 + } 5419 + if (isError) { 5420 + triggerErrorEvent( 5421 + elt, 5422 + "htmx:responseError", 5423 + mergeObjects( 5424 + { 5425 + error: 5426 + "Response Status Error Code " + 5427 + xhr.status + 5428 + " from " + 5429 + responseInfo.pathInfo.requestPath, 5430 + }, 5431 + responseInfo, 5432 + ), 5433 + ); 5434 + } 5435 + } 5436 + 5437 + //= =================================================================== 5438 + // Extensions API 5439 + //= =================================================================== 5440 + 5441 + /** @type {Object<string, HtmxExtension>} */ 5442 + const extensions = {}; 5443 + 5444 + /** 5445 + * extensionBase defines the default functions for all extensions. 5446 + * @returns {HtmxExtension} 5447 + */ 5448 + function extensionBase() { 5449 + return { 5450 + init: function (api) { 5451 + return null; 5452 + }, 5453 + getSelectors: function () { 5454 + return null; 5455 + }, 5456 + onEvent: function (name, evt) { 5457 + return true; 5458 + }, 5459 + transformResponse: function (text, xhr, elt) { 5460 + return text; 5461 + }, 5462 + isInlineSwap: function (swapStyle) { 5463 + return false; 5464 + }, 5465 + handleSwap: function (swapStyle, target, fragment, settleInfo) { 5466 + return false; 5467 + }, 5468 + encodeParameters: function (xhr, parameters, elt) { 5469 + return null; 5470 + }, 5471 + }; 5472 + } 5473 + 5474 + /** 5475 + * defineExtension initializes the extension and adds it to the htmx registry 5476 + * 5477 + * @see https://htmx.org/api/#defineExtension 5478 + * 5479 + * @param {string} name the extension name 5480 + * @param {Partial<HtmxExtension>} extension the extension definition 5481 + */ 5482 + function defineExtension(name, extension) { 5483 + if (extension.init) { 5484 + extension.init(internalAPI); 5485 + } 5486 + extensions[name] = mergeObjects(extensionBase(), extension); 5487 + } 5488 + 5489 + /** 5490 + * removeExtension removes an extension from the htmx registry 5491 + * 5492 + * @see https://htmx.org/api/#removeExtension 5493 + * 5494 + * @param {string} name 5495 + */ 5496 + function removeExtension(name) { 5497 + delete extensions[name]; 5498 + } 5499 + 5500 + /** 5501 + * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element 5502 + * 5503 + * @param {Element} elt 5504 + * @param {HtmxExtension[]=} extensionsToReturn 5505 + * @param {string[]=} extensionsToIgnore 5506 + * @returns {HtmxExtension[]} 5507 + */ 5508 + function getExtensions(elt, extensionsToReturn, extensionsToIgnore) { 5509 + if (extensionsToReturn == undefined) { 5510 + extensionsToReturn = []; 5511 + } 5512 + if (elt == undefined) { 5513 + return extensionsToReturn; 5514 + } 5515 + if (extensionsToIgnore == undefined) { 5516 + extensionsToIgnore = []; 5517 + } 5518 + const extensionsForElement = getAttributeValue(elt, "hx-ext"); 5519 + if (extensionsForElement) { 5520 + forEach(extensionsForElement.split(","), function (extensionName) { 5521 + extensionName = extensionName.replace(/ /g, ""); 5522 + if (extensionName.slice(0, 7) == "ignore:") { 5523 + extensionsToIgnore.push(extensionName.slice(7)); 5524 + return; 5525 + } 5526 + if (extensionsToIgnore.indexOf(extensionName) < 0) { 5527 + const extension = extensions[extensionName]; 5528 + if (extension && extensionsToReturn.indexOf(extension) < 0) { 5529 + extensionsToReturn.push(extension); 5530 + } 5531 + } 5532 + }); 5533 + } 5534 + return getExtensions( 5535 + asElement(parentElt(elt)), 5536 + extensionsToReturn, 5537 + extensionsToIgnore, 5538 + ); 5539 + } 5540 + 5541 + //= =================================================================== 5542 + // Initialization 5543 + //= =================================================================== 5544 + var isReady = false; 5545 + getDocument().addEventListener("DOMContentLoaded", function () { 5546 + isReady = true; 5547 + }); 5548 + 5549 + /** 5550 + * Execute a function now if DOMContentLoaded has fired, otherwise listen for it. 5551 + * 5552 + * This function uses isReady because there is no reliable way to ask the browser whether 5553 + * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded 5554 + * firing and readystate=complete. 5555 + */ 5556 + function ready(fn) { 5557 + // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by 5558 + // some means other than the initial page load. 5559 + if (isReady || getDocument().readyState === "complete") { 5560 + fn(); 5561 + } else { 5562 + getDocument().addEventListener("DOMContentLoaded", fn); 5563 + } 5564 + } 5565 + 5566 + function insertIndicatorStyles() { 5567 + if (htmx.config.includeIndicatorStyles !== false) { 5568 + const nonceAttribute = htmx.config.inlineStyleNonce 5569 + ? ` nonce="${htmx.config.inlineStyleNonce}"` 5570 + : ""; 5571 + getDocument().head.insertAdjacentHTML( 5572 + "beforeend", 5573 + "<style" + 5574 + nonceAttribute + 5575 + ">\ 5576 + ." + 5577 + htmx.config.indicatorClass + 5578 + "{opacity:0}\ 5579 + ." + 5580 + htmx.config.requestClass + 5581 + " ." + 5582 + htmx.config.indicatorClass + 5583 + "{opacity:1; transition: opacity 200ms ease-in;}\ 5584 + ." + 5585 + htmx.config.requestClass + 5586 + "." + 5587 + htmx.config.indicatorClass + 5588 + "{opacity:1; transition: opacity 200ms ease-in;}\ 5589 + </style>", 5590 + ); 5591 + } 5592 + } 5593 + 5594 + function getMetaConfig() { 5595 + /** @type HTMLMetaElement */ 5596 + const element = getDocument().querySelector('meta[name="htmx-config"]'); 5597 + if (element) { 5598 + return parseJSON(element.content); 5599 + } else { 5600 + return null; 5601 + } 5602 + } 5603 + 5604 + function mergeMetaConfig() { 5605 + const metaConfig = getMetaConfig(); 5606 + if (metaConfig) { 5607 + htmx.config = mergeObjects(htmx.config, metaConfig); 5608 + } 5609 + } 5610 + 5611 + // initialize the document 5612 + ready(function () { 5613 + mergeMetaConfig(); 5614 + insertIndicatorStyles(); 5615 + let body = getDocument().body; 5616 + processNode(body); 5617 + const restoredElts = getDocument().querySelectorAll( 5618 + "[hx-trigger='restored'],[data-hx-trigger='restored']", 5619 + ); 5620 + body.addEventListener("htmx:abort", function (evt) { 5621 + const target = evt.target; 5622 + const internalData = getInternalData(target); 5623 + if (internalData && internalData.xhr) { 5624 + internalData.xhr.abort(); 5625 + } 5626 + }); 5627 + /** @type {(ev: PopStateEvent) => any} */ 5628 + const originalPopstate = window.onpopstate 5629 + ? window.onpopstate.bind(window) 5630 + : null; 5631 + /** @type {(ev: PopStateEvent) => any} */ 5632 + window.onpopstate = function (event) { 5633 + if (event.state && event.state.htmx) { 5634 + restoreHistory(); 5635 + forEach(restoredElts, function (elt) { 5636 + triggerEvent(elt, "htmx:restored", { 5637 + document: getDocument(), 5638 + triggerEvent, 5639 + }); 5640 + }); 5641 + } else { 5642 + if (originalPopstate) { 5643 + originalPopstate(event); 5644 + } 5645 + } 5646 + }; 5647 + getWindow().setTimeout(function () { 5648 + triggerEvent(body, "htmx:load", {}); // give ready handlers a chance to load up before firing this event 5649 + body = null; // kill reference for gc 5650 + }, 0); 5651 + }); 5652 + 5653 + return htmx; 5654 + })(); 5655 + 5656 + /** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'} HttpVerb */ 5657 + 5658 + /** 5659 + * @typedef {Object} SwapOptions 5660 + * @property {string} [select] 5661 + * @property {string} [selectOOB] 5662 + * @property {*} [eventInfo] 5663 + * @property {string} [anchor] 5664 + * @property {Element} [contextElement] 5665 + * @property {swapCallback} [afterSwapCallback] 5666 + * @property {swapCallback} [afterSettleCallback] 5667 + */ 5668 + 5669 + /** 5670 + * @callback swapCallback 5671 + */ 5672 + 5673 + /** 5674 + * @typedef {'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string} HtmxSwapStyle 5675 + */ 5676 + 5677 + /** 5678 + * @typedef HtmxSwapSpecification 5679 + * @property {HtmxSwapStyle} swapStyle 5680 + * @property {number} swapDelay 5681 + * @property {number} settleDelay 5682 + * @property {boolean} [transition] 5683 + * @property {boolean} [ignoreTitle] 5684 + * @property {string} [head] 5685 + * @property {'top' | 'bottom'} [scroll] 5686 + * @property {string} [scrollTarget] 5687 + * @property {string} [show] 5688 + * @property {string} [showTarget] 5689 + * @property {boolean} [focusScroll] 5690 + */ 5691 + 5692 + /** 5693 + * @typedef {((this:Node, evt:Event) => boolean) & {source: string}} ConditionalFunction 5694 + */ 5695 + 5696 + /** 5697 + * @typedef {Object} HtmxTriggerSpecification 5698 + * @property {string} trigger 5699 + * @property {number} [pollInterval] 5700 + * @property {ConditionalFunction} [eventFilter] 5701 + * @property {boolean} [changed] 5702 + * @property {boolean} [once] 5703 + * @property {boolean} [consume] 5704 + * @property {number} [delay] 5705 + * @property {string} [from] 5706 + * @property {string} [target] 5707 + * @property {number} [throttle] 5708 + * @property {string} [queue] 5709 + * @property {string} [root] 5710 + * @property {string} [threshold] 5711 + */ 5712 + 5713 + /** 5714 + * @typedef {{elt: Element, message: string, validity: ValidityState}} HtmxElementValidationError 5715 + */ 5716 + 5717 + /** 5718 + * @typedef {Record<string, string>} HtmxHeaderSpecification 5719 + * @property {'true'} HX-Request 5720 + * @property {string|null} HX-Trigger 5721 + * @property {string|null} HX-Trigger-Name 5722 + * @property {string|null} HX-Target 5723 + * @property {string} HX-Current-URL 5724 + * @property {string} [HX-Prompt] 5725 + * @property {'true'} [HX-Boosted] 5726 + * @property {string} [Content-Type] 5727 + * @property {'true'} [HX-History-Restore-Request] 5728 + */ 5729 + 5730 + /** @typedef HtmxAjaxHelperContext 5731 + * @property {Element|string} [source] 5732 + * @property {Event} [event] 5733 + * @property {HtmxAjaxHandler} [handler] 5734 + * @property {Element|string} [target] 5735 + * @property {HtmxSwapStyle} [swap] 5736 + * @property {Object|FormData} [values] 5737 + * @property {Record<string,string>} [headers] 5738 + * @property {string} [select] 5739 + */ 5740 + 5741 + /** 5742 + * @typedef {Object} HtmxRequestConfig 5743 + * @property {boolean} boosted 5744 + * @property {boolean} useUrlParams 5745 + * @property {FormData} formData 5746 + * @property {Object} parameters formData proxy 5747 + * @property {FormData} unfilteredFormData 5748 + * @property {Object} unfilteredParameters unfilteredFormData proxy 5749 + * @property {HtmxHeaderSpecification} headers 5750 + * @property {Element} target 5751 + * @property {HttpVerb} verb 5752 + * @property {HtmxElementValidationError[]} errors 5753 + * @property {boolean} withCredentials 5754 + * @property {number} timeout 5755 + * @property {string} path 5756 + * @property {Event} triggeringEvent 5757 + */ 5758 + 5759 + /** 5760 + * @typedef {Object} HtmxResponseInfo 5761 + * @property {XMLHttpRequest} xhr 5762 + * @property {Element} target 5763 + * @property {HtmxRequestConfig} requestConfig 5764 + * @property {HtmxAjaxEtc} etc 5765 + * @property {boolean} boosted 5766 + * @property {string} select 5767 + * @property {{requestPath: string, finalRequestPath: string, responsePath: string|null, anchor: string}} pathInfo 5768 + * @property {boolean} [failed] 5769 + * @property {boolean} [successful] 5770 + * @property {boolean} [keepIndicators] 5771 + */ 5772 + 5773 + /** 5774 + * @typedef {Object} HtmxAjaxEtc 5775 + * @property {boolean} [returnPromise] 5776 + * @property {HtmxAjaxHandler} [handler] 5777 + * @property {string} [select] 5778 + * @property {Element} [targetOverride] 5779 + * @property {HtmxSwapStyle} [swapOverride] 5780 + * @property {Record<string,string>} [headers] 5781 + * @property {Object|FormData} [values] 5782 + * @property {boolean} [credentials] 5783 + * @property {number} [timeout] 5784 + */ 5785 + 5786 + /** 5787 + * @typedef {Object} HtmxResponseHandlingConfig 5788 + * @property {string} [code] 5789 + * @property {boolean} swap 5790 + * @property {boolean} [error] 5791 + * @property {boolean} [ignoreTitle] 5792 + * @property {string} [select] 5793 + * @property {string} [target] 5794 + * @property {string} [swapOverride] 5795 + * @property {string} [event] 5796 + */ 5797 + 5798 + /** 5799 + * @typedef {HtmxResponseInfo & {shouldSwap: boolean, serverResponse: any, isError: boolean, ignoreTitle: boolean, selectOverride:string, swapOverride:string}} HtmxBeforeSwapDetails 5800 + */ 5801 + 5802 + /** 5803 + * @callback HtmxAjaxHandler 5804 + * @param {Element} elt 5805 + * @param {HtmxResponseInfo} responseInfo 5806 + */ 5807 + 5808 + /** 5809 + * @typedef {(() => void)} HtmxSettleTask 5810 + */ 5811 + 5812 + /** 5813 + * @typedef {Object} HtmxSettleInfo 5814 + * @property {HtmxSettleTask[]} tasks 5815 + * @property {Element[]} elts 5816 + * @property {string} [title] 5817 + */ 5818 + 5819 + /** 5820 + * @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md 5821 + * @typedef {Object} HtmxExtension 5822 + * @property {(api: any) => void} init 5823 + * @property {(name: string, event: Event|CustomEvent) => boolean} onEvent 5824 + * @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse 5825 + * @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap 5826 + * @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap 5827 + * @property {(xhr: XMLHttpRequest, parameters: FormData, elt: Node) => *|string|null} encodeParameters 5828 + * @property {() => string[]|null} getSelectors 5829 + */
-43
bin/forester/theme/index.html
··· 1 - <!doctype html> 2 - <html> 3 - <head> 4 - <meta name="viewport" content="width=device-width" /> 5 - <link rel="stylesheet" href="style.css" /> 6 - <link rel="icon" type="image/x-icon" href="favicon.ico" /> 7 - <script type="module" src="min.js"></script> 8 - <script 9 - src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" 10 - integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" 11 - crossorigin="anonymous" 12 - ></script> 13 - 14 - <link 15 - rel="stylesheet" 16 - href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css" 17 - integrity="sha384-zh0CIslj+VczCZtlzBcjt5ppRcsAmDnRem7ESsYwWwg3m/OaJ2l4x7YBZl9Kxxib" 18 - crossorigin="anonymous" 19 - /> 20 - 21 - <script 22 - src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.js" 23 - integrity="sha384-CAltQiu9myJj3FAllEacN6FT+rOyXo+hFZKGuR2p4HB8JvJlyUHm31eLfL4eEiJL" 24 - crossorigin="anonymous" 25 - ></script> 26 - 27 - <title></title> 28 - </head> 29 - <body> 30 - <header></header> 31 - <div id="grid-wrapper"> 32 - <!-- I would like to use hx-push-url. See: https://github.com/bigskysoftware/htmx/discussions/1700 --> 33 - <article hx-get="/home" hx-trigger="load" hx-swap="innerHTML"></article> 34 - <nav id="toc"> 35 - <div class="block"> 36 - <h1>Table of contents</h1> 37 - <ul id="toc-list"></ul> 38 - </div> 39 - </nav> 40 - </div> 41 - <div id="modal-container"></div> 42 - </body> 43 - </html>
+136 -89
bin/forester/theme/style.css
··· 1 1 /* SPDX-License-Identifier: CC0-1.0 */ 2 2 3 + #search-results:empty { 4 + display: none; 5 + } 6 + 7 + #search-modal-trigger { 8 + display: none; 9 + } 10 + 11 + .search-wrapper { 12 + display: flex; 13 + } 14 + 15 + .search { 16 + padding: 1.25em; 17 + font-size: 1.25em; 18 + flex-grow: 1; 19 + } 20 + 21 + .modal-content { 22 + position: fixed; 23 + top: 50%; 24 + left: 50%; 25 + transform: translate(-50%, -50%); 26 + width: 600px; 27 + max-width: 100%; 28 + height: 400px; 29 + max-height: 100%; 30 + border-radius: 0.5em; 31 + box-shadow: rgb(0 0 0 / 50%) 0px 16px 70px; 32 + } 33 + 34 + .modal-overlay { 35 + z-index: 100; 36 + position: fixed; 37 + top: 0; 38 + left: 0; 39 + width: 100%; 40 + height: 100%; 41 + backdrop-filter: blur(10px); 42 + } 43 + 3 44 .katex { 4 - font-size: 1.15em !important; 45 + font-size: 1.15em !important; 5 46 } 6 47 7 48 /* inria-sans-300 - latin_latin-ext */ 8 49 @font-face { 9 50 font-display: swap; 10 51 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 11 - font-family: 'Inria Sans'; 52 + font-family: "Inria Sans"; 12 53 font-style: normal; 13 54 font-weight: 300; 14 - src: url('fonts/inria-sans-v14-latin_latin-ext-300.woff2') format('woff2'); 55 + src: url("fonts/inria-sans-v14-latin_latin-ext-300.woff2") format("woff2"); 15 56 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 16 57 } 17 58 ··· 19 60 @font-face { 20 61 font-display: swap; 21 62 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 22 - font-family: 'Inria Sans'; 63 + font-family: "Inria Sans"; 23 64 font-style: italic; 24 65 font-weight: 300; 25 - src: url('fonts/inria-sans-v14-latin_latin-ext-300italic.woff2') format('woff2'); 66 + src: url("fonts/inria-sans-v14-latin_latin-ext-300italic.woff2") 67 + format("woff2"); 26 68 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 27 69 } 28 70 ··· 30 72 @font-face { 31 73 font-display: swap; 32 74 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 33 - font-family: 'Inria Sans'; 75 + font-family: "Inria Sans"; 34 76 font-style: normal; 35 77 font-weight: 400; 36 - src: url('fonts/inria-sans-v14-latin_latin-ext-regular.woff2') format('woff2'); 78 + src: url("fonts/inria-sans-v14-latin_latin-ext-regular.woff2") format("woff2"); 37 79 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 38 80 } 39 81 ··· 41 83 @font-face { 42 84 font-display: swap; 43 85 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 44 - font-family: 'Inria Sans'; 86 + font-family: "Inria Sans"; 45 87 font-style: italic; 46 88 font-weight: 400; 47 - src: url('fonts/inria-sans-v14-latin_latin-ext-italic.woff2') format('woff2'); 89 + src: url("fonts/inria-sans-v14-latin_latin-ext-italic.woff2") format("woff2"); 48 90 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 49 91 } 50 92 ··· 52 94 @font-face { 53 95 font-display: swap; 54 96 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 55 - font-family: 'Inria Sans'; 97 + font-family: "Inria Sans"; 56 98 font-style: normal; 57 99 font-weight: 700; 58 - src: url('fonts/inria-sans-v14-latin_latin-ext-700.woff2') format('woff2'); 100 + src: url("fonts/inria-sans-v14-latin_latin-ext-700.woff2") format("woff2"); 59 101 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 60 102 } 61 103 ··· 63 105 @font-face { 64 106 font-display: swap; 65 107 /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 66 - font-family: 'Inria Sans'; 108 + font-family: "Inria Sans"; 67 109 font-style: italic; 68 110 font-weight: 700; 69 - src: url('fonts/inria-sans-v14-latin_latin-ext-700italic.woff2') format('woff2'); 111 + src: url("fonts/inria-sans-v14-latin_latin-ext-700italic.woff2") 112 + format("woff2"); 70 113 /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ 71 114 } 72 115 ··· 95 138 h2, 96 139 h3, 97 140 h4 { 98 - margin-top: .5em; 141 + margin-top: 0.5em; 99 142 } 100 143 101 144 pre, ··· 109 152 pre { 110 153 border-radius: var(--radius); 111 154 background-color: rgba(0, 100, 100, 0.04); 112 - padding: .5em; 155 + padding: 0.5em; 113 156 font-size: 11pt; 114 157 margin-top: 0em; 115 158 overflow-x: auto; ··· 134 177 } 135 178 136 179 math { 137 - font-size: 1.12em; 180 + font-size: 1.12em; 138 181 } 139 182 140 183 mrow:hover { 141 - background-color: rgba(0, 100, 255, 0.04); 184 + background-color: rgba(0, 100, 255, 0.04); 142 185 } 143 186 144 187 .logo { ··· 155 198 color: #aaa; 156 199 } 157 200 158 - .block.hide-metadata>details>summary>header>.metadata { 201 + .block.hide-metadata > details > summary > header > .metadata { 159 202 display: none; 160 203 } 161 204 162 - article>section>details>summary>header>h1>.taxon { 205 + article > section > details > summary > header > h1 > .taxon { 163 206 display: block; 164 - font-size: .9em; 207 + font-size: 0.9em; 165 208 color: #888; 166 209 padding-bottom: 5pt; 167 210 } 168 211 169 - section section[data-taxon="Reference"]>details>summary>header>h1>.taxon, 170 - section section[data-taxon="Person"]>details>summary>header>h1>.taxon { 212 + section 213 + section[data-taxon="Reference"] 214 + > details 215 + > summary 216 + > header 217 + > h1 218 + > .taxon, 219 + section 220 + section[data-taxon="Person"] 221 + > details 222 + > summary 223 + > header 224 + > h1 225 + > .taxon { 171 226 display: none; 172 227 } 173 228 174 - footer>section { 229 + footer > section { 175 230 margin-bottom: 1em; 176 231 } 177 232 ··· 179 234 font-size: 14pt; 180 235 } 181 236 182 - .metadata>address { 237 + .metadata > address { 183 238 display: inline; 184 239 } 185 240 186 241 @media only screen and (max-width: 1000px) { 187 242 body { 188 243 margin-top: 1em; 189 - margin-left: .5em; 190 - margin-right: .5em; 191 - transition: ease all .2s; 244 + margin-left: 0.5em; 245 + margin-right: 0.5em; 246 + transition: ease all 0.2s; 192 247 } 193 248 194 - #grid-wrapper>nav { 249 + #grid-wrapper > nav { 195 250 display: none; 196 - transition: ease all .2s; 251 + transition: ease all 0.2s; 197 252 } 198 253 } 199 254 ··· 201 256 body { 202 257 margin-top: 2em; 203 258 margin-left: 2em; 204 - transition: ease all .2s; 259 + transition: ease all 0.2s; 205 260 } 206 261 207 262 #grid-wrapper { ··· 210 265 } 211 266 } 212 267 213 - body>header { 268 + body > header { 214 269 margin-bottom: 0.5em; 215 270 } 216 271 217 - #grid-wrapper>article { 272 + #grid-wrapper > article { 218 273 max-width: 90ex; 219 274 margin-right: auto; 220 275 grid-column: 1; 221 276 } 222 277 223 - #grid-wrapper>nav { 278 + #grid-wrapper > nav { 224 279 grid-column: 2; 225 280 } 226 281 227 - details>summary>header { 282 + details > summary > header { 228 283 display: inline; 229 284 } 230 285 ··· 237 292 display: inline; 238 293 } 239 294 240 - section .block[data-taxon] details>summary>header>h1 { 295 + section .block[data-taxon] details > summary > header > h1 { 241 296 font-size: 12pt; 242 297 } 243 298 ··· 246 301 font-weight: bolder; 247 302 } 248 303 249 - 250 - .link-list>section>details>summary>header h1 { 304 + .link-list > section > details > summary > header h1 { 251 305 font-size: 12pt; 252 306 } 253 307 254 - 255 - article>section>details>summary>header>h1 { 308 + article > section > details > summary > header > h1 { 256 309 font-size: 1.5em; 257 310 } 258 311 259 - details>summary { 312 + details > summary { 260 313 list-style-type: none; 261 314 } 262 315 263 - details>summary::marker, 264 - details>summary::-webkit-details-marker { 316 + details > summary::marker, 317 + details > summary::-webkit-details-marker { 265 318 display: none; 266 319 } 267 320 268 - article>section>details>summary>header { 321 + article > section > details > summary > header { 269 322 display: block; 270 - margin-bottom: .5em; 323 + margin-bottom: 0.5em; 271 324 } 272 325 273 - section.block>details { 326 + section.block > details { 274 327 margin-bottom: 0.4em; 275 328 } 276 329 277 - 278 - section.block>details[open] { 330 + section.block > details[open] { 279 331 margin-bottom: 1em; 280 332 } 281 333 282 - 283 - .link-list>section.block>details { 284 - margin-bottom: .25em; 334 + .link-list > section.block > details { 335 + margin-bottom: 0.25em; 285 336 } 286 337 287 338 nav#toc { ··· 299 350 } 300 351 301 352 nav#toc .link { 302 - box-shadow: none; 303 - text-decoration: none; 353 + box-shadow: none; 354 + text-decoration: none; 304 355 } 305 356 306 357 nav#toc a.bullet { 307 - opacity: 0.7; 308 - margin-left: 0.4em; 309 - margin-right: 0.3em; 310 - padding-left: 0.2em; 311 - padding-right: 0.2em; 312 - text-decoration: none; 358 + opacity: 0.7; 359 + margin-left: 0.4em; 360 + margin-right: 0.3em; 361 + padding-left: 0.2em; 362 + padding-right: 0.2em; 363 + text-decoration: none; 313 364 } 314 365 315 366 nav#toc h2 { ··· 321 372 } 322 373 323 374 nav#toc li > ul { 324 - padding-left: 1em; 375 + padding-left: 1em; 325 376 } 326 377 327 378 nav#toc li { 328 - list-style-position: inside; 379 + list-style-position: inside; 329 380 } 330 381 331 382 .block { 332 - border-radius: var(--radius) 383 + border-radius: var(--radius); 333 384 } 334 385 335 386 .block:hover { ··· 342 393 } 343 394 344 395 .highlighted { 345 - background-color: rgba(255, 255, 140, .3); 396 + background-color: rgba(255, 255, 140, 0.3); 346 397 border-color: #ccc; 347 398 } 348 399 349 400 .highlighted:hover { 350 - background-color: rgba(255, 255, 140, .6); 401 + background-color: rgba(255, 255, 140, 0.6); 351 402 border-color: #aaa; 352 403 } 353 404 ··· 380 431 box-shadow: none; 381 432 text-decoration-line: underline; 382 433 text-decoration-style: dotted; 383 - } 434 + } 384 435 385 436 ninja-keys::part(ninja-action) { 386 437 white-space: nowrap; ··· 422 473 } 423 474 424 475 td.macro-doc { 425 - font-size: .9em; 476 + font-size: 0.9em; 426 477 } 427 478 428 - .enclosing.macro-scope>.enclosing { 479 + .enclosing.macro-scope > .enclosing { 429 480 border-radius: 2px; 430 481 } 431 482 432 - .enclosing.macro-scope>.enclosing:hover { 483 + .enclosing.macro-scope > .enclosing:hover { 433 484 background-color: rgba(0, 100, 255, 0.1); 434 485 } 435 486 ··· 448 499 .display.tooltip { 449 500 display: block; 450 501 } 451 - 452 502 453 503 /* The tooltip class is applied to the span element that is the tooltip */ 454 504 ··· 479 529 margin-left: -5px; 480 530 border-width: 5px; 481 531 } 482 - 483 532 484 533 /* Show the tooltip */ 485 534 ··· 489 538 } 490 539 491 540 .tooltiptext a { 492 - color: white 541 + color: white; 493 542 } 494 543 495 544 .macro-doc { ··· 512 561 a.bullet:hover, 513 562 .edit-button:hover, 514 563 .link:hover { 515 - background-color: rgba(0, 100, 255, .1); 564 + background-color: rgba(0, 100, 255, 0.1); 516 565 } 517 566 518 567 .link { 519 - cursor: pointer; 568 + cursor: pointer; 520 569 } 521 570 522 571 a { ··· 529 578 } 530 579 531 580 .nocite { 532 - display: none 581 + display: none; 533 582 } 534 583 535 584 blockquote { 536 585 font-style: italic; 537 586 } 538 - 539 - 540 587 541 588 address { 542 589 display: inline; 543 590 } 544 591 545 - 546 592 .metadata ul { 547 593 padding-left: 0; 548 594 display: inline; 549 595 } 550 596 551 597 .metadata li::after { 552 - content: " · "; 598 + content: " \00B7 "; 553 599 } 554 600 555 601 .metadata li:last-child::after { ··· 557 603 } 558 604 559 605 .metadata ul li { 560 - display: inline 606 + display: inline; 561 607 } 562 608 563 609 img { ··· 587 633 height: 2px; 588 634 } 589 635 590 - ul, ol { 591 - padding-bottom: .5em; 636 + ul, 637 + ol { 638 + padding-bottom: 0.5em; 592 639 } 593 640 594 641 ol { 595 - list-style-type: decimal; 642 + list-style-type: decimal; 596 643 } 597 644 598 645 ol li ol { 599 - list-style-type: lower-alpha; 646 + list-style-type: lower-alpha; 600 647 } 601 648 602 649 ol li ol li ol { 603 - list-style-type: lower-roman; 650 + list-style-type: lower-roman; 604 651 } 605 652 606 - .error, .info { 653 + .error, 654 + .info { 607 655 border-radius: 4pt; 608 656 padding-left: 3pt; 609 657 padding-right: 3pt; ··· 613 661 } 614 662 615 663 .error { 616 - background-color: red; 617 - color: white; 664 + background-color: red; 665 + color: white; 618 666 } 619 667 620 - 621 668 .info { 622 - background-color: #bbb; 623 - color: white; 669 + background-color: #bbb; 670 + color: white; 624 671 }
+1 -1
lib/compiler/Phases.ml
··· 25 25 let resources = Forest.create 1000 in 26 26 let diagnostics = Diagnostic_store.create 100 in 27 27 let units = Expand.Env.empty in 28 - let search_index = State.Search_index.empty in 28 + let search_index = State.Search_index.empty in 29 29 { 30 30 env; 31 31 dev;
+515 -122
lib/frontend/Htmx_client.ml
··· 10 10 open Forester_compiler 11 11 12 12 module T = Types 13 - module P = Pure_html 14 - module H = P.HTML 13 + open Pure_html 14 + open HTML 15 15 16 16 type query = { 17 17 query: (string, T.content T.vertex) Forester_core.Datalog_expr.query; ··· 33 33 |> Seq.filter_map (fun iri -> Forest.get_article iri forest.resources) 34 34 |> List.of_seq 35 35 |> List.sort C.compare_article 36 + 37 + let home_iri ~(config : Config.t) = 38 + (* let config = State.get_config forest in *) 39 + let@ root = Option.bind config.home in 40 + let base = Iri_scheme.base_iri ~host: config.host in 41 + try 42 + Option.some @@ Iri.resolve ~base @@ Iri.of_string root 43 + with 44 + | _ -> None 45 + 46 + let iri_is_home ~config iri = 47 + match home_iri ~config with 48 + | Some home_iri -> 49 + (* By this point, any IRI should be in normal form. *) 50 + Iri.equal ~normalize: false home_iri iri 51 + | None -> false 36 52 37 53 let route forest addr = 38 54 let config = State.config forest in ··· 41 57 else 42 58 Format.asprintf "%s" (Iri.path_string addr) 43 59 60 + let title_flags_to_http_header (flags : T.title_flags) = 61 + match flags with 62 + | {empty_when_untitled} -> 63 + `Assoc ([("Empty-When-Untitled", `String (Bool.to_string empty_when_untitled))]) 64 + 65 + (* I am encoding these headers to JSON because that is what HTMX 66 + requires, but it would be more beautiful if we could directly use the 67 + header type*) 68 + let section_flags_to_http_header (flags : T.section_flags) = 69 + match flags with 70 + | {hidden_when_empty; 71 + included_in_toc; 72 + header_shown; 73 + metadata_shown; 74 + numbered; 75 + expanded 76 + } -> 77 + let to_header l t = 78 + match t with 79 + | Some v -> Some (l, `String (Bool.to_string v)) 80 + | None -> None 81 + in 82 + let a = to_header "Hidden-When-Empty" hidden_when_empty in 83 + let b = to_header "Included-In-Toc" included_in_toc in 84 + let c = to_header "Header-Shown" header_shown in 85 + let d = to_header "Metadata-Shown" metadata_shown in 86 + let e = to_header "Numbered" numbered in 87 + let f = to_header "Expanded" expanded in 88 + `Assoc (List.filter_map Fun.id [a; b; c; d; e; f]) 89 + 90 + let content_target_to_http_header (target : T.content_target) = 91 + match target with 92 + | T.Full flags -> 93 + let `Assoc flags = section_flags_to_http_header flags in 94 + `Assoc (("Full", `String "true") :: flags) 95 + | T.Mainmatter -> 96 + `Assoc ["Mainmatter", `String "true"] 97 + | T.Title flags -> 98 + let `Assoc flags = title_flags_to_http_header flags in 99 + `Assoc (("Title", `String "true") :: flags) 100 + | T.Taxon -> 101 + `Assoc ["Taxon", `String "true"] 102 + 44 103 let render_xml_qname = function 45 104 | {prefix = ""; uname; _} -> uname 46 105 | {prefix; uname; _} -> Format.sprintf "%s:%s" prefix uname ··· 48 107 let render_xml_attr 49 108 : T.content T.xml_attr -> _ 50 109 = fun T.{key; value = _} -> 51 - P.string_attr (render_xml_qname key) "todo" 110 + string_attr (render_xml_qname key) "todo" 52 111 (* "%a" render_content value *) 53 112 54 - let tag_of_prim_node : Prim.t -> P.attr list -> P.node list -> P.node = function 55 - | `P -> H.p 56 - | `Em -> H.em 57 - | `Strong -> H.strong 58 - | `Figure -> H.figure 59 - | `Figcaption -> H.figcaption 60 - | `Ul -> H.ul 61 - | `Ol -> H.ol 62 - | `Li -> H.li 63 - | `Blockquote -> H.blockquote 64 - | `Code -> H.code 65 - | `Pre -> H.pre 113 + let tag_of_prim_node : Prim.t -> attr list -> node list -> node = function 114 + | `P -> p 115 + | `Em -> em 116 + | `Strong -> strong 117 + | `Figure -> figure 118 + | `Figcaption -> figcaption 119 + | `Ul -> ul 120 + | `Ol -> ol 121 + | `Li -> li 122 + | `Blockquote -> blockquote 123 + | `Code -> code 124 + | `Pre -> pre 66 125 67 126 let render_prim_node p = 68 127 tag_of_prim_node p [] 69 128 70 129 let render_img = function 71 130 | T.Inline {format; base64} -> 72 - H.img [H.src "data:image/%s;base64,%s" format base64] 131 + img [src "data:image/%s;base64,%s" format base64] 73 132 | T.Remote url -> 74 - H.img [H.src "%s" url] 133 + img [src "%s" url] 75 134 76 135 let render_xmlns_prefix Xmlns.{prefix; xmlns} = 77 - P.string_attr ("xmlns:" ^ prefix) "%s" xmlns 136 + string_attr ("xmlns:" ^ prefix) "%s" xmlns 78 137 79 - let rec render_article (forest : State.t) (article : T.content T.article) : P.node = 138 + let render_date (date : Human_datetime.t) = 139 + let year = txt "%i" (Human_datetime.year date) in 140 + let month = 141 + match Human_datetime.month date with 142 + | None -> None 143 + | Some i -> 144 + match i with 145 + | 1 -> Some (txt "January") 146 + | 2 -> Some (txt "February") 147 + | 3 -> Some (txt "March") 148 + | 4 -> Some (txt "April") 149 + | 5 -> Some (txt "May") 150 + | 6 -> Some (txt "June") 151 + | 7 -> Some (txt "July") 152 + | 8 -> Some (txt "August") 153 + | 9 -> Some (txt "September") 154 + | 10 -> Some (txt "October") 155 + | 11 -> Some (txt "November") 156 + | 12 -> Some (txt "December") 157 + | _ -> assert false 158 + in 159 + let day = 160 + match Human_datetime.day date with 161 + | None -> null [] 162 + | Some i -> txt "%i" i 163 + in 164 + li 165 + [class_ "meta-item"] 166 + [ 167 + a 168 + [class_ "link local"] 169 + [ 170 + Option.value ~default: (null []) month; 171 + if Option.is_some month then txt " " else null []; 172 + day; 173 + if Option.is_some month then txt ", " else null []; 174 + year 175 + ] 176 + ] 177 + 178 + let rec render_article (forest : State.t) (article : T.content T.article) : node = 80 179 (* FIXME: What should reserved be here? *) 81 180 let@ () = Xmlns.run ~reserved: [] in 82 - H.article 83 - [] 181 + HTML.article 182 + [id "tree-container";] 183 + [ 184 + (* FIXME: Should be reusing render_section *) 185 + HTML.section 186 + [class_ "block"] 187 + [ 188 + details 189 + [ 190 + (* TODO: check if expanded*) 191 + open_ 192 + ] 193 + ( 194 + summary 195 + [] 196 + [ 197 + render_frontmatter forest article.frontmatter; 198 + ] :: render_content forest article.mainmatter; 199 + ); 200 + ]; 201 + match Option.map (iri_is_home ~config: forest.config) article.frontmatter.iri with 202 + | None -> 203 + footer 204 + [] 205 + (render_backmatter forest article.backmatter) 206 + | Some false -> 207 + footer 208 + [] 209 + (render_backmatter forest article.backmatter) 210 + | Some true -> 211 + null [] 212 + ] 213 + 214 + and render_section (forest : State.t) (section : T.content T.section) : node = 215 + match section with 216 + | {frontmatter; 217 + mainmatter; 218 + flags = {header_shown; 219 + metadata_shown; 220 + expanded; 221 + numbered = _; 222 + included_in_toc = _; 223 + hidden_when_empty = _; 224 + } 225 + } -> 226 + let test k = function 227 + | Some true -> true 228 + | Some false -> false 229 + | None -> k 230 + in 231 + let class_ = 232 + if test false metadata_shown then class_ "block" 233 + else 234 + class_ "block hide-metadata" 235 + in 236 + let data_taxon = 237 + match frontmatter.taxon with 238 + | None -> null_ 239 + | Some _c -> 240 + (* string_attr "data-taxon" () *) 241 + null_ 242 + in 243 + HTML.section 244 + [ 245 + class_; 246 + data_taxon; 247 + ] 248 + [ 249 + if test true header_shown then 250 + details 251 + [if test true expanded then open_ else null_] 252 + [ 253 + summary 254 + [] 255 + [ 256 + render_frontmatter forest frontmatter; 257 + ]; 258 + null @@ render_content forest mainmatter; 259 + ] 260 + else null @@ render_content forest mainmatter; 261 + (* render_frontmatter forest frontmatter; *) 262 + (* null @@ render_content forest mainmatter; *) 263 + ] 264 + 265 + (* Same as render_section, but adds the backmatter-section class *) 266 + and render_backmatter (forest : State.t) backmatter = 267 + List.map 268 + (fun node -> 269 + let attrs = Format.asprintf "%s backmatter-section" node.@["class"] in 270 + node +@ class_ "%s" attrs 271 + ) 272 + (render_content forest backmatter) 273 + 274 + and render_attributions forest (attributions : T.content T.attribution list) = 275 + let render_attribution attribution = 276 + match attribution with 277 + | T.{vertex; _} -> 278 + match vertex with 279 + | T.Iri_vertex href -> 280 + let content = T.Content [T.Transclude {href; target = Title {empty_when_untitled = false}; modifier = Identity}] in 281 + null @@ render_link forest T.{href; content} 282 + | T.Content_vertex content -> 283 + null @@ render_content forest content 284 + in 285 + let authors, contributors = 286 + List.partition_map 287 + (fun a -> 288 + match T.(a.role) with 289 + | T.Author -> Left a 290 + | Contributor -> Right a 291 + ) 292 + attributions 293 + in 294 + li 295 + [class_ "meta-item"] 84 296 [ 85 - render_frontmatter forest article.frontmatter; 86 - H.null @@ render_content forest article.mainmatter; 87 - H.section [H.class_ "backmatter"] @@ render_content forest article.backmatter 297 + address 298 + [class_ "author"] @@ 299 + (List.map render_attribution authors) @ 300 + ( 301 + if List.length contributors > 0 then 302 + [txt "with contributions from "] 303 + else [] 304 + ) @ 305 + List.map render_attribution contributors 88 306 ] 89 307 90 - and render_section (forest : State.t) (section : T.content T.section) : P.node = 91 - H.section 308 + and render_frontmatter (forest : State.t) (frontmatter : T.content T.frontmatter) : node = 309 + let taxon = 310 + Option.value ~default: [] @@ 311 + Option.map 312 + (fun c -> 313 + ( 314 + render_content forest @@ 315 + T.apply_modifier_to_content T.Sentence_case c 316 + ) @ 317 + [txt "."] 318 + ) 319 + frontmatter.taxon 320 + in 321 + let title = 322 + Option.value ~default: [] @@ 323 + Option.map 324 + (fun c -> 325 + render_content forest @@ 326 + T.apply_modifier_to_content 327 + T.Sentence_case 328 + c 329 + ) 330 + frontmatter.title 331 + in 332 + let iri = 333 + match frontmatter.iri with 334 + | None -> null [] 335 + | Some iri -> 336 + let iri_str = 337 + if Iri.host iri = Some forest.config.host then 338 + Scanf.(sscanf (Iri.path_string iri) "/%s") Fun.id 339 + else 340 + Format.asprintf "%a" pp_iri iri 341 + in 342 + a 343 + [ 344 + class_ "slug"; 345 + href "%s" iri_str; 346 + ] 347 + [txt "[%s]" iri_str] 348 + in 349 + let source_path = 350 + match frontmatter.source_path with 351 + | Some path -> 352 + [ 353 + a 354 + [ 355 + class_ "edit-button"; 356 + href "vscode://file%s" path 357 + ] 358 + [txt "[edit]"] 359 + ] 360 + | None -> [] 361 + in 362 + let find_meta key = 363 + List.find_map 364 + (fun (str, content) -> 365 + if str = key then Some content 366 + else None 367 + ) 368 + frontmatter.metas 369 + in 370 + let render_meta key f = 371 + Option.value 372 + ~default: (null []) 373 + (Option.map f (find_meta key)) 374 + in 375 + let default_meta_item content = 376 + li 377 + [class_ "meta-item"] 378 + (render_content forest content) 379 + in 380 + let labelled_external_link ~href ~label = 381 + li 382 + [class_ "meta-item"] 383 + [a [class_ "link external"; href] [txt "%s" label]] 384 + in 385 + let to_string = 386 + Plain_text_client.string_of_content 387 + ~forest: forest.resources 388 + ~router: (Legacy_xml_client.route forest) 389 + in 390 + let position = render_meta "position" default_meta_item in 391 + let institution = render_meta "institution" default_meta_item in 392 + let venue = render_meta "venue" default_meta_item in 393 + let source = render_meta "source" default_meta_item in 394 + let doi = render_meta "doi" default_meta_item in 395 + let orcid = 396 + render_meta "orcid" (fun c -> 397 + let content = to_string c in 398 + li 399 + [class_ "meta-item"] 400 + [ 401 + a 402 + [ 403 + class_ "doi link"; 404 + href "https://www.doi.org/%s" content; 405 + ] 406 + [txt "%s" content] 407 + ] 408 + ) 409 + in 410 + let external_ = 411 + render_meta "external" (fun c -> 412 + let content = to_string c in 413 + li 414 + [class_ "meta-item"] 415 + [ 416 + a 417 + [ 418 + class_ "link external"; 419 + href "%s" content; 420 + ] 421 + [txt "%s" content] 422 + ] 423 + ) 424 + in 425 + let slides = 426 + render_meta "slides" (fun c -> 427 + labelled_external_link ~href: (href "%s" (to_string c)) ~label: "Slides" 428 + ) 429 + in 430 + let video = 431 + render_meta "video" (fun c -> 432 + labelled_external_link ~href: (href "%s" (to_string c)) ~label: "Video" 433 + ) 434 + in 435 + header 92 436 [] 93 437 [ 94 - render_frontmatter forest section.frontmatter; 95 - H.null @@ render_content forest section.mainmatter 438 + ( 439 + h1 440 + [] 441 + ( 442 + [ 443 + span 444 + [class_ "taxon"] 445 + taxon 446 + ] @ 447 + title @ 448 + [txt " "] @ 449 + [iri] @ 450 + source_path 451 + ) 452 + ); 453 + div 454 + [class_ "metadata"] 455 + [ 456 + ul 457 + [] 458 + ( 459 + (List.map render_date frontmatter.dates) @ 460 + [ 461 + render_attributions forest frontmatter.attributions; 462 + position; 463 + institution; 464 + venue; 465 + source; 466 + doi; 467 + orcid; 468 + external_; 469 + slides; 470 + video; 471 + ] 472 + ) 473 + ]; 96 474 ] 97 475 98 - and render_frontmatter (forest : State.t) (frontmatter : T.content T.frontmatter) : P.node = 99 - H.header 100 - [] 476 + and render_transclusion transclusion = 477 + match transclusion with 478 + | T.{href; target; modifier = _} -> 479 + let headers = Yojson.Safe.to_string @@ content_target_to_http_header target in 101 480 [ 102 - H.h1 [] @@ 103 - List.concat @@ 104 - Option.to_list @@ 105 - Option.map 106 - (render_content forest) 107 - frontmatter.title 481 + span 482 + [ 483 + Hx.trigger "load"; 484 + Hx.get "/trees%s" (Iri.path_string href); 485 + Hx.target "this"; 486 + Hx.swap "outerHTML"; 487 + (* TODO: Update dream-html: https://github.com/yawaramin/dream-html/commit/2f358cc25ef34a590937b1f1e2740141ad06efa9 *) 488 + attr (Format.asprintf "data-hx-headers='%s'" headers) 489 + ] 490 + [txt "transclusion: %s" (Format.asprintf "%a" pp_iri href)] 108 491 ] 109 492 110 - and render_content (forest : State.t) (Content content: T.content) : P.node list = 493 + and render_content (forest : State.t) (Content content: T.content) : node list = 111 494 List.concat_map (render_content_node forest) content 112 495 113 496 and render_content_node 114 - : State.t -> 'a T.content_node -> P.node list 497 + : State.t -> 'a T.content_node -> node list 115 498 = fun forest node -> 116 - let open P in 117 - (* let open H in *) 118 499 match node with 119 500 | Text str -> 120 - [P.txt "%s" str] 501 + [txt "%s" str] 121 502 | CDATA str -> 122 - [P.txt ~raw: true "<![CDATA[%s]]>" str] 503 + [txt ~raw: true "<![CDATA[%s]]>" str] 123 504 | Xml_elt elt -> 124 505 let prefixes_to_add, (name, attrs, content) = 125 506 let@ () = Xmlns.within_scope in ··· 131 512 let xmlns_attrs = List.map render_xmlns_prefix prefixes_to_add in 132 513 attrs @ xmlns_attrs 133 514 in 134 - [P.std_tag name attrs content] 515 + [std_tag name attrs content] 135 516 | Prim (p, content) -> 136 517 [render_prim_node p @@ render_content forest content] 137 518 | Transclude transclusion -> 138 - render_transclusion forest transclusion 519 + render_transclusion transclusion 139 520 | Contextual_number addr -> 140 521 let custom_number = 141 522 let@ article = Option.bind @@ Forest.get_article addr forest.resources in ··· 146 527 | None -> Format.asprintf "[%a]" Iri.pp addr 147 528 | Some num -> num 148 529 in 149 - [P.txt "%s" num] 530 + [txt "%s" num] 150 531 | Link link -> 151 532 render_link forest link 152 533 | Results_of_query q -> ··· 158 539 | Section section -> 159 540 [render_section forest section] 160 541 | KaTeX (mode, content) -> 161 - let l, r = 162 - match mode with 163 - | Display -> {|\[|}, {|\]|} 164 - | Inline -> {|\(|}, {|\)|} 165 - in 166 542 let body = Plain_text_client.string_of_content ~forest: forest.resources ~router: Iri.to_uri content in 167 - [P.txt ~raw: true "%s%s%s" l body r] 543 + (* [txt ~raw: true "%s%s%s" l body r] *) 544 + begin 545 + match mode with 546 + | Inline -> 547 + [span [class_ "math"] [txt ~raw: true "%s" body]] 548 + | Display -> 549 + [div [class_ "math"] [txt ~raw: true "%s" body]] 550 + end 168 551 | TeX_cs cs -> 169 - [P.txt ~raw: true "\\%s" (TeX_cs.show cs)] 552 + [txt ~raw: true "\\%s" (TeX_cs.show cs)] 170 553 | Img img -> 171 554 [render_img img] 172 - | T.Results_of_datalog_query q -> 555 + | Results_of_datalog_query q -> 173 556 (* We could just evaluate the query immediately. This is just experimental*) 174 557 [ 175 - H.div 176 - [] 558 + span 177 559 [ 178 - H.div 179 - [ 180 - Hx.get "/query"; 181 - Hx.trigger "load"; 182 - (* Hx.headers {|{"Content-Type": "application/json"}|}; *) 183 - (* Hx.ext "json-enc"; *) 184 - Hx.vals 185 - "%s" 186 - Repr.( 187 - to_json_string 188 - ~minify: true 189 - (* Datalog_expr.(query_t Repr.string (T.vertex_t T.content_t)) *) 190 - query_t 191 - {query = q} 192 - ) 193 - ] 194 - [] 560 + Hx.get "/query"; 561 + Hx.trigger "load"; 562 + Hx.swap "outerHTML"; 563 + Hx.target "this"; 564 + Hx.vals 565 + "%s" 566 + Repr.( 567 + to_json_string 568 + ~minify: true 569 + query_t 570 + {query = q} 571 + ) 195 572 ] 573 + [] 196 574 ] 197 575 | T.Datalog_script _ -> [] 198 576 | T.Artefact _ 199 577 | T.Iri _ 200 578 | T.Route_of_iri _ -> 201 - [P.txt "todo"] 202 - 203 - and _render_resource resource = 204 - render_content resource.contents 205 - 206 - and render_transclusion (forest : State.t) (transclusion : T.transclusion) : P.node list = 207 - List.concat @@ 208 - Option.to_list @@ 209 - Option.map (render_content forest) @@ 210 - Forest.get_content_of_transclusion transclusion forest.resources 579 + [txt "todo"] 211 580 212 581 (* TODO: links need to be flattened in order to produce valid HTML. *) 213 - and render_link (forest : State.t) (link : T.content T.link) : P.node list = 214 - let article_opt = Forest.get_article link.href forest.resources in 582 + and render_link (forest : State.t) (link : T.content T.link) : node list = 215 583 let attrs = 216 - match article_opt with 584 + match Forest.get_article link.href forest.resources with 217 585 | None -> 218 - [H.href "%s" (Format.asprintf "%a" Iri.pp link.href)] 219 - | Some article -> 586 + (* TODO: rendering of hrefs is suboptimal... *) 220 587 [ 221 - begin 222 - match article.frontmatter.iri with 223 - | Some iri -> H.href "%s" @@ route forest iri 224 - | None -> P.HTML.null_ 225 - end; 226 - (* H.title_ "%s" @@ PT.string_of_content article.frontmatter.title *) 588 + href "%s" (Format.asprintf "%a" Iri.pp link.href); 227 589 ] 590 + | Some article -> 591 + begin 592 + match article.frontmatter.iri with 593 + | Some _iri -> 594 + [ 595 + title_ "%s" @@ 596 + Option.value ~default: "" @@ 597 + Option.map 598 + ( 599 + Plain_text_client.string_of_content 600 + ~forest: forest.resources 601 + ~router: (Legacy_xml_client.route forest) 602 + ) 603 + article.frontmatter.title; 604 + href "/trees%s" (Format.asprintf "%s" (Iri.path_string link.href)); 605 + Hx.target "#tree-container"; 606 + Hx.swap "innerHTML"; 607 + ] 608 + | None -> [HTML.null_] 609 + end; 228 610 in 229 - [H.a attrs @@ render_content forest link.content] 611 + [ 612 + span 613 + [class_ "link local"] 614 + [a attrs (render_content forest link.content)] 615 + ] 230 616 231 - let render_query_result (forest : State.t) vs = 232 - let render_vertex = function 233 - | T.Iri_vertex iri -> 234 - begin 235 - match Forest.find_opt forest.resources iri with 236 - | None -> 237 - H.li 238 - [] 239 - [ 240 - H.a 241 - [H.href "%s" (Format.asprintf "%a" pp_iri iri)] 242 - [P.txt "%s" (Format.asprintf "%a" pp_iri iri)] 243 - ] 244 - | Some (T.Article a) -> 245 - H.li 246 - [] 247 - [ 248 - render_frontmatter forest a.frontmatter; 249 - H.div [] @@ render_content forest a.mainmatter 250 - ] 251 - | Some (T.Asset _) -> 252 - P.txt "todo: render asset" 253 - end 254 - | T.Content_vertex c -> H.div [] (render_content forest c) 255 - in 617 + let render_query_result (forest : State.t) (vs : Vertex_set.t) = 618 + let module C = Types.Comparators(struct 619 + let string_of_content = 620 + Plain_text_client.string_of_content 621 + ~forest: forest.resources 622 + ~router: (route forest) 623 + end) in 256 624 vs 257 - |> Vertex_set.to_list (* TODO: Needs to be sorted *) 258 - |> List.map render_vertex 625 + |> Vertex_set.to_seq 626 + |> Seq.filter_map Vertex.iri_of_vertex 627 + |> Seq.filter_map (fun iri -> Forest.get_article iri forest.resources) 628 + |> List.of_seq 629 + |> List.sort C.compare_article 630 + |> List.map 631 + ( 632 + T.article_to_section 633 + ~flags: {T.default_section_flags with 634 + expanded = Some false; 635 + numbered = Some false; 636 + included_in_toc = Some false; 637 + metadata_shown = Some true 638 + } 639 + ) 640 + |> List.map (render_section forest) |> fun nodes -> 641 + if List.length nodes = 0 then None 642 + else Some (div [class_ "tree-content"] nodes) 643 + 644 + let render_toc _article = 645 + nav 646 + [id "toc"; Hx.swap_oob "true"] 647 + [ 648 + div 649 + [class_ "block"] 650 + [h1 [] [txt "Table of contents"]] 651 + ]
+6 -6
lib/frontend/Htmx_client.mli
··· 9 9 10 10 module T = Forester_core.Types 11 11 12 - module P = Pure_html 13 - module H = P.HTML 14 12 15 13 type query = { 16 14 query: (string, T.content T.vertex) Forester_core.Datalog_expr.query; ··· 18 16 val query_t : query Repr.t 19 17 20 18 val route : State.t -> Forester_core.iri -> string 21 - val render_article : State.t -> T.content T.article -> P.node 19 + val render_article : State.t -> T.content T.article -> Pure_html.node 22 20 23 - val render_content : State.t -> T.content -> P.node list 24 - val render_frontmatter : State.t -> T.content T.frontmatter -> P.node 21 + val render_content : State.t -> T.content -> Pure_html.node list 22 + val render_frontmatter : State.t -> T.content T.frontmatter -> Pure_html.node 25 23 26 - val render_query_result : State.t -> Vertex_set.t -> P.node list 24 + val render_query_result : State.t -> Vertex_set.t -> Pure_html.node option 25 + 26 + val render_toc : 'a -> Pure_html.node
+1 -1
lib/frontend/Legacy_xml_client.mli
··· 13 13 val route : State.t -> Iri.t -> string 14 14 val render_article : State.t -> T.content T.article -> P.node 15 15 val render_content : State.t -> T.content -> P.node list 16 - val pp_xml : forest:State.t -> ?stylesheet:string -> Format.formatter -> T.content T.article -> unit 16 + val pp_xml : forest: State.t -> ?stylesheet: string -> Format.formatter -> T.content T.article -> unit
+70
lib/server/Headers.ml
··· 1 + (* 2 + * SPDX-FileCopyrightText: 2024 The Forester Project Contributors 3 + * 4 + * SPDX-License-Identifier: GPL-3.0-or-later 5 + *) 6 + 7 + open Forester_core 8 + 9 + module T = Types 10 + 11 + let parse_flag field header = 12 + match Http.Header.get header field with 13 + | Some "true" -> Some true 14 + | Some "false" -> Some false 15 + | Some _ -> None 16 + | None -> None 17 + 18 + let parse_title_flags 19 + (header : Http.Header.t) 20 + : T.title_flags option 21 + = 22 + match parse_flag "Empty-When-Untitled" header with 23 + | Some b -> Some T.{empty_when_untitled = b;} 24 + | None -> None 25 + 26 + let parse_section_flags (header : Http.Header.t) : T.section_flags option = 27 + let hidden_when_empty = parse_flag "Hidden-When-Empty" header in 28 + let included_in_toc = parse_flag "Included-In-Toc" header in 29 + let header_shown = parse_flag "Header-Shown" header in 30 + let metadata_shown = parse_flag "Metadata-Shown" header in 31 + let numbered = parse_flag "Numbered" header in 32 + let expanded = parse_flag "Expanded" header in 33 + Some 34 + { 35 + hidden_when_empty; 36 + included_in_toc; 37 + header_shown; 38 + metadata_shown; 39 + numbered; 40 + expanded 41 + } 42 + 43 + let parse_modifier (header : Http.Header.t) : T.modifier option = 44 + match Http.Header.get header "Modifier" with 45 + | Some "Identity" -> Some Identity 46 + | Some "Sentence-Case" -> Some Sentence_case 47 + | _ -> None 48 + 49 + let parse_content_target (header : Http.Header.t) : T.content_target option = 50 + let open Http in 51 + match Header.get header "Taxon" with 52 + | Some _ -> Some T.Taxon 53 + | None -> 54 + match Header.get header "Mainmatter" with 55 + | Some _ -> Some T.Mainmatter 56 + | None -> 57 + match Header.get header "Full" with 58 + | Some _ -> 59 + begin 60 + match parse_section_flags header with 61 + | Some flags -> Some (T.Full flags) 62 + | None -> None 63 + end 64 + | None -> 65 + match Header.get header "Title" with 66 + | None -> None 67 + | Some _ -> 68 + match parse_title_flags header with 69 + | Some flags -> Some (T.Title flags) 70 + | None -> None
+62
lib/server/Index.ml
··· 1 + (* 2 + * SPDX-FileCopyrightText: 2024 The Forester Project Contributors 3 + * 4 + * SPDX-License-Identifier: GPL-3.0-or-later 5 + *) 6 + 7 + open Pure_html 8 + open HTML 9 + 10 + let v ?c () = 11 + html 12 + [] 13 + [ 14 + head 15 + [] 16 + [ 17 + meta [name "viewport"; content "width=device-width";]; 18 + link [rel "stylesheet"; href "/style.css";]; 19 + link [rel "icon"; type_ "image/x-icon"; href "/favicon.ico";]; 20 + (* script [type_ "module"; src "min.js";] ""; *) 21 + script [src "/htmx.js"] ""; 22 + link [rel "stylesheet"; href "https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"; integrity "sha384-zh0CIslj+VczCZtlzBcjt5ppRcsAmDnRem7ESsYwWwg3m/OaJ2l4x7YBZl9Kxxib"; crossorigin `anonymous;]; 23 + script [src "https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.js"; integrity "sha384-CAltQiu9myJj3FAllEacN6FT+rOyXo+hFZKGuR2p4HB8JvJlyUHm31eLfL4eEiJL"; crossorigin `anonymous;] ""; 24 + title [] ""; 25 + ]; 26 + body 27 + [Hx.boost true;] 28 + [ 29 + header [] []; 30 + div 31 + [id "grid-wrapper";] 32 + [ 33 + match c with 34 + | Some stuff -> stuff 35 + | None -> 36 + article 37 + [ 38 + id "tree-container"; 39 + Hx.get "/home"; 40 + Hx.trigger "load"; 41 + Hx.target "this"; 42 + Hx.swap "outerHTML"; 43 + ] 44 + []; 45 + (* nav *) 46 + (* [id "toc";] *) 47 + (* [ *) 48 + (* div *) 49 + (* [class_ "block";] *) 50 + (* [ *) 51 + (* h1 *) 52 + (* [] *) 53 + (* [ *) 54 + (* txt "Table of contents"; *) 55 + (* ]; *) 56 + (* ul [id "toc-list";] []; *) 57 + (* ]; *) 58 + (* ]; *) 59 + ]; 60 + div [id "modal-container";] []; 61 + ]; 62 + ]
+2
lib/server/Router.ml
··· 18 18 | Nil 19 19 | Home 20 20 | Query 21 + | Htmx 21 22 22 23 let routes : route router = 23 24 one_of ··· 33 34 route (s "nil" /? nil) Nil; 34 35 route (s "home" /? nil) Home; 35 36 route (s "query" /? nil) Query; 37 + route (s "htmx.js" /? nil) Htmx; 36 38 ]
+98 -27
lib/server/Server.ml
··· 15 15 16 16 type theme = { 17 17 stylesheet: string; 18 - index: string; 18 + htmx: string; 19 19 js_bundle: string; 20 20 font_dir: string; 21 21 favicon: string; ··· 26 26 let base_dir = List.hd theme_location in 27 27 let theme_dir = EP.(env#fs / base_dir / "theme") in 28 28 let stylesheet = EP.(load (theme_dir / "style.css")) in 29 - let index = EP.(load (theme_dir / "index.html")) in 29 + let htmx = EP.(load (theme_dir / "htmx.js")) in 30 30 let js_bundle = EP.(load (env#fs / base_dir / "min.js")) in 31 31 let favicon = EP.(load (theme_dir / "favicon.ico")) in 32 32 let font_dir = EP.(native_exn @@ theme_dir / "fonts") in 33 - {stylesheet; index; js_bundle; font_dir; favicon;} 33 + {stylesheet; htmx; js_bundle; font_dir; favicon;} 34 34 35 35 let lookup_font ~env theme font = 36 36 Eio.Path.(load (env#fs / theme.font_dir / font)) ··· 38 38 let handler 39 39 : env: < fs: [> Eio.Fs.dir_ty] Eio.Path.t; .. > -> 40 40 theme: theme -> 41 - forest: _ -> 42 - 'a -> 41 + forest: State.t -> 42 + Cohttp_eio.Server.conn -> 43 43 Http.Request.t -> 44 44 Cohttp_eio.Body.t -> 45 45 Cohttp_eio.Server.response ··· 73 73 in 74 74 Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body () 75 75 | Stylesheet -> 76 - let headers = Http.Header.of_list ["Content-Type", "text/css"] in 76 + let headers = Http.Header.of_list ["Content-Type", "text/css"; "charset", "utf-8"] in 77 77 Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: theme.stylesheet () 78 78 | Js_bundle -> 79 79 let headers = Http.Header.of_list ["Content-Type", "application/javascript"] in 80 80 Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: theme.js_bundle () 81 81 | Index -> 82 - Cohttp_eio.Server.respond_string ~status: `OK ~body: theme.index () 82 + Cohttp_eio.Server.respond_string ~status: `OK ~body: (Pure_html.to_string (Index.v ())) () 83 83 | Favicon -> 84 84 let headers = Http.Header.of_list ["Content-Type", "image/x-icon"] in 85 85 Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: theme.favicon () 86 86 | Tree s -> 87 - let iri = Iri_scheme.user_iri ~host: State.(forest.config.host) s in 87 + let href = Iri_scheme.user_iri ~host: State.(forest.config.host) s in 88 + let request_headers = Http.Request.headers request in 89 + let is_htmx = Option.is_some @@ Http.Header.get request_headers "Hx-Request" in 88 90 begin 89 - match Forest.get_article iri forest.resources with 90 - | None -> Cohttp_eio.Server.respond_string ~status: `Not_found ~body: "" () 91 - | Some article -> 92 - let content = Pure_html.to_string @@ Htmx_client.render_article forest article in 93 - Cohttp_eio.Server.respond_string ~status: `OK ~body: content () 91 + if is_htmx then 92 + (* If it is an HTMX request, we just send a fragment. *) 93 + begin 94 + match Headers.parse_content_target request_headers with 95 + | None -> 96 + begin 97 + match Forest.get_article href forest.resources with 98 + | None -> Cohttp_eio.Server.respond_string ~status: `Not_found ~body: "" () 99 + | Some content -> 100 + let response = 101 + Pure_html.( 102 + to_string @@ 103 + (Htmx_client.render_article forest content) 104 + ) 105 + in 106 + Cohttp_eio.Server.respond_string ~status: `OK ~body: response () 107 + end 108 + | Some target -> 109 + let modifier = Option.value ~default: T.Identity (Headers.parse_modifier request_headers) in 110 + match Forest.get_content_of_transclusion 111 + {target; href; modifier;} 112 + forest.resources with 113 + | None -> Cohttp_eio.Server.respond_string ~status: `Not_found ~body: "" () 114 + | Some content -> 115 + let response = 116 + Pure_html.( 117 + to_string @@ 118 + HTML.span [] (Htmx_client.render_content forest content) 119 + ) 120 + in 121 + Cohttp_eio.Server.respond_string ~status: `OK ~body: response () 122 + end 123 + else 124 + (* If it is not an HTMX request, we need to send the whole page. *) 125 + match Forest.get_article href forest.resources with 126 + | Some article -> 127 + let content = 128 + Pure_html.to_string @@ 129 + Index.v 130 + ~c: (Htmx_client.render_article forest article) 131 + () 132 + in 133 + let headers = Http.Header.of_list ["Content-Type", "text/html"] in 134 + Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: content () 135 + | None -> Cohttp_eio.Server.respond_string ~status: `Not_found ~body: "" () 94 136 end 95 137 | Search -> 96 138 if request.meth = `POST then ··· 133 175 Cohttp_eio.Server.respond_string ~status: `OK ~body: "" () 134 176 | Some home_tree -> 135 177 let content = Pure_html.to_string @@ Htmx_client.render_article forest home_tree in 136 - Cohttp_eio.Server.respond_string ~status: `OK ~body: content () 178 + let headers = Http.Header.of_list ["Content-Type", "text/html"] in 179 + Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: content () 137 180 end 138 181 | Query -> 139 182 let q = Uri.get_query_param resource "query" in ··· 148 191 let (_, _, result) = State_machine.update (Query q) forest in 149 192 begin 150 193 match result with 151 - | Vertex_set vs -> 152 - Htmx_client.render_query_result forest vs 194 + | Vertex_set vs -> Htmx_client.render_query_result forest vs 153 195 | Got _ 154 196 | Error _ 155 197 | Nothing -> 156 - [Pure_html.txt "failed to run"] 198 + None 199 + (* Pure_html.txt "failed to run" *) 157 200 end 158 201 | Error (`Msg str) -> 159 - [Pure_html.txt "failed to parse: %s" str] 202 + Logs.app (fun m -> m "failed to parse: %s" str); 203 + (* Pure_html.txt "failed to parse: %s" str *) 204 + None 160 205 in 161 - Cohttp_eio.Server.respond_string 162 - ~status: `OK 163 - ~body: ( 164 - Format.asprintf 165 - "%a" 166 - Pure_html.pp 167 - (Pure_html.HTML.ul [] response) 168 - ) 169 - () 206 + begin 207 + match response with 208 + | Some nodes -> 209 + Cohttp_eio.Server.respond_string 210 + ~status: `OK 211 + ~body: (Format.asprintf "%a" Pure_html.pp nodes) 212 + () 213 + | None -> 214 + (* If result is empty, use 215 + [hx-retarget](https://htmx.org/reference/#response_headers) to 216 + hide the entire section. Right now I am just trying to get the 217 + backmatter to render correctly, I don't know if this is 218 + compatible with the other use cases of queries. I can think of 219 + multiple ways to work around this. We could use a separate 220 + endpoint to get the backmatter, or we could do some more 221 + HTMXing. I guess the question boils down to which approach is 222 + more in line with our overarching goal of making forester a 223 + genuine hypermedia format 224 + *) 225 + let headers = 226 + Http.Header.of_list 227 + [ 228 + "Hx-Retarget", "closest section.backmatter-section"; 229 + "Hx-Swap", "delete" 230 + ] 231 + in 232 + Cohttp_eio.Server.respond_string 233 + ~headers 234 + ~status: `OK 235 + ~body: "" 236 + () 237 + end 238 + | Htmx -> 239 + let headers = Http.Header.of_list ["Content-Type", "application/javascript"] in 240 + Cohttp_eio.Server.respond_string ~headers ~status: `OK ~body: theme.htmx () 170 241 end 171 242 | Routes.NoMatch -> 172 243 Cohttp_eio.Server.respond_string ~status: `Not_found ~body: "" ()