Compare commits

...

386 Commits
master ... rm

Author SHA1 Message Date
Carson Gross
21fbcab37d changelog for alpha-5 release 2025-12-31 13:52:54 -07:00
Carson Gross
bcbd9944da update sha 2025-12-31 13:43:31 -07:00
Carson Gross
ef6195dd5f build for next release 2025-12-31 13:13:46 -07:00
Carson Gross
36f5b3146b build for next release 2025-12-31 13:06:32 -07:00
Carson Gross
cf35ea5e07 only use href for cross-domain requests 2025-12-31 13:01:49 -07:00
Carson Gross
98130683cb Merge remote-tracking branch 'origin/four' into four 2025-12-31 12:47:17 -07:00
MichaelWest22
7dbb8acfa6
Upsert swap extension (#3595)
* add upsert swap extension

* improve upsert

* simplify upsert to not use morph

* add doco

* Add hx-upsert tag support as well

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-31 12:47:08 -07:00
Carson Gross
4606fdc4ae fix cross domain request support 2025-12-31 12:33:56 -07:00
MichaelWest22
77e5c4724b
Handle newContent properly in insertContent (#3607)
* handle newContent processing in other swap styles

* Handle outerMorph newContent processing by adding target to newContent so it will be processed as well. also simplified textContent swaps

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-30 07:54:52 -07:00
MichaelWest22
095db015a4
Push response url (#3608)
* handle response url from redirects in push Url true

* add test

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-30 07:53:58 -07:00
MichaelWest22
9bab1d6704
handle no id found with hx-preserve (#3610)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-30 07:53:03 -07:00
Carson Gross
3e47d556c0 invert boolean to make things clearer 2025-12-29 08:28:22 -07:00
Carson Gross
b7fb573d01 docs cleanup 2025-12-29 08:28:06 -07:00
Carson Gross
f1db10f0eb trigger event on target 2025-12-24 14:25:22 -07:00
Carson Gross
50fbb1f91d update docs 2025-12-24 14:05:53 -07:00
Carson Gross
017934f816 parameterize settle time & don't do CSS transition work in case of transitions 2025-12-24 13:49:57 -07:00
Carson Gross
777ec267e7 remove unused param 2025-12-24 13:42:00 -07:00
Carson Gross
0dea4bc308 Merge branch 'four' into feature/settle-restoration
# Conflicts:
#	src/htmx.js
2025-12-24 13:40:25 -07:00
MichaelWest22
55227c058c
Improve moprh exact node matching with scan ahead (#3591)
* Improve moprh exact node matching with scan ahead

* better inline comments documentation

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-19 11:43:24 -07:00
Stu Kennedy
37cf0e8c6c
WebSocket Extension (hx-ws) Improvements (#3592)
* refactor: Enhance WebSocket extension with URL normalization, improved request management, and refined message handling for better reliability and clarity.

feat: Add manual WebSocket server script and enhance WebSocket documentation with detailed message formats and connection management improvements.

feat: Include event type in WebSocket messages and update documentation for message format

* refactor: Update WebSocket extension to connect immediately by default, enhance documentation on connection triggers, and improve message handling examples.

* feat: Introduce URL validation for WebSocket send attributes to ensure proper connection handling and prevent non-URL markers from being processed.
2025-12-19 11:42:36 -07:00
MichaelWest22
56e6810284
add textContent swap style (#3593)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-19 11:39:09 -07:00
Carson Gross
2f816af101 move swap delay and settle delay inside of __insertContent (makes more sense to me here) 2025-12-18 11:18:41 -07:00
Carson Gross
7fea06f00d remove bad images 2025-12-15 17:26:00 -07:00
Carson Gross
08f6b0e431 remove demo 2025-12-15 17:22:50 -07:00
Carson Gross
1a4268dab4 docs cleanup 2025-12-15 14:52:50 -07:00
Carson Gross
c603dc9543 Merge remote-tracking branch 'origin/four' into four 2025-12-15 14:42:07 -07:00
Carson Gross
5dffcd4bc2 fix docs 2025-12-15 14:41:52 -07:00
Carson Gross
ab862afe74 finish up hx-sync 2025-12-15 14:41:45 -07:00
MichaelWest22
4403f8eff6
optimize hx-vals and headers async with a callback and fix ws async (#3581)
optimize hx-vals and headers async with a callback and fix ws async hx-vals

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-11 11:04:08 -07:00
MichaelWest22
a624a90dee
Add Morhing documentation (#3582)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-11 11:03:41 -07:00
MichaelWest22
40ecbfd6b6
bug morph cleanup of textNodes should work (#3584)
* bug morph cleanup of textNodes should work

* use firstchild check instead

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-12 01:26:41 +13:00
Carson Gross
628b5a6a6c add update-sha.sh 2025-12-09 12:15:54 -07:00
Carson Gross
c917b4e880 prep release 2025-12-09 10:20:10 -07:00
MichaelWest22
f0ff590fb4
handle returning this from findAllExt for any inheritance level (#3579)
* change thisElt to thisAttr to simplify

* Handle returning this properlty at any level

* add this wrapper for clarity

* move to callback instead

* rename to eltCollector

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-09 09:23:30 -07:00
Carson Gross
b2f4abae76 add hx-disable.js test and fix logic to be the same as hx-indicator 2025-12-07 10:22:35 -07:00
Carson Gross
42e84477dd add tests to test.html 2025-12-07 10:09:03 -07:00
Carson Gross
cab78da9ee make hx-headers dynamic like hx-vals and port 2.x tests 2025-12-07 10:08:51 -07:00
Carson Gross
36acc330f4 add sanity tests for core attributes 2025-12-07 09:31:58 -07:00
Carson Gross
c96179f3e7 port 2.0 tests and fix hx-include overriding 2025-12-07 09:24:23 -07:00
MichaelWest22
3b21c64193
improve parseConfig and append to allow joining json for hx-vals (#3577)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-07 09:00:47 -07:00
Carson Gross
aa61c169fd changelog 2025-12-06 16:45:15 -07:00
Carson Gross
30371270f7 regen release 2025-12-06 16:44:27 -07:00
Carson Gross
c878a939d6 remove historyReload in favor of "reload" value for htmx.config.history 2025-12-06 16:43:11 -07:00
Carson Gross
1d42990568 fix stream/sse docs 2025-12-06 16:31:39 -07:00
Carson Gross
f0c9858405 fix stream/sse docs 2025-12-06 16:15:38 -07:00
Carson Gross
79b02f13be clean up docs + TODOs.md 2025-12-06 11:58:14 -07:00
Carson Gross
7619e303a1 update site to use new goodies 2025-12-06 11:54:41 -07:00
Carson Gross
8334be5065 update transition recommendations to match what we use on htmx.org 2025-12-06 11:51:07 -07:00
Carson Gross
9a41d40fd6 remove bad links 2025-12-06 11:50:52 -07:00
Carson Gross
0df6c98e4e update web 2025-12-06 11:37:16 -07:00
Carson Gross
7972c4ea5a update distribution & web 2025-12-06 11:36:57 -07:00
Carson Gross
c499c799cb release on Monday 2025-12-06 11:34:51 -07:00
Carson Gross
e8b63f9130 changelog 2025-12-06 11:34:23 -07:00
Carson Gross
a56b6220fb migration guide cleanup 2025-12-06 11:29:46 -07:00
Carson Gross
fc0c2c6857 oops, missed hx-vals.md 2025-12-06 11:29:28 -07:00
Carson Gross
fde4f4616f remove obsolete events 2025-12-06 11:29:06 -07:00
Carson Gross
7c3909fa07 update swap docs 2025-12-06 11:28:54 -07:00
Carson Gross
ef730551a6 add strip documentation 2025-12-06 11:16:12 -07:00
Carson Gross
04c110efc1 mention extension locations 2025-12-06 11:16:03 -07:00
Carson Gross
95478ba43a include all events in docs 2025-12-06 11:11:22 -07:00
Carson Gross
dbac18235c include all swaps in docs 2025-12-06 11:11:15 -07:00
Carson Gross
c5f6e88ffd docs updates 2025-12-06 11:01:23 -07:00
Carson Gross
78b5e2b865 update version 2025-12-06 08:32:56 -07:00
Carson Gross
24507afacc move htmx config back to htmx-config 2025-12-06 07:57:41 -07:00
Carson Gross
71167c7248 Merge remote-tracking branch 'origin/four' into four 2025-12-06 07:38:54 -07:00
MichaelWest22
101110521a
handle transition overrides on oob/partial (#3570)
* handle transition overrides on oob/partial

* Handle swap dealys properly for transition true/false and respect main swap transition and swap delay.

* swapDelay let

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-06 07:35:56 -07:00
Carson Gross
99ad2bfb16 turning view transitions off by default 2025-12-06 07:32:57 -07:00
Carson Gross
9146038611 Merge remote-tracking branch 'origin/four' into four 2025-12-05 11:01:46 -07:00
Carson Gross
259ab69b0f fix hx-indicator 2025-12-05 11:01:37 -07:00
MichaelWest22
a5f48867b0
Morph skip support for web components (#3573)
* add morphSkip configs to allow web component no morph override options

* add tests

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-04 11:46:48 -07:00
MichaelWest22
757bd19fea
improve hx-boost advanced config to use new mergeConfig for nesting (#3572)
improve hx-boost advanced config to use new mergeConfig for nesting support

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-04 11:45:54 -07:00
MichaelWest22
0bb59973f8
fix hx-validate to respect noValidate and validate inputs outside of … (#3562)
fix hx-validate to respect noValidate and validate inputs outside of just forms

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-04 11:45:04 -07:00
MichaelWest22
ab37e06ccc
fix missing target/swap via envelope and fix re-connection tests (#3571)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-04 11:39:14 -07:00
MichaelWest22
7e7592aeb8
fix bug with empty response after partial removal detection (#3569)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-03 13:21:18 -07:00
MichaelWest22
22205ce3f4
Add mergeConfig support and remove + hx-config merging (#3561)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-03 13:21:01 -07:00
MichaelWest22
1ca51d2fde
implement advanced hx-boost overrides (#3549)
* implement advanced hx-boost overrides

* fix hx-boost false and change to override hx-* attributes

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-03 13:20:30 -07:00
Carson Gross
a47f4ccd7d retrigger 2.x events 2025-12-03 11:00:28 -07:00
Carson Gross
65c8fb4791 Merge remote-tracking branch 'origin/four' into four 2025-12-02 10:04:32 -07:00
Carson Gross
c6089e320c update TODOs 2025-12-02 10:04:24 -07:00
MichaelWest22
69fb2e099a
fix hx-preload module import example 2025-11-28 10:05:11 +13:00
Carson Gross
d4aa31348a changelog 2025-11-27 13:27:15 -07:00
Carson Gross
e27d311f2a prep release 2025-11-27 13:23:55 -07:00
Carson Gross
9a7568bd88 Merge remote-tracking branch 'origin/four' into four 2025-11-27 13:20:53 -07:00
Carson Gross
2841257abc prep release 2025-11-27 13:20:42 -07:00
Ben Michie
2bb9542cff
[docs] Fix typos in documentation (#3558)
Fix typos in documentation
2025-11-27 13:06:45 -07:00
Ben Michie
f4812fd131
[docs] Remove redundant migration note (#3559)
[docs] Remove redundant migration now

The same note was repeated twice
2025-11-27 13:06:22 -07:00
Ben Michie
2b9a89eb98
Fix closing tags for hx-partial in htmx-4.md (#3557)
Corrected closing tags for hx-partial elements in the example.
2025-11-27 13:05:28 -07:00
MichaelWest22
0c8dff8e4d
add findall and fix find (#3555)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-25 18:36:18 -07:00
MichaelWest22
db92b4c755
allow string swapSpecs in insertContent (#3551)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-25 09:35:53 -07:00
Carson Gross
6b05e5117d make extension swaps just a plain function invocation 2025-11-24 17:40:10 -07:00
Carson Gross
b0a343c5fa fix registration 2025-11-24 16:27:13 -07:00
Carson Gross
04dd71d82f clean up headers 2025-11-24 16:09:15 -07:00
MichaelWest22
90727149c7
add hx-target hx-source and hx-request-type headers (#3548)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-24 14:57:54 -07:00
Carson Gross
acf93fc989 support multiple events and the touchstart event for boosted links 2025-11-23 13:41:38 -07:00
Carson Gross
4afa4a9bfe add stu 2025-11-23 13:28:29 -07:00
Carson Gross
eab3084943 add stu 2025-11-23 13:27:54 -07:00
Carson Gross
5616fed975 add stu 2025-11-23 13:25:06 -07:00
Carson Gross
b6f09a0825 update TODOs 2025-11-23 13:08:24 -07:00
Carson Gross
d209b34d0a hx-head extension 2025-11-23 12:14:49 -07:00
Stu Kennedy
7510030e59
Websocket extension for HTMX (#3547)
* feat: add hx-ws WebSocket extension with accompanying tests and debug utilities.

* feat: Refine WebSocket extension initialization and processing logic, preventing re-initialization and ensuring comprehensive element handling.

* refactor: Enhance WebSocket connection handling with improved event triggering and dynamic configuration management

* refactor: Introduce dynamic configuration management for WebSocket connections, consolidating default settings into a dedicated function for improved maintainability and clarity.

* refactor: Update WebSocket extension to improve reconnection logic and dynamic configuration handling, enhancing maintainability and clarity.

* docs: Expand WebSocket extension documentation with detailed architecture, attributes, message formats, and example use cases for improved clarity and usability.

* refactor: Update WebSocket message structure to include default values for channel and format, enhancing clarity in documentation and implementation.

* feat: Add default `channel` and `format` values to WebSocket messages if not provided.
2025-11-23 11:38:02 -07:00
Carson Gross
b747eb015b extensions proposal 2025-11-22 11:40:42 -07:00
Carson Gross
b2c6d68345 remove live for now 2025-11-22 11:20:52 -07:00
Carson Gross
45dfd97a5d Merge remote-tracking branch 'origin/four' into four 2025-11-22 11:20:47 -07:00
Carson Gross
9c91fe3bbf placeholder for alex to work on the compat extension 2025-11-22 11:20:40 -07:00
MichaelWest22
1a14d3d9ea
handle duplicates in action query params and form data (#3543)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-22 11:01:12 -07:00
MichaelWest22
d569ce6ccf
move to registerExtension to avoid extension upgrade confusion (#3546)
* move to registerExtension to avoid extension upgrade confusion

* Also fix window.htmx export to make modules work with extensions and add initial types file

* update docs on module use

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-22 10:58:38 -07:00
MichaelWest22
24ee8ef50f
extension trigger can not replace meta character or extensions don't fire 2025-11-21 13:09:38 +13:00
Carson Gross
1134ab2f6f i'm an idiot, make onLoad use htmx:after:process 2025-11-20 10:21:56 -07:00
Carson Gross
5dc0301f9f update TODOs 2025-11-19 11:27:07 -07:00
Carson Gross
2046d05e8f tests that I missed 2025-11-19 11:18:38 -07:00
Carson Gross
0e1cdad9f1 merge streaming changes 2025-11-19 11:18:25 -07:00
Carson Gross
e52f853dab update scum's photo 2025-11-19 11:05:35 -07:00
Carson Gross
53691cac7f add stephen to the team page 2025-11-19 10:54:15 -07:00
Carson Gross
bee0e5cae0 web socket skeleton code for stu 2025-11-19 10:51:02 -07:00
Carson Gross
376c8b540f prep alpha3 release 2025-11-18 10:58:27 -07:00
Carson Gross
7dc5d03a16 prep alpha3 release 2025-11-18 10:47:23 -07:00
Carson Gross
a7f8bebe7b prep alpha3 release 2025-11-18 10:45:30 -07:00
Carson Gross
6225ad3743 prep alpha3 release 2025-11-18 10:45:26 -07:00
Carson Gross
0794ae8572 Merge in headers from https://github.com/bigskysoftware/htmx/pull/3533 (leave HX-Request-Type out for now until we discuss) 2025-11-18 10:42:49 -07:00
MichaelWest22
aaff01750e
Implement hx-custom template tag support (#3537)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-18 07:19:03 -07:00
MichaelWest22
75b2a57d1f
handle inputs for web components that are form associated (#3523)
* use new formData(form) to capture web component inputs as well

* Handle duplicate name inputs better

* Add Tests

* restore comments

* restore default check

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-18 07:17:32 -07:00
MichaelWest22
0d72f8b2e2
add hx-confirm htmx:confirm event with async issueRequest (#3532)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-18 07:16:03 -07:00
MichaelWest22
44599fb4de
generalize hx-status and fix ajax issues (#3528)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-16 07:36:02 -07:00
Carson Gross
d94fd73fe0 Docs error for hx-status
fixes   https://github.com/bigskysoftware/htmx/issues/3520
2025-11-15 10:46:40 -07:00
Carson Gross
73dda15f0c support js: prefix in hx-vals
fixes  https://github.com/bigskysoftware/htmx/issues/3521
2025-11-15 10:35:38 -07:00
Carson Gross
45543851be support js: prefix in hx-vals 2025-11-15 08:41:43 -07:00
Carson Gross
d1675b81bb Merge remote-tracking branch 'origin/four' into four 2025-11-15 08:26:19 -07:00
Carson Gross
4c06231682 change langauge 2025-11-15 08:26:09 -07:00
MichaelWest22
f3953b29b0
Remove find ext tokenizer (#3518)
* move tokenizer to parseConfig for all the things

* move sse mode logic

* upgrade all JSON.parse!

* convert the find extended selector tokenizer to regex solution

* convert the find extended selector tokenizer to regex solution

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-12 06:15:34 -07:00
MichaelWest22
a9ba0a1c35
move tokenizer to parseConfig for all the things (#3516)
* move tokenizer to parseConfig for all the things

* move sse mode logic

* upgrade all JSON.parse!

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-12 04:55:02 -07:00
Christian Tanul
ee4ef5f18d
update /patterns & /help page (#3513)
* fix autofocus on mobile search bar

* improve pattern documentation structure and titles

- inline-validation.md → active-validation.md
- bulk-update.md → bulk-actions.md
- delete-row.md → delete-in-place.md
- sortable.md → drag-to-reorder.md
- click-to-edit.md → edit-in-place.md
- value-select.md → linked-selects.md
- reset-user-input.md → reset-on-submit.md
- Merged tabs-hateoas.md and tabs-javascript.md into single tabs.md
- Removed obsolete files:
- file-upload-input.md (merged into file-upload.md)
- web-components.md (content moved to /docs - not a pattern)
- Refreshed titles and descriptions
- Updated icons for better visual consistency
- Disabled interactive demos:
- Commented out {{ demo_environment() }} and demo code blocks in: animations, click-to-load, infinite-scroll, file-upload, bulk-actions, drag-to-reorder, edit-in-place, active-search
- Minor formatting cleanup across multiple pattern files

* update /help page
2025-11-10 19:56:53 -07:00
Tristan Druyen
11d38e33b6
[4] Small docs fix for <hx-partial> (#3509)
Another small docs fix
2025-11-10 17:16:06 -07:00
Carson Gross
a1e8be7261 lol fix links 2025-11-10 17:01:20 -07:00
Carson Gross
5a83dc99e2 missed a file 2025-11-10 12:47:21 -07:00
Carson Gross
1f3298f6f3 alpha announcement 2025-11-10 12:42:40 -07:00
Carson Gross
374073e7ad docs 2025-11-10 12:25:26 -07:00
Carson Gross
5690bbc64c alpha2 prep 2025-11-10 12:14:17 -07:00
Carson Gross
4e8b5b53a0 support autofocus 2025-11-10 12:07:34 -07:00
Christian Tanul
0168bbca84
update "Patterns" page (#3510) 2025-11-10 11:12:51 -07:00
Carson Gross
98a0e2b2a7 Merge remote-tracking branch 'origin/four' into four 2025-11-10 11:12:38 -07:00
Carson Gross
d69aa7ac6d make it so 4xx and 5xx can be added to noSwap as strings 2025-11-10 11:12:31 -07:00
Christian Tanul
b16b96e757
website fixes (#3504)
* comment out TailwindCSS play CDN from base.html

* simplify tailwindcss classes in favor of server-side rendering

* fix table of contents anchors mistakenly using hx-boost

* fix _hyperscript not working after boost

* update href attributes in ToC to avoid boosting links
2025-11-10 07:15:03 -07:00
Christian Tanul
006a87441f
Fix title not decoded on boost ("&lt;/&gt; htmx" instead of "</> htmx") (#3507)
* fix page title HTML entities being escaped with hx-boost

* add test for decoding HTML in title

* simplify title HTML entity decoding, update website htmx.js

* simplify further & update htmx.js in www/
2025-11-11 02:46:33 +13:00
MichaelWest22
778a40f03f
replace state on initial state load (#3506)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-10 06:31:12 -07:00
MichaelWest22
d353a0909c
fix up handle swap extension point and doco (#3505)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-10 06:30:00 -07:00
MichaelWest22
5568bc2d73
fix bug in hx-config setting wrong level for + merge (#3508)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-11 02:07:27 +13:00
MichaelWest22
d121ba520e
Handle event.state null by reloading to handle back event to first page 2025-11-11 00:49:06 +13:00
MichaelWest22
d622812882
Allow any extension to be regisgtered unless you allow list via meta tag (#3503)
* optimize oob and fix partial tests

* Allow any extension to be registered unless you allow list via meta tag.

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-09 21:38:15 -07:00
Carson Gross
68b5d9137f lets get a little aggressive on the ol nav... 2025-11-09 18:30:12 -07:00
Carson Gross
dbbda6b9f4 rebuild and re-www 2025-11-09 18:23:51 -07:00
Carson Gross
0e254520af start re-implementing scroll & show, still needs focus
website now uses real preload extension!
2025-11-09 18:18:37 -07:00
Carson Gross
2d2b77b3f7 make preload handle boosted elements, defaulting to mousedown 2025-11-09 17:24:26 -07:00
Carson Gross
29d1f765a4 update build task to include extensions 2025-11-09 17:22:25 -07:00
Carson Gross
00ab434e75 clean up boost logic to not boost non-same-origin links & forms 2025-11-09 17:21:51 -07:00
MichaelWest22
0ea0fb0e1c
Ext doco (#3502)
* optimize oob and fix partial tests

* add extension doco and expose ctx.text to extensions

* fix handleAnchorScroll position

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-09 16:50:50 -07:00
MichaelWest22
dd10454f20
optimize oob and fix partial tests (#3500)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-10 10:11:21 +13:00
Carson Gross
bb046fdafc fix boosts 2025-11-09 14:09:03 -07:00
Christian Tanul
25c2a08617
Pimp out the website for htmx 4.0 (#3499)
flatten website directory structure and redesign UI
- move all templates from themes/htmx-theme/templates/ to www/templates/
- move all static assets from themes/htmx-theme/static/ to www/static/
- remove theme layer and update config.toml
- add tailwind css with typography plugin
- update hero section with 3D effects and subtle synthwave aesthetic
- update header with improved design and navigation
- add shortcut for search bar in header (CMD+K on macos, CTRL+K elsewhere)
- add status bar in header that displays title=... attribute on hover for any element
- update footer with improved design and navigation
- rename "examples" to "patterns" throughout site and docs
- reorganize patterns page from 7 categories to 4 (loading, interaction, display, advanced)
- add mac os9 finder-style design to patterns page with Chicago FLF font (classic OS9 window title font)
- add sidebar table of contents for /docs/, /reference/, and /htmx-4/ pages (mobile version still TODO)
- add `npm run site` script that uses npx for tailwindcss and zola-bin (no external dependencies needed)
- remove github buttons.js dependency and replace with custom buttons
- consolidate sponsor images into www/static/img/sponsors/ with logo-<company>.png|svg format
- standardize all image filenames to lowercase kebab-case
- clean up unused sponsor images from www/static/img/
- create construction_warning() shortcode for work-in-progress sections
- add reusable template components (toc, anchor links, sponsors shortcode)
- add horse easter egg when URL includes ?horse=true
- update website htmx.js to latest version
- update npm dependencie
2025-11-09 10:53:39 -07:00
Carson Gross
a419bc81f2 switch to hx-partial 2025-11-09 08:18:24 -07:00
Carson Gross
24e6d5d7c1 docs cleanup 2025-11-08 20:07:14 -07:00
Carson Gross
503313c42b add etag support 2025-11-08 19:54:13 -07:00
Carson Gross
ddb7bcd45a Merge branch 'feature/etag' into four 2025-11-08 19:28:10 -07:00
Carson Gross
76cb4bdea9 updated team 2025-11-08 19:27:59 -07:00
Carson Gross
497d85bc1a updated team 2025-11-08 19:00:20 -07:00
Carson Gross
5d26beadbb updated TODO 2025-11-08 17:51:32 -07:00
Carson Gross
f78de5a79f update docs 2025-11-08 17:47:27 -07:00
Carson Gross
7a7b5e9ba3 docs work 2025-11-08 17:31:15 -07:00
Carson Gross
61dcd67edd docs work 2025-11-08 17:17:07 -07:00
Carson Gross
e8eeb33db6 docs work 2025-11-08 16:53:57 -07:00
Carson Gross
324949d324 docs work 2025-11-08 16:50:39 -07:00
Carson Gross
de931ba2eb docs work 2025-11-08 13:17:47 -07:00
Carson Gross
bbcbf294fc docs work 2025-11-08 12:47:19 -07:00
Carson Gross
fca4729764 update the htmx-4.md document 2025-11-08 09:39:41 -07:00
Carson Gross
f2bcf9d3ce etag support 2025-11-08 08:03:05 -07:00
Carson Gross
8534ab1a7b add 304 as a default no-swap response code 2025-11-08 07:37:26 -07:00
Carson Gross
5f93b0d7c4 expand variables and code to make debugging easier 2025-11-08 07:36:46 -07:00
Krzysztof Szularz
d57874953f
Update index page with correct link to jsdelivr (#3490)
The current link is not working as the specified version doesn't exist as opposed to https://www.jsdelivr.com/package/npm/htmx.org?tab=files&version=4.0.0-alpha1
2025-11-06 19:35:37 -07:00
MichaelWest22
46c978c912
Attribute value only return string again (#3493)
* move attributeValue back to return string and optinally return source only for this target

* remove fetchOverride

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-06 19:35:20 -07:00
Tristan Druyen
91c3dfa87d
[4] Bind window context for Firefox compatibility (#3486)
Bind window context for Firefox compatibility

Causes "'fetch' called on an object that does not implement interface Window" otherwise.
2025-11-07 12:28:06 +13:00
Carson Gross
e339abdcd2 move extension tests out to their own directory 2025-11-06 12:38:22 -07:00
Carson Gross
55f1c17dc0 abort any outstanding requests after tests, make the trigger tests properly await the requests they create to avoid cross-test contamination. :/ 2025-11-06 12:34:53 -07:00
Carson Gross
08a6820d63 clean up html creation 2025-11-06 11:59:03 -07:00
Carson Gross
8fd2c86e84 tighten up the timeout 2025-11-06 11:54:48 -07:00
Carson Gross
e10768991d test cleanup 2025-11-06 11:53:18 -07:00
Carson Gross
0b48282a0f test cleanup 2025-11-06 11:52:14 -07:00
Carson Gross
3858a82242 test reorg and docs 2025-11-06 11:30:55 -07:00
Carson Gross
30ff306c31 support the ability to configure the "meta" character for systems like fastHTML that can't use the colon character 2025-11-06 11:13:19 -07:00
Carson Gross
83da589d33 update __attributeValue to return both the attribute value and the source 2025-11-06 10:32:10 -07:00
Carson Gross
ba18716cc5 clean up last old extensions 2025-11-06 10:31:42 -07:00
MichaelWest22
994decb6fa
fix select and strip and title handling (#3492)
Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-11-06 23:47:53 +13:00
MichaelWest22
77c17f3717
Update build:esm to replace __ with # correclty
The build system for esm is not replacing __ with # for some async prefixed functions causing the esm version to not run correctly
2025-11-06 16:10:41 +13:00
Carson Gross
8e994700ea update TODOS.md 2025-11-05 07:12:23 -07:00
Carson Gross
aa6f9dc759 fix base URL 2025-11-04 07:51:18 -07:00
Carson Gross
c41cef3584 update package lock 2025-11-03 12:20:14 -07:00
Carson Gross
cac0f1387f fix package 2025-11-03 11:46:20 -07:00
Carson Gross
72d6b7888f fix URL 2025-11-03 11:43:54 -07:00
Carson Gross
1480292fea kick netlify 2025-11-03 11:34:09 -07:00
Carson Gross
e3578992e9 kick netlify 2025-11-03 11:33:06 -07:00
Carson Gross
cd9ff02c0d kick netlify 2025-11-03 11:30:58 -07:00
Carson Gross
94b887962a kick netlify 2025-11-03 11:28:59 -07:00
Carson Gross
434808e8bb skip test failing in FF 2025-11-03 10:28:34 -07:00
Carson Gross
387b6f38d3 add distribution for dist 2025-11-03 10:15:12 -07:00
Carson Gross
c5f6e67fb0 final tests fixes for alpha-1 2025-11-03 10:10:44 -07:00
Carson Gross
2c835bcf3f Merge remote-tracking branch 'origin/four' into four
# Conflicts:
#	src/htmx.js
2025-11-03 10:06:14 -07:00
MichaelWest22
b8c2db7633 also run shouldCancel for event filters 2025-11-04 03:11:31 +13:00
MichaelWest22
a69c20f783 Remove settle delay, add initial non transition swap delay option. 2025-11-04 02:09:59 +13:00
MichaelWest22
a9b5ebd3c0 fix shouldCancel test with virtual click event 2025-11-04 01:22:37 +13:00
MichaelWest22
f09899bbe0 improve script handling and custom swap styles 2025-11-04 01:14:46 +13:00
MichaelWest22
bf620a0716 fix #actionSelector 2025-11-03 23:57:20 +13:00
Carson Gross
c978dd499c docs and pattern-based fixes 2025-11-02 23:57:36 -07:00
Carson Gross
ab7a6a2fe1 update site's htmx 2025-11-02 23:02:47 -07:00
Carson Gross
8b4db416ca make all fields private 2025-11-02 23:00:54 -07:00
Carson Gross
66024ee1ea change __htmx prefix to _htmx so it doesn't get privatized 2025-11-02 22:56:58 -07:00
Carson Gross
b0f2a6fed2 add changes 2025-11-02 22:51:42 -07:00
Carson Gross
e98cc7c6d5 fix fade in 2025-11-02 22:51:33 -07:00
Carson Gross
acf8f7ca84 demo fixes 2025-11-02 22:51:22 -07:00
Carson Gross
3525a8cfd6 demo-based fixes + tests 2025-11-02 21:08:06 -07:00
Carson Gross
1aeb2a663a Merge branch 'refs/heads/examples-to-patterns' into four 2025-11-02 19:40:28 -07:00
Carson Gross
d64e43a76a make relative selectors resolve relative to the current element in hx-on 2025-11-02 19:29:34 -07:00
Carson Gross
488fed10b3 save some bytes 2025-11-02 18:59:44 -07:00
Carson Gross
395240dbd9 small cleanup 2025-11-02 18:53:47 -07:00
Carson Gross
835593025c fix tests 2025-11-02 17:02:44 -07:00
Carson Gross
081c4420f3 Merge remote-tracking branch 'origin/four' into four 2025-11-02 16:24:28 -07:00
Carson Gross
b74cdbd00b more unit tests, code coverage at 96.5% 2025-11-02 16:15:56 -07:00
Carson Gross
307a51be9b test extract headers 2025-11-02 16:03:49 -07:00
MichaelWest22
10edc67d51 Merge branch 'four' of https://github.com/bigskysoftware/htmx-temp-private into four 2025-11-03 11:54:19 +13:00
MichaelWest22
24eec024ec rename preload cache test 2025-11-03 11:54:11 +13:00
Carson Gross
96fb6b8525 disable & indicator unit tests 2025-11-02 15:47:28 -07:00
MichaelWest22
49cec5554a code coverage 2025-11-03 11:44:45 +13:00
MichaelWest22
8d350c84c2 fix perload tests 2025-11-03 11:23:24 +13:00
MichaelWest22
8af66aeaff
Merge pull request #14 from bigskysoftware/response-headers
response-headers implmentation
2025-11-03 11:17:43 +13:00
MichaelWest22
b78301b195
Merge branch 'four' into response-headers 2025-11-03 11:17:07 +13:00
MichaelWest22
9d6c0cc8aa response-headers implmentation 2025-11-03 11:10:59 +13:00
Carson Gross
d241e3052f request queue tests 2025-11-02 14:51:53 -07:00
Carson Gross
c483c83667 __handleStatusCodes unit test 2025-11-02 14:39:09 -07:00
Carson Gross
1bf6aa0768 __handleHistoryUpdate unit test 2025-11-02 13:33:45 -07:00
Carson Gross
7b43aa4913 __issueRequest tests 2025-11-02 13:19:23 -07:00
Carson Gross
4dfa2184e3 tests 2025-11-02 12:34:51 -07:00
Carson Gross
77fdf75756 rename to reflect historical nature of test 2025-11-02 11:18:18 -07:00
Carson Gross
057fbbc923 unit tests for remaining public methods 2025-11-02 11:17:16 -07:00
Carson Gross
f889eb55c7 more unit tests 2025-11-02 11:05:44 -07:00
Carson Gross
e575afbc8b add __normalizeSwapStyle unit tests 2025-11-02 10:43:48 -07:00
Carson Gross
1ca7fa4cd6 add parseInterval unit tests 2025-11-02 10:42:12 -07:00
Carson Gross
5d5fe1dd32 __parseTriggerSpecs unit tests 2025-11-02 10:38:43 -07:00
Carson Gross
4c8592d632 __tokenize unit tests 2025-11-02 10:34:23 -07:00
Carson Gross
68e277777b __parseSwapSpec unit tests 2025-11-02 10:31:40 -07:00
Carson Gross
2edc7a923b added view transition test 2025-11-02 10:15:29 -07:00
Carson Gross
fd349a3852 event tests 2025-11-02 10:08:25 -07:00
Carson Gross
1a565595c4 css transition unit test (!!!!!!!!!) 2025-11-02 10:06:14 -07:00
Carson Gross
a2e4eba596 oob unit tests (so good) 2025-11-02 09:35:55 -07:00
Carson Gross
8a4271f673 swap unit tests 2025-11-02 09:26:53 -07:00
Carson Gross
3f3dad37cd remove mutation observer in favor of directly calling process 2025-11-02 08:47:02 -07:00
Carson Gross
d28b5c6f58 docs work 2025-11-02 07:36:11 -07:00
Carson Gross
ae9ecd9d67 docs work 2025-11-02 07:36:11 -07:00
1cg
75952fb4bd
Merge pull request #13 from bigskysoftware/internalAPI
test using internalAPI for preload
2025-11-02 07:35:59 -07:00
MichaelWest22
cd2409d9a3 test using internalAPI for preload 2025-11-03 01:53:48 +13:00
Carson Gross
dc20119ecd Merge branch 'four' into examples-to-patterns
# Conflicts:
#	www/content/_index.md
#	www/content/docs.md
2025-11-01 20:23:40 -06:00
Carson Gross
6747849e7a docs work 2025-11-01 20:22:10 -06:00
Carson Gross
f54927e91a docs work 2025-11-01 20:22:05 -06:00
Carson Gross
27d2f0e4f1 CSS transitions are BACK ON THE MENU 2025-11-01 19:44:05 -06:00
Carson Gross
d951834dc8 move preload/optimistic out of core 2025-11-01 13:16:35 -06:00
1cg
6d7594aabe
Merge pull request #12 from bigskysoftware/extension-upgrade
upgrade extension to be a prebuilt function map
2025-10-31 18:41:53 -05:00
Carson Gross
09547b6788 cleanups 2025-10-31 17:33:43 -06:00
Carson Gross
529242bb84 make handleTriggerEvent private 2025-10-31 16:53:00 -06:00
Carson Gross
865eb0c5ad support :append modifier for attributes 2025-10-31 16:42:13 -06:00
Carson Gross
969009716c optimistic tests and a fix 2025-10-31 16:23:14 -06:00
Carson Gross
32169cb9b2 add tests for prefix functionality 2025-10-31 15:48:28 -06:00
Carson Gross
d30de6f334 handle prefix customization properly 2025-10-31 15:45:11 -06:00
Carson Gross
225668e7d0 make find and findAll really call findExt and findAllExt (right thing) 2025-10-31 15:29:37 -06:00
Carson Gross
db02394df9 test clean up and added more for status retargeting (+ bug fix) 2025-10-31 15:08:39 -06:00
Carson Gross
55676cfcce buff out trigger tests 2025-10-31 14:52:13 -06:00
Carson Gross
85b55aabaa remove polling test in favor of faster direct test 2025-10-31 14:26:07 -06:00
Carson Gross
dc6445f0a8 clean up tests 2025-10-31 14:25:32 -06:00
Carson Gross
607a85fc16 fix tests 2025-10-31 13:05:59 -06:00
Carson Gross
6f26994c91 implement viewTransition queue (tests are broken, will work through next) 2025-10-31 13:03:27 -06:00
scriptogre
00bbfd8aae
rename examples to patterns 2025-10-31 19:58:36 +02:00
scriptogre
ff8cb2aeb6
update debug toolbar
- reimplement debug toolbar using _hyperscript
- use OS9 aesthetic for debug toolbar
2025-10-31 19:58:35 +02:00
MichaelWest22
54430b3ef9 improve empty return performance 2025-10-31 17:29:43 +13:00
MichaelWest22
9bfdc8d1ba upgrade extension to be a prebuilt function map 2025-10-31 17:20:23 +13:00
Carson Gross
50767016d1 support attribute prefixes 2025-10-30 21:24:50 -06:00
Carson Gross
6409d1dd69 more savings 2025-10-30 21:19:00 -06:00
Carson Gross
ee933faf0a remove dead code 2025-10-30 21:12:53 -06:00
Carson Gross
04d6e66169 remove extra events that we've never had before anyway, save bytes 2025-10-30 21:11:00 -06:00
Carson Gross
a274166add more byte savings 2025-10-30 21:08:09 -06:00
Carson Gross
25336c904a more byte savings, config cleanup 2025-10-30 21:04:57 -06:00
Carson Gross
cde3da7362 more byte savings 2025-10-30 20:55:01 -06:00
Carson Gross
d701bcdd65 save bytes 2025-10-30 20:45:32 -06:00
Carson Gross
4acd40ee0d pass through fetch response args 2025-10-30 20:41:22 -06:00
Carson Gross
c1a6f71ec7 simplify code 2025-10-30 20:32:34 -06:00
Carson Gross
973de64633 surface public API in js expressions 2025-10-30 20:25:16 -06:00
Carson Gross
06f6b555fd fix up status handling 2025-10-30 20:00:31 -06:00
Carson Gross
fff493075b whitespace 2025-10-30 18:47:29 -06:00
Carson Gross
eccff9a956 save more bytes, remove unused prop 2025-10-30 17:33:45 -06:00
Carson Gross
da86fbc272 save more bytes 2025-10-30 17:26:42 -06:00
Carson Gross
d794855c9b save some bytes 2025-10-30 15:09:49 -06:00
Carson Gross
9eff4f9c3d merge michaels extensions work 2025-10-30 14:55:44 -06:00
Carson Gross
dcdabda985 merge michaels extensions work 2025-10-30 14:52:48 -06:00
Carson Gross
957f9631f1 Merge branch 'four' into extension
# Conflicts:
#	src/htmx.js
2025-10-30 14:49:50 -06:00
Carson Gross
467911be6a Merge branch 'four' into morph
# Conflicts:
#	src/htmx.js
2025-10-30 14:41:11 -06:00
Carson Gross
07fa52367b clean up preserve to not use an id to smuggle the pantry around 2025-10-30 11:29:44 -06:00
Carson Gross
da8205b3fd add swap configurations per response code inspired by https://htmx.org/extensions/response-targets/ 2025-10-30 10:53:24 -06:00
Carson Gross
a151bd72ea redirect /talk to /help 2025-10-30 10:52:40 -06:00
scriptogre
d6ea211333
add fetch demo environment & update "Click to Edit" example 2025-10-30 16:49:37 +02:00
MichaelWest22
eb5e7d8bc2 morph to inside class 2025-10-31 00:45:23 +13:00
MichaelWest22
e996484c74 morph extension integration 2025-10-31 00:32:08 +13:00
MichaelWest22
890b824a3f initial extension implementation 2025-10-30 19:10:25 +13:00
1cg
45030b403c
Merge pull request #8 from bigskysoftware/oob-slimdown
Oob slimdown
2025-10-29 20:58:08 -05:00
Carson Gross
43330fc028 team content 2025-10-29 19:45:18 -06:00
Carson Gross
d59babf7fa fix history note 2025-10-29 12:18:29 -06:00
Carson Gross
de1621dd5a more website cleanup 2025-10-29 12:01:29 -06:00
1cg
15d664d5e8
Merge pull request #9 from bigskysoftware/selectorTokenizer
slim down selector tokenizer
2025-10-29 12:36:59 -05:00
Carson Gross
3f68b47926 website update 2025-10-29 11:23:17 -06:00
Carson Gross
42fc2febab Merge remote-tracking branch 'origin/four' into four 2025-10-29 11:09:47 -06:00
Carson Gross
b588ae7c33 website update 2025-10-29 11:09:35 -06:00
scriptogre
d4bab403a1
remove unused variable 2025-10-29 16:14:29 +02:00
Carson Gross
5a9a9b7e8e add docs for /wwww 2025-10-29 07:09:48 -06:00
Carson Gross
b3ad4cb292 site update 2025-10-29 06:55:31 -06:00
Carson Gross
26aa4ad212 Merge branch 'four' into website-rework 2025-10-28 20:50:25 -06:00
MichaelWest22
f3a3205071 slim down selector tokenizer 2025-10-29 15:00:46 +13:00
MichaelWest22
88ba531bf4 fragment var rename 2025-10-29 12:36:28 +13:00
MichaelWest22
9920bf3ca4 slim down oob handling code 2025-10-29 11:54:05 +13:00
Carson Gross
7a5b18a38d docs 2025-10-28 12:20:51 -06:00
Carson Gross
186c3d948d basic docs on the scripting API 2025-10-28 12:09:33 -06:00
Carson Gross
9515515bfe introduce htmx scripting API concept 2025-10-28 11:54:33 -06:00
Carson Gross
871a5c6147 make it possible to execute async javascript 2025-10-28 11:32:34 -06:00
Carson Gross
321da7045c update TODOS.md 2025-10-28 11:32:19 -06:00
Carson Gross
1b6127731b website work 2025-10-28 10:28:32 -06:00
1cg
d349d2c06f
Merge pull request #7 from bigskysoftware/sse
add server-sent events (sse) streaming support
2025-10-28 11:21:36 -05:00
scriptogre
db09daa7ac
update SSE section in docs.md 2025-10-28 18:07:14 +02:00
scriptogre
2f9318a4cd
add demo for custom events via SSE in manual tests 2025-10-28 17:19:54 +02:00
scriptogre
23885b409a
enable sending custom events using SSE 2025-10-28 17:15:23 +02:00
scriptogre
5a97b83afb
rename chunk to sseMessage 2025-10-28 16:42:48 +02:00
scriptogre
314a540bc8
replace <partial> with <htmx-action type="partial"> 2025-10-28 16:29:07 +02:00
scriptogre
afb1af04e1
add server-sent events (sse) streaming support
Implement native SSE handling with configurable stream modes (once/continuous), automatic reconnection with exponential backoff, and lifecycle events.

Streams are configured via hx-stream attribute or htmx.config.streams global defaults.

Update test infrastructure with ReadableStream-based mocking utilities.

# Conflicts:
#	src/htmx.js
2025-10-28 16:13:43 +02:00
1cg
751da3b01a
Merge pull request #6 from bigskysoftware/refresh-support2
Refresh support2
2025-10-28 08:01:59 -05:00
MichaelWest22
f817eceb27 Try moving to ajax api for history reload 2025-10-29 01:52:52 +13:00
Carson Gross
53adb72dac add support for fetch() based refreshes 2025-10-29 01:33:55 +13:00
MichaelWest22
bec53b64b7 fix hx-select-oob test after innerHTML default change 2025-10-29 01:24:41 +13:00
Carson Gross
d844a0022e Merge branch 'four' into hx-select-oob
# Conflicts:
#	src/htmx.js
2025-10-28 06:07:26 -06:00
MichaelWest22
8ae0bfb5ff fix other outerHTML defaults 2025-10-29 01:03:27 +13:00
MichaelWest22
8e50b49841 implement hx-select-oob and improve ajax api pus/replace/selectOOB 2025-10-29 00:41:39 +13:00
Carson Gross
82d2e580fd i declare outerHTML default ANETHMA 2025-10-28 05:37:14 -06:00
Carson Gross
bfc378d07d server side include 2025-10-28 05:15:50 -06:00
Carson Gross
d36dbf8b05 trying out some different styling 2025-10-27 21:29:52 -06:00
1cg
afad328c5f
Merge pull request #1 from bigskysoftware/swap-refactor
Swap refactor
2025-10-27 21:47:25 -05:00
1cg
894c474898
Merge pull request #3 from bigskysoftware/ajax-api
implement ajax api
2025-10-27 21:47:11 -05:00
MichaelWest22
9ed426c987 implement ajax api 2025-10-28 15:37:51 +13:00
MichaelWest22
b8830d6931
Merge pull request #2 from bigskysoftware/swap-refactor2
Swap refactor2
2025-10-28 09:40:24 +13:00
Carson Gross
1bd602625b update TODOs 2025-10-28 09:33:35 +13:00
Carson Gross
515ff04acb fix race in request queueing 2025-10-28 09:33:35 +13:00
scriptogre
8bd473c7b0 update SSE implementation
- replace previous implementation with a simple fetch-based approach
- long-lived streams still to be implemented using @microsoft/fetch-event-source, but SSE tests need to be restructured
2025-10-28 09:33:35 +13:00
Carson Gross
8d5a3a9f00 more trigger tests 2025-10-28 09:30:43 +13:00
Carson Gross
266b6a63a3 add trigger test 2025-10-28 09:30:43 +13:00
Carson Gross
da84d0f955 TODOs cleanup 2025-10-28 09:30:43 +13:00
MichaelWest22
449440dd61 fix test breakage 2025-10-28 09:18:45 +13:00
MichaelWest22
dc5a327483 minor reformat 2025-10-28 09:18:45 +13:00
Carson Gross
4c2c6e5ed9 update TODOs 2025-10-27 12:52:12 -06:00
Carson Gross
083b0100de Merge branch 'four' into swap-refactor
# Conflicts:
#	src/htmx.js
2025-10-27 10:13:19 -06:00
Carson Gross
42efea585c fix race in request queueing 2025-10-27 10:11:22 -06:00
Carson Gross
a54d8a7ed6 Merge branch 'four' of github.com:bigskysoftware/htmx-temp-private into four 2025-10-27 09:59:35 -06:00
Carson Gross
93f6775179 more trigger tests 2025-10-27 07:41:53 -06:00
scriptogre
2ac86e2c44
update SSE implementation
- replace previous implementation with a simple fetch-based approach
- long-lived streams still to be implemented using @microsoft/fetch-event-source, but SSE tests need to be restructured
2025-10-27 15:39:16 +02:00
MichaelWest22
af16a46287 fix test breakage 2025-10-28 02:38:19 +13:00
Carson Gross
b3ec7ff326 add trigger test 2025-10-27 07:35:15 -06:00
Carson Gross
3d1ab46133 TODOs cleanup 2025-10-27 07:29:50 -06:00
MichaelWest22
aab719b426 minor reformat 2025-10-28 02:18:51 +13:00
MichaelWest22
e208901cbd refactor swap 2025-10-28 01:53:32 +13:00
scriptogre
347d1e3bfc
rename cfg to ctx 2025-10-27 11:28:28 +02:00
Carson Gross
6235540f9a docs cleanup 2025-10-26 22:10:43 -06:00
Carson Gross
fe1ead65db docs cleanup 2025-10-26 22:07:33 -06:00
Carson Gross
ff3eab933e docs cleanup 2025-10-26 22:06:25 -06:00
Carson Gross
8429333c9d docs cleanup 2025-10-26 21:58:18 -06:00
Carson Gross
6ff58703c8 initial migration guide 2025-10-26 21:33:25 -06:00
Carson Gross
e4ea5dc7dc implement htmx:abort event 2025-10-26 21:32:49 -06:00
Carson Gross
c78ae8a9bf content fixes 2025-10-26 20:55:40 -06:00
Carson Gross
9ef4006e78 update attributes 2025-10-26 20:42:19 -06:00
Carson Gross
404e3861ee first pass over docs 2025-10-26 18:42:11 -06:00
Carson Gross
48dfd56501 test deploy 2025-10-26 16:12:52 -06:00
Carson Gross
8ba138f230 add a status flag to ctx 2025-10-26 16:01:02 -06:00
Carson Gross
0e5d488e01 more ctx.request work 2025-10-26 15:16:54 -06:00
Carson Gross
ccdb704740 more ctx.request work 2025-10-26 15:11:42 -06:00
Carson Gross
333808d63f rename property 2025-10-26 14:19:35 -06:00
Carson Gross
03175bf02e rename event 2025-10-26 14:12:41 -06:00
Carson Gross
8fabd24eb7 begin context name refactor 2025-10-26 14:10:41 -06:00
Carson Gross
3430916dbc check htmx back in for now (need to figure out how to resolve things properly later) 2025-10-26 13:56:17 -06:00
Carson Gross
2d0839a928 move code into four branch 2025-10-26 13:26:44 -06:00
838 changed files with 60907 additions and 92644 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ test/scratch/scratch.html
.DS_Store
.vscode
/coverage
/tmp

View File

@ -1,5 +1,59 @@
# Changelog
## [4.0.0-alpha6] - TBD
### Breaking Changes
* Re-introduced swap/settle mechanism from 2.0 for CSS transitions
* Extension hooks: `htmx_after_restore` replaced with `htmx_before_settle` and `htmx_after_settle`
* Added `upsert` swap style - updates existing elements by ID and inserts new ones ([#3595](https://github.com/bigskysoftware/htmx/pull/3595))
* Added `textContent` swap style - replaces text content without parsing HTML ([#3593](https://github.com/bigskysoftware/htmx/pull/3593))
* History now uses response URL when server redirects ([#3608](https://github.com/bigskysoftware/htmx/pull/3608))
* Fixed `hx-preserve` handling when preserved element ID not found ([#3610](https://github.com/bigskysoftware/htmx/pull/3610))
* Fixed cross-domain request URL handling
* Improved morph matching with exact node scan-ahead ([#3591](https://github.com/bigskysoftware/htmx/pull/3591))
* WebSocket extension improvements ([#3592](https://github.com/bigskysoftware/htmx/pull/3592))
* Settle events now fire on target element (not document) for better event handling
* Optimized async handling in `hx-vals` and `hx-headers` ([#3581](https://github.com/bigskysoftware/htmx/pull/3581))
## [4.0.0-alpha5] - 2025-12-08
* **BREAKING**: View transitions now default to `false` (previously `true`)
* **BREAKING**: Moved the htmx meta config name from `htmx:config` back to `htmx-config`
* Fixed `hx-indicator` attribute handling
* Added support for transition overrides on OOB and partial swaps ([#3570](https://github.com/bigskysoftware/htmx/pull/3570))
* Added morph skip support for Web Components ([#3573](https://github.com/bigskysoftware/htmx/pull/3573))
* Implemented advanced `hx-boost` configuration overrides ([#3549](https://github.com/bigskysoftware/htmx/pull/3549), [#3572](https://github.com/bigskysoftware/htmx/pull/3572))
* Added `mergeConfig` support and improved `hx-config` merging ([#3561](https://github.com/bigskysoftware/htmx/pull/3561))
* Fixed `hx-validate` to respect `noValidate` and validate inputs outside of forms ([#3562](https://github.com/bigskysoftware/htmx/pull/3562))
* Fixed bug with empty response after partial removal detection ([#3569](https://github.com/bigskysoftware/htmx/pull/3569))
* Removed `htmx.config.historyReload` in favor of `htmx.config.history = 'reload'`
## [4.0.0-alpha4] - 2025-11-27
* WebSocket extension for htmx 4.0 ([#3547](https://github.com/bigskysoftware/htmx/pull/3547))
* Allow string swapSpecs in `insertContent()` ([#3551](https://github.com/bigskysoftware/htmx/pull/3551))
* Added `HX-Target`, `HX-Source`, and `HX-Request-Type` request headers ([#3548](https://github.com/bigskysoftware/htmx/pull/3548))
* Handle duplicates in action query params and form data ([#3543](https://github.com/bigskysoftware/htmx/pull/3543))
* Changed extension registration to `registerExtension()` to avoid upgrade confusion ([#3546](https://github.com/bigskysoftware/htmx/pull/3546))
## [4.0.0-alpha3] - 2025-11-18
* Added `HX-Source` request header containing element `id` ([#3533](https://github.com/bigskysoftware/htmx/pull/3533))
* Re-added `HX-Current-URL` request header ([#3533](https://github.com/bigskysoftware/htmx/pull/3533))
* Extensions can now handle custom `<hx-*>` template tags via `htmx:process:*` events ([#3537](https://github.com/bigskysoftware/htmx/pull/3537))
* `hx-confirm` now fires `htmx:confirm` event with async `issueRequest()` callback for custom confirmation dialogs ([#3532](https://github.com/bigskysoftware/htmx/pull/3532))
* `hx-status` can now set multiple context properties using config syntax ([#3528](https://github.com/bigskysoftware/htmx/pull/3528))
* `hx-vals` now supports `js:` prefix for JavaScript expressions ([#3516](https://github.com/bigskysoftware/htmx/pull/3516))
* Unified configuration parser across all attributes, removed separate tokenizer ([#3516](https://github.com/bigskysoftware/htmx/pull/3516))
* Form data collection now uses native `FormData(form)` to support Web Components ([#3523](https://github.com/bigskysoftware/htmx/pull/3523))
* Fixed `htmx.ajax()` API target/source resolution issues ([#3528](https://github.com/bigskysoftware/htmx/pull/3528))
* Thanks to @MichaelWest22 for the majority of this release!
## [4.0.0-alpha1] - 2025-11-3
* See https://four.htmx.org/htmx-4/
## [2.0.8] - 2025-10-24
* [Updated](https://github.com/bigskysoftware/htmx/commit/b9336a96fbdcf28550699971dc2218a90c7a4e01) `parseHTML` to use to use the (unfortunately named) [`Document.parseHTMLUnsafe()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/parseHTMLUnsafe_static)
@ -55,7 +109,7 @@
## [2.0.3] - 2024-10-03
* Added support for the experimental `moveBefore()` functionality in [Chrome Canary](https://www.google.com/chrome/canary/),
see the [demo page](/examples/move-before) for more information.
see the [demo page](/patterns/move-before) for more information.
* Fixed `revealed` event when a resize reveals an element
* Enabled `hx-preserve` in oob-swaps
* Better degredation of `hx-boost` on forms with query parameters in their `action`
@ -85,8 +139,8 @@
* Removed extensions and moved to their own repos linked off of <https://extensions.htmx.org>
* The website now supports dark mode! (Thanks [@pokonski](https://github.com/pokonski)!)
* The older, deprecated [SSE & WS](https://v1.htmx.org/docs/#websockets-and-sse) attributes were removed
* Better support for [Web Components & Shadow DOM](https://htmx.org/examples/web-components/)
* The older, deprecated [SSE & WS](https://v1.htmx.org/docs/) attributes were removed TODO fix link
* Better support for [Web Components & Shadow DOM](https://htmx.org/patterns/web-components/)
* HTTP `DELETE` requests now use parameters, rather than form encoded bodies, for their payload (This is in accordance w/ the spec.)
* Module support was split into different files:
* We now provide specific files in `/dist` for the various JavaScript module styles:
@ -487,7 +541,7 @@
## [1.0.1] - 2020-12-04
* AJAX file upload now correctly fires events, allowing for [a proper progress bar](https://htmx.org/examples/file-upload)
* AJAX file upload now correctly fires events, allowing for [a proper progress bar](https://htmx.org/patterns/file-upload)
* htmx api functions that expect an element now can accept a string selector instead:
```js
htmx.on('#form', 'htmx:xhr:progress', function(evt) {

View File

@ -41,7 +41,7 @@ By removing these arbitrary constraints htmx completes HTML as a
The [`hx-post`](https://htmx.org/attributes/hx-post) and [`hx-swap`](https://htmx.org/attributes/hx-swap) attributes tell htmx:
> "When a user clicks on this button, issue an AJAX request to /clicked, and replace the entire button with the response"
> When a user clicks on this button, issue an AJAX request to /clicked, and replace the entire button with the response
htmx is the successor to [intercooler.js](http://intercoolerjs.org)
@ -61,7 +61,7 @@ Note there is an old broken package called `htmx`. This is `htmx.org`.
* <https://htmx.org/docs>
## contributing
Want to contribute? Check out our [contribution guidelines](CONTRIBUTING.md)
Want to contribute? Check out our [contribution guidelines](dev/CONTRIBUTING.md)
No time? Then [become a sponsor](https://github.com/sponsors/bigskysoftware#sponsors)

53
dev/CODING_STANDARDS.md Normal file
View File

@ -0,0 +1,53 @@
# htmx Coding Standards
* General Code Style
* Prefer `for` loop to `forEach` (easier to debug, compresses better)
* Assign complex expressions to a local variable rather than using them directly in statements (easier to debug)
* Use `let` rather than `const`
* Local variables should have descriptive names in most cases. `ctx` and `elt` are acceptable.
* Terser does a good job of minimizing names, so there is no benefit from a size perspective to using short variable names.
* There is no size code benefit to naked if statements, use curlies to make debugging easier:
```js
// terser turns these two forms into the same compressed code
if(bool) return;
if(bool) {
return;
}
```
* Method/Field Conventions
* Private methods should be prefixed with `__`. The `dist` task will replace double underscore with `#` when it builds
the final script. This allows us to unit test private methods.
* "Internal" methods should be prefixed with a `_`. These methods are _not_ guaranteed to never change, but may be useful
for special cases (e.g. the `quirks` htmx 2.0 compatibility extension)
* Public methods are forever, be very careful with them
* Publicly surfaced properties should not be shortened, _except_ "Configuration" which can be shortened to "Config"
* Architectural Style
* Generally all state in the trigger -> request -> swap life cycle should be stored on `ctx`. Try to avoid overwriting
an existing property, pick a new property name. These properties are part of the public API and *must* be documented.
## Testing
Tests for htmx are organized in the following manner:
* `/test/unit` - These tests should for the most part *directly* exercise public and private methods. Because in
dev private methods are just public methods that start with `__` this is easy to do. Unit tests should be created
after a method has stabilized and the behavior is reasonably well understood.
* `/test/attributes` - These are integration tests that test the full behavior of a given attribute and should do things
like set up a response mock using `mockResponse()`, create a live HTML button with the `createProcessedHTML` method,
invoke `click()` on the button, await the `"htmx:finally:request" event, and assert something about the updated DOM.
* `/test/end2end` - These are end-to-end tests that do not fit in the other two categories
* `/test/ext` - These tests are for the core extensions, which ship as part of htmx
## AI Policy
AI may _not_ be used to generate any significant amount of code that is added to htmx.js. It may be used to _suggest_ code,
but that code must be audited and every line understood by the author.
AI _may_ be used to generate tests for htmx. These tests should follow the existing standards as much as possible and
should ideally be relatively small. No more than one test should be added at a time, and the test should be reviewed
for correctness.
In general, try to keep any AI contributions small and well understood.
> “A computer can never be held accountable, therefore a computer must never make a management decision.”

35
dev/four/TODOS.md Normal file
View File

@ -0,0 +1,35 @@
# Current
* complete port of examples to patterns - @scriptogre
* updated website design - @scriptogre
* Fix issues in https://gist.github.com/dienhoa/af44af16ee91f5a1c8a01c6bfcf53af5
* Port old websocket and sse extensions as `legacy-wc` & `legacy-sse` - @stu
* Figure out final form of the extensions documentation and layouts
* We should port the old extensions, possibly in a new repo, to 4.0
* Update docs, clarify "core" vs "supported" (e.g. not in htmx project, but maintained by us) vs "community" extensions
# Backlog
* Final documentation scrub
# Done
* ~~`hx-compat.js` - backwards compatibility extension that allows auditing inheritance issues - @1cg~~
* ~~Determine final request headers (https://github.com/bigskysoftware/htmx/issues/3496)~~
* ~~document new extensions architecture - @latent~~
* ~~* Determine how to handle form actions that have query parameters (https://github.com/bigskysoftware/htmx/issues/2151)~~
* ~~`websockets.js` - web sockets - @stu~~
* ~~`head-support` - @latent?~~
* ~~etags - @1cg/@latent~~
* ~~Optimistic response support (would be easy now)~~
* ~~Preload support?~~
* ~~Add in extended selectors (1cg)~~
* ~~Add in custom events like revealed, etc~~ (needs review)
* ~~Add `<partial>` support for SSE & out of band swaps~~
* ~~Add history support~~
* ~~full refresh every time (no local stuff, htmx has taught us our lesson)~~
* ~~Add explicit inheritances support~~
* ~~Create test infrastructure~~
* ~~Decide how we are going to distinguish public from private API~~
* ~~When should we `preventDefault()` on a triggering event?~~
* ~~trigger modifiers~~

View File

@ -0,0 +1,58 @@
# Extension Organization
Extensions have had a rough history with htmx. First, extensions were bundled directly in htmx. Eventually this became
untenable for some extensions: many were either too small and too experimental to keep in sync with the htmx releases.
In htmx 2.0, we moved to separate repository for extensions:
<https://github.com/bigskysoftware/htmx-extensions>
We organized extensions into two categories:
* `core` - core extensions maintained by the htmx team
* `community` - community extensions maintained by other people (or htmx team members occasionally)
At first, moved the documentation out to a separate website. We eventually realized this was a mistake, making it
too hard to find the extensions, and moved the documentation back to the main htmx website:
https://htmx.org/extensions/
All in all, a pretty standard open source charlie fargo.
# htmx4
In htmx4 we have a chance to reorganize things again, based on this experience. One thing that has changed since
the early days is we have a much stronger idea of what extensions should look like and how they should function. htmx4
has a much better extension architecture, requiring far fewer hacks. So my hope is that extensions should be much more
stable, at least the core ones.
We have also moved a lot of extension functionality _into_ htmx 4:
* morphing/idiomorph (via the `morph` swaps)
* response targets (via the `hx-status` attribute)
* sse (via normal streaming response handling)
So, given this state of affairs, I want to propose the following extension layout:
## Core Extensions
I propose that the (much smaller set of) core extensions move _back_ into the main repo and are released with htmx
versions. This includes the following:
* `hx-compat` - an htmx 2.0 compatibility extension
* `hx-optimistic` - an optimistic update extension
* `hx-preload` - a preloading extension
* `hx-ws` - a websockets extension
* `hx-head` - head tag support
We have a much better idea what all these extensions should look like now and they should be stable and easily tied to
the htmx release cycles. All of them fill in major bits of functionality that htmx users might want in their htmx-based
applications.
## Community Extensions
Community extensions will remain in external repos and ported on demand. We will link to these extensions from the
<https://htmx.org/extensions> webpage, but will not try to keep them in a centralized repository. They can each become
their own, independent project (some maintained by htmx contributors, some managed individually)

View File

@ -0,0 +1,384 @@
# WebSocket Extension (hx-ws)
This document describes the WebSocket extension implementation for htmx 4.
## Overview
The `hx-ws` extension provides WebSocket support for htmx, enabling real-time bidirectional communication between the browser and server. It manages WebSocket connections efficiently through reference counting, automatic reconnection, and seamless integration with htmx's swap and event model.
## Core Architecture
### Connection Registry
The extension maintains a global connection registry that ensures:
- **Connection Pooling**: Multiple elements can share a single WebSocket connection to the same URL
- **Reference Counting**: Connections are automatically opened when the first element needs them and closed when the last element is removed
- **Reconnection Management**: Automatic reconnection with exponential backoff when connections drop
### Lifecycle Management
1. **Connection Establishment**: When an element with `hx-ws:connect` is processed, it increments the reference count for that WebSocket URL
2. **Element Cleanup**: When an element is removed from the DOM, it decrements the reference count
3. **Auto-Disconnect**: When the reference count reaches zero, the connection is closed automatically
## Attributes
### `hx-ws:connect`
Establishes a WebSocket connection to the specified URL.
```html
<div hx-ws:connect="/chat">
<!-- Content updated via WebSocket messages -->
</div>
```
**Key Features:**
- Connects immediately when element is processed (default behavior)
- Use `hx-trigger` to defer connection until a specific event (e.g., `hx-trigger="click"`)
- Can set `hx-target` and `hx-swap` for default message handling
- Connection is shared across all elements using the same URL
### `hx-ws:send`
Sends data to the server via WebSocket.
**Form Submission:**
```html
<form hx-ws:send hx-trigger="submit">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
```
**Button with Values:**
```html
<button hx-ws:send hx-vals='{"action":"increment"}' hx-trigger="click">
Increment
</button>
```
**Explicit URL (establishes new connection):**
```html
<button hx-ws:send="/actions" hx-vals='{"type":"ping"}'>
Ping
</button>
```
**Data Sent:**
The extension sends a JSON object containing:
```json
{
"type": "request",
"request_id": "unique-id",
"event": "click",
"headers": {
"HX-Request": "true",
"HX-Current-URL": "https://example.com/page",
"HX-Trigger": "element-id",
"HX-Target": "#target"
},
"values": { /* form data or hx-vals - arrays for multi-value fields */ },
"path": "wss://example.com/ws",
"id": "element-id"
}
```
| Field | Description |
|-------|-------------|
| `type` | Always `"request"` for client-to-server messages |
| `request_id` | Unique ID for request/response matching |
| `event` | The DOM event type that triggered the send (e.g., `"click"`, `"submit"`, `"change"`) |
| `headers` | HTMX-style headers for server-side routing/processing |
| `values` | Form data and `hx-vals` - multi-value fields preserved as arrays |
| `path` | The normalized WebSocket URL |
| `id` | Element ID (only if the triggering element has an `id` attribute) |
## Message Format
### Server → Client (JSON Envelope)
Messages from the server should be JSON objects with this structure:
```json
{
"channel": "ui", // Optional: Channel identifier (default: "ui")
"format": "html", // Optional: Message format (default: "html")
"target": "#element-id", // Optional: specific target selector
"swap": "innerHTML", // Optional: swap strategy
"payload": "<div>...</div>", // The actual content
"request_id": "unique-id" // Optional: matches original request
}
```
**Minimal Example** (using defaults):
```json
{
"payload": "<div>Hello World</div>"
}
```
**Standard Channels:**
- **`ui`** (default): UI updates (HTML content swapping)
- `format: "html"` (default): Swap HTML content into target element
- **Custom channels**: Emit `htmx:wsMessage` event for application handling
**Legacy Format (Deprecated):**
```html
<hx-partial id="target-id">
<div>Content</div>
</hx-partial>
```
### Non-JSON Messages
If the server sends non-JSON data, the extension emits an `htmx:wsUnknownMessage` event with the raw data, allowing applications to handle custom protocols.
## Configuration
Configure via `htmx.config.websockets`:
```javascript
htmx.config.websockets = {
reconnect: true, // Enable auto-reconnect (default: true)
reconnectDelay: 1000, // Initial delay in ms (default: 1000)
reconnectMaxDelay: 30000, // Max delay in ms (default: 30000)
reconnectJitter: true, // Add jitter to reconnect delays (default: true)
pendingRequestTTL: 30000 // TTL for pending requests in ms (default: 30000)
};
```
**Reconnection Strategy:**
- Exponential backoff: `delay = min(reconnectDelay * 2^(attempts-1), reconnectMaxDelay)`
- Jitter adds ±25% randomization to avoid thundering herd
- Attempts counter resets to 0 on successful connection
- To implement visibility-aware behavior, listen for `htmx:ws:reconnect` and cancel if `document.hidden`
## Events
### Connection Events
**`htmx:before:ws:connect`**
- Triggered before establishing a WebSocket connection
- `detail`: `{ url, element }`
- Cancellable via `preventDefault()`
**`htmx:after:ws:connect`**
- Triggered after successful connection
- `detail`: `{ url, element, socket }`
**`htmx:ws:reconnect`**
- Triggered before each reconnection attempt
- `detail`: `{ url, attempts }`
**`htmx:ws:close`**
- Triggered when connection closes
- `detail`: `{ url, code, reason }` (code and reason from WebSocket CloseEvent)
**`htmx:ws:error`**
- Triggered on connection error
- `detail`: `{ url, error }`
### Message Events
**`htmx:before:ws:send`**
- Triggered before sending a message
- `detail`: `{ data, element, url }` (data is the message object, can be modified)
- Cancellable via `preventDefault()`
**`htmx:after:ws:send`**
- Triggered after message is sent
- `detail`: `{ data, url }` (data is the sent message object)
**`htmx:wsSendError`**
- Triggered when send fails (e.g., no connection URL found)
- `detail`: `{ element }`
**`htmx:wsMessage`**
- Triggered for any non-UI channel message (json, audio, binary, custom channels, etc.)
- `detail`: `{ channel, format, payload, element, ... }` (entire envelope plus target element)
- Use this event to implement custom message handling for your application
**`htmx:wsUnknownMessage`**
- Triggered for messages that fail JSON parsing (invalid JSON)
- `detail`: `{ data, parseError }` (raw message data and parse error)
## Implementation Details
### URL Normalization
WebSocket URLs are automatically normalized:
- Relative paths (`/ws/chat`) are converted to absolute WebSocket URLs based on current page location
- `http://` is converted to `ws://`
- `https://` is converted to `wss://`
- Protocol-relative URLs (`//example.com/ws`) use `ws:` or `wss:` based on current page protocol
```html
<!-- All of these work: -->
<div hx-ws:connect="/ws/chat"> <!-- becomes wss://example.com/ws/chat on HTTPS -->
<div hx-ws:connect="ws://localhost:8080/ws">
<div hx-ws:connect="https://api.example.com/ws"> <!-- becomes wss://api.example.com/ws -->
```
### Trigger Semantics
By default, WebSocket connections are established immediately when the element is processed. Use `hx-trigger` only when you want to **defer** connection until a specific event.
```html
<!-- Connects immediately (default) -->
<div hx-ws:connect="/ws">
<!-- Defers connection until click -->
<div hx-ws:connect="/ws" hx-trigger="click">
```
**Important:** Only **bare event names** are supported for connection triggers. Modifiers like `once`, `delay`, `throttle`, `target`, `from`, `revealed`, and `intersect` are **not supported**.
```html
<!-- NOT supported: trigger modifiers -->
<div hx-ws:connect="/ws" hx-trigger="click delay:500ms"> <!-- delay ignored -->
<div hx-ws:connect="/ws" hx-trigger="intersect"> <!-- won't work -->
```
For complex connection control, use the `htmx:before:ws:connect` event:
```javascript
document.addEventListener('htmx:before:ws:connect', (e) => {
if (someCondition) {
e.preventDefault(); // Cancel connection
}
});
```
### HTML Swapping
When a `channel: "ui"` message arrives, the extension uses htmx's internal `insertContent` API:
1. Determine target element (from message `target`, request context, or default `hx-target`)
2. Determine swap strategy (from message `swap`, or default `hx-swap`, or `innerHTML`)
3. Create a document fragment from the payload
4. Call `api.insertContent({target, swapSpec, fragment})`
This ensures WebSocket swaps get proper htmx behavior:
- All swap styles (innerHTML, outerHTML, beforebegin, afterend, etc.)
- Preserved elements (`hx-preserve`)
- Auto-focus handling
- Scroll handling
- Proper cleanup of removed elements
- `htmx.process()` called on newly inserted content (not the old target)
### Request-Response Matching
The extension generates a unique `request_id` for each `hx-ws:send`. When the server includes this `request_id` in the response, the extension:
- Swaps content into the element that originated the request
- Respects that element's `hx-target` and `hx-swap` attributes
- Enables request-response patterns over WebSocket
### Form Integration
Works seamlessly with htmx form handling:
- Collects form data using `api.collectFormData()`
- Processes `hx-vals` using `api.handleHxVals()`
- Respects `hx-include` for additional inputs
- Triggers standard htmx events
### Compatibility with htmx Core
The extension leverages htmx core APIs to avoid duplication:
- `api.attributeValue()`: Read prefixed attributes
- `api.parseTriggerSpecs()`: Parse trigger specifications
- `api.collectFormData()`: Gather form data
- `api.handleHxVals()`: Process hx-vals attribute
- `htmx.process()`: Initialize swapped content
- `htmx.trigger()`: Emit custom events
- `htmx.config.prefix`: Respect custom attribute prefixes
## Comparison to Original Design
The initial design concept proposed:
```html
<button hx-get="ws:/websocket">Send It...</button>
```
### What We Kept
**Reference Counting**: Connections auto-close when last element is removed
**Connection Pooling**: Multiple elements share connections to the same URL
**Context Sending**: Form data and values sent as JSON
**Partial Responses**: Support for `<hx-partial>` (legacy)
**Request Matching**: `request_id` ties responses to originating elements
### What Changed
🔄 **Separate Attributes**: Used `hx-ws:connect` and `hx-ws:send` instead of overloading `hx-get`
- Clearer separation of concerns
- Better developer experience
- Avoids conflicts with HTTP methods
🔄 **JSON Envelope**: Standardized on JSON message format with channels
- More flexible than HTML-only partials
- Supports multiple channels (UI, custom)
- Enables structured metadata (target, swap, etc.)
🔄 **Event-Driven**: Rich event model for application integration
- Before/after hooks for all operations
- Custom message handling via events
- Better observability and debugging
🔄 **Configuration**: Global config object for behavior tuning
- Reconnection strategies
- Auto-connect control
- Background behavior
## Example Use Cases
### Live Chat
```html
<div hx-ws:connect="/chat"
hx-target="#messages"
hx-swap="beforeend">
<div id="messages"></div>
<form hx-ws:send hx-trigger="submit">
<input type="text" name="message">
<button>Send</button>
</form>
</div>
```
### Real-Time Notifications
```html
<div hx-ws:connect="/notifications"
hx-target="#notifications"
hx-swap="afterbegin">
<div id="notifications"></div>
</div>
```
### Interactive Controls
```html
<div hx-ws:connect="/counter">
<div id="counter">0</div>
<button hx-ws:send hx-vals='{"action":"increment"}'>+</button>
<button hx-ws:send hx-vals='{"action":"decrement"}'>-</button>
</div>
```
## Testing
See `test/manual/ws.html` for a comprehensive manual test suite demonstrating:
- Live chat with message history
- Real-time notifications
- Shared counter with multiple controls
- Live stock ticker
- System dashboard with metrics
- Event logging
Run the test server: `node test/manual/ws-server.js`
## Future Considerations
- **Binary Messages**: Support for binary WebSocket frames
- **Compression**: Per-message deflate extension support
- **Authentication**: Token refresh and re-auth patterns
- **Multiplexing**: Virtual channels over single connection
- **Backpressure**: Client-side message queuing and flow control

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

9
dist/ext/README.md vendored
View File

@ -1,9 +0,0 @@
# Why Are These Files Here?
These are legacy extensions for htmx 1.x and are **NOT** actively maintained or guaranteed to work with htmx 2.x.
They are here because we unfortunately linked to unversioned unpkg URLs in the installation guides for them
in 1.x, so we need to keep them here to preserve those URLs and not break existing users functionality.
If you are looking for extensions for htmx 2.x, please see the [htmx 2.0 extensions site](https://htmx.org/extensions),
which has links to the new extensions repos (They have all been moved to their own NPM projects and URLs, like
they should have been from the start!)

View File

@ -1,5 +0,0 @@
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! THESE FILES ARE DEPRECATED AND UNSUPPORTED !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
SEE README.md FOR MORE DETAILS

View File

@ -1,11 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@ -1,20 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('alpine-morph', {
isInlineSwap: function (swapStyle) {
return swapStyle === 'morph';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morph') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
Alpine.morph(target, fragment.firstElementChild);
return [target];
} else {
Alpine.morph(target, fragment.outerHTML);
return [target];
}
}
}
});

View File

@ -1,97 +0,0 @@
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
}
function parseClassOperation(trimmedValue) {
var split = splitOnWhitespace(trimmedValue);
if (split.length > 1) {
var operation = split[0];
var classDef = split[1].trim();
var cssClass;
var delay;
if (classDef.indexOf(":") > 0) {
var splitCssClass = classDef.split(':');
cssClass = splitCssClass[0];
delay = htmx.parseInterval(splitCssClass[1]);
} else {
cssClass = classDef;
delay = 100;
}
return {
operation: operation,
cssClass: cssClass,
delay: delay
}
} else {
return null;
}
}
function performOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, currentRunTime)
}
function toggleOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
setInterval(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, classOperation.delay);
}, currentRunTime)
}
function processClassList(elt, classList) {
var runs = classList.split("&");
for (var i = 0; i < runs.length; i++) {
var run = runs[i];
var currentRunTime = 0;
var classOperations = run.split(",");
for (var j = 0; j < classOperations.length; j++) {
var value = classOperations[j];
var trimmedValue = value.trim();
var classOperation = parseClassOperation(trimmedValue);
if (classOperation) {
if (classOperation.operation === "toggle") {
toggleOperation(elt, classOperation, classList, currentRunTime);
currentRunTime = currentRunTime + classOperation.delay;
} else {
currentRunTime = currentRunTime + classOperation.delay;
performOperation(elt, classOperation, classList, currentRunTime);
}
}
}
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}
}
});
})();

View File

@ -1,100 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('client-side-templates', {
transformResponse : function(text, xhr, elt) {
var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
if (mustacheTemplate) {
var data = JSON.parse(text);
var templateId = mustacheTemplate.getAttribute('mustache-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, data);
} else {
throw "Unknown mustache template: " + templateId;
}
}
var mustacheArrayTemplate = htmx.closest(elt, "[mustache-array-template]");
if (mustacheArrayTemplate) {
var data = JSON.parse(text);
var templateId = mustacheArrayTemplate.getAttribute('mustache-array-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, {"data": data });
} else {
throw "Unknown mustache template: " + templateId;
}
}
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
if (handlebarsTemplate) {
var data = JSON.parse(text);
var templateId = handlebarsTemplate.getAttribute('handlebars-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
if (handlebarsArrayTemplate) {
var data = JSON.parse(text);
var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
var xsltTemplate = htmx.closest(elt, "[xslt-template]");
if (xsltTemplate) {
var templateId = xsltTemplate.getAttribute('xslt-template');
var template = htmx.find("#" + templateId);
if (template) {
var content = template.innerHTML ? new DOMParser().parseFromString(template.innerHTML, 'application/xml')
: template.contentDocument;
var processor = new XSLTProcessor();
processor.importStylesheet(content);
var data = new DOMParser().parseFromString(text, "application/xml");
var frag = processor.transformToFragment(data, document);
return new XMLSerializer().serializeToString(frag);
} else {
throw "Unknown XSLT template: " + templateId;
}
}
var nunjucksArrayTemplate = htmx.closest(elt, "[nunjucks-array-template]");
if (nunjucksArrayTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksArrayTemplate.getAttribute('nunjucks-array-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, {"data": data});
} else {
return nunjucks.render(templateName, {"data": data});
}
}
return text;
}
});

15
dist/ext/debug.js vendored
View File

@ -1,15 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('debug', {
onEvent: function (name, evt) {
if (console.debug) {
console.debug(name, evt);
} else if (console) {
console.log("DEBUG:", name, evt);
} else {
throw "NO CONSOLE SUPPORTED"
}
}
});

View File

@ -1,20 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// Disable Submit Button
htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
let elt = evt.detail.elt;
let target = elt.getAttribute("hx-disable-element");
let targetElements = (target == "self") ? [ elt ] : document.querySelectorAll(target);
for (var i = 0; i < targetElements.length; i++) {
if (name === "htmx:beforeRequest" && targetElements[i]) {
targetElements[i].disabled = true;
} else if (name == "htmx:afterRequest" && targetElements[i]) {
targetElements[i].disabled = false;
}
}
}
});

View File

@ -1,41 +0,0 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function stringifyEvent(event) {
var obj = {};
for (var key in event) {
obj[key] = event[key];
}
return JSON.stringify(obj, function(key, value){
if(value instanceof Node){
var nodeRep = value.tagName;
if (nodeRep) {
nodeRep = nodeRep.toLowerCase();
if(value.id){
nodeRep += "#" + value.id;
}
if(value.classList && value.classList.length){
nodeRep += "." + value.classList.toString().replace(" ", ".")
}
return nodeRep;
} else {
return "Node"
}
}
if (value instanceof Window) return 'Window';
return value;
});
}
htmx.defineExtension('event-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
if (evt.detail.triggeringEvent) {
evt.detail.headers['Triggering-Event'] = stringifyEvent(evt.detail.triggeringEvent);
}
}
}
});
})();

View File

@ -1,146 +0,0 @@
//==========================================================
// head-support.js
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
var api = null;
function log() {
//console.log(arguments);
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
// if the head tag exists...
if (headTag) {
var added = []
var removed = []
var preserved = []
var nodesToAppend = []
htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;
if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}
// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt);
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}
// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);
for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt);
added.push(newElt);
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}
htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
htmx.on('htmx:afterSwap', function(evt){
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})
htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})
htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});
})()

91
dist/ext/hx-compat.js vendored Normal file
View File

@ -0,0 +1,91 @@
(()=>{
//========================================================
// htmx 2.0 compatibility extension
//========================================================
let api
function maybeRetriggerEvent(elt, evtName, detail) {
if (!htmx.config.compat?.doNotTriggerOldEvents) {
htmx.trigger(elt, evtName, detail);
}
}
htmx.registerExtension('compat', {
init: (internalAPI) => {
api = internalAPI;
// revert inheritance
if (!htmx.config.compat?.useExplicitInheritace) {
htmx.config.implicitInheritance = true;
}
// do not swap 4xx and 5xx responses
if (!htmx.config.compat?.swapErrorResponseCodes) {
htmx.config.noSwap.push("4xx", "5xx");
}
},
// Re-delegate new events to old event names for backwards compatibility
htmx_after_implicitInheritance: function (elt, detail) {
if (!htmx.config.compat?.suppressInheritanceLogs) {
console.log("IMPLICIT INHERITANCE DETECTED, attribute: " + detail.name + ", elt: ", elt, ", inherited from: ", detail.parent)
let evt = new CustomEvent("htmxImplicitInheritace", {
detail,
cancelable: true,
bubbles : true,
composed: true,
});
elt.dispatchEvent(evt)
}
},
htmx_after_init: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterOnLoad", detail);
maybeRetriggerEvent(elt, "htmx:afterProcessNode", detail);
maybeRetriggerEvent(elt, "htmx:load", detail);
},
htmx_after_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterRequest", detail);
},
htmx_after_swap: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterSettle", detail);
maybeRetriggerEvent(elt, "htmx:afterSwap", detail);
},
htmx_before_cleanup: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeCleanupElement", detail);
},
htmx_before_history_update: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeHistoryUpdate", detail);
maybeRetriggerEvent(elt, "htmx:beforeHistorySave", detail);
},
htmx_before_init: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeOnLoad", detail);
},
htmx_before_process: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeProcessNode", detail);
},
htmx_before_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeRequest", detail);
maybeRetriggerEvent(elt, "htmx:beforeSend", detail);
},
htmx_before_swap: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeSwap", detail);
},
htmx_before_viewTransition: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeTransition", detail);
},
htmx_config_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:configRequest", detail);
},
htmx_before_restore_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:historyRestore", detail);
},
htmx_after_push_into_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:pushedIntoHistory", detail);
},
htmx_after_replace_into_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:replacedInHistory", detail);
},
htmx_error: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:targetError", detail);
},
});
})()

BIN
dist/ext/hx-compat.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-compat.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{let t;function e(t,e,o){htmx.config.compat?.doNotTriggerOldEvents||htmx.trigger(t,e,o)}htmx.registerExtension("compat",{init:e=>{t=e,htmx.config.compat?.useExplicitInheritace||(htmx.config.implicitInheritance=!0),htmx.config.compat?.swapErrorResponseCodes||htmx.config.noSwap.push("4xx","5xx")},htmx_after_implicitInheritance:function(t,e){if(!htmx.config.compat?.suppressInheritanceLogs){console.log("IMPLICIT INHERITANCE DETECTED, attribute: "+e.name+", elt: ",t,", inherited from: ",e.parent);let o=new CustomEvent("htmxImplicitInheritace",{detail:e,cancelable:!0,bubbles:!0,composed:!0});t.dispatchEvent(o)}},htmx_after_init:function(t,o){e(t,"htmx:afterOnLoad",o),e(t,"htmx:afterProcessNode",o),e(t,"htmx:load",o)},htmx_after_request:function(t,o){e(t,"htmx:afterRequest",o)},htmx_after_swap:function(t,o){e(t,"htmx:afterSettle",o),e(t,"htmx:afterSwap",o)},htmx_before_cleanup:function(t,o){e(t,"htmx:beforeCleanupElement",o)},htmx_before_history_update:function(t,o){e(t,"htmx:beforeHistoryUpdate",o),e(t,"htmx:beforeHistorySave",o)},htmx_before_init:function(t,o){e(t,"htmx:beforeOnLoad",o)},htmx_before_process:function(t,o){e(t,"htmx:beforeProcessNode",o)},htmx_before_request:function(t,o){e(t,"htmx:beforeRequest",o),e(t,"htmx:beforeSend",o)},htmx_before_swap:function(t,o){e(t,"htmx:beforeSwap",o)},htmx_before_viewTransition:function(t,o){e(t,"htmx:beforeTransition",o)},htmx_config_request:function(t,o){e(t,"htmx:configRequest",o)},htmx_before_restore_history:function(t,o){e(t,"htmx:historyRestore",o)},htmx_after_push_into_history:function(t,o){e(t,"htmx:pushedIntoHistory",o)},htmx_after_replace_into_history:function(t,o){e(t,"htmx:replacedInHistory",o)},htmx_error:function(t,o){e(t,"htmx:targetError",o)}})})();

BIN
dist/ext/hx-compat.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-compat.min.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"names":["api","maybeRetriggerEvent","elt","evtName","detail","htmx","config","compat","doNotTriggerOldEvents","trigger","registerExtension","init","internalAPI","useExplicitInheritace","implicitInheritance","swapErrorResponseCodes","noSwap","push","htmx_after_implicitInheritance","suppressInheritanceLogs","console","log","name","parent","evt","CustomEvent","cancelable","bubbles","composed","dispatchEvent","htmx_after_init","htmx_after_request","htmx_after_swap","htmx_before_cleanup","htmx_before_history_update","htmx_before_init","htmx_before_process","htmx_before_request","htmx_before_swap","htmx_before_viewTransition","htmx_config_request","htmx_before_restore_history","htmx_after_push_into_history","htmx_after_replace_into_history","htmx_error"],"sources":["dist/ext/hx-compat.js"],"mappings":"AAAA,MAII,IAAIA,EAEJ,SAASC,EAAoBC,EAAKC,EAASC,GAClCC,KAAKC,OAAOC,QAAQC,uBACrBH,KAAKI,QAAQP,EAAKC,EAASC,EAEnC,CAEAC,KAAKK,kBAAkB,SAAU,CAC7BC,KAAOC,IACHZ,EAAMY,EAGDP,KAAKC,OAAOC,QAAQM,wBACrBR,KAAKC,OAAOQ,qBAAsB,GAIjCT,KAAKC,OAAOC,QAAQQ,wBACrBV,KAAKC,OAAOU,OAAOC,KAAK,MAAO,QAIvCC,+BAAgC,SAAUhB,EAAKE,GAC3C,IAAKC,KAAKC,OAAOC,QAAQY,wBAAyB,CAC9CC,QAAQC,IAAI,6CAA+CjB,EAAOkB,KAAO,UAAWpB,EAAK,qBAAsBE,EAAOmB,QACtH,IAAIC,EAAM,IAAIC,YAAY,yBAA0B,CAChDrB,SACAsB,YAAY,EACZC,SAAU,EACVC,UAAU,IAEd1B,EAAI2B,cAAcL,EACtB,CACJ,EACAM,gBAAiB,SAAU5B,EAAKE,GAC5BH,EAAoBC,EAAK,mBAAoBE,GAC7CH,EAAoBC,EAAK,wBAAyBE,GAClDH,EAAoBC,EAAK,YAAaE,EAC1C,EACA2B,mBAAoB,SAAU7B,EAAKE,GAC/BH,EAAoBC,EAAK,oBAAqBE,EAClD,EACA4B,gBAAiB,SAAU9B,EAAKE,GAC5BH,EAAoBC,EAAK,mBAAoBE,GAC7CH,EAAoBC,EAAK,iBAAkBE,EAC/C,EACA6B,oBAAqB,SAAU/B,EAAKE,GAChCH,EAAoBC,EAAK,4BAA6BE,EAC1D,EACA8B,2BAA4B,SAAUhC,EAAKE,GACvCH,EAAoBC,EAAK,2BAA4BE,GACrDH,EAAoBC,EAAK,yBAA0BE,EACvD,EACA+B,iBAAkB,SAAUjC,EAAKE,GAC7BH,EAAoBC,EAAK,oBAAqBE,EAClD,EACAgC,oBAAqB,SAAUlC,EAAKE,GAChCH,EAAoBC,EAAK,yBAA0BE,EACvD,EACAiC,oBAAqB,SAAUnC,EAAKE,GAChCH,EAAoBC,EAAK,qBAAsBE,GAC/CH,EAAoBC,EAAK,kBAAmBE,EAChD,EACAkC,iBAAkB,SAAUpC,EAAKE,GAC7BH,EAAoBC,EAAK,kBAAmBE,EAChD,EACAmC,2BAA4B,SAAUrC,EAAKE,GACvCH,EAAoBC,EAAK,wBAAyBE,EACtD,EACAoC,oBAAqB,SAAUtC,EAAKE,GAChCH,EAAoBC,EAAK,qBAAsBE,EACnD,EACAqC,4BAA6B,SAAUvC,EAAKE,GACxCH,EAAoBC,EAAK,sBAAuBE,EACpD,EACAsC,6BAA8B,SAAUxC,EAAKE,GACzCH,EAAoBC,EAAK,yBAA0BE,EACvD,EACAuC,gCAAiC,SAAUzC,EAAKE,GAC5CH,EAAoBC,EAAK,yBAA0BE,EACvD,EACAwC,WAAY,SAAU1C,EAAKE,GACvBH,EAAoBC,EAAK,mBAAoBE,EACjD,GAEP,EA1FD","ignoreList":[]}

129
dist/ext/hx-head.js vendored Normal file
View File

@ -0,0 +1,129 @@
//==========================================================
// head-support.js
//
// An extension to add head tag merging.
//==========================================================
(function () {
let api
function log() {
//console.log(arguments)
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html")
// remove svgs to avoid conflicts
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '')
// extract head tag
let headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im)
// if the head tag exists...
if (headTag) {
let added = []
let removed = []
let preserved = []
let nodesToAppend = []
htmlDoc.innerHTML = headTag
let newHeadTag = htmlDoc.querySelector("head")
let currentHead = document.head
if (newHeadTag == null) {
return
}
// put all new head elements into a Map, by their outerHTML
let srcToNewHeadNodes = new Map()
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild)
}
// determine merge strategy
let mergeStrategy = api.attributeValue(newHeadTag, "hx-head") || defaultMergeStrategy
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML)
let isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"
let isPreserved = api.attributeValue(currentHeadElt, "hx-preserve") === "true"
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt)
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML)
preserved.push(currentHeadElt)
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt)
nodesToAppend.push(currentHeadElt)
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt)
}
}
}
}
// Push the remaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values())
log("to append: ", nodesToAppend)
for (const newNode of nodesToAppend) {
log("adding: ", newNode)
let newElt = document.createRange().createContextualFragment(newNode.outerHTML)
log(newElt)
if (htmx.trigger(document.body, "htmx:before:head:add", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt)
added.push(newElt)
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement)
}
}
htmx.trigger(document.body, "htmx:after:head:merge", {
added: added,
kept: preserved,
removed: removed
})
}
}
}
htmx.registerExtension("hx-head", {
init: (internalAPI) => {
api = internalAPI;
},
htmx_after_swap: (elt, detail) => {
let ctx = detail.ctx
let target = ctx.target
// TODO - is there a better way to handle this? it used to be based on if the element was boosted
let defaultMergeStrategy = target === document.body ? "merge" : "append";
if (htmx.trigger(document.body, "htmx:before:head:merge", detail)) {
mergeHead(ctx.text, defaultMergeStrategy)
}
}
})
})()

BIN
dist/ext/hx-head.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-head.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){let e;function t(){}htmx.registerExtension("hx-head",{init:t=>{e=t},htmx_after_swap:(r,d)=>{let h=d.ctx,o=h.target===document.body?"merge":"append";htmx.trigger(document.body,"htmx:before:head:merge",d)&&function(r,d){if(r&&r.indexOf("<head")>-1){const h=document.createElement("html");let o=r.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"").match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);if(o){let r=[],a=[],n=[],m=[];h.innerHTML=o;let u=h.querySelector("head"),l=document.head;if(null==u)return;let c=new Map;for(const e of u.children)c.set(e.outerHTML,e);let i=e.attributeValue(u,"hx-head")||d;for(const t of l.children){let r=c.has(t.outerHTML),d="re-eval"===t.getAttribute("hx-head"),h="true"===e.attributeValue(t,"hx-preserve");r||h?d?a.push(t):(c.delete(t.outerHTML),n.push(t)):"append"===i?d&&(a.push(t),m.push(t)):!1!==htmx.trigger(document.body,"htmx:before:head:remove",{headElement:t})&&a.push(t)}m.push(...c.values());for(const e of m){t();let d=document.createRange().createContextualFragment(e.outerHTML);t(),!1!==htmx.trigger(document.body,"htmx:before:head:add",{headElement:d})&&(l.appendChild(d),r.push(d))}for(const e of a)!1!==htmx.trigger(document.body,"htmx:before:head:remove",{headElement:e})&&l.removeChild(e);htmx.trigger(document.body,"htmx:after:head:merge",{added:r,kept:n,removed:a})}}}(h.text,o)}})}();

BIN
dist/ext/hx-head.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-head.min.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"names":["api","log","htmx","registerExtension","init","internalAPI","htmx_after_swap","elt","detail","ctx","defaultMergeStrategy","target","document","body","trigger","newContent","indexOf","htmlDoc","createElement","headTag","replace","match","added","removed","preserved","nodesToAppend","innerHTML","newHeadTag","querySelector","currentHead","head","srcToNewHeadNodes","Map","newHeadChild","children","set","outerHTML","mergeStrategy","attributeValue","currentHeadElt","inNewContent","has","isReAppended","getAttribute","isPreserved","push","delete","headElement","values","newNode","newElt","createRange","createContextualFragment","appendChild","removedElement","removeChild","kept","mergeHead","text"],"sources":["dist/ext/hx-head.js"],"mappings":"CAKA,WAEI,IAAIA,EAEJ,SAASC,IAET,CAsGAC,KAAKC,kBAAkB,UAAW,CAC9BC,KAAOC,IACHL,EAAMK,GAEVC,gBAAiB,CAACC,EAAKC,KACnB,IAAIC,EAAMD,EAAOC,IAGbC,EAFSD,EAAIE,SAEqBC,SAASC,KAAO,QAAU,SAC5DX,KAAKY,QAAQF,SAASC,KAAM,yBAA0BL,IA7GlE,SAAmBO,EAAYL,GAE3B,GAAIK,GAAcA,EAAWC,QAAQ,UAAY,EAAG,CAChD,MAAMC,EAAUL,SAASM,cAAc,QAEvC,IAEIC,EAFyBJ,EAAWK,QAAQ,uCAAwC,IAEnDC,MAAM,2CAG3C,GAAIF,EAAS,CAET,IAAIG,EAAQ,GACRC,EAAU,GACVC,EAAY,GACZC,EAAgB,GAEpBR,EAAQS,UAAYP,EACpB,IAAIQ,EAAaV,EAAQW,cAAc,QACnCC,EAAcjB,SAASkB,KAE3B,GAAkB,MAAdH,EACA,OAIJ,IAAII,EAAoB,IAAIC,IAC5B,IAAK,MAAMC,KAAgBN,EAAWO,SAClCH,EAAkBI,IAAIF,EAAaG,UAAWH,GAIlD,IAAII,EAAgBrC,EAAIsC,eAAeX,EAAY,YAAcjB,EAGjE,IAAK,MAAM6B,KAAkBV,EAAYK,SAAU,CAG/C,IAAIM,EAAeT,EAAkBU,IAAIF,EAAeH,WACpDM,EAA0D,YAA3CH,EAAeI,aAAa,WAC3CC,EAAoE,SAAtD5C,EAAIsC,eAAeC,EAAgB,eACjDC,GAAgBI,EACZF,EAEAnB,EAAQsB,KAAKN,IAIbR,EAAkBe,OAAOP,EAAeH,WACxCZ,EAAUqB,KAAKN,IAGG,WAAlBF,EAGIK,IACAnB,EAAQsB,KAAKN,GACbd,EAAcoB,KAAKN,KAIuE,IAA1FrC,KAAKY,QAAQF,SAASC,KAAM,0BAA2B,CAACkC,YAAaR,KACrEhB,EAAQsB,KAAKN,EAI7B,CAIAd,EAAcoB,QAAQd,EAAkBiB,UAGxC,IAAK,MAAMC,KAAWxB,EAAe,CACjCxB,IACA,IAAIiD,EAAStC,SAASuC,cAAcC,yBAAyBH,EAAQb,WACrEnC,KACmF,IAA/EC,KAAKY,QAAQF,SAASC,KAAM,uBAAwB,CAACkC,YAAaG,MAClErB,EAAYwB,YAAYH,GACxB5B,EAAMuB,KAAKK,GAEnB,CAIA,IAAK,MAAMI,KAAkB/B,GACqE,IAA1FrB,KAAKY,QAAQF,SAASC,KAAM,0BAA2B,CAACkC,YAAaO,KACrEzB,EAAY0B,YAAYD,GAIhCpD,KAAKY,QAAQF,SAASC,KAAM,wBAAyB,CACjDS,MAAOA,EACPkC,KAAMhC,EACND,QAASA,GAEjB,CACJ,CACJ,CAYYkC,CAAUhD,EAAIiD,KAAMhD,KAKnC,CA3HD","ignoreList":[]}

624
dist/ext/hx-live.js vendored Normal file
View File

@ -0,0 +1,624 @@
htmxLive = (()=>{
class HtmxLive {
#mutationObserver = new MutationObserver((records) => this.#onMutation(records));
#dependencies = []
#actionSelector = "[hx-live]"
constructor() {
this.#initInternals()
}
observe(elt, properties, prototype) {
if (!elt.__htmxLiveObserved) {
elt.__htmxLiveObserved = true
for (const property of properties) {
let descriptor = Object.getOwnPropertyDescriptor(prototype, property);
Object.defineProperty(elt, property, {
get() {
return descriptor.get.call(this);
},
set(newValue) {
descriptor.set.call(this, newValue);
htmxLive.updated(this)
}
});
}
}
}
#process(elt) {
if (elt.matches) {
if (elt.matches(this.#actionSelector)) {
this.#init(elt)
}
if (elt.matches('input')) {
this.observeInput(elt);
}
if (elt.matches('select')) {
this.observeSelect(elt);
}
}
if (elt.querySelectorAll) {
elt.querySelectorAll(this.#actionSelector).forEach((elt) => this.#init(elt))
elt.querySelectorAll('input').forEach((elt) => this.observeInput(elt))
elt.querySelectorAll('select').forEach((elt) => this.observeSelect(elt))
}
}
observeSelect(elt) {
this.observe(elt, ['value', 'disabled'], HTMLSelectElement.prototype)
}
observeInput(elt) {
this.observe(elt, ['value', 'checked', 'disabled'], HTMLInputElement.prototype)
}
#init(elt) {
if (!elt.__htmxLiveCode) {
elt.__htmxLiveCode = true
let liveCode = elt.getAttribute("hx-live");
let tokenizer = new Tokenizer(liveCode);
let parser = new Parser(tokenizer.tokenize());
let features = parser.parseFeatures();
for (const feature of features) {
feature.install(elt);
}
}
}
registerDependency(bindFeature) {
this.#dependencies.push(bindFeature)
}
#onMutation(records) {
for (let record of records) {
if(record.addedNodes.length > 0) {
this.#process(document.documentElement)
break
}
}
let toRefresh = []
outer:
for (const dependency of this.#dependencies) {
for (let record of records) {
if (dependency.dependsOn(record.target)) {
toRefresh.push(dependency);
break outer;
}
for (const elt of record.addedNodes) {
if (dependency.dependsOn(elt)) {
toRefresh.push(dependency)
break outer;
}
}
for (const elt of record.removedNodes) {
if (dependency.dependsOn(elt)) {
toRefresh.push(dependency)
break outer;
}
}
}
}
setTimeout(() => {
for (const dependency of toRefresh) {
dependency.refresh()
}
})
}
updated(elt) {
let toRefresh = []
for (const dependency of this.#dependencies) {
if (dependency.dependsOn(elt)) {
toRefresh.push(dependency)
}
}
for (const dependency of toRefresh) {
dependency.refresh()
}
}
#initInternals() {
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("input", (evt) => {
this.updated(evt.target);
})
this.#mutationObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
characterData: true
})
this.#process(document.body)
})
}
}
let htmxLive = new HtmxLive();
class Tokenizer {
WHITESPACE = /\s/
SYNTAX = /[.()<>[\]+\-*\/]/
ALPHA = /[a-zA-Z]/
NUMERIC = /[0-9]/
constructor(src) {
this.src = src
this.offset = 0
this.prev = ''
}
tokenize() {
let tokens = []
while (this.more()) {
let token = this.nextToken()
if(token) tokens.push(token)
}
return tokens
}
more() {
return this.offset < this.src.length;
}
peek(off = 0) {
return this.src[this.offset + off]
}
match(thing, char = this.peek()) {
if (this.more()) {
if(thing.test) {
return thing.test(char)
} else {
return thing === char
}
}
}
consume() {
this.prev = this.peek()
this.offset++
return this.prev
}
nextToken() {
// consume whitespace
if (this.match(this.WHITESPACE)) {
return this.consumeWhitespace();
} else if (!this.ALPHA.test(this.prev) &&
this.match('.') &&
this.match(this.ALPHA, this.peek(1))) {
return this.consumeClass();
} else if (this.match(this.SYNTAX)) {
return this.consumeSyntax();
} else if ("#" === this.peek()) {
return this.consumeId();
} else if (this.match(this.ALPHA)) {
return this.consumeSymbol();
} else if (this.match(this.NUMERIC)) {
return this.consumeNumber();
} else if (this.match('"') || this.match("'")) {
return this.consumeString();
} else {
throw "Unknown token : " + this.peek()
}
}
consumeSyntax() {
return {type:'syntax', val: this.consume()}
}
consumeWhitespace() {
while (this.match(this.WHITESPACE)) {
this.consume();
}
return null
}
consumeId() {
let token = this.consume()
while (this.validCss(this.peek())) {
token += this.consume();
}
return {type:"id", val:token}
}
consumeSymbol() {
let token = this.consume()
while (this.match(this.ALPHA)) {
token += this.consume();
}
return {type:"sym", val:token}
}
consumeString() {
let start = this.consume()
let val = ""
while (!this.match(start) && this.more()) {
val += this.consume();
}
if(this.consume() !== start) throw "Unterminated string!";
return {type:"str", val:val}
}
consumeNumber() {
let token = this.consume()
while (this.match(this.NUMERIC)) {
token += this.consume();
}
if (this.match(".")) {
token += this.consume();
while (this.match(this.NUMERIC)) {
token += this.consume();
}
return {type:"num", val:Number.parseFloat(token)};
} else {
return {type:"num", val:Number.parseInt(token)};
}
}
consumeClass() {
let token = this.consume()
while (this.validCss(this.peek())) {
token += this.consume();
}
return {type:"class", val:token}
}
validCss(c) {
return this.match(this.ALPHA, c) || this.match(this.NUMERIC, c) || c === "-" || c === "_" || c === ":";
}
}
//========================================================================================
// Live Features
//========================================================================================
class BindFeature {
constructor(lhs, rhs) {
this.lhs = lhs
this.rhs = rhs
this.cssDependences = []
rhs.deps(this.cssDependences)
}
install(elt) {
this.elt = elt
htmxLive.registerDependency(this)
}
async refresh() {
let runtime = {"this": this.elt}
this.elt[this.lhs.property.val] = await this.rhs.eval(runtime)
}
dependsOn(elt) {
for (const css of this.cssDependences) {
if (elt.matches?.(css)) {
return true
}
}
return false
}
}
class OnFeature {
constructor(event, commands) {
this.event = event
this.commands = commands
}
install(elt) {
this.elt = elt
elt.addEventListener(this.event.val, async (evt) => {
let runtime = {}
for (const command of this.commands) {
await command.execute(runtime)
}
})
}
}
class Bindable {
constructor(property) {
this.property = property
}
}
//========================================================================================
// Commands
//========================================================================================
class CallCommand {
constructor(functionCall) {
this.call = functionCall
}
execute(runtime) {
this.call.eval(runtime)
}
}
//========================================================================================
// Expressions
//========================================================================================
class AdditiveExpression {
constructor(token, lhs, rhs) {
this.token = token
this.lhs = lhs
this.rhs = rhs
}
async eval(runtime) {
let result = await this.lhs.eval(runtime) + await this.rhs.eval(runtime);
return result
}
deps(deps) {
this.lhs.deps?.(deps);
this.rhs.deps?.(deps);
}
}
class StringLiteral {
constructor(token) {
this.token = token
}
async eval(runtime) {
return this.token.val
}
}
class IdLiteral {
constructor(token) {
this.token = token
}
async eval(runtime) {
return htmx.find(this.token.val)
}
deps(deps) {
deps.push(this.token.val);
}
}
class Identifier {
constructor(token) {
this.token = token
}
async eval(runtime) {
return runtime[this.token.val] || window[this.token.val]
}
}
class PropertyAccess {
constructor(root, property) {
this.root = root
this.property = property
}
async eval(runtime) {
let root = await this.root.eval(runtime);
return root[this.property.val]
}
deps(deps) {
this.root?.deps(deps);
}
}
class FunctionCall {
constructor(root, args) {
this.root = root
this.rootRoot = root.root
this.args = args
}
async eval(runtime) {
let rootRoot = null;
let func;
if (this.rootRoot) {
rootRoot = await this.rootRoot.eval(runtime);
func = rootRoot[this.root.property.val].bind(rootRoot);
} else {
func = await this.root.eval(runtime)
}
let args = []
for (const arg of this.args) {
args.push(await arg.eval(runtime))
}
return func.call(rootRoot, args)
}
deps(deps) {
this.root?.deps(deps);
}
}
//========================================================================================
// Parser
//========================================================================================
class Parser {
constructor(tokens) {
this.tokens = tokens
this.current = 0
}
moreTokens() {
return this.current < this.tokens.length
}
matchAndConsume(term, type = "sym") {
if (this.match(term, type)) {
return this.consumeToken()
}
}
match(term, type) {
return (this.currentToken().val === term || term === null) &&
(this.currentToken().type === type || type === null);
}
require(term, type = "sym") {
let match = this.matchAndConsume(term, type);
if (!match) throw "Expected: " + (term ? term : type) + " found " + JSON.stringify(this.currentToken());
return match
}
currentToken() {
let token = this.tokens[this.current];
return token || {type:null, val:null};
}
consumeToken() {
this.lastToken = this.currentToken();
this.current++
return this.lastToken
}
//======================================================
// Entry Point
//======================================================
parseFeatures() {
let features = []
while (this.moreTokens()) {
features.push(this.parseFeature())
}
return features
}
parseFeature() {
let bind = this.parseBind();
if(bind){
return bind;
}
let on = this.parseOn();
if(on){
return on;
}
throw "Unknown token: " + JSON.stringify(this.currentToken());
}
parseBind() {
if (this.matchAndConsume("bind")) {
let lhs = this.parseBindable();
this.require("to");
let rhs = this.parseExpression();
return new BindFeature(lhs, rhs)
}
}
parseOn() {
if (this.matchAndConsume("on")) {
let event = this.require(null, "sym");
let commands = this.parseCommands();
return new OnFeature(event, commands)
}
}
parseBindable() {
let property = this.require(null, "sym");
return new Bindable(property)
}
//======================================================
// Commands
//======================================================
parseCommands() {
let commands = []
while (this.moreTokens() && !this.endOfCommands()) {
commands.push(this.parseCommand())
}
return commands
}
endOfCommands() {
return this.matchAndConsume("end", "sym") ||
this.match("on") ||
this.match("bind");
}
parseCommand() {
let call = this.callCommand()
if(call) {
return call
}
throw "Unknown token: " + JSON.stringify(this.currentToken());
}
callCommand() {
if (this.matchAndConsume("call")) {
let expression = this.parseExpression();
if (expression instanceof FunctionCall) {
return new CallCommand(expression)
}
}
}
//======================================================
// Expressions
//======================================================
parseExpression() {
return this.parseAdditiveExpression();
}
parseAdditiveExpression() {
let lhs = this.parseIndirectExpression();
while (this.matchAndConsume("+", "syntax") || this.matchAndConsume("+", "syntax")) {
let rhs = this.parseIndirectExpression()
lhs = new AdditiveExpression(this.lastToken, lhs, rhs)
}
return lhs;
}
parseIndirectExpression() {
let root = this.parsePrimaryExpression();
while (true) {
if (this.matchAndConsume(".", "syntax")) {
let property = this.require(null, "sym");
root = new PropertyAccess(root, property)
} else if (this.matchAndConsume("(", "syntax")) {
let args = []
if (!this.match(")")) {
do {
args.push(this.parseExpression());
} while (this.matchAndConsume(",", "syntax"))
}
this.require(")", "syntax")
root = new FunctionCall(root, args);
} else {
break
}
}
return root
}
parsePrimaryExpression() {
if (this.currentToken()?.type === "str") {
return new StringLiteral(this.consumeToken());
}
if (this.currentToken()?.type === "id") {
return new IdLiteral(this.consumeToken());
}
if (this.currentToken()?.type === "sym") {
return new Identifier(this.consumeToken());
}
if (this.matchAndConsume("(", "syntax")) {
let expr = this.parseExpression();
this.require(")", "syntax");
return expr;
}
throw "Unknown token: " + JSON.stringify(this.currentToken());
}
}
return htmxLive
})()

BIN
dist/ext/hx-live.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-live.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/ext/hx-live.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-live.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

81
dist/ext/hx-optimistic.js vendored Normal file
View File

@ -0,0 +1,81 @@
(() =>{
// TODO - this needs to be updated to use the new internal API
function normalizeSwapStyle(style) {
return style === 'before' ? 'beforebegin' :
style === 'after' ? 'afterend' :
style === 'prepend' ? 'afterbegin' :
style === 'append' ? 'beforeend' : style;
}
function insertOptimisticContent(ctx) {
// TODO - handle htmx.config.prefix
ctx.optimistic = ctx.sourceElement.getAttribute("hx-optimistic");
if (!ctx.optimistic) {
return
}
// TODO - handle inheritance?
let sourceElt = document.querySelector(ctx.optimistic);
if (!sourceElt) return;
let target = ctx.target;
if (!target) return;
if (typeof target === 'string') {
target = document.querySelector(target);
}
// Create optimistic div with reset styling
let optimisticDiv = document.createElement('div');
optimisticDiv.style.cssText = 'all: initial';
optimisticDiv.innerHTML = sourceElt.innerHTML;
let swapStyle = normalizeSwapStyle(ctx.swap);
ctx.optHidden = [];
if (swapStyle === 'innerHTML') {
// Hide children of target
for (let child of target.children) {
child.style.display = 'none';
ctx.optHidden.push(child)
}
target.appendChild(optimisticDiv);
ctx.optimisticDiv = optimisticDiv;
} else if (['beforebegin', 'afterbegin', 'beforeend', 'afterend'].includes(swapStyle)) {
target.insertAdjacentElement(swapStyle, optimisticDiv);
ctx.optimisticDiv = optimisticDiv;
} else {
// Assume outerHTML-like behavior, Hide target and insert div after it
target.style.display = 'none';
ctx.optHidden.push(target)
target.after(optimisticDiv)
ctx.optimisticDiv = optimisticDiv;
}
}
function removeOptimisticContent(ctx) {
if (!ctx.optimisticDiv) return;
// Remove optimistic div
ctx.optimisticDiv.remove();
// Unhide any hidden elements
for (let elt of ctx.optHidden) {
elt.style.display = '';
}
}
htmx.registerExtension('hx-optimistic', {
htmx_before_request : (elt, detail) => {
insertOptimisticContent(detail.ctx);
},
htmx_error : (elt, detail) => {
removeOptimisticContent(detail.ctx)
},
htmx_before_swap : (elt, detail) => {
removeOptimisticContent(detail.ctx)
}
});
})();

BIN
dist/ext/hx-optimistic.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-optimistic.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{function e(e){if(e.optimisticDiv){e.optimisticDiv.remove();for(let t of e.optHidden)t.style.display=""}}htmx.registerExtension("hx-optimistic",{htmx_before_request:(e,t)=>{!function(e){if(e.optimistic=e.sourceElement.getAttribute("hx-optimistic"),!e.optimistic)return;let t=document.querySelector(e.optimistic);if(!t)return;let i=e.target;if(!i)return;"string"==typeof i&&(i=document.querySelector(i));let n=document.createElement("div");n.style.cssText="all: initial",n.innerHTML=t.innerHTML;let r="before"===(o=e.swap)?"beforebegin":"after"===o?"afterend":"prepend"===o?"afterbegin":"append"===o?"beforeend":o;var o;if(e.optHidden=[],"innerHTML"===r){for(let t of i.children)t.style.display="none",e.optHidden.push(t);i.appendChild(n),e.optimisticDiv=n}else["beforebegin","afterbegin","beforeend","afterend"].includes(r)?(i.insertAdjacentElement(r,n),e.optimisticDiv=n):(i.style.display="none",e.optHidden.push(i),i.after(n),e.optimisticDiv=n)}(t.ctx)},htmx_error:(t,i)=>{e(i.ctx)},htmx_before_swap:(t,i)=>{e(i.ctx)}})})();

2
dist/ext/hx-optimistic.min.js.br vendored Normal file
View File

@ -0,0 +1,2 @@
 dUÕëµ™´%Vøe°ûù2&<26>0CTf¬‰kü=?GÄ¢Ú¢Ö´óù°Uç5&ʇ0ÆF Æ` "ÉÇ=׿ŠJwëQC.nûw[ýÙéq½U^"u}®¸Wo¼É}“}©Ñò)ƒçsã‡K¨±¼s°¹¼k°ß;s7³#QÙ«Ë·{£Ã.ŒÅHÛ|TWÓq+ÎQW…Ø#N¡ÍUB¤™ÖEý&Ü<>$ƒÆþ 2òÞe¯¯hzlFTàP©x°¶P— ø×û}œG² œ&ÖL‰GÒíUÀÃY "˜]—8/
}& ø²A Á¹ÔüµhZ§xJÕr×BH(^"&çŽWtöL5NR2š0TÜ­t•n¯ 6¸˜.eô±Sg(ß—ˆl•ôäuïÈb€±xuý»Ø|~²Vy•>ÑBçXö½Þ.Vš¼<C5A1>VV{ Šhf@FŒ„Æ<£'zXù…žÌ„êшÎFEKǨ·qƒ\Ê“F

1
dist/ext/hx-optimistic.min.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"names":["removeOptimisticContent","ctx","optimisticDiv","remove","elt","optHidden","style","display","htmx","registerExtension","htmx_before_request","detail","optimistic","sourceElement","getAttribute","sourceElt","document","querySelector","target","createElement","cssText","innerHTML","swapStyle","swap","child","children","push","appendChild","includes","insertAdjacentElement","after","insertOptimisticContent","htmx_error","htmx_before_swap"],"sources":["dist/ext/hx-optimistic.js"],"mappings":"AAAA,MAyDI,SAASA,EAAwBC,GAC7B,GAAKA,EAAIC,cAAT,CAGAD,EAAIC,cAAcC,SAGlB,IAAK,IAAIC,KAAOH,EAAII,UAChBD,EAAIE,MAAMC,QAAU,EAPM,CASlC,CAEAC,KAAKC,kBAAkB,gBAAiB,CACpCC,oBAAsB,CAACN,EAAKO,MA3DhC,SAAiCV,GAG7B,GADAA,EAAIW,WAAaX,EAAIY,cAAcC,aAAa,kBAC3Cb,EAAIW,WACL,OAIJ,IAAIG,EAAYC,SAASC,cAAchB,EAAIW,YAC3C,IAAKG,EAAW,OAEhB,IAAIG,EAASjB,EAAIiB,OACjB,IAAKA,EAAQ,OAES,iBAAXA,IACPA,EAASF,SAASC,cAAcC,IAIpC,IAAIhB,EAAgBc,SAASG,cAAc,OAC3CjB,EAAcI,MAAMc,QAAU,eAC9BlB,EAAcmB,UAAYN,EAAUM,UAEpC,IAAIC,EA7Ba,YADOhB,EA8BWL,EAAIsB,MA7BX,cACd,UAAVjB,EAAoB,WACN,YAAVA,EAAsB,aACR,WAAVA,EAAqB,YAAcA,EAJnD,IAA4BA,EAiCxB,GAFAL,EAAII,UAAY,GAEE,cAAdiB,EAA2B,CAE3B,IAAK,IAAIE,KAASN,EAAOO,SACrBD,EAAMlB,MAAMC,QAAU,OACtBN,EAAII,UAAUqB,KAAKF,GAEvBN,EAAOS,YAAYzB,GACnBD,EAAIC,cAAgBA,CACxB,KAAW,CAAC,cAAe,aAAc,YAAa,YAAY0B,SAASN,IACvEJ,EAAOW,sBAAsBP,EAAWpB,GACxCD,EAAIC,cAAgBA,IAGpBgB,EAAOZ,MAAMC,QAAU,OACvBN,EAAII,UAAUqB,KAAKR,GACnBA,EAAOY,MAAM5B,GACbD,EAAIC,cAAgBA,EAE5B,CAgBQ6B,CAAwBpB,EAAOV,MAEnC+B,WAAa,CAAC5B,EAAKO,KACfX,EAAwBW,EAAOV,MAEnCgC,iBAAmB,CAAC7B,EAAKO,KACrBX,EAAwBW,EAAOV,OAG1C,EAhFD","ignoreList":[]}

96
dist/ext/hx-preload.js vendored Normal file
View File

@ -0,0 +1,96 @@
(()=>{
let api;
function initializePreload(elt) {
let preloadSpec = api.attributeValue(elt, "hx-preload");
if (!preloadSpec && !elt._htmx?.boosted) return;
let preloadEvents = []
let timeout = 5000;
if (preloadSpec) {
let specs = api.parseTriggerSpecs(preloadSpec);
if (specs.length === 0) return;
for (const spec of specs) {
preloadEvents.push(spec.name)
if (spec.timeout) {
timeout = htmx.parseInterval(spec.timeout)
}
}
} else {
//only boosted links are supported
if (elt.tagName === "A") {
if(htmx.config?.preload?.boostTimeout) {
timeout = htmx.parseInterval(htmx.config.preload.boostTimeout)
}
preloadEvents.push(htmx.config?.preload?.boostEvent || "mousedown");
preloadEvents.push("touchstart");
}
}
let preloadListener = async (evt) => {
let {method} = api.determineMethodAndAction(elt, evt);
if (method !== 'GET') return;
if (elt._htmx?.preload) return;
let ctx = api.createRequestContext(elt, evt);
let form = elt.form || elt.closest("form");
let body = api.collectFormData(elt, form, evt.submitter);
api.handleHxVals(elt, body);
let action = ctx.request.action.replace?.(/#.*$/, '');
let params = new URLSearchParams(body);
if (params.size) action += (/\?/.test(action) ? "&" : "?") + params;
elt._htmx.preload = {
prefetch: fetch(action, ctx.request),
action: action,
expiresAt: Date.now() + timeout
};
try {
await elt._htmx.preload.prefetch;
} catch (error) {
delete elt._htmx.preload;
}
};
for (let eventName of preloadEvents) {
elt.addEventListener(eventName, preloadListener);
}
elt._htmx.preloadListener = preloadListener;
elt._htmx.preloadEvents = preloadEvents;
}
htmx.registerExtension('preload', {
init: (internalAPI) => {
api = internalAPI;
},
htmx_after_init: (elt) => {
initializePreload(elt);
},
htmx_before_request: (elt, detail) => {
let {ctx} = detail;
if (elt._htmx?.preload &&
elt._htmx.preload.action === ctx.request.action &&
Date.now() < elt._htmx.preload.expiresAt) {
let prefetch = elt._htmx.preload.prefetch;
ctx.fetch = () => prefetch;
delete elt._htmx.preload;
} else {
if (elt._htmx) delete elt._htmx.preload;
}
},
htmx_before_cleanup: (elt) => {
if (elt._htmx?.preloadListener) {
for (let eventName of elt._htmx.preloadEvents) {
elt.removeEventListener(eventName, elt._htmx.preloadListener);
}
}
}
});
})()

BIN
dist/ext/hx-preload.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-preload.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{let e;htmx.registerExtension("preload",{init:t=>{e=t},htmx_after_init:t=>{!function(t){let r=e.attributeValue(t,"hx-preload");if(!r&&!t._htmx?.boosted)return;let o=[],a=5e3;if(r){let t=e.parseTriggerSpecs(r);if(0===t.length)return;for(const e of t)o.push(e.name),e.timeout&&(a=htmx.parseInterval(e.timeout))}else"A"===t.tagName&&(htmx.config?.preload?.boostTimeout&&(a=htmx.parseInterval(htmx.config.preload.boostTimeout)),o.push(htmx.config?.preload?.boostEvent||"mousedown"),o.push("touchstart"));let l=async r=>{let{method:o}=e.determineMethodAndAction(t,r);if("GET"!==o)return;if(t._htmx?.preload)return;let l=e.createRequestContext(t,r),n=t.form||t.closest("form"),h=e.collectFormData(t,n,r.submitter);e.handleHxVals(t,h);let m=l.request.action.replace?.(/#.*$/,""),s=new URLSearchParams(h);s.size&&(m+=(/\?/.test(m)?"&":"?")+s),t._htmx.preload={prefetch:fetch(m,l.request),action:m,expiresAt:Date.now()+a};try{await t._htmx.preload.prefetch}catch(e){delete t._htmx.preload}};for(let e of o)t.addEventListener(e,l);t._htmx.preloadListener=l,t._htmx.preloadEvents=o}(t)},htmx_before_request:(e,t)=>{let{ctx:r}=t;if(e._htmx?.preload&&e._htmx.preload.action===r.request.action&&Date.now()<e._htmx.preload.expiresAt){let t=e._htmx.preload.prefetch;r.fetch=()=>t,delete e._htmx.preload}else e._htmx&&delete e._htmx.preload},htmx_before_cleanup:e=>{if(e._htmx?.preloadListener)for(let t of e._htmx.preloadEvents)e.removeEventListener(t,e._htmx.preloadListener)}})})();

BIN
dist/ext/hx-preload.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-preload.min.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"names":["api","htmx","registerExtension","init","internalAPI","htmx_after_init","elt","preloadSpec","attributeValue","_htmx","boosted","preloadEvents","timeout","specs","parseTriggerSpecs","length","spec","push","name","parseInterval","tagName","config","preload","boostTimeout","boostEvent","preloadListener","async","evt","method","determineMethodAndAction","ctx","createRequestContext","form","closest","body","collectFormData","submitter","handleHxVals","action","request","replace","params","URLSearchParams","size","test","prefetch","fetch","expiresAt","Date","now","error","eventName","addEventListener","initializePreload","htmx_before_request","detail","htmx_before_cleanup","removeEventListener"],"sources":["dist/ext/hx-preload.js"],"mappings":"AAAA,MACI,IAAIA,EAgEJC,KAAKC,kBAAkB,UAAW,CAC9BC,KAAOC,IACHJ,EAAMI,GAGVC,gBAAkBC,KAnEtB,SAA2BA,GACvB,IAAIC,EAAcP,EAAIQ,eAAeF,EAAK,cAC1C,IAAKC,IAAgBD,EAAIG,OAAOC,QAAS,OAEzC,IAAIC,EAAgB,GAChBC,EAAU,IACd,GAAIL,EAAa,CACb,IAAIM,EAAQb,EAAIc,kBAAkBP,GAClC,GAAqB,IAAjBM,EAAME,OAAc,OACxB,IAAK,MAAMC,KAAQH,EACfF,EAAcM,KAAKD,EAAKE,MACpBF,EAAKJ,UACLA,EAAUX,KAAKkB,cAAcH,EAAKJ,SAG9C,KAEwB,MAAhBN,EAAIc,UACDnB,KAAKoB,QAAQC,SAASC,eACrBX,EAAUX,KAAKkB,cAAclB,KAAKoB,OAAOC,QAAQC,eAErDZ,EAAcM,KAAKhB,KAAKoB,QAAQC,SAASE,YAAc,aACvDb,EAAcM,KAAK,eAI3B,IAAIQ,EAAkBC,MAAOC,IACzB,IAAIC,OAACA,GAAU5B,EAAI6B,yBAAyBvB,EAAKqB,GACjD,GAAe,QAAXC,EAAkB,OAEtB,GAAItB,EAAIG,OAAOa,QAAS,OAExB,IAAIQ,EAAM9B,EAAI+B,qBAAqBzB,EAAKqB,GACpCK,EAAO1B,EAAI0B,MAAQ1B,EAAI2B,QAAQ,QAC/BC,EAAOlC,EAAImC,gBAAgB7B,EAAK0B,EAAML,EAAIS,WAC9CpC,EAAIqC,aAAa/B,EAAK4B,GAEtB,IAAII,EAASR,EAAIS,QAAQD,OAAOE,UAAU,OAAQ,IAG9CC,EAAS,IAAIC,gBAAgBR,GAC7BO,EAAOE,OAAML,IAAW,KAAKM,KAAKN,GAAU,IAAM,KAAOG,GAE7DnC,EAAIG,MAAMa,QAAU,CAChBuB,SAAUC,MAAMR,EAAQR,EAAIS,SAC5BD,OAAQA,EACRS,UAAWC,KAAKC,MAAQrC,GAG5B,UACUN,EAAIG,MAAMa,QAAQuB,QAC5B,CAAE,MAAOK,UACE5C,EAAIG,MAAMa,OACrB,GAEJ,IAAK,IAAI6B,KAAaxC,EAClBL,EAAI8C,iBAAiBD,EAAW1B,GAEpCnB,EAAIG,MAAMgB,gBAAkBA,EAC5BnB,EAAIG,MAAME,cAAgBA,CAC9B,CAQQ0C,CAAkB/C,IAGtBgD,oBAAqB,CAAChD,EAAKiD,KACvB,IAAIzB,IAACA,GAAOyB,EACZ,GAAIjD,EAAIG,OAAOa,SACXhB,EAAIG,MAAMa,QAAQgB,SAAWR,EAAIS,QAAQD,QACzCU,KAAKC,MAAQ3C,EAAIG,MAAMa,QAAQyB,UAAW,CAC1C,IAAIF,EAAWvC,EAAIG,MAAMa,QAAQuB,SACjCf,EAAIgB,MAAQ,IAAMD,SACXvC,EAAIG,MAAMa,OACrB,MACQhB,EAAIG,cAAcH,EAAIG,MAAMa,SAIxCkC,oBAAsBlD,IAClB,GAAIA,EAAIG,OAAOgB,gBACX,IAAK,IAAI0B,KAAa7C,EAAIG,MAAME,cAC5BL,EAAImD,oBAAoBN,EAAW7C,EAAIG,MAAMgB,mBAKhE,EA/FD","ignoreList":[]}

89
dist/ext/hx-upsert.js vendored Normal file
View File

@ -0,0 +1,89 @@
//==========================================================
// hx-upsert.js
//
// An extension to add 'upsert' swap style that updates
// existing elements by ID and inserts new ones.
//
// Modifiers:
// key:attr - attribute name for sorting (default: id)
// sort - sort ascending
// sort:desc - sort descending
// prepend - prepend elements without keys (default: append)
//==========================================================
(() => {
let api;
htmx.registerExtension('upsert', {
init: (internalAPI) => {
api = internalAPI;
},
htmx_process_upsert: (templateElt, detail) => {
let {ctx, tasks} = detail;
let swapSpec = {style: 'upsert'};
let key = templateElt.getAttribute('key');
let sort = templateElt.getAttribute('sort');
let prepend = templateElt.hasAttribute('prepend');
if (key) swapSpec.key = key;
if (sort !== null) swapSpec.sort = sort || true;
if (prepend) swapSpec.prepend = true;
tasks.push({
type: 'partial',
fragment: templateElt.content.cloneNode(true),
target: api.attributeValue(templateElt, 'hx-target'),
swapSpec,
sourceElement: ctx.sourceElement
});
},
handle_swap: (style, target, fragment, swapSpec) => {
if (style === 'upsert') {
let keyAttr = swapSpec.key || 'id';
let desc = swapSpec.sort === 'desc';
let firstChild = target.firstChild;
let getKey = (el) => el.getAttribute(keyAttr) || el.id;
let compare = (a, b) => {
let result = a.localeCompare(b, undefined, {numeric: true});
return desc ? -result : result;
};
for (let newEl of Array.from(fragment.children)) {
let id = newEl.id;
if (id) {
let existing = document.getElementById(id);
if (existing) {
existing.outerHTML = newEl.outerHTML
continue;
}
}
let newKey = getKey(newEl);
if (!newKey) {
if (swapSpec.prepend) {
target.insertBefore(newEl, firstChild);
} else {
target.appendChild(newEl);
}
continue;
}
let inserted = false;
for (let child of target.children) {
let childKey = getKey(child);
if (childKey && compare(newKey, childKey) < 0) {
target.insertBefore(newEl, child);
inserted = true;
break;
}
}
if (!inserted) {
target.appendChild(newEl);
}
}
return true;
}
return false;
}
});
})();

BIN
dist/ext/hx-upsert.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-upsert.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{let e;htmx.registerExtension("upsert",{init:t=>{e=t},htmx_process_upsert:(t,r)=>{let{ctx:i,tasks:n}=r,l={style:"upsert"},o=t.getAttribute("key"),s=t.getAttribute("sort"),u=t.hasAttribute("prepend");o&&(l.key=o),null!==s&&(l.sort=s||!0),u&&(l.prepend=!0),n.push({type:"partial",fragment:t.content.cloneNode(!0),target:e.attributeValue(t,"hx-target"),swapSpec:l,sourceElement:i.sourceElement})},handle_swap:(e,t,r,i)=>{if("upsert"===e){let e=i.key||"id",n="desc"===i.sort,l=t.firstChild,o=t=>t.getAttribute(e)||t.id,s=(e,t)=>{let r=e.localeCompare(t,void 0,{numeric:!0});return n?-r:r};for(let e of Array.from(r.children)){let r=e.id;if(r){let t=document.getElementById(r);if(t){t.outerHTML=e.outerHTML;continue}}let n=o(e);if(!n){i.prepend?t.insertBefore(e,l):t.appendChild(e);continue}let u=!1;for(let r of t.children){let i=o(r);if(i&&s(n,i)<0){t.insertBefore(e,r),u=!0;break}}u||t.appendChild(e)}return!0}return!1}})})();

BIN
dist/ext/hx-upsert.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-upsert.min.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"names":["api","htmx","registerExtension","init","internalAPI","htmx_process_upsert","templateElt","detail","ctx","tasks","swapSpec","style","key","getAttribute","sort","prepend","hasAttribute","push","type","fragment","content","cloneNode","target","attributeValue","sourceElement","handle_swap","keyAttr","desc","firstChild","getKey","el","id","compare","a","b","result","localeCompare","undefined","numeric","newEl","Array","from","children","existing","document","getElementById","outerHTML","newKey","insertBefore","appendChild","inserted","child","childKey"],"sources":["dist/ext/hx-upsert.js"],"mappings":"AAYA,MACI,IAAIA,EAEJC,KAAKC,kBAAkB,SAAU,CAC7BC,KAAOC,IACHJ,EAAMI,GAEVC,oBAAqB,CAACC,EAAaC,KAC/B,IAAIC,IAACA,EAAGC,MAAEA,GAASF,EACfG,EAAW,CAACC,MAAO,UACnBC,EAAMN,EAAYO,aAAa,OAC/BC,EAAOR,EAAYO,aAAa,QAChCE,EAAUT,EAAYU,aAAa,WACnCJ,IAAKF,EAASE,IAAMA,GACX,OAATE,IAAeJ,EAASI,KAAOA,IAAQ,GACvCC,IAASL,EAASK,SAAU,GAChCN,EAAMQ,KAAK,CACPC,KAAM,UACNC,SAAUb,EAAYc,QAAQC,WAAU,GACxCC,OAAQtB,EAAIuB,eAAejB,EAAa,aACxCI,WACAc,cAAehB,EAAIgB,iBAG3BC,YAAa,CAACd,EAAOW,EAAQH,EAAUT,KACnC,GAAc,WAAVC,EAAoB,CACpB,IAAIe,EAAUhB,EAASE,KAAO,KAC1Be,EAAyB,SAAlBjB,EAASI,KAChBc,EAAaN,EAAOM,WAEpBC,EAAUC,GAAOA,EAAGjB,aAAaa,IAAYI,EAAGC,GAEhDC,EAAU,CAACC,EAAGC,KACd,IAAIC,EAASF,EAAEG,cAAcF,OAAGG,EAAW,CAACC,SAAS,IACrD,OAAOX,GAAQQ,EAASA,GAG5B,IAAK,IAAII,KAASC,MAAMC,KAAKtB,EAASuB,UAAW,CAC7C,IAAIX,EAAKQ,EAAMR,GACf,GAAIA,EAAI,CACJ,IAAIY,EAAWC,SAASC,eAAed,GACvC,GAAIY,EAAU,CACVA,EAASG,UAAYP,EAAMO,UAC3B,QACJ,CACJ,CAEA,IAAIC,EAASlB,EAAOU,GACpB,IAAKQ,EAAQ,CACLrC,EAASK,QACTO,EAAO0B,aAAaT,EAAOX,GAE3BN,EAAO2B,YAAYV,GAEvB,QACJ,CAEA,IAAIW,GAAW,EACf,IAAK,IAAIC,KAAS7B,EAAOoB,SAAU,CAC/B,IAAIU,EAAWvB,EAAOsB,GACtB,GAAIC,GAAYpB,EAAQe,EAAQK,GAAY,EAAG,CAC3C9B,EAAO0B,aAAaT,EAAOY,GAC3BD,GAAW,EACX,KACJ,CACJ,CAEKA,GACD5B,EAAO2B,YAAYV,EAE3B,CACA,OAAO,CACX,CACA,OAAO,IAGlB,EA5ED","ignoreList":[]}

761
dist/ext/hx-ws.js vendored Normal file
View File

@ -0,0 +1,761 @@
(() => {
let api;
// ========================================
// ATTRIBUTE HELPERS
// ========================================
// Helper to build proper attribute name respecting htmx prefix
function buildAttrName(suffix) {
// htmx.config.prefix replaces 'hx-' entirely, e.g. 'data-hx-'
// So 'hx-ws:connect' becomes 'data-hx-ws:connect'
let prefix = htmx.config.prefix || 'hx-';
return prefix + 'ws' + suffix;
}
// Helper to get attribute value, checking colon, hyphen, and plain variants
// Uses api.attributeValue for automatic prefix handling and inheritance support
function getWsAttribute(element, attrName) {
// Try colon variant first (hx-ws:connect) - prefix applied automatically by htmx
let colonValue = api.attributeValue(element, 'hx-ws:' + attrName);
if (colonValue != null) return colonValue;
// Try hyphen variant for JSX (hx-ws-connect)
let hyphenValue = api.attributeValue(element, 'hx-ws-' + attrName);
if (hyphenValue != null) return hyphenValue;
// For 'send', also check plain 'hx-ws' (marker attribute)
if (attrName === 'send') {
let plainValue = api.attributeValue(element, 'hx-ws');
if (plainValue != null) return plainValue;
}
return null;
}
// Helper to check if element has WebSocket attribute (any variant)
function hasWsAttribute(element, attrName) {
let value = getWsAttribute(element, attrName);
return value !== null && value !== undefined;
}
// Build selector for WS attributes
function buildWsSelector(attrName) {
let colonAttr = buildAttrName(':' + attrName);
let hyphenAttr = buildAttrName('-' + attrName);
// Escape colon for CSS selector
return `[${colonAttr.replace(':', '\\:')}],[${hyphenAttr}]`;
}
// ========================================
// CONFIGURATION
// ========================================
function getConfig() {
const defaults = {
reconnect: true,
reconnectDelay: 1000,
reconnectMaxDelay: 30000,
reconnectJitter: true,
// Note: pauseInBackground is NOT implemented. Reconnection continues in background tabs.
// To implement visibility-aware behavior, listen for htmx:ws:reconnect and cancel if needed.
pendingRequestTTL: 30000 // TTL for pending requests in ms
};
return { ...defaults, ...(htmx.config.websockets || {}) };
}
// ========================================
// URL NORMALIZATION
// ========================================
function normalizeWebSocketUrl(url) {
// Already a WebSocket URL
if (url.startsWith('ws://') || url.startsWith('wss://')) {
return url;
}
// Convert http(s):// to ws(s)://
if (url.startsWith('http://')) {
return 'ws://' + url.slice(7);
}
if (url.startsWith('https://')) {
return 'wss://' + url.slice(8);
}
// Relative URL - build absolute ws(s):// URL based on current location
let protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let host = window.location.host;
if (url.startsWith('//')) {
// Protocol-relative URL
return protocol + url;
}
if (url.startsWith('/')) {
// Absolute path
return protocol + '//' + host + url;
}
// Relative path - resolve against current location
let basePath = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
return protocol + '//' + host + basePath + url;
}
// ========================================
// CONNECTION REGISTRY
// ========================================
const connectionRegistry = new Map();
function getOrCreateConnection(url, element) {
let normalizedUrl = normalizeWebSocketUrl(url);
if (connectionRegistry.has(normalizedUrl)) {
let entry = connectionRegistry.get(normalizedUrl);
entry.refCount++;
entry.elements.add(element);
return entry;
}
// Create entry but DON'T add to registry yet - wait for before:ws:connect
let entry = {
url: normalizedUrl,
socket: null,
refCount: 1,
elements: new Set([element]),
reconnectAttempts: 0,
reconnectTimer: null,
pendingRequests: new Map(),
listeners: {} // Store listener references for proper cleanup
};
// Fire cancelable event BEFORE storing in registry
if (!triggerEvent(element, 'htmx:before:ws:connect', { url: normalizedUrl })) {
// Event was cancelled - don't create connection or store entry
return null;
}
// Event passed - now store in registry and create socket
connectionRegistry.set(normalizedUrl, entry);
createWebSocket(normalizedUrl, entry);
return entry;
}
function createWebSocket(url, entry) {
let firstElement = entry.elements.values().next().value;
// Close and remove listeners from old socket properly
if (entry.socket) {
let oldSocket = entry.socket;
entry.socket = null;
// Remove listeners using stored references
if (entry.listeners.open) oldSocket.removeEventListener('open', entry.listeners.open);
if (entry.listeners.message) oldSocket.removeEventListener('message', entry.listeners.message);
if (entry.listeners.close) oldSocket.removeEventListener('close', entry.listeners.close);
if (entry.listeners.error) oldSocket.removeEventListener('error', entry.listeners.error);
try {
if (oldSocket.readyState === WebSocket.OPEN || oldSocket.readyState === WebSocket.CONNECTING) {
oldSocket.close();
}
} catch (e) {}
}
try {
entry.socket = new WebSocket(url);
// Create and store listener references
entry.listeners.open = () => {
// Reset reconnect attempts on successful connection
entry.reconnectAttempts = 0;
if (firstElement) {
triggerEvent(firstElement, 'htmx:after:ws:connect', { url, socket: entry.socket });
}
};
entry.listeners.message = (event) => {
handleMessage(entry, event);
};
entry.listeners.close = (event) => {
// Check if this socket is still the active one
if (event.target !== entry.socket) return;
if (firstElement) {
triggerEvent(firstElement, 'htmx:ws:close', {
url,
code: event.code,
reason: event.reason
});
}
// Check if entry is still valid (not cleared)
if (!connectionRegistry.has(url)) return;
let config = getConfig();
if (config.reconnect && entry.refCount > 0) {
scheduleReconnect(url, entry);
} else {
cleanupPendingRequests(entry);
connectionRegistry.delete(url);
}
};
entry.listeners.error = (error) => {
if (firstElement) {
triggerEvent(firstElement, 'htmx:ws:error', { url, error });
}
};
// Add listeners
entry.socket.addEventListener('open', entry.listeners.open);
entry.socket.addEventListener('message', entry.listeners.message);
entry.socket.addEventListener('close', entry.listeners.close);
entry.socket.addEventListener('error', entry.listeners.error);
} catch (error) {
if (firstElement) {
triggerEvent(firstElement, 'htmx:ws:error', { url, error });
}
}
}
function scheduleReconnect(url, entry) {
let config = getConfig();
// Increment attempts FIRST, then calculate delay
entry.reconnectAttempts++;
let attempts = entry.reconnectAttempts;
let delay = Math.min(
(config.reconnectDelay || 1000) * Math.pow(2, attempts - 1),
config.reconnectMaxDelay || 30000
);
if (config.reconnectJitter) {
delay = delay * (0.75 + Math.random() * 0.5);
}
entry.reconnectTimer = setTimeout(() => {
if (entry.refCount > 0) {
let firstElement = entry.elements.values().next().value;
if (firstElement) {
// attempts now means "this is attempt number N"
triggerEvent(firstElement, 'htmx:ws:reconnect', { url, attempts });
}
createWebSocket(url, entry);
}
}, delay);
}
function decrementRef(url, element) {
// Try both original and normalized URL
let normalizedUrl = normalizeWebSocketUrl(url);
if (!connectionRegistry.has(normalizedUrl)) return;
let entry = connectionRegistry.get(normalizedUrl);
entry.elements.delete(element);
entry.refCount--;
if (entry.refCount <= 0) {
if (entry.reconnectTimer) {
clearTimeout(entry.reconnectTimer);
}
cleanupPendingRequests(entry);
if (entry.socket && entry.socket.readyState === WebSocket.OPEN) {
entry.socket.close();
}
connectionRegistry.delete(normalizedUrl);
}
}
// ========================================
// PENDING REQUEST MANAGEMENT
// ========================================
function cleanupPendingRequests(entry) {
entry.pendingRequests.clear();
}
function cleanupExpiredRequests(entry) {
let config = getConfig();
let now = Date.now();
let ttl = config.pendingRequestTTL || 30000;
for (let [requestId, pending] of entry.pendingRequests) {
if (now - pending.timestamp > ttl) {
entry.pendingRequests.delete(requestId);
}
}
}
// ========================================
// MESSAGE SENDING
// ========================================
// Check if a value looks like a URL (vs a boolean marker like "" or "true")
function looksLikeUrl(value) {
if (!value) return false;
// Check for URL-like patterns: paths, protocols, protocol-relative
return value.startsWith('/') ||
value.startsWith('.') ||
value.startsWith('ws:') ||
value.startsWith('wss:') ||
value.startsWith('http:') ||
value.startsWith('https:') ||
value.startsWith('//');
}
async function sendMessage(element, event) {
// Find connection URL
let url = getWsAttribute(element, 'send');
if (!looksLikeUrl(url)) {
// Value is empty, "true", or other non-URL marker - look for ancestor connection
let selector = buildWsSelector('connect');
let ancestor = element.closest(selector);
if (ancestor) {
url = getWsAttribute(ancestor, 'connect');
} else {
url = null;
}
}
if (!url) {
// Emit error event instead of console.error
triggerEvent(element, 'htmx:wsSendError', {
element,
error: 'No WebSocket connection found for element'
});
return;
}
let normalizedUrl = normalizeWebSocketUrl(url);
let entry = connectionRegistry.get(normalizedUrl);
if (!entry || !entry.socket || entry.socket.readyState !== WebSocket.OPEN) {
triggerEvent(element, 'htmx:wsSendError', { url: normalizedUrl, error: 'Connection not open' });
return;
}
// Cleanup expired pending requests periodically
cleanupExpiredRequests(entry);
// Build message
let form = element.form || element.closest('form');
let body = api.collectFormData(element, form, event.submitter);
let valsResult = api.handleHxVals(element, body);
if (valsResult) await valsResult;
// Preserve multi-value form fields (checkboxes, multi-selects)
let values = {};
for (let [key, value] of body) {
if (key in values) {
// Convert to array if needed
if (!Array.isArray(values[key])) {
values[key] = [values[key]];
}
values[key].push(value);
} else {
values[key] = value;
}
}
// Build headers object
let headers = {
'HX-Request': 'true',
'HX-Current-URL': window.location.href
};
if (element.id) {
headers['HX-Trigger'] = element.id;
}
let targetAttr = api.attributeValue(element, 'hx-target');
if (targetAttr) {
headers['HX-Target'] = targetAttr;
}
let requestId = generateUUID();
let message = {
type: 'request',
request_id: requestId,
event: event.type,
headers: headers,
values: values,
path: normalizedUrl
};
if (element.id) {
message.id = element.id;
}
// Allow modification via event - use 'data' as documented
let detail = { data: message, element, url: normalizedUrl };
if (!triggerEvent(element, 'htmx:before:ws:send', detail)) {
return;
}
try {
entry.socket.send(JSON.stringify(detail.data));
// Store pending request for response matching
entry.pendingRequests.set(requestId, { element, timestamp: Date.now() });
triggerEvent(element, 'htmx:after:ws:send', { data: detail.data, url: normalizedUrl });
} catch (error) {
triggerEvent(element, 'htmx:wsSendError', { url: normalizedUrl, error });
}
}
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0;
let v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// ========================================
// MESSAGE RECEIVING & ROUTING
// ========================================
function handleMessage(entry, event) {
let envelope;
try {
envelope = JSON.parse(event.data);
} catch (e) {
// Not JSON, emit unknown message event for parse failures
let firstElement = entry.elements.values().next().value;
if (firstElement) {
triggerEvent(firstElement, 'htmx:wsUnknownMessage', { data: event.data, parseError: e });
}
return;
}
// Apply defaults for channel and format
envelope.channel = envelope.channel || 'ui';
envelope.format = envelope.format || 'html';
// Find target element for this message
let targetElement = null;
if (envelope.request_id && entry.pendingRequests.has(envelope.request_id)) {
targetElement = entry.pendingRequests.get(envelope.request_id).element;
entry.pendingRequests.delete(envelope.request_id);
} else {
// Use first element in the connection
targetElement = entry.elements.values().next().value;
}
// Emit before:message event (cancelable)
if (!triggerEvent(targetElement, 'htmx:before:ws:message', { envelope, element: targetElement })) {
return;
}
// Route based on channel
if (envelope.channel === 'ui' && envelope.format === 'html') {
handleHtmlMessage(targetElement, envelope);
} else {
// Any non-ui/html message emits htmx:wsMessage for application handling
// This is extensible - apps can handle json, audio, binary, custom channels, etc.
triggerEvent(targetElement, 'htmx:wsMessage', { ...envelope, element: targetElement });
}
triggerEvent(targetElement, 'htmx:after:ws:message', { envelope, element: targetElement });
}
// ========================================
// HTML PARTIAL HANDLING - Using htmx.swap(ctx)
// ========================================
function handleHtmlMessage(element, envelope) {
let parser = new DOMParser();
let doc = parser.parseFromString(envelope.payload || '', 'text/html');
// Find all hx-partial elements (legacy format)
let partials = doc.querySelectorAll('hx-partial');
if (partials.length === 0) {
// No partials, treat entire payload as content for element's target
let target = resolveTarget(element, envelope.target);
if (target) {
swapWithHtmx(target, envelope.payload, element, envelope.swap);
}
return;
}
// Process each partial
for (let partial of partials) {
let targetId = partial.getAttribute('id');
if (!targetId) continue;
let target = document.getElementById(targetId);
if (!target) continue;
swapWithHtmx(target, partial.innerHTML, element);
}
}
function resolveTarget(element, envelopeTarget) {
if (envelopeTarget) {
if (envelopeTarget === 'this') {
return element;
}
return document.querySelector(envelopeTarget);
}
let targetSelector = api.attributeValue(element, 'hx-target');
if (targetSelector) {
if (targetSelector === 'this') {
return element;
}
return document.querySelector(targetSelector);
}
return element;
}
function swapWithHtmx(target, content, sourceElement, envelopeSwap) {
// Determine swap style from envelope, element attribute, or default
let swapStyle = envelopeSwap || api.attributeValue(sourceElement, 'hx-swap') || htmx.config.defaultSwap;
// Create a document fragment from the HTML content
let template = document.createElement('template');
template.innerHTML = content || '';
let fragment = template.content;
// Use htmx's internal insertContent which handles:
// - All swap styles correctly
// - Processing new content with htmx.process()
// - Preserved elements
// - Auto-focus
// - Scroll handling
let task = {
target: target,
swapSpec: swapStyle, // Can be a string - insertContent will parse it
fragment: fragment
};
api.insertContent(task);
}
// ========================================
// EVENT HELPERS
// ========================================
function triggerEvent(element, eventName, detail = {}) {
if (!element) return true;
return htmx.trigger(element, eventName, detail);
}
// ========================================
// ELEMENT LIFECYCLE
// ========================================
function initializeElement(element) {
if (element._htmx?.wsInitialized) return;
let connectUrl = getWsAttribute(element, 'connect');
if (!connectUrl) return;
element._htmx = element._htmx || {};
element._htmx.wsInitialized = true;
let triggerSpec = api.attributeValue(element, 'hx-trigger');
if (!triggerSpec) {
// No trigger specified - connect immediately (default behavior)
// This is the most common use case: connect when element appears
let entry = getOrCreateConnection(connectUrl, element);
if (entry) {
element._htmx.wsUrl = entry.url;
}
} else {
// Connect based on explicit trigger
// Note: We only support bare event names for connection triggers.
// Modifiers like once, delay, throttle, from, target are NOT supported
// for connection establishment. Use htmx:before:ws:connect event for
// custom connection control logic.
let specs = api.parseTriggerSpecs(triggerSpec);
if (specs.length > 0) {
let spec = specs[0];
if (spec.name === 'load') {
// Explicit load trigger - connect immediately
let entry = getOrCreateConnection(connectUrl, element);
if (entry) {
element._htmx.wsUrl = entry.url;
}
} else {
// Set up event listener for other triggers (bare event name only)
element.addEventListener(spec.name, () => {
if (!element._htmx?.wsUrl) {
let entry = getOrCreateConnection(connectUrl, element);
if (entry) {
element._htmx.wsUrl = entry.url;
}
}
}, { once: true });
}
}
}
}
function initializeSendElement(element) {
if (element._htmx?.wsSendInitialized) return;
let sendAttr = getWsAttribute(element, 'send');
// Only treat as URL if it looks like one (not "", "true", etc.)
let sendUrl = looksLikeUrl(sendAttr) ? sendAttr : null;
let triggerSpec = api.attributeValue(element, 'hx-trigger');
if (!triggerSpec) {
// Default trigger based on element type
triggerSpec = element.matches('form') ? 'submit' :
element.matches('input:not([type=button]),select,textarea') ? 'change' :
'click';
}
// Note: We only support bare event names for send triggers.
// Modifiers like once, delay, throttle, from, target are NOT supported.
// For complex trigger logic, use htmx:before:ws:send to implement custom behavior.
let specs = api.parseTriggerSpecs(triggerSpec);
if (specs.length > 0) {
let spec = specs[0];
let handler = async (evt) => {
// Prevent default for forms
if (element.matches('form') && evt.type === 'submit') {
evt.preventDefault();
}
// If this element has its own URL, ensure connection exists
if (sendUrl) {
if (!element._htmx?.wsUrl) {
let entry = getOrCreateConnection(sendUrl, element);
if (entry) {
element._htmx.wsUrl = entry.url;
}
}
}
await sendMessage(element, evt);
};
element.addEventListener(spec.name, handler);
element._htmx = element._htmx || {};
element._htmx.wsSendInitialized = true;
element._htmx.wsSendHandler = handler;
element._htmx.wsSendEvent = spec.name;
}
}
function cleanupElement(element) {
if (element._htmx?.wsUrl) {
decrementRef(element._htmx.wsUrl, element);
}
if (element._htmx?.wsSendHandler) {
element.removeEventListener(element._htmx.wsSendEvent, element._htmx.wsSendHandler);
}
}
// ========================================
// BACKWARD COMPATIBILITY
// ========================================
function checkLegacyAttributes(element) {
// Check for old ws-connect / ws-send attributes
if (element.hasAttribute('ws-connect') || element.hasAttribute('ws-send')) {
console.warn('HTMX WebSocket: Legacy attributes ws-connect and ws-send are deprecated. Please use hx-ws:connect/hx-ws-connect and hx-ws:send/hx-ws-send instead.');
// Map legacy attributes to new ones (prefer hyphen variant for broader compatibility)
if (element.hasAttribute('ws-connect')) {
let url = element.getAttribute('ws-connect');
let hyphenAttr = buildAttrName('-connect');
if (!element.hasAttribute(hyphenAttr)) {
element.setAttribute(hyphenAttr, url);
}
}
if (element.hasAttribute('ws-send')) {
let hyphenAttr = buildAttrName('-send');
if (!element.hasAttribute(hyphenAttr)) {
element.setAttribute(hyphenAttr, '');
}
}
}
}
// ========================================
// EXTENSION REGISTRATION
// ========================================
htmx.registerExtension('ws', {
init: (internalAPI) => {
api = internalAPI;
// Initialize default config if not set
if (!htmx.config.websockets) {
htmx.config.websockets = {};
}
},
htmx_after_process: (element) => {
const processNode = (node) => {
// Check for legacy attributes
checkLegacyAttributes(node);
// Initialize WebSocket connection elements (check both variants)
if (hasWsAttribute(node, 'connect')) {
initializeElement(node);
}
// Initialize send elements (check both variants)
if (hasWsAttribute(node, 'send')) {
initializeSendElement(node);
}
};
// Process the element itself
processNode(element);
// Process descendants - build proper selector respecting prefix
let connectSelector = buildWsSelector('connect');
let sendSelector = buildWsSelector('send');
let plainAttr = buildAttrName('');
let fullSelector = `${connectSelector},${sendSelector},[${plainAttr}],[ws-connect],[ws-send]`;
element.querySelectorAll(fullSelector).forEach(processNode);
},
htmx_before_cleanup: (element) => {
cleanupElement(element);
}
});
// Expose registry for testing
if (typeof window !== 'undefined' && window.htmx) {
window.htmx.ext = window.htmx.ext || {};
window.htmx.ext.ws = {
getRegistry: () => ({
clear: () => {
let entries = Array.from(connectionRegistry.values());
connectionRegistry.clear(); // Clear first to prevent reconnects
entries.forEach(entry => {
entry.refCount = 0; // Prevent pending timeouts from reconnecting
if (entry.reconnectTimer) {
clearTimeout(entry.reconnectTimer);
}
if (entry.socket) {
// Remove listeners if possible or just close
entry.socket.close();
}
entry.elements.clear();
entry.pendingRequests.clear();
});
},
get: (key) => connectionRegistry.get(normalizeWebSocketUrl(key)),
has: (key) => connectionRegistry.has(normalizeWebSocketUrl(key)),
size: connectionRegistry.size
})
};
}
})();

BIN
dist/ext/hx-ws.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-ws.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/ext/hx-ws.min.js.br vendored Normal file

Binary file not shown.

1
dist/ext/hx-ws.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,28 +0,0 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
htmx.defineExtension('include-vals', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var includeValsElt = htmx.closest(evt.detail.elt, "[include-vals],[data-include-vals]");
if (includeValsElt) {
var includeVals = includeValsElt.getAttribute("include-vals") || includeValsElt.getAttribute("data-include-vals");
var valuesToInclude = eval("({" + includeVals + "})");
mergeObjects(evt.detail.parameters, valuesToInclude);
}
}
}
});
})();

16
dist/ext/json-enc.js vendored
View File

@ -1,16 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});

View File

@ -1,189 +0,0 @@
;(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
let loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(function () {
doCallback()
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, function () { clearTimeout(timeout) })
})
} else {
doCallback()
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, "[" + type + "]")).filter(
function (elt) { return mayProcessLoadingStateByPath(elt, path) }
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function (name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy',
]
let loadingStateEltsByType = {}
loadingStateTypes.forEach(function (type) {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})
loadingStateEltsByType['data-loading'].forEach(function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block' },
function () { targetElt.style.display = 'none' }
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.disabled = true },
function() { targetElt.disabled = false }
)
})
}
)
loadingStateEltsByType['data-loading-aria-busy'].forEach(
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () { targetElt.setAttribute("aria-busy", "true") },
function () { targetElt.removeAttribute("aria-busy") }
)
})
}
)
}
if (name === 'htmx:beforeOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
},
})
})()

View File

@ -1,15 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

View File

@ -1,21 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('morphdom-swap', {
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
// IE11 doesn't support DocumentFragment.firstElementChild
morphdom(target, fragment.firstElementChild || fragment.firstChild);
return [target];
} else {
morphdom(target, fragment.outerHTML);
return [target];
}
}
}
});

View File

@ -1,50 +0,0 @@
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension('multi-swap', {
init: function (apiRef) {
api = apiRef;
},
isInlineSwap: function (swapStyle) {
return swapStyle.indexOf('multi:') === 0;
},
handleSwap: function (swapStyle, target, fragment, settleInfo) {
if (swapStyle.indexOf('multi:') === 0) {
var selectorToSwapStyle = {};
var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/);
elements.map(function (element) {
var split = element.split(/\s*:\s*/);
var elementSelector = split[0];
var elementSwapStyle = typeof (split[1]) !== "undefined" ? split[1] : "innerHTML";
if (elementSelector.charAt(0) !== '#') {
console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.");
return;
}
selectorToSwapStyle[elementSelector] = elementSwapStyle;
});
for (var selector in selectorToSwapStyle) {
var swapStyle = selectorToSwapStyle[selector];
var elementToSwap = fragment.querySelector(selector);
if (elementToSwap) {
api.oobSwap(swapStyle, elementToSwap, settleInfo);
} else {
console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.");
}
}
return true;
}
}
});
})();

63
dist/ext/path-deps.js vendored
View File

@ -1,63 +0,0 @@
(function(undefined){
'use strict';
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// Save a reference to the global object (window in the browser)
var _root = this;
function dependsOn(pathSpec, url) {
if (pathSpec === "ignore") {
return false;
}
var dependencyPath = pathSpec.split("/");
var urlPath = url.split("/");
for (var i = 0; i < urlPath.length; i++) {
var dependencyElement = dependencyPath.shift();
var pathElement = urlPath[i];
if (dependencyElement !== pathElement && dependencyElement !== "*") {
return false;
}
if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === "")) {
return true;
}
}
return false;
}
function refreshPath(path) {
var eltsWithDeps = htmx.findAll("[path-deps]");
for (var i = 0; i < eltsWithDeps.length; i++) {
var elt = eltsWithDeps[i];
if (dependsOn(elt.getAttribute('path-deps'), path)) {
htmx.trigger(elt, "path-deps");
}
}
}
htmx.defineExtension('path-deps', {
onEvent: function (name, evt) {
if (name === "htmx:beforeOnLoad") {
var config = evt.detail.requestConfig;
// mutating call
if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
refreshPath(config.path);
}
}
}
});
/**
* ********************
* Expose functionality
* ********************
*/
_root.PathDeps = {
refresh: function(path) {
refreshPath(path);
}
};
}).call(this);

View File

@ -1,15 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('path-params', {
onEvent: function(name, evt) {
if (name === "htmx:configRequest") {
evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function (_, param) {
var val = evt.detail.parameters[param];
delete evt.detail.parameters[param];
return val === undefined ? "{" + param + "}" : encodeURIComponent(val);
})
}
}
});

151
dist/ext/preload.js vendored
View File

@ -1,151 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
// if they also have a `preload` attribute as well. See documentation
// for more details
htmx.defineExtension("preload", {
onEvent: function(name, event) {
// Only take actions on "htmx:afterProcessNode"
if (name !== "htmx:afterProcessNode") {
return;
}
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
// attr gets the closest non-empty value from the attribute.
var attr = function(node, property) {
if (node == undefined) {return undefined;}
return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property)
}
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
var load = function(node) {
// Called after a successful AJAX request, to mark the
// content as loaded (and prevent additional AJAX calls.)
var done = function(html) {
if (!node.preloadAlways) {
node.preloadState = "DONE"
}
if (attr(node, "preload-images") == "true") {
document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too.
}
}
return function() {
// If this value has already been loaded, then do not try again.
if (node.preloadState !== "READY") {
return;
}
// Special handling for HX-GET - use built-in htmx.ajax function
// so that headers match other htmx requests, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
if (hxGet) {
htmx.ajax("GET", hxGet, {
source: node,
handler:function(elt, info) {
done(info.xhr.responseText);
}
});
return;
}
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future.
if (node.getAttribute("href")) {
var r = new XMLHttpRequest();
r.open("GET", node.getAttribute("href"));
r.onload = function() {done(r.responseText);};
r.send();
return;
}
}
}
// This function processes a specific node and sets up event handlers.
// We'll search for nodes and use it below.
var init = function(node) {
// If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") {
return;
}
// Guarantee that we only initialize each node once.
if (node.preloadState !== undefined) {
return;
}
// Get event name from config.
var on = attr(node, "preload") || "mousedown"
const always = on.indexOf("always") !== -1
if (always) {
on = on.replace('always', '').trim()
}
// FALL THROUGH to here means we need to add an EventListener
// Apply the listener to the node
node.addEventListener(on, function(evt) {
if (node.preloadState === "PAUSE") { // Only add one event listener
node.preloadState = "READY"; // Required for the `load` function to trigger
// Special handling for "mouseover" events. Wait 100ms before triggering load.
if (on === "mouseover") {
window.setTimeout(load(node), 100);
} else {
load(node)() // all other events trigger immediately.
}
}
})
// Special handling for certain built-in event handlers
switch (on) {
case "mouseover":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
// WHhen the mouse leaves, immediately disable the preload
node.addEventListener("mouseout", function(evt) {
if ((evt.target === node) && (node.preloadState === "READY")) {
node.preloadState = "PAUSE";
}
})
break;
case "mousedown":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
break;
}
// Mark the node as ready to run.
node.preloadState = "PAUSE";
node.preloadAlways = always;
htmx.trigger(node, "preload:init") // This event can be used to load content immediately.
}
// Search for all child nodes that have a "preload" attribute
event.target.querySelectorAll("[preload]").forEach(function(node) {
// Initialize the node with the "preload" attribute
init(node)
// Initialize all child elements that are anchors or have `hx-get` (use with care)
node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init)
})
}
})

View File

@ -1,14 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('rails-method', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var methodOverride = evt.detail.headers['X-HTTP-Method-Override'];
if (methodOverride) {
evt.detail.parameters['_method'] = methodOverride;
}
}
}
});

31
dist/ext/remove-me.js vendored
View File

@ -1,31 +0,0 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}
}
});
})();

View File

@ -1,135 +0,0 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
var attrPrefix = 'hx-target-';
// IE11 doesn't support string.startsWith
function startsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix
}
/**
* @param {HTMLElement} elt
* @param {number} respCodeNumber
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt, respCodeNumber) {
if (!elt || !respCodeNumber) return null;
var respCode = respCodeNumber.toString();
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
var attrPossibilities = [
respCode,
respCode.substr(0, 2) + '*',
respCode.substr(0, 2) + 'x',
respCode.substr(0, 1) + '*',
respCode.substr(0, 1) + 'x',
respCode.substr(0, 1) + '**',
respCode.substr(0, 1) + 'xx',
'*',
'x',
'***',
'xxx',
];
if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
attrPossibilities.push('error');
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i];
var attrValue = api.getClosestAttributeValue(elt, attr);
if (attrValue) {
if (attrValue === "this") {
return api.findThisElement(elt, attr);
} else {
return api.querySelectorExt(elt, attrValue);
}
}
}
return null;
}
/** @param {Event} evt */
function handleErrorFlag(evt) {
if (evt.detail.isError) {
if (htmx.config.responseTargetUnsetsError) {
evt.detail.isError = false;
}
} else if (htmx.config.responseTargetSetsError) {
evt.detail.isError = true;
}
}
htmx.defineExtension('response-targets', {
/** @param {import("../htmx").HtmxInternalApi} apiRef */
init: function (apiRef) {
api = apiRef;
if (htmx.config.responseTargetUnsetsError === undefined) {
htmx.config.responseTargetUnsetsError = true;
}
if (htmx.config.responseTargetSetsError === undefined) {
htmx.config.responseTargetSetsError = false;
}
if (htmx.config.responseTargetPrefersExisting === undefined) {
htmx.config.responseTargetPrefersExisting = false;
}
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
htmx.config.responseTargetPrefersRetargetHeader = true;
}
},
/**
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
if (name === "htmx:beforeSwap" &&
evt.detail.xhr &&
evt.detail.xhr.status !== 200) {
if (evt.detail.target) {
if (htmx.config.responseTargetPrefersExisting) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
if (htmx.config.responseTargetPrefersRetargetHeader &&
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
}
if (!evt.detail.requestConfig) {
return true;
}
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
if (target) {
handleErrorFlag(evt);
evt.detail.shouldSwap = true;
evt.detail.target = target;
}
return true;
}
}
});
})();

19
dist/ext/restored.js vendored
View File

@ -1,19 +0,0 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('restored', {
onEvent : function(name, evt) {
if (name === 'htmx:restored'){
var restoredElts = evt.detail.document.querySelectorAll(
"[hx-trigger='restored'],[data-hx-trigger='restored']"
);
// need a better way to do this, would prefer to just trigger from evt.detail.elt
var foundElt = Array.from(restoredElts).find(
(x) => (x.outerHTML === evt.detail.elt.outerHTML)
);
var restoredEvent = evt.detail.triggerEvent(foundElt, 'restored');
}
return;
}
})

374
dist/ext/sse.js vendored
View File

@ -1,374 +0,0 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function() {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("sse", {
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt;
switch (name) {
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close();
}
return;
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(parent);
}
}
});
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true });
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacySSEURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
function getLegacySSESwaps(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
var returnArr = [];
if (legacySSEValue != null) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "swap") {
returnArr.push(value[1]);
}
}
}
return returnArr;
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
if (sseSwapAttr) {
var sseEventNames = sseSwapAttr.split(",");
} else {
var sseEventNames = getLegacySSESwaps(child);
}
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim();
var listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return;
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
return;
}
// swap the response into the DOM and trigger a notification
if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
return;
}
swap(child, event.data);
api.triggerEvent(elt, "htmx:sseMessage", event);
};
// Register the new listener
api.getInternalData(child).sseEventListener = listener;
source.addEventListener(sseEventName, listener);
}
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
var sseEventName = api.getAttributeValue(child, "hx-trigger");
if (sseEventName == null) {
return;
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
// remove the sse: prefix from here on out
sseEventName = sseEventName.substr(4);
var listener = function() {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
}
});
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
var sseURL = api.getAttributeValue(child, "sse-connect");
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
// handle legacy sse, remove for HTMX2
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
var sseURL = getLegacySSEURL(child);
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
registerSSE(elt);
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url);
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return;
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0;
var timeout = Math.random() * (2 ^ retryCount) * 500;
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
}, timeout);
}
};
source.onopen = function(evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
}
api.getInternalData(elt).sseEventSource = source;
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) {
source.close();
// source = null
return true;
}
}
return false;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = [];
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
result.push(node);
});
return result;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
var settleInfo = api.makeSettleInfo(elt);
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:beforeSettle');
});
// Handle settle tasks (with delay if requested)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
} else {
doSettle(settleInfo)();
}
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call();
});
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:afterSettle');
});
}
}
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null;
}
})();

481
dist/ext/ws.js vendored
View File

@ -1,481 +0,0 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("ws", {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function (apiRef) {
// Store reference to internal API
api = apiRef;
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt;
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close();
}
return;
// Try to create websockets when elements are processed
case "htmx:beforeProcessNode":
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return;
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (legacySource == null) {
return;
} else {
wssSource = legacySource;
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf("/") === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '');
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function () {
return htmx.createWebSocket(wssSource)
});
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
}
api.withExtensions(socketElt, function (extension) {
response = extension.transformResponse(response, null, socketElt);
});
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
if (fragment.children.length) {
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}
api.settleImmediately(settleInfo.tasks);
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
});
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper;
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function (event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
}
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
},
sendImmediately: function (message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
socketWrapper: this.publicInterface
})
}
},
send: function (message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
} else {
this.sendImmediately(message, sendElt);
}
},
handleQueuedMessages: function () {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
this.messageQueue.shift();
} else {
break;
}
}
},
init: function () {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
this.socket = socket;
socket.onopen = function (e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
}
socket.onclose = function (e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
setTimeout(function () {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
};
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
maybeCloseWebSocketSource(socketElt);
};
var events = this.events;
Object.keys(events).forEach(function (k) {
events[k].forEach(function (e) {
socket.addEventListener(k, e);
})
});
},
close: function () {
this.socket.close()
}
}
wrapper.init();
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
return wrapper;
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(sendElt);
triggerSpecs.forEach(function (ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket;
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
var results = api.getInputValues(sendElt, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, sendElt);
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers: headers,
errors: errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
}
var body = sendConfig.messageBody;
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
}
socketWrapper.send(body, elt);
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});
});
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
if (typeof delay === 'function') {
return delay(retryCount);
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
}
return false;
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();

5347
dist/htmx.amd.js vendored

File diff suppressed because it is too large Load Diff

5345
dist/htmx.cjs.js vendored

File diff suppressed because it is too large Load Diff

52
dist/htmx.d.ts vendored Normal file
View File

@ -0,0 +1,52 @@
export interface HtmxConfig {
version: string;
logAll: boolean;
prefix: string;
transitions: boolean;
history: boolean;
historyReload: boolean;
mode: 'same-origin' | 'cors' | 'no-cors';
defaultSwap: string;
indicatorClass: string;
requestClass: string;
includeIndicatorCSS: boolean;
defaultTimeout: number;
inlineScriptNonce: string;
inlineStyleNonce: string;
extensions: string;
morphIgnore: string[];
noSwap: number[];
implicitInheritance: boolean;
metaCharacter?: string;
streams?: {
reconnect: boolean;
reconnectMaxAttempts: number;
reconnectDelay: number;
reconnectMaxDelay: number;
reconnectJitter: number;
pauseInBackground: boolean;
};
}
export interface Htmx {
config: HtmxConfig;
ajax(verb: string, path: string, context?: any): Promise<void>;
find(selector: string): Element | null;
find(elt: Element, selector: string): Element | null;
findAll(selector: string): Element[];
findAll(elt: Element, selector: string): Element[];
on(event: string, handler: (evt: Event) => void): void;
on(target: string | Element, event: string, handler: (evt: Event) => void): void;
onLoad(callback: (elt: Element) => void): void;
process(elt: Element): void;
registerExtension(name: string, ext: any): boolean;
trigger(elt: Element | string, event: string, detail?: any): boolean;
timeout(ms: number): Promise<void>;
parseInterval(str: string): number;
forEvent(event: string, timeout?: number): Promise<Event | null>;
swap(target: Element | string, content: string, swapSpec: any): void;
takeClass(elt: Element, className: string, container?: Element): void;
}
declare const htmx: Htmx;
export default htmx;

219
dist/htmx.esm.d.ts vendored
View File

@ -1,219 +0,0 @@
export default htmx;
export type HttpVerb = "get" | "head" | "post" | "put" | "delete" | "connect" | "options" | "trace" | "patch";
export type SwapOptions = {
select?: string;
selectOOB?: string;
eventInfo?: any;
anchor?: string;
contextElement?: Element;
afterSwapCallback?: swapCallback;
afterSettleCallback?: swapCallback;
beforeSwapCallback?: swapCallback;
title?: string;
historyRequest?: boolean;
};
export type swapCallback = () => any;
export type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string;
export type HtmxSwapSpecification = {
swapStyle: HtmxSwapStyle;
swapDelay: number;
settleDelay: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: "top" | "bottom" | number;
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
};
export type ConditionalFunction = ((this: Node, evt: Event) => boolean) & {
source: string;
};
export type HtmxTriggerSpecification = {
trigger: string;
pollInterval?: number;
eventFilter?: ConditionalFunction;
changed?: boolean;
once?: boolean;
consume?: boolean;
delay?: number;
from?: string;
target?: string;
throttle?: number;
queue?: string;
root?: string;
threshold?: string;
};
export type HtmxElementValidationError = {
elt: Element;
message: string;
validity: ValidityState;
};
export type HtmxHeaderSpecification = Record<string, string>;
export type HtmxAjaxHelperContext = {
source?: Element | string;
event?: Event;
handler?: HtmxAjaxHandler;
target?: Element | string;
swap?: HtmxSwapStyle;
values?: any | FormData;
headers?: Record<string, string>;
select?: string;
push?: string;
replace?: string;
selectOOB?: string;
};
export type HtmxRequestConfig = {
boosted: boolean;
useUrlParams: boolean;
formData: FormData;
/**
* formData proxy
*/
parameters: any;
unfilteredFormData: FormData;
/**
* unfilteredFormData proxy
*/
unfilteredParameters: any;
headers: HtmxHeaderSpecification;
elt: Element;
target: Element;
verb: HttpVerb;
errors: HtmxElementValidationError[];
withCredentials: boolean;
timeout: number;
path: string;
triggeringEvent: Event;
};
export type HtmxResponseInfo = {
xhr: XMLHttpRequest;
target: Element;
requestConfig: HtmxRequestConfig;
etc: HtmxAjaxEtc;
boosted: boolean;
select: string;
pathInfo: {
requestPath: string;
finalRequestPath: string;
responsePath: string | null;
anchor: string;
};
failed?: boolean;
successful?: boolean;
keepIndicators?: boolean;
};
export type HtmxAjaxEtc = {
returnPromise?: boolean;
handler?: HtmxAjaxHandler;
select?: string;
targetOverride?: Element;
swapOverride?: HtmxSwapStyle;
headers?: Record<string, string>;
values?: any | FormData;
credentials?: boolean;
timeout?: number;
push?: string;
replace?: string;
selectOOB?: string;
};
export type HtmxResponseHandlingConfig = {
code?: string;
swap: boolean;
error?: boolean;
ignoreTitle?: boolean;
select?: string;
target?: string;
swapOverride?: string;
event?: string;
};
export type HtmxBeforeSwapDetails = HtmxResponseInfo & {
shouldSwap: boolean;
serverResponse: any;
isError: boolean;
ignoreTitle: boolean;
selectOverride: string;
swapOverride: string;
};
export type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any;
export type HtmxSettleTask = (() => void);
export type HtmxSettleInfo = {
tasks: HtmxSettleTask[];
elts: Element[];
title?: string;
};
export type HtmxExtension = {
init: (api: any) => void;
onEvent: (name: string, event: CustomEvent) => boolean;
transformResponse: (text: string, xhr: XMLHttpRequest, elt: Element) => string;
isInlineSwap: (swapStyle: HtmxSwapStyle) => boolean;
handleSwap: (swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean | Node[];
encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null;
getSelectors: () => string[] | null;
};
declare namespace htmx {
let onLoad: (callback: (elt: Node) => void) => EventListener;
let process: (elt: Element | string) => void;
let on: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener | any | boolean, arg4?: any | boolean) => EventListener;
let off: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener) => EventListener;
let trigger: (elt: EventTarget | string, eventName: string, detail?: any | undefined) => boolean;
let ajax: (verb: HttpVerb, path: string, context: Element | string | HtmxAjaxHelperContext) => Promise<void>;
let find: (eltOrSelector: ParentNode | string, selector?: string) => Element | null;
let findAll: (eltOrSelector: ParentNode | string, selector?: string) => NodeListOf<Element>;
let closest: (elt: Element | string, selector: string) => Element | null;
function values(elt: Element, type: HttpVerb): any;
let remove: (elt: Node, delay?: number) => void;
let addClass: (elt: Element | string, clazz: string, delay?: number) => void;
let removeClass: (node: Node | string, clazz: string, delay?: number) => void;
let toggleClass: (elt: Element | string, clazz: string) => void;
let takeClass: (elt: Node | string, clazz: string) => void;
let swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void;
let defineExtension: (name: string, extension: Partial<HtmxExtension>) => void;
let removeExtension: (name: string) => void;
let logAll: () => void;
let logNone: () => void;
let logger: any;
namespace config {
let historyEnabled: boolean;
let historyCacheSize: number;
let refreshOnHistoryMiss: boolean;
let defaultSwapStyle: HtmxSwapStyle;
let defaultSwapDelay: number;
let defaultSettleDelay: number;
let includeIndicatorStyles: boolean;
let indicatorClass: string;
let requestClass: string;
let addedClass: string;
let settlingClass: string;
let swappingClass: string;
let allowEval: boolean;
let allowScriptTags: boolean;
let inlineScriptNonce: string;
let inlineStyleNonce: string;
let attributesToSettle: string[];
let withCredentials: boolean;
let timeout: number;
let wsReconnectDelay: "full-jitter" | ((retryCount: number) => number);
let wsBinaryType: BinaryType;
let disableSelector: string;
let scrollBehavior: "auto" | "instant" | "smooth";
let defaultFocusScroll: boolean;
let getCacheBusterParam: boolean;
let globalViewTransitions: boolean;
let methodsThatUseUrlParams: (HttpVerb)[];
let selfRequestsOnly: boolean;
let ignoreTitle: boolean;
let scrollIntoViewOnBoost: boolean;
let triggerSpecsCache: any | null;
let disableInheritance: boolean;
let responseHandling: HtmxResponseHandlingConfig[];
let allowNestedOobSwaps: boolean;
let historyRestoreAsHxRequest: boolean;
let reportValidityOfForms: boolean;
}
let parseInterval: (str: string) => number | undefined;
let location: Location;
let _: (str: string) => any;
let version: string;
}

7504
dist/htmx.esm.js vendored

File diff suppressed because it is too large Load Diff

BIN
dist/htmx.esm.js.br vendored Normal file

Binary file not shown.

1
dist/htmx.esm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/htmx.esm.min.js.br vendored Normal file

Binary file not shown.

1
dist/htmx.esm.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

7502
dist/htmx.js vendored

File diff suppressed because it is too large Load Diff

BIN
dist/htmx.js.br vendored Normal file

Binary file not shown.

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.br vendored Normal file

Binary file not shown.

BIN
dist/htmx.min.js.gz vendored

Binary file not shown.

1
dist/htmx.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ command = "zola build"
[build.environment]
ZOLA_VERSION = "0.19.1"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"
[context.deploy-preview]
command = "zola build --base-url $DEPLOY_PRIME_URL"

3670
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +1,65 @@
{
"name": "htmx.org",
"description": "high power tools for html",
"keywords": [
"AJAX",
"HTML"
],
"version": "2.0.8",
"homepage": "https://htmx.org/",
"bugs": {
"url": "https://github.com/bigskysoftware/htmx/issues"
},
"license": "0BSD",
"version": "4.0.0-alpha5",
"description": "A hypermedia-oriented JavaScript library",
"main": "dist/htmx.js",
"module": "dist/htmx.esm.js",
"types": "dist/htmx.d.ts",
"files": [
"LICENSE",
"README.md",
"dist/htmx.esm.d.ts",
"dist/*.js",
"dist/*.map",
"dist/*.d.ts",
"dist/ext/*.js",
"dist/*.js.gz",
"editors/jetbrains/htmx.web-types.json"
"dist/ext/*.map",
"dist/editors"
],
"main": "dist/htmx.esm.js",
"types": "dist/htmx.esm.d.ts",
"jsdelivr": "dist/htmx.min.js",
"unpkg": "dist/htmx.min.js",
"web-types": "editors/jetbrains/htmx.web-types.json",
"scripts": {
"dist": "./scripts/dist.sh && npm run types-generate && npm run web-types-generate",
"lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/ scripts/*.mjs",
"format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/ scripts/*.mjs",
"types-check": "tsc src/htmx.js --noEmit --checkJs --target es6 --lib dom,dom.iterable --moduleResolution node",
"types-generate": "tsc dist/htmx.esm.js --declaration --emitDeclarationOnly --allowJs --outDir dist",
"test": "npm run lint && npm run types-check && npm run test:chrome",
"test:debug": "web-test-runner --manual --open",
"test:chrome": "playwright install chromium && web-test-runner --browsers chromium --playwright",
"test:firefox": "playwright install firefox && web-test-runner --concurrency 1 --browsers firefox --playwright",
"test:webkit": "playwright install webkit && web-test-runner --browsers webkit --playwright",
"test:all": "playwright install chromium firefox webkit && web-test-runner --concurrency 1 --browsers chromium firefox webkit --playwright",
"test:ci": "npm run lint && npm run types-check && npm run test:all",
"ws-tests": "cd ./test/ws-sse && node ./server.js",
"web-types-generate": "node ./scripts/generate-web-types.mjs",
"www": "bash ./scripts/www.sh",
"sha": "bash ./scripts/sha.sh"
"build": "npm run build:iife && npm run build:esm && npm run build:minify && npm run build:ext && npm run build:editors && npm run build:compress",
"build:iife": "sed 's/__/#/g' src/htmx.js > dist/htmx.js",
"build:esm": "sed 's/__/#/g' src/htmx.js > dist/htmx.esm.js && echo '\\nif (typeof window !== \"undefined\") window.htmx = htmx;\\nexport default htmx;' >> dist/htmx.esm.js",
"build:minify": "terser --compress --mangle --source-map -o dist/htmx.min.js dist/htmx.js && terser --compress --mangle --source-map -o dist/htmx.esm.min.js dist/htmx.esm.js",
"build:ext": "mkdir -p dist/ext && for file in src/ext/*.js; do name=$(basename \"$file\" .js); cp \"$file\" \"dist/ext/$name.js\" && terser --compress --mangle --source-map -o \"dist/ext/$name.min.js\" \"dist/ext/$name.js\"; done",
"build:editors": "mkdir -p dist/editors && cp -r src/editors/* dist/editors/",
"build:compress": "brotli-cli compress dist/*.js dist/ext/*.js",
"test": "npm run test:chrome",
"test:chrome": "web-test-runner --browsers chromium --config test/web-test-runner.config.mjs --playwright",
"test:firefox": "web-test-runner --browsers firefox --config test/web-test-runner.config.mjs --playwright",
"test:webkit": "web-test-runner --browsers webkit --config test/web-test-runner.config.mjs --playwright",
"test:all": "web-test-runner --browsers chromium firefox webkit --config test/web-test-runner.config.mjs --playwright --concurrency 1",
"test:manual": "node test/manual/server.js",
"test:manual:ws": "node test/manual/ws-server.js",
"www": "bash scripts/www.sh",
"site": "npm run site:css & npm run site:serve",
"site:css": "cd www && npx @tailwindcss/cli -i ./static/css/input.css -o ./static/css/output.css --watch",
"site:serve": "cd www && npx zola-bin serve --interface 0.0.0.0 --port 1111 --open",
"merge": " (echo \"src/** merge=ours\ndist/** merge=ours\ntest/** merge=ours\n\" > .gitattributes && git add .gitattributes && git merge master); git reset HEAD .gitattributes 2>/dev/null; rm .gitattributes 2>/dev/null\n",
"update-sha": "bash scripts/update-sha.sh"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bigskysoftware/htmx.git"
},
"eslintConfig": {
"extends": ["standard", "plugin:mocha/recommended"],
"rules": {
"mocha/consistent-spacing-between-blocks": 0,
"mocha/no-setup-in-describe": 0,
"mocha/no-skipped-tests": 0,
"camelcase": 0,
"no-var": 0,
"no-undef": 0,
"eqeqeq": 0,
"no-multi-str": 0,
"no-prototype-builtins": 0,
"no-cond-assign": 0,
"no-empty": 0,
"no-eval": 0,
"no-new-func": 0,
"no-redeclare": 0,
"no-return-assign": 0,
"no-unused-vars": 0,
"no-useless-call": 0,
"no-useless-escape": 0,
"no-unused-expressions": 0,
"no-restricted-properties": ["error", {
"property": "substr",
"message": "Use .slice or .substring instead of .substr"
}],
"space-before-function-paren": [
"error",
"never"
]
}
"url": "https://github.com/bigskysoftware/htmx"
},
"keywords": [
"hypermedia",
"htmx",
"ajax",
"sse"
],
"workspaces": [
"ext/*"
],
"author": "Big Sky Software",
"license": "BSD-0-Clause",
"devDependencies": {
"@types/node": "^22.18.8",
"@types/parse5": "^7.0.0",
"@tailwindcss/typography": "^0.5.19",
"@web/test-runner": "^0.20.2",
"@web/test-runner-playwright": "^0.11.0",
"brotli-cli": "^2.1.1",
"chai": "^4.5.0",
"chai-dom": "^1.12.1",
"eslint": "^8.57.1",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-mocha": "^10.5.0",
"fs-extra": "^9.1.0",
"mocha": "^11.7.4",
"mock-socket": "^9.3.1",
"sinon": "^10.0.1",
"typescript": "^5.9.3",
"uglify-js": "^3.19.3",
"ws": "^8.18.1"
"terser": "^5.36.0",
"ws": "^8.18.3"
}
}

View File

@ -1,38 +0,0 @@
#!/bin/bash
# This script is intended to be run from npm, via `npm run dist`
set -euo pipefail
HTMX_SRC="src/htmx.js"
# Clean the dist directory
rm -rf dist/*.js dist/*.ts dist/*.gz
# Regular IIFE script
cp $HTMX_SRC dist/htmx.js
# Generate minified script
uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js
# Generate gzipped script
gzip -9 -k -f dist/htmx.min.js > dist/htmx.min.js.gz
# Generate AMD script
cat > dist/htmx.amd.js << EOF
define(() => {
$(cat $HTMX_SRC)
return htmx
})
EOF
# Generate CJS script
cat > dist/htmx.cjs.js << EOF
$(cat $HTMX_SRC)
module.exports = htmx;
EOF
# Generate ESM script
cat > dist/htmx.esm.js << EOF
$(cat $HTMX_SRC)
export default htmx
EOF

View File

@ -1,145 +0,0 @@
import fs from 'fs'
const classes = []
const attributes = []
const events = []
const rootPath = fs.existsSync('./www') ? './' : '../'
for (const file of fs.readdirSync(rootPath + 'www/content/attributes').sort()) {
if (file.startsWith('hx-') && file.endsWith('.md')) {
const name = file.slice(0, -3)
const info = readAttributeInfo(name, rootPath + 'www/content/attributes/' + file)
attributes.push({
name,
...info,
'doc-url': 'https://htmx.org/attributes/' + name + '/'
})
}
}
readClassInfo()
readEventInfo()
const pkg = JSON.parse(fs.readFileSync(rootPath + 'package.json', { encoding: 'utf8' }))
const webTypes = {
$schema: 'https://json.schemastore.org/web-types',
name: 'htmx',
version: pkg.version,
'default-icon': './htmx.svg',
'js-types-syntax': 'typescript',
'description-markup': 'markdown',
contributions: {
html: {
attributes
},
css: {
classes
},
js: {
events: [
{
name: 'HTMX event',
pattern: {
items: ['/js/htmx-events'],
template: ['htmx:', '$...', '#item:HTMX event']
}
}
],
'htmx-events': events
}
}
}
fs.writeFileSync(rootPath + 'editors/jetbrains/htmx.web-types.json', JSON.stringify(webTypes, null, 2))
function readAttributeInfo(name, file) {
const content = fs.readFileSync(file, { encoding: 'utf8' })
const isInherited = content.indexOf('`' + name + '` is inherited') !== -1
const isNotInherited = content.indexOf('`' + name + '` is not inherited') !== -1
const deprecated = content.indexOf('`' + name + '` has been deprecated') !== -1
const sections = {}
if (isInherited) {
sections.Inherited = ''
} else if (isNotInherited) {
sections['Not Inherited'] = ''
}
const descSections = /\+\+\+\n(?:[^\n]*\n)+\+\+\+\n\n((?:[^\n]+\n)+)(?:\n((?:[^\n]+\n)+))?(?:\n((?:[^\n]+\n)+))?/mg.exec(content)
const para1 = descSections[1].trim()
const para2 = descSections[2]?.trim()
const para3 = descSections[3]?.trim()
let description = para1
if (para2) {
description += '\n\n' + para2
}
if (para2 && para2.endsWith(':') && para3) {
description += '\n\n' + para3
}
let pattern
if (name === 'hx-on') {
pattern = {
or: [
{
items: ['/js/events'],
template: ['hx-on:', '#...', '#item:JS event']
},
{
items: ['/js/htmx-events'],
template: ['hx-on::', '#...', '#item:HTMX event']
}
]
}
}
return {
pattern,
description,
'description-sections': sections,
deprecated: deprecated ? true : undefined
}
}
function readClassInfo() {
const content = fs.readFileSync(rootPath + 'www/content/reference.md', { encoding: 'utf8' })
const start = content.indexOf('| Class | Description |')
const cssTable = content.slice(start, content.indexOf('</div>', start))
const expr = /\| `([^`]+)` \| ([^\n]+)/mg
let match = expr.exec(cssTable)
while (match) {
const name = match[1]
if (name && name.startsWith('htmx-')) {
classes.push({
name,
description: match[2].trim(),
'doc-url': 'https://htmx.org/reference/#classes'
})
}
match = expr.exec(cssTable)
}
}
function readEventInfo() {
const content = fs.readFileSync(rootPath + 'www/content/events.md', { encoding: 'utf8' })
const expr = /### Event - `([^`]+)`[^\n]*\n+((?:(?:[^#\n]|#####)[^\n]*\n+)+)/mg
let match = expr.exec(content)
while (match) {
let name = match[1]
if (name && name.startsWith('htmx:')) {
name = name.slice(5)
events.push({
name,
description: match[2],
'doc-url': 'https://htmx.org/events/#htmx:' + name
})
}
match = expr.exec(content)
}
}

22
scripts/update-sha.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Calculate SHAs
MINIFIED_SHA=$(cat dist/htmx.min.js | openssl dgst -sha384 -binary | openssl base64 -A)
FULL_SHA=$(cat dist/htmx.js | openssl dgst -sha384 -binary | openssl base64 -A)
echo "Updating docs.md with new SHAs..."
echo "htmx.min.js: sha384-$MINIFIED_SHA"
echo "htmx.js: sha384-$FULL_SHA"
# Update both integrity attributes in docs.md using awk for precise control
awk -v minified="sha384-$MINIFIED_SHA" -v full="sha384-$FULL_SHA" '
/integrity="sha384-[^"]*"/ && /htmx\.min\.js/ {
sub(/sha384-[^"]*/, minified)
}
/integrity="sha384-[^"]*"/ && /htmx\.js"/ && !/htmx\.min\.js/ {
sub(/sha384-[^"]*/, full)
}
{print}
' www/content/docs.md > www/content/docs.md.tmp && mv www/content/docs.md.tmp www/content/docs.md
echo "✓ docs.md updated successfully"

View File

@ -1,20 +1,11 @@
#!/bin/bash
set -euo pipefail
STATIC_ROOT="www/static"
PACKAGE_VERSION=$(cat package.json | grep version | cut -d '"' -f 4)
cp node_modules/mocha/mocha.js "$STATIC_ROOT/node_modules/mocha/mocha.js"
cp node_modules/mocha/mocha.css "$STATIC_ROOT/node_modules/mocha/mocha.css"
cp node_modules/chai/chai.js "$STATIC_ROOT/node_modules/chai/chai.js"
cp node_modules/chai-dom/chai-dom.js "$STATIC_ROOT/node_modules/chai-dom/chai-dom.js"
cp node_modules/sinon/pkg/sinon.js "$STATIC_ROOT/node_modules/sinon/pkg/sinon.js"
cp node_modules/mock-socket/dist/mock-socket.js "$STATIC_ROOT/node_modules/mock-socket/dist/mock-socket.js"
rm -rf "$STATIC_ROOT/test" "$STATIC_ROOT/src"
cp -r "./test" "$STATIC_ROOT/test"
cp -r "./src" "$STATIC_ROOT/src"
# copy the current htmx to the main website
cp "src/htmx.js" "www/themes/htmx-theme/static/js/htmx.js"
cp -r ./dist/* "$STATIC_ROOT/js"

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 256 256"
version="1.1"
id="svg13"
sodipodi:docname="htmx.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview15"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="32"
inkscape:cx="0.859375"
inkscape:cy="8.90625"
inkscape:window-width="1489"
inkscape:window-height="998"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg13" />
<path
fill="#3d72d7"
d="M 92.875785,213.11596 140.82049,41.850192 a 2.3748439,2.8675381 0 0 1 2.49698,-1.93354 l 19.88084,2.45789 a 2.3748439,2.8675381 0 0 1 2.022,3.752378 l -46.81835,168.90619 a 2.3748439,2.8675381 0 0 1 -2.2527,1.96631 l -21.034336,-0.0983 a 2.3748439,2.8675381 0 0 1 -2.239139,-3.78515 z"
id="path9"
style="stroke-width:1.49119" />
<path
fill="#333333"
d="m 33.830382,133.30008 c -1.592276,0.75375 -1.583229,1.4802 0.02714,2.17933 16.438443,7.17704 32.28883,13.91165 47.551159,20.20386 0.727419,0.30748 1.219852,1.12423 1.234919,2.04824 -0.214237,10.63702 -0.384508,19.1784 -0.401503,28.62622 -0.325692,1.01593 -1.52099,1.31634 -2.353315,0.90122 L 2.3060265,148.37514 c -0.357622,-0.18597 -0.5802653,-0.61873 -0.556392,-1.08148 l 0.123809,-25.31188 c 0.024863,-0.63224 0.8744512,-1.12426 1.7082134,-1.57743 L 79.440954,81.143657 c 0.804287,-0.421268 2.617287,0.182689 2.875279,1.21475 -0.180356,10.46196 0.296376,20.583873 0.286657,29.345013 -0.018,0.48554 -0.274377,0.9112 -0.651386,1.08148 -16.752569,7.66439 -33.358905,14.70553 -48.121122,20.51518 z m 189.580388,-0.27856 -48.43324,-20.41687 c -0.0776,-10.64493 -0.0238,-13.917539 0.0176,-30.506823 0.1719,-0.546198 0.98658,-0.599141 1.44798,-0.446206 27.0655,12.611655 55.00987,27.040189 77.98987,38.588869 0.52473,0.26218 0.78709,0.73737 0.78709,1.42558 l 0.0407,25.7423 c -0.001,0.63571 -0.31626,1.2093 -0.80066,1.45835 l -77.22992,38.50694 c -1.24994,0.10087 -2.28748,-0.64701 -2.43862,-1.68019 -0.12039,-9.77693 -0.0127,-18.13379 -0.11264,-28.51909 0.0323,-0.59314 0.35336,-1.10357 0.81423,-1.29449 16.72794,-6.80562 32.70951,-13.7314 47.94471,-20.77736 1.56513,-0.72098 1.5561,-1.41465 -0.0271,-2.08101 z"
id="path11"
style="stroke-width:1.49119"
sodipodi:nodetypes="ccccccccccccccccccccccccccccsc" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,595 @@
{
"$schema": "https://json.schemastore.org/web-types",
"name": "htmx",
"version": "2.0.8",
"default-icon": "./htmx.svg",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"contributions": {
"html": {
"attributes": [
{
"name": "hx-boost",
"description": "The `hx-boost` attribute allows you to \"boost\" normal anchors and form tags to use AJAX instead. This\nhas the [nice fallback](https://en.wikipedia.org/wiki/Progressive_enhancement) that, if the user does not \nhave javascript enabled, the site will continue to work.\n\nFor anchor tags, clicking on the anchor will issue a `GET` request to the url specified in the `href` and\nwill push the url so that a history entry is created. The target is the `<body>` tag, and the `innerHTML`\nswap strategy is used by default. All of these can be modified by using the appropriate attributes, except\nthe `click` trigger.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-boost/"
},
{
"name": "hx-confirm",
"description": "The `hx-confirm` attribute allows you to confirm an action before issuing a request. This can be useful\nin cases where the action is destructive and you want to ensure that the user really wants to do it.\n\nHere is an example:\n\n```html\n<button hx-delete=\"/account\" hx-confirm=\"Are you sure you wish to delete your account?\">\n Delete My Account\n</button>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-confirm/"
},
{
"name": "hx-delete",
"description": "The `hx-delete` attribute will cause an element to issue a `DELETE` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-delete=\"/account\" hx-target=\"body\">\n Delete Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-delete/"
},
{
"name": "hx-disable",
"description": "The `hx-disable` attribute will disable htmx processing for a given element and all its children. This can be \nuseful as a backup for HTML escaping, when you include user generated content in your site, and you want to \nprevent malicious scripting attacks.\n\nThe value of the tag is ignored, and it cannot be reversed by any content beneath it.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-disable/"
},
{
"name": "hx-disabled-elt",
"description": "The `hx-disabled-elt` attribute allows you to specify elements that will have the `disabled` attribute\nadded to them for the duration of the request. The value of this attribute can be:\n\n* A CSS query selector of the element to disable.\n* `this` to disable the element itself\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest fieldset` will disable the closest to the element `fieldset`).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector\n* `next` which resolves to [element.nextElementSibling](https://developer.mozilla.org/docs/Web/API/Element/nextElementSibling)\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector\n (e.g. `next button` will disable the closest following sibling `button` element)\n* `previous` which resolves to [element.previousElementSibling](https://developer.mozilla.org/docs/Web/API/Element/previousElementSibling)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous input` will disable the closest previous sibling `input` element)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-disabled-elt/"
},
{
"name": "hx-disinherit",
"description": "The default behavior for htmx is to \"inherit\" many attributes automatically: that is, an attribute such as\n[hx-target](@/attributes/hx-target.md) may be placed on a parent element, and all child elements will inherit\nthat target.\n\nThe `hx-disinherit` attribute allows you to control this automatic attribute inheritance. An example scenario is to \nallow you to place an `hx-boost` on the `body` element of a page, but overriding that behavior in a specific part\nof the page to allow for more specific behaviors.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-disinherit/"
},
{
"name": "hx-encoding",
"description": "The `hx-encoding` attribute allows you to switch the request encoding from the usual `application/x-www-form-urlencoded`\nencoding to `multipart/form-data`, usually to support file uploads in an ajax request.\n\nThe value of this attribute should be `multipart/form-data`.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-encoding/"
},
{
"name": "hx-ext",
"description": "The `hx-ext` attribute enables an htmx [extension](https://htmx.org/extensions) for an element and all its children.\n\nThe value can be a single extension name or a comma-separated list of extensions to apply.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-ext/"
},
{
"name": "hx-get",
"description": "The `hx-get` attribute will cause an element to issue a `GET` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n <button hx-get=\"/example\">Get Some HTML</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-get/"
},
{
"name": "hx-headers",
"description": "The `hx-headers` attribute allows you to add to the headers that will be submitted with an AJAX request.\n\nBy default, the value of this attribute is a list of name-expression values in [JSON (JavaScript Object Notation)](https://www.json.org/json-en.html)\nformat.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-headers/"
},
{
"name": "hx-history-elt",
"description": "The `hx-history-elt` attribute allows you to specify the element that will be used to snapshot and\nrestore page state during navigation. By default, the `body` tag is used. This is typically\ngood enough for most setups, but you may want to narrow it down to a child element. Just make\nsure that the element is always visible in your application, or htmx will not be able to restore\nhistory navigation properly.",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-history-elt/"
},
{
"name": "hx-history",
"description": "Set the `hx-history` attribute to `false` on any element in the current document, or any html fragment loaded into the current document by htmx, to prevent sensitive data being saved to the localStorage cache when htmx takes a snapshot of the page state.\n\nHistory navigation will work as expected, but on restoration the URL will be requested from the server instead of the history cache.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-history/"
},
{
"name": "hx-include",
"description": "The `hx-include` attribute allows you to include additional element values in an AJAX request. The value of this\nattribute can be:\n\n* A CSS query selector of the elements to include.\n* `this` which will include the descendants of the element.\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest tr` will target the closest table row to the element).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.\n (e.g. `next .error` will target the closest following sibling element with `error` class)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous .error` will target the closest previous sibling with `error` class)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-include/"
},
{
"name": "hx-indicator",
"description": "The `hx-indicator` attribute allows you to specify the element that will have the `htmx-request` class\nadded to it for the duration of the request. This can be used to show spinners or progress indicators\nwhile the request is in flight.\n\nThe value of this attribute is a CSS query selector of the element or elements to apply the class to,\nor the keyword [`closest`](https://developer.mozilla.org/docs/Web/API/Element/closest), followed by a CSS selector, \nwhich will find the closest ancestor element or itself, that matches the given CSS selector (e.g. `closest tr`);",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-indicator/"
},
{
"name": "hx-inherit",
"description": "The default behavior for htmx is to \"inherit\" many attributes automatically: that is, an attribute such as\n[hx-target](@/attributes/hx-target.md) may be placed on a parent element, and all child elements will inherit\nthat target. Some people do not like this feature and instead prefer to explicitly specify inheritance for attributes.\n\nTo support this mode of development, htmx offers the `htmx.config.disableInheritance` setting, which can be set to\n`true` to prevent inheritance from being the default behavior for any of the htmx attributes.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-inherit/"
},
{
"name": "hx-on",
"pattern": {
"or": [
{
"items": [
"/js/events"
],
"template": [
"hx-on:",
"#...",
"#item:JS event"
]
},
{
"items": [
"/js/htmx-events"
],
"template": [
"hx-on::",
"#...",
"#item:HTMX event"
]
}
]
},
"description": "The `hx-on*` attributes allow you to embed scripts inline to respond to events directly on an element; similar to the \n[`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) found in HTML, such as `onClick`.\n\nThe `hx-on*` attributes improve upon `onevent` by enabling the handling of any arbitrary JavaScript event,\nfor enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/) even when dealing with non-standard DOM events. For example, these\nattributes allow you to handle [htmx events](/reference#events).",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-on/"
},
{
"name": "hx-params",
"description": "The `hx-params` attribute allows you to filter the parameters that will be submitted with an AJAX request.\n\nThe possible values of this attribute are:\n\n* `*` - Include all parameters (default)\n* `none` - Include no parameters\n* `not <param-list>` - Include all except the comma separated list of parameter names\n* `<param-list>` - Include all the comma separated list of parameter names",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-params/"
},
{
"name": "hx-patch",
"description": "The `hx-patch` attribute will cause an element to issue a `PATCH` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-patch=\"/account\" hx-target=\"body\">\n Patch Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-patch/"
},
{
"name": "hx-post",
"description": "The `hx-post` attribute will cause an element to issue a `POST` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-post=\"/account/enable\" hx-target=\"body\">\n Enable Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-post/"
},
{
"name": "hx-preserve",
"description": "The `hx-preserve` attribute allows you to keep an element unchanged during HTML replacement.\nElements with `hx-preserve` set are preserved by `id` when htmx updates any ancestor element.\nYou *must* set an unchanging `id` on elements for `hx-preserve` to work.\nThe response requires an element with the same `id`, but its type and other attributes are ignored.\n\n## Notes",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-preserve/"
},
{
"name": "hx-prompt",
"description": "The `hx-prompt` attribute allows you to show a prompt before issuing a request. The value of\nthe prompt will be included in the request in the `HX-Prompt` header.\n\nHere is an example:\n\n```html\n<button hx-delete=\"/account\" hx-prompt=\"Enter your account name to confirm deletion\">\n Delete My Account\n</button>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-prompt/"
},
{
"name": "hx-push-url",
"description": "The `hx-push-url` attribute allows you to push a URL into the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\nThis creates a new history entry, allowing navigation with the browsers back and forward buttons.\nhtmx snapshots the current DOM and saves it into its history cache, and restores from this cache on navigation.\n\nThe possible values of this attribute are:\n\n1. `true`, which pushes the fetched URL into history.\n2. `false`, which disables pushing the fetched URL if it would otherwise be pushed due to inheritance or [`hx-boost`](/attributes/hx-boost).\n3. A URL to be pushed into the location bar.\n This may be relative or absolute, as per [`history.pushState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState).",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-push-url/"
},
{
"name": "hx-put",
"description": "The `hx-put` attribute will cause an element to issue a `PUT` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-put=\"/account\" hx-target=\"body\">\n Put Money In Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-put/"
},
{
"name": "hx-replace-url",
"description": "The `hx-replace-url` attribute allows you to replace the current url of the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\n\nThe possible values of this attribute are:\n\n1. `true`, which replaces the fetched URL in the browser navigation bar.\n2. `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.\n3. A URL to be replaced into the location bar.\n This may be relative or absolute, as per [`history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-replace-url/"
},
{
"name": "hx-request",
"description": "The `hx-request` attribute allows you to configure various aspects of the request via the following attributes:\n \n* `timeout` - the timeout for the request, in milliseconds\n* `credentials` - if the request will send credentials\n* `noHeaders` - strips all headers from the request\n\nThese attributes are set using a JSON-like syntax:\n\n```html\n<div ... hx-request='{\"timeout\":100}'>\n ...\n</div>\n```",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-request/"
},
{
"name": "hx-select-oob",
"description": "The `hx-select-oob` attribute allows you to select content from a response to be swapped in via an out-of-band swap. \nThe value of this attribute is comma separated list of elements to be swapped out of band. This attribute is almost\nalways paired with [hx-select](@/attributes/hx-select.md).\n\nHere is an example that selects a subset of the response content:\n\n```html\n<div>\n <div id=\"alert\"></div>\n <button hx-get=\"/info\" \n hx-select=\"#info-details\" \n hx-swap=\"outerHTML\"\n hx-select-oob=\"#alert\">\n Get Info!\n </button>\n</div>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-select-oob/"
},
{
"name": "hx-select",
"description": "The `hx-select` attribute allows you to select the content you want swapped from a response. The value of\nthis attribute is a CSS query selector of the element or elements to select from the response.\n\nHere is an example that selects a subset of the response content:\n\n```html\n<div>\n <button hx-get=\"/info\" hx-select=\"#info-detail\" hx-swap=\"outerHTML\">\n Get Info!\n </button>\n</div>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-select/"
},
{
"name": "hx-swap-oob",
"description": "The `hx-swap-oob` attribute allows you to specify that some content in a response should be\nswapped into the DOM somewhere other than the target, that is \"Out of Band\". This allows you to piggyback updates to other element updates on a response.\n\nConsider the following response HTML:\n\n```html\n<div>\n ...\n</div>\n<div id=\"alerts\" hx-swap-oob=\"true\">\n Saved!\n</div>",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-swap-oob/"
},
{
"name": "hx-swap",
"description": "The `hx-swap` attribute allows you to specify how the response will be swapped in relative to the\n[target](@/attributes/hx-target.md) of an AJAX request. If you do not specify the option, the default is\n`htmx.config.defaultSwapStyle` (`innerHTML`).\n\nThe possible values of this attribute are:\n\n* `innerHTML` - Replace the inner html of the target element\n* `outerHTML` - Replace the entire target element with the response\n* `textContent` - Replace the text content of the target element, without parsing the response as HTML\n* `beforebegin` - Insert the response before the target element\n* `afterbegin` - Insert the response before the first child of the target element\n* `beforeend` - Insert the response after the last child of the target element\n* `afterend` - Insert the response after the target element\n* `delete` - Deletes the target element regardless of the response\n* `none`- Does not append content from response (out of band items will still be processed).",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-swap/"
},
{
"name": "hx-sync",
"description": "The `hx-sync` attribute allows you to synchronize AJAX requests between multiple elements.\n\nThe `hx-sync` attribute consists of a CSS selector to indicate the element to synchronize on, followed optionally\nby a colon and then by an optional syncing strategy. The available strategies are:\n\n* `drop` - drop (ignore) this request if an existing request is in flight (the default)\n* `abort` - drop (ignore) this request if an existing request is in flight, and, if that is not the case, \n *abort* this request if another request occurs while it is still in flight\n* `replace` - abort the current request, if any, and replace it with this request\n* `queue` - place this request in the request queue associated with the given element",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-sync/"
},
{
"name": "hx-target",
"description": "The `hx-target` attribute allows you to target a different element for swapping than the one issuing the AJAX\nrequest. The value of this attribute can be:\n\n* A CSS query selector of the element to target.\n* `this` which indicates that the element that the `hx-target` attribute is on is the target.\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest tr` will target the closest table row to the element).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.\n* `next` which resolves to [element.nextElementSibling](https://developer.mozilla.org/docs/Web/API/Element/nextElementSibling)\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.\n (e.g. `next .error` will target the closest following sibling element with `error` class)\n* `previous` which resolves to [element.previousElementSibling](https://developer.mozilla.org/docs/Web/API/Element/previousElementSibling)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous .error` will target the closest previous sibling with `error` class)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-target/"
},
{
"name": "hx-trigger",
"description": "The `hx-trigger` attribute allows you to specify what triggers an AJAX request. A trigger\nvalue can be one of the following:\n\n* An event name (e.g. \"click\" or \"my-custom-event\") followed by an event filter and a set of event modifiers\n* A polling definition of the form `every <timing declaration>`\n* A comma-separated list of such events",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-trigger/"
},
{
"name": "hx-validate",
"description": "The `hx-validate` attribute will cause an element to validate itself by way of the [HTML5 Validation API](@/docs.md#validation)\nbefore it submits a request.\n\nOnly `<form>` elements validate data by default, but other elements do not. Adding `hx-validate=\"true\"` to `<input>`, `<textarea>` or `<select>` enables validation before sending requests.",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-validate/"
},
{
"name": "hx-vals",
"description": "The `hx-vals` attribute allows you to add to the parameters that will be submitted with an AJAX request.\n\nBy default, the value of this attribute is a list of name-expression values in [JSON (JavaScript Object Notation)](https://www.json.org/json-en.html)\nformat.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-vals/"
},
{
"name": "hx-vars",
"description": "**NOTE: `hx-vars` has been deprecated in favor of [`hx-vals`](@/attributes/hx-vals.md), which is safer by default.**\n\nThe `hx-vars` attribute allows you to dynamically add to the parameters that will be submitted with an AJAX request.",
"description-sections": {
"Inherited": ""
},
"deprecated": true,
"doc-url": "https://htmx.org/attributes/hx-vars/"
}
]
},
"css": {
"classes": [
{
"name": "htmx-added",
"description": "Applied to a new piece of content before it is swapped, removed after it is settled.",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-indicator",
"description": "A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-request",
"description": "Applied to either the element or the element specified with [`hx-indicator`](@/attributes/hx-indicator.md) while a request is ongoing",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-settling",
"description": "Applied to a target after content is swapped, removed after it is settled. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-swapping",
"description": "Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).",
"doc-url": "https://htmx.org/reference/#classes"
}
]
},
"js": {
"events": [
{
"name": "HTMX event",
"pattern": {
"items": [
"/js/htmx-events"
],
"template": [
"htmx:",
"$...",
"#item:HTMX event"
]
}
}
],
"htmx-events": [
{
"name": "abort",
"description": "This event is different than other events: htmx does not *trigger* it, but rather *listens* for it.\n\nIf you send an `htmx:abort` event to an element making a request, it will abort the request:\n\n```html\n<button id=\"request-button\" hx-post=\"/example\">Issue Request</button>\n<button onclick=\"htmx.trigger('#request-button', 'htmx:abort')\">Cancel Request</button>\n```\n\n",
"doc-url": "https://htmx.org/events/#htmx:abort"
},
{
"name": "afterOnLoad",
"description": "This event is triggered after an AJAX `onload` has finished. Note that this does not mean that the content\nhas been swapped or settled yet, only that the request has finished.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request or if the body no longer contains the element then the closest parent\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterOnLoad"
},
{
"name": "afterProcessNode",
"description": "This event is triggered after htmx has initialized a DOM node. It can be useful for extensions to build additional features onto a node.\n\n##### Details\n\n* `detail.elt` - the element being initialized\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterProcessNode"
},
{
"name": "afterRequest",
"description": "This event is triggered after an AJAX request has finished either in the case of a successful request (although\none that may have returned a remote error code such as a `404`) or in a network error situation. This event\ncan be paired with [`htmx:beforeRequest`](#htmx:beforeRequest) to wrap behavior around a request cycle.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request or if the body no longer contains the element then the closest parent\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.successful` - true if the response has a 20x status code or is marked `detail.isError = false` in the\n `htmx:beforeSwap` event, else false\n* `detail.failed` - true if the response does not have a 20x status code or is marked `detail.isError = true` in the\n `htmx:beforeSwap` event, else false\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterRequest"
},
{
"name": "afterSettle",
"description": "This event is triggered after the DOM has [settled](@/docs.md#request-operations).\n\n##### Details\n\n* `detail.elt` - the updated element\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterSettle"
},
{
"name": "afterSwap",
"description": "This event is triggered after new content has been [swapped into the DOM](@/docs.md#swapping).\n\n##### Details\n\n* `detail.elt` - the swapped in element\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterSwap"
},
{
"name": "beforeCleanupElement",
"description": "This event is triggered before htmx [disables](@/attributes/hx-disable.md) an element or removes it from the DOM.\n\n##### Details\n\n* `detail.elt` - the element to be cleaned up\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeCleanupElement"
},
{
"name": "beforeOnLoad",
"description": "This event is triggered before any response processing occurs. If you call `preventDefault()` on the event to cancel it, no swap will occur.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeOnLoad"
},
{
"name": "beforeProcessNode",
"description": "This event is triggered before htmx initializes a DOM node and has processed all of its `hx-` attributes. This gives extensions and other external code the ability to modify the contents of a DOM node before it is processed.\n\n##### Details\n\n* `detail.elt` - the element being initialized\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeProcessNode"
},
{
"name": "beforeRequest",
"description": "This event is triggered before an AJAX request is issued. If you call `preventDefault()` on the event to cancel it, no request will occur.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeRequest"
},
{
"name": "beforeSend",
"description": "This event is triggered right before a request is sent. You may not cancel the request with this event.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeSend"
},
{
"name": "beforeSwap",
"description": "This event is triggered before any new content has been [swapped into the DOM](@/docs.md#swapping).\nMost values on `detail` can be set to override subsequent behavior, other than where response headers take precedence.\nIf you call `preventDefault()` on the event to cancel it, no swap will occur.\n\nYou can modify the default swap behavior by modifying the `shouldSwap`, `selectOverride`, `swapOverride` and `target` properties of the event detail.\nSee the documentation on [configuring swapping](@/docs.md#modifying_swapping_behavior_with_events) for more details.\n\n##### Details\n\n* `detail.elt` - the target of the swap\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.requestConfig.elt` - the element that dispatched the request\n* `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes)\n* `detail.ignoreTitle` - if `true` any title tag in the response will be ignored\n* `detail.isError` - whether error events should be triggered and also determines the values of `detail.successful` and `detail.failed` in later events\n* `detail.serverResponse` - the server response as text to be used for the swap\n* `detail.selectOverride` - add this to use instead of an [`hx-select`](@/attributes/hx-select.md) value\n* `detail.swapOverride` - add this to use instead of an [`hx-swap`](@/attributes/hx-swap.md) value\n* `detail.target` - the target of the swap\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeSwap"
},
{
"name": "beforeTransition",
"description": "This event is triggered before a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) \nwrapped swap occurs. If you call `preventDefault()` on the event to cancel it, the View Transition will not occur and the normal swapping logic will\nhappen instead.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes)\n* `detail.target` - the target of the swap\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeTransition"
},
{
"name": "configRequest",
"description": "This event is triggered after htmx has collected parameters for inclusion in the request. It can be\nused to include or update the parameters that htmx will send:\n\n```javascript\ndocument.body.addEventListener('htmx:configRequest', function(evt) {\n evt.detail.parameters['auth_token'] = getAuthToken(); // add a new parameter into the mix\n});\n```\n\nNote that if an input value appears more than once the value in the `parameters` object will be an array, rather\nthan a single value.\n\n##### Details\n\n* `detail.parameters` - the parameters that will be submitted in the request\n* `detail.unfilteredParameters` - the parameters that were found before filtering by [`hx-params`](@/attributes/hx-params.md)\n* `detail.headers` - the request headers\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.verb` - the HTTP verb in use\n\n",
"doc-url": "https://htmx.org/events/#htmx:configRequest"
},
{
"name": "confirm",
"description": "This event is fired on every trigger for a request (not just on elements that have a hx-confirm attribute).\nIt allows you to cancel (or delay) issuing the AJAX request.\nIf you call `preventDefault()` on the event, it will not issue the given request.\nThe `detail` object contains a function, `evt.detail.issueRequest(skipConfirmation=false)`, that can be used to issue the actual AJAX request at a later point.\nCombining these two features allows you to create an asynchronous confirmation dialog.\n\nHere is a basic example that shows the basic usage of the `htmx:confirm` event without altering the default behavior:\n\n```javascript\ndocument.body.addEventListener('htmx:confirm', function(evt) {\n // 0. To modify the behavior only for elements with the hx-confirm attribute,\n // check if evt.detail.target.hasAttribute('hx-confirm')\n\n // 1. Prevent the default behavior (this will prevent the request from being issued)\n evt.preventDefault();\n \n // 2. Do your own logic here\n console.log(evt.detail)\n\n // 3. Manually issue the request when you are ready\n evt.detail.issueRequest(); // or evt.detail.issueRequest(true) to skip the built-in window.confirm()\n});\n```\n\nAnd here is an example using [sweet alert](https://sweetalert.js.org/guides/) on any element with a `confirm-with-sweet-alert=\"{question}\"` attribute on it:\n\n```javascript\ndocument.body.addEventListener('htmx:confirm', function(evt) {\n // 1. The requirement to show the sweet alert is that the element has a confirm-with-sweet-alert\n // attribute on it, if it doesn't we can return early and let the default behavior happen\n if (!evt.detail.target.hasAttribute('confirm-with-sweet-alert')) return\n\n // 2. Get the question from the attribute\n const question = evt.detail.target.getAttribute('confirm-with-sweet-alert');\n\n // 3. Prevent the default behavior (this will prevent the request from being issued)\n evt.preventDefault();\n\n // 4. Show the sweet alert\n swal({\n title: \"Are you sure?\",\n text: question || \"Are you sure you want to continue?\",\n icon: \"warning\",\n buttons: true,\n dangerMode: true,\n }).then((confirmed) => {\n if (confirmed) {\n // 5. If the user confirms, we can manually issue the request\n evt.detail.issueRequest(true); // true to skip the built-in window.confirm()\n }\n });\n});\n```\n\n##### Details\n\n* `detail.elt` - the element in question\n* `detail.etc` - additional request information (mostly unused)\n* `detail.issueRequest(skipConfirmation=false)` - a function that can be invoked to issue the request (should be paired with `evt.preventDefault()`!), if skipConfirmation is `true` the original `window.confirm()` is not executed\n* `detail.path` - the path of the request\n* `detail.target` - the element that triggered the request\n* `detail.triggeringEvent` - the original event that triggered this request\n* `detail.verb` - the verb of the request (e.g. `GET`)\n* `detail.question` - the question passed to `hx-confirm` attribute (only available if `hx-confirm` attribute is present)\n\n",
"doc-url": "https://htmx.org/events/#htmx:confirm"
},
{
"name": "historyCacheError",
"description": "This event is triggered when an attempt to save the cache to `localStorage` fails\n\n##### Details\n\n* `detail.cause` - the `Exception` that was thrown when attempting to save history to `localStorage`\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheError"
},
{
"name": "historyCacheHit",
"description": "This event is triggered when a cache hit occurs when restoring history\n\nYou can prevent the history restoration via `preventDefault()` to allow alternative restore handling.\nYou can also override the details of the history restoration request in this event if required\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.item.content` - the content of the cache that will be swapped in\n* `detail.item.title` - the page title to update from the cache\n* `detail.path` - the path and query of the page being restored\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheHit"
},
{
"name": "historyCacheMiss",
"description": "This event is triggered when a cache miss occurs when restoring history\n\nYou can prevent the history restoration via `preventDefault()` to allow alternative restore handling.\nYou can also modify the xhr request or other details before it makes the the request to restore history\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.xhr` - the `XMLHttpRequest` that will retrieve the remote content for restoration\n* `detail.path` - the path and query of the page being restored\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMiss"
},
{
"name": "historyCacheMissLoadError",
"description": "This event is triggered when a cache miss occurs and a response has been retrieved from the server\nfor the content to restore, but the response is an error (e.g. `404`)\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.path` - the path and query of the page being restored\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMissLoadError"
},
{
"name": "historyCacheMissLoad",
"description": "This event is triggered when a cache miss occurs and a response has been retrieved successfully from the server\nfor the content to restore\n\nYou can modify the details before it makes the swap to restore the history\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.path` - the path and query of the page being restored\n* `detail.response` - the response text that will be swapped in\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMissLoad"
},
{
"name": "historyRestore",
"description": "This event is triggered when htmx handles a history restoration action\n\n##### Details\n\n* `detail.path` - the path and query of the page being restored\n* `detail.cacheMiss` - set `true` if restore was a cache miss\n* `detail.serverResponse` - with cache miss has the response text replaced\n* `detail.item` - with cache hit the cache details that was restored\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyRestore"
},
{
"name": "beforeHistorySave",
"description": "This event is triggered before the content is saved in the history cache.\n\nYou can modify the contents of the historyElt to remove 3rd party javascript changes so a clean copy of the content can be backed up to the history cache\n\n##### Details\n\n* `detail.path` - the path and query of the page being saved\n* `detail.historyElt` - the history element about to be saved\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeHistorySave"
},
{
"name": "load",
"description": "This event is triggered when a new node is loaded into the DOM by htmx. Note that this event is also triggered when htmx is first initialized, with the document body as the target.\n\n##### Details\n\n* `detail.elt` - the newly added element\n\n",
"doc-url": "https://htmx.org/events/#htmx:load"
},
{
"name": "noSSESourceError",
"description": "This event is triggered when an element refers to an SSE event in its trigger, but no parent SSE source has been defined\n\n##### Details\n\n* `detail.elt` - the element with the bad SSE trigger\n\n",
"doc-url": "https://htmx.org/events/#htmx:noSSESourceError"
},
{
"name": "oobAfterSwap",
"description": "This event is triggered as part of an [out of band swap](@/docs.md#oob_swaps) and behaves identically to an [after swap event](#htmx:afterSwap)\n\n##### Details\n\n* `detail.elt` - the swapped in element\n* `detail.shouldSwap` - if the content will be swapped (defaults to `true`)\n* `detail.target` - the target of the swap\n* `detail.fragment` - the response fragment\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobAfterSwap"
},
{
"name": "oobBeforeSwap",
"description": "This event is triggered as part of an [out of band swap](@/docs.md#oob_swaps) and behaves identically to a [before swap event](#htmx:beforeSwap)\n\n##### Details\n\n* `detail.elt` - the target of the swap\n* `detail.shouldSwap` - if the content will be swapped (defaults to `true`)\n* `detail.target` - the target of the swap\n* `detail.fragment` - the response fragment\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobBeforeSwap"
},
{
"name": "oobErrorNoTarget",
"description": "This event is triggered when an [out of band swap](@/docs.md#oob_swaps) does not have a corresponding element\nin the DOM to switch with.\n\n##### Details\n\n* `detail.content` - the element with the bad oob `id`\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobErrorNoTarget"
},
{
"name": "onLoadError",
"description": "This event is triggered when an error occurs during the `load` handling of an AJAX call\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.exception` - the exception that occurred\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:onLoadError"
},
{
"name": "prompt",
"description": "This event is triggered after a prompt has been shown to the user with the [`hx-prompt`](@/attributes/hx-prompt.md)\nattribute. If this event is cancelled, the AJAX request will not occur.\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.prompt` - the user response to the prompt\n\n",
"doc-url": "https://htmx.org/events/#htmx:prompt"
},
{
"name": "beforeHistoryUpdate",
"description": "This event is triggered before a history update is performed. It can be\nused to modify the `path` or `type` used to update the history.\n\n##### Details\n\n* `detail.history` - the `path` and `type` (push, replace) for the history update\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeHistoryUpdate"
},
{
"name": "pushedIntoHistory",
"description": "This event is triggered after a URL has been pushed into history.\n\n##### Details\n\n* `detail.path` - the path and query of the URL that has been pushed into history\n\n",
"doc-url": "https://htmx.org/events/#htmx:pushedIntoHistory"
},
{
"name": "replacedInHistory",
"description": "This event is triggered after a URL has been replaced in history.\n\n##### Details\n\n* `detail.path` - the path and query of the URL that has been replaced in history\n\n",
"doc-url": "https://htmx.org/events/#htmx:replacedInHistory"
},
{
"name": "responseError",
"description": "This event is triggered when an HTTP error response occurs\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:responseError"
},
{
"name": "sendAbort",
"description": "This event is triggered when a request is aborted\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:sendAbort"
},
{
"name": "sendError",
"description": "This event is triggered when a network error prevents an HTTP request from occurring\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:sendError"
},
{
"name": "sseError",
"description": "This event is triggered when an error occurs with an SSE source\n\n##### Details\n\n* `detail.elt` - the element with the bad SSE source\n* `detail.error` - the error\n* `detail.source` - the SSE source\n\n",
"doc-url": "https://htmx.org/events/#htmx:sseError"
},
{
"name": "swapError",
"description": "This event is triggered when an error occurs during the swap phase\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:swapError"
},
{
"name": "targetError",
"description": "This event is triggered when a bad selector is used for a [`hx-target`](@/attributes/hx-target.md) attribute (e.g. an\nelement ID without a preceding `#`)\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the bad CSS selector\n\n",
"doc-url": "https://htmx.org/events/#htmx:targetError"
},
{
"name": "timeout",
"description": "This event is triggered when a request timeout occurs. This wraps the typical `timeout` event of XMLHttpRequest.\n\nTimeout time can be set using `htmx.config.timeout` or per element using [`hx-request`](@/attributes/hx-request.md)\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:timeout"
},
{
"name": "trigger",
"description": "This event is triggered whenever an AJAX request would be, even if no AJAX request is specified. It\nis primarily intended to allow `hx-trigger` to execute client-side scripts; AJAX requests have more\ngranular events available, like [`htmx:beforeRequest`](#htmx:beforeRequest) or [`htmx:afterRequest`](#htmx:afterRequest).\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:trigger"
},
{
"name": "validateUrl",
"description": "This event is triggered before a request is made, allowing you to validate the URL that htmx is going to request. If\n`preventDefault()` is invoked on the event, the request will not be made.\n\n```javascript\ndocument.body.addEventListener('htmx:validateUrl', function (evt) {\n // only allow requests to the current server as well as myserver.com\n if (!evt.detail.sameHost && evt.detail.url.hostname !== \"myserver.com\") {\n evt.preventDefault();\n }\n});\n```\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.url` - the URL Object representing the URL that a request will be sent to.\n* `detail.sameHost` - will be `true` if the request is to the same host as the document\n\n",
"doc-url": "https://htmx.org/events/#htmx:validateUrl"
},
{
"name": "validation:validate",
"description": "This event is triggered before an element is validated. It can be used with the `elt.setCustomValidity()` method\nto implement custom validation rules.\n\n```html\n<form hx-post=\"/test\">\n <input _=\"on htmx:validation:validate\n if my.value != 'foo'\n call me.setCustomValidity('Please enter the value foo')\n else\n call me.setCustomValidity('')\"\n name=\"example\">\n</form>\n```\n\n##### Details\n\n* `detail.elt` - the element to be validated\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:validate"
},
{
"name": "validation:failed",
"description": "This event is triggered when an element fails validation. If `preventDefault()` is invoked on the event, the reportValidity() enabled by `htmx.config.reportValidityOfForms` will not be called.\n\n##### Details\n\n* `detail.elt` - the element that failed validation\n* `detail.message` - the validation error message\n* `detail.validity` - the validity object, which contains properties specifying how validation failed\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:failed"
},
{
"name": "validation:halted",
"description": "This event is triggered when a request is halted due to validation errors.\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.errors` - an array of error objects with the invalid elements and errors associated with them\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:halted"
},
{
"name": "xhr:abort",
"description": "This event is triggered when an ajax request aborts\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:abort"
},
{
"name": "xhr:loadstart",
"description": "This event is triggered when an ajax request starts\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:loadstart"
},
{
"name": "xhr:loadend",
"description": "This event is triggered when an ajax request finishes\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:loadend"
},
{
"name": "xhr:progress",
"description": "This event is triggered periodically when an ajax request that supports progress is in flight\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:progress"
}
]
}
}
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 256 256"
version="1.1"
id="svg287"
sodipodi:docname="htmx_dark.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs291" />
<sodipodi:namedview
id="namedview289"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="27.40625"
inkscape:cx="6.3854048"
inkscape:cy="7.3705815"
inkscape:window-width="1259"
inkscape:window-height="820"
inkscape:window-x="2060"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg287" />
<path
fill="#3d72d7"
d="m 92.80931,211.93595 47.9447,-171.265771 a 2.3748439,2.8675381 0 0 1 2.49698,-1.93354 l 19.88084,2.45789 a 2.3748439,2.8675381 0 0 1 2.022,3.75238 L 118.33548,213.8531 a 2.3748439,2.8675381 0 0 1 -2.2527,1.96631 l -21.034331,-0.0983 a 2.3748439,2.8675381 0 0 1 -2.239139,-3.78515 z"
id="path9"
style="stroke-width:1.49119" />
<path
fill="#333333"
d="m 33.763907,132.12007 c -1.592276,0.75375 -1.583229,1.4802 0.02714,2.17933 16.438443,7.17704 32.28883,13.91165 47.551159,20.20386 0.727419,0.30748 1.219852,1.12423 1.234919,2.04824 -0.214237,10.63702 -0.384508,19.1784 -0.401503,28.62622 -0.325692,1.01593 -1.52099,1.31634 -2.353315,0.90122 L 2.2395547,147.19513 c -0.35763,-0.18597 -0.58027,-0.61873 -0.5564,-1.08148 l 0.12381,-25.31188 c 0.0249,-0.63224 0.87445,-1.12426 1.70822,-1.57743 L 79.374479,79.963647 c 0.804287,-0.42127 2.617287,0.18269 2.875279,1.21475 -0.180356,10.46196 0.296376,20.583873 0.286657,29.345013 -0.018,0.48554 -0.274377,0.9112 -0.651386,1.08148 -16.752569,7.66439 -33.358905,14.70553 -48.121122,20.51518 z m 189.580383,-0.27856 -48.43324,-20.41687 c -0.0776,-10.64493 -0.0238,-13.917543 0.0176,-30.506823 0.1719,-0.5462 0.98658,-0.59914 1.44798,-0.44621 27.0655,12.61166 55.00987,27.040193 77.98987,38.588873 0.52473,0.26218 0.78709,0.73737 0.78709,1.42558 l 0.0407,25.7423 c -0.001,0.63571 -0.31626,1.2093 -0.80066,1.45835 l -77.22992,38.50694 c -1.24994,0.10087 -2.28748,-0.64701 -2.43862,-1.68019 -0.12039,-9.77693 -0.0127,-18.13379 -0.11264,-28.51909 0.0323,-0.59314 0.35336,-1.10357 0.81423,-1.29449 16.72794,-6.80562 32.70951,-13.7314 47.94471,-20.77736 1.56513,-0.72098 1.5561,-1.41465 -0.0271,-2.08101 z"
id="path11"
style="stroke-width:1.49119;fill:#f5f5f5;fill-opacity:1"
sodipodi:nodetypes="ccccccccccccccccccccccccccccsc" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

91
src/ext/hx-compat.js Normal file
View File

@ -0,0 +1,91 @@
(()=>{
//========================================================
// htmx 2.0 compatibility extension
//========================================================
let api
function maybeRetriggerEvent(elt, evtName, detail) {
if (!htmx.config.compat?.doNotTriggerOldEvents) {
htmx.trigger(elt, evtName, detail);
}
}
htmx.registerExtension('compat', {
init: (internalAPI) => {
api = internalAPI;
// revert inheritance
if (!htmx.config.compat?.useExplicitInheritace) {
htmx.config.implicitInheritance = true;
}
// do not swap 4xx and 5xx responses
if (!htmx.config.compat?.swapErrorResponseCodes) {
htmx.config.noSwap.push("4xx", "5xx");
}
},
// Re-delegate new events to old event names for backwards compatibility
htmx_after_implicitInheritance: function (elt, detail) {
if (!htmx.config.compat?.suppressInheritanceLogs) {
console.log("IMPLICIT INHERITANCE DETECTED, attribute: " + detail.name + ", elt: ", elt, ", inherited from: ", detail.parent)
let evt = new CustomEvent("htmxImplicitInheritace", {
detail,
cancelable: true,
bubbles : true,
composed: true,
});
elt.dispatchEvent(evt)
}
},
htmx_after_init: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterOnLoad", detail);
maybeRetriggerEvent(elt, "htmx:afterProcessNode", detail);
maybeRetriggerEvent(elt, "htmx:load", detail);
},
htmx_after_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterRequest", detail);
},
htmx_after_swap: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:afterSettle", detail);
maybeRetriggerEvent(elt, "htmx:afterSwap", detail);
},
htmx_before_cleanup: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeCleanupElement", detail);
},
htmx_before_history_update: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeHistoryUpdate", detail);
maybeRetriggerEvent(elt, "htmx:beforeHistorySave", detail);
},
htmx_before_init: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeOnLoad", detail);
},
htmx_before_process: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeProcessNode", detail);
},
htmx_before_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeRequest", detail);
maybeRetriggerEvent(elt, "htmx:beforeSend", detail);
},
htmx_before_swap: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeSwap", detail);
},
htmx_before_viewTransition: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:beforeTransition", detail);
},
htmx_config_request: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:configRequest", detail);
},
htmx_before_restore_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:historyRestore", detail);
},
htmx_after_push_into_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:pushedIntoHistory", detail);
},
htmx_after_replace_into_history: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:replacedInHistory", detail);
},
htmx_error: function (elt, detail) {
maybeRetriggerEvent(elt, "htmx:targetError", detail);
},
});
})()

129
src/ext/hx-head.js Normal file
View File

@ -0,0 +1,129 @@
//==========================================================
// head-support.js
//
// An extension to add head tag merging.
//==========================================================
(function () {
let api
function log() {
//console.log(arguments)
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html")
// remove svgs to avoid conflicts
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '')
// extract head tag
let headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im)
// if the head tag exists...
if (headTag) {
let added = []
let removed = []
let preserved = []
let nodesToAppend = []
htmlDoc.innerHTML = headTag
let newHeadTag = htmlDoc.querySelector("head")
let currentHead = document.head
if (newHeadTag == null) {
return
}
// put all new head elements into a Map, by their outerHTML
let srcToNewHeadNodes = new Map()
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild)
}
// determine merge strategy
let mergeStrategy = api.attributeValue(newHeadTag, "hx-head") || defaultMergeStrategy
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML)
let isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"
let isPreserved = api.attributeValue(currentHeadElt, "hx-preserve") === "true"
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt)
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML)
preserved.push(currentHeadElt)
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt)
nodesToAppend.push(currentHeadElt)
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt)
}
}
}
}
// Push the remaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values())
log("to append: ", nodesToAppend)
for (const newNode of nodesToAppend) {
log("adding: ", newNode)
let newElt = document.createRange().createContextualFragment(newNode.outerHTML)
log(newElt)
if (htmx.trigger(document.body, "htmx:before:head:add", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt)
added.push(newElt)
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (htmx.trigger(document.body, "htmx:before:head:remove", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement)
}
}
htmx.trigger(document.body, "htmx:after:head:merge", {
added: added,
kept: preserved,
removed: removed
})
}
}
}
htmx.registerExtension("hx-head", {
init: (internalAPI) => {
api = internalAPI;
},
htmx_after_swap: (elt, detail) => {
let ctx = detail.ctx
let target = ctx.target
// TODO - is there a better way to handle this? it used to be based on if the element was boosted
let defaultMergeStrategy = target === document.body ? "merge" : "append";
if (htmx.trigger(document.body, "htmx:before:head:merge", detail)) {
mergeHead(ctx.text, defaultMergeStrategy)
}
}
})
})()

Some files were not shown because too many files have changed in this diff Show More