mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-01-25 13:17:08 +00:00
Compare commits
670 Commits
ws-fix-hx-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580549355a | ||
|
|
709512c1ac | ||
|
|
5a374d546b | ||
|
|
1a30b9130e | ||
|
|
ad65bc77ce | ||
|
|
381449089d | ||
|
|
2e229462e3 | ||
|
|
6b214f11e7 | ||
|
|
58dc1e247d | ||
|
|
749d5f2f4c | ||
|
|
fcfca903af | ||
|
|
563fff67db | ||
|
|
9c1297c5f3 | ||
|
|
e495b68dc3 | ||
|
|
3abaf7eb3f | ||
|
|
e9f2ee94e3 | ||
|
|
bced397c28 | ||
|
|
7a0086fceb | ||
|
|
2a1339287e | ||
|
|
a275707f4a | ||
|
|
f1e0b926d8 | ||
|
|
e71f746bad | ||
|
|
8b249b1544 | ||
|
|
b7f833b6d5 | ||
|
|
20f97a68ae | ||
|
|
5bfeb977d4 | ||
|
|
cf6609d454 | ||
|
|
1e80780963 | ||
|
|
022e53c0a1 | ||
|
|
41c55c941e | ||
|
|
49d6aa3752 | ||
|
|
a6c3323e61 | ||
|
|
915a240de5 | ||
|
|
b9336a96fb | ||
|
|
83a1449a89 | ||
|
|
deecda151c | ||
|
|
cd045c3e0e | ||
|
|
69ecb9f85d | ||
|
|
04d6c7249b | ||
|
|
148fc95cbc | ||
|
|
e2faeaf7a9 | ||
|
|
887524c734 | ||
|
|
4e183e6da4 | ||
|
|
014cd4ee69 | ||
|
|
7de98197f3 | ||
|
|
56299971ce | ||
|
|
0da136f4a5 | ||
|
|
2d0c0f1b56 | ||
|
|
81a6e25fb9 | ||
|
|
2b83121f27 | ||
|
|
3b85139c61 | ||
|
|
96d361d440 | ||
|
|
ccdce87ec3 | ||
|
|
449c8e9531 | ||
|
|
cee310e4d5 | ||
|
|
d5de7d4a03 | ||
|
|
458ae04d17 | ||
|
|
e4f8fe9a77 | ||
|
|
d7507ddf2c | ||
|
|
448db781ef | ||
|
|
d818268c4c | ||
|
|
2289e3176e | ||
|
|
081adf8eeb | ||
|
|
7ae66f9b33 | ||
|
|
29363deb1a | ||
|
|
7619551349 | ||
|
|
90f94753ec | ||
|
|
9d598f8d6e | ||
|
|
dc804932a7 | ||
|
|
03c3af724c | ||
|
|
93cafa50f8 | ||
|
|
ec886c81c1 | ||
|
|
9c5a646395 | ||
|
|
85e499d174 | ||
|
|
bb4d877330 | ||
|
|
91f4472548 | ||
|
|
9a686ea2de | ||
|
|
28fae544c2 | ||
|
|
032972be35 | ||
|
|
9fb3c0e492 | ||
|
|
b0c87bf363 | ||
|
|
d91c8820f9 | ||
|
|
7529444e86 | ||
|
|
a440c6d4f4 | ||
|
|
1b3e78c331 | ||
|
|
599d152c48 | ||
|
|
fe7f103eab | ||
|
|
8e489ef6ee | ||
|
|
17a7dd1fc4 | ||
|
|
17f417c923 | ||
|
|
5df061fb65 | ||
|
|
c838cfb7a6 | ||
|
|
726292af1d | ||
|
|
683c0e8ae2 | ||
|
|
4f95de2e58 | ||
|
|
e2353e26bc | ||
|
|
dc71b317d0 | ||
|
|
84306ccf3d | ||
|
|
3f49db3936 | ||
|
|
bb4eb0f813 | ||
|
|
e38d6a7147 | ||
|
|
1d01b94b90 | ||
|
|
c091b95fa3 | ||
|
|
c9e2bea954 | ||
|
|
7388d0c057 | ||
|
|
508e332544 | ||
|
|
3c1ac71573 | ||
|
|
859708c379 | ||
|
|
7df5969664 | ||
|
|
5b4d77da6b | ||
|
|
e7bb245ef4 | ||
|
|
0aaab5a3c9 | ||
|
|
d1d0cd916b | ||
|
|
dd1914d503 | ||
|
|
e783c88670 | ||
|
|
8ec48d9d29 | ||
|
|
11ff1940f0 | ||
|
|
28b31f23db | ||
|
|
53c5cf6df7 | ||
|
|
82546dbd47 | ||
|
|
4184d1fd0c | ||
|
|
6d238f3d61 | ||
|
|
075ed73799 | ||
|
|
70f41e0c6c | ||
|
|
e4ecc55586 | ||
|
|
11a8e9c6c2 | ||
|
|
646c8583be | ||
|
|
730bd8224d | ||
|
|
8409ebca3b | ||
|
|
083dbcdd6f | ||
|
|
d2e39716fb | ||
|
|
407408b947 | ||
|
|
0404d0137b | ||
|
|
f4cc8382a4 | ||
|
|
ff190eef26 | ||
|
|
53496ff428 | ||
|
|
a9b673c93c | ||
|
|
5520566fc3 | ||
|
|
db8e5e03cb | ||
|
|
e01027b938 | ||
|
|
6a585f9f3c | ||
|
|
05d37e6ea6 | ||
|
|
d3bcd787ba | ||
|
|
408850a08e | ||
|
|
21dc121fce | ||
|
|
0da03839ce | ||
|
|
86893ebf4c | ||
|
|
4d16626cf4 | ||
|
|
bc7a6b8c55 | ||
|
|
63016891a6 | ||
|
|
f8b71843da | ||
|
|
24a0106f76 | ||
|
|
e8f5990c1b | ||
|
|
6836e87a6c | ||
|
|
3830fa7b2c | ||
|
|
0578564a25 | ||
|
|
b1c1a1ba23 | ||
|
|
aa3dbf0c50 | ||
|
|
838f49d977 | ||
|
|
a2681e3ee2 | ||
|
|
f42117628c | ||
|
|
15085704f2 | ||
|
|
0d7434f998 | ||
|
|
d1aa89192f | ||
|
|
a3ac341994 | ||
|
|
abaf3de237 | ||
|
|
1b4e778ba6 | ||
|
|
fec926d354 | ||
|
|
f8f7466d5c | ||
|
|
1d1a3ceeee | ||
|
|
fcf7457766 | ||
|
|
b76b1f6b9b | ||
|
|
b8a29903dc | ||
|
|
7b34baed84 | ||
|
|
07d35186fb | ||
|
|
efcc6b2211 | ||
|
|
103c72ed74 | ||
|
|
d8dc1ee93d | ||
|
|
61ad9549c3 | ||
|
|
14f3c5efd8 | ||
|
|
70736c4c8f | ||
|
|
163d226988 | ||
|
|
0a135f95ce | ||
|
|
90a91a60e0 | ||
|
|
0f9c4202ba | ||
|
|
72b425f5fb | ||
|
|
46badfe0b1 | ||
|
|
6dbf554e49 | ||
|
|
d8b12de2fb | ||
|
|
a60cdf1854 | ||
|
|
10e8656af5 | ||
|
|
4328259749 | ||
|
|
6ea1029960 | ||
|
|
a9b11187ba | ||
|
|
1c3556c30e | ||
|
|
ac03dc540f | ||
|
|
b0797be8cd | ||
|
|
6d39919b99 | ||
|
|
f760d3f27d | ||
|
|
bdbbeee284 | ||
|
|
f206885422 | ||
|
|
d941a204fc | ||
|
|
7b8e26e193 | ||
|
|
0e1794f0fd | ||
|
|
8ef8fe369b | ||
|
|
fd9af68ed8 | ||
|
|
d1e693a15a | ||
|
|
63975c72fe | ||
|
|
8ed85cdb22 | ||
|
|
94b903ebab | ||
|
|
27f0b31076 | ||
|
|
4177071ee4 | ||
|
|
1ac87b9f9c | ||
|
|
d890547012 | ||
|
|
5fd60a5745 | ||
|
|
05c88d80ed | ||
|
|
aeeb2165a9 | ||
|
|
29ace831cb | ||
|
|
02bc415735 | ||
|
|
c802494f7e | ||
|
|
65145db5bf | ||
|
|
1a38467f9c | ||
|
|
9fcb5c5c32 | ||
|
|
f46989b24a | ||
|
|
e0905ff94a | ||
|
|
bc95b40004 | ||
|
|
69edc99956 | ||
|
|
9062996025 | ||
|
|
271a3869c5 | ||
|
|
e64dd0f8ba | ||
|
|
9f0de8199f | ||
|
|
5f3cb583ab | ||
|
|
d3996e1eec | ||
|
|
b7a3221c07 | ||
|
|
3455ebe8b3 | ||
|
|
c6679b18eb | ||
|
|
667e07f432 | ||
|
|
9a4483ea83 | ||
|
|
1e844f67c3 | ||
|
|
3080b41a4e | ||
|
|
38f135fa42 | ||
|
|
30880c3af7 | ||
|
|
02ef84fca3 | ||
|
|
762604f6af | ||
|
|
5cd6402065 | ||
|
|
726a64f853 | ||
|
|
da049e2a4c | ||
|
|
3f230d6c73 | ||
|
|
b9b5dfa80d | ||
|
|
4c0b2e1b4a | ||
|
|
1ee743332f | ||
|
|
a8b01016b2 | ||
|
|
b15d185aec | ||
|
|
3523732baa | ||
|
|
fb3b6a13a7 | ||
|
|
16933aa7cc | ||
|
|
7acc9db6da | ||
|
|
e1cf65abb4 | ||
|
|
2d3fbbf09f | ||
|
|
87de42f0c7 | ||
|
|
51c603ef9d | ||
|
|
eab4f77af2 | ||
|
|
69857dfdbb | ||
|
|
db71a58af0 | ||
|
|
a76b68d722 | ||
|
|
1814f99023 | ||
|
|
7cd560571d | ||
|
|
68cff62b56 | ||
|
|
1625f92eaf | ||
|
|
bd67c53dd6 | ||
|
|
b6467ae035 | ||
|
|
876beb67b4 | ||
|
|
35b2ef31a0 | ||
|
|
5744603751 | ||
|
|
c2579039f2 | ||
|
|
240ceb6cb7 | ||
|
|
e362655f20 | ||
|
|
790bbf8aaf | ||
|
|
22086c534b | ||
|
|
646bb02662 | ||
|
|
d0922265d5 | ||
|
|
03f606a2c0 | ||
|
|
616c0c4e87 | ||
|
|
6efba5e5ef | ||
|
|
87e3dc53bc | ||
|
|
a399672a4a | ||
|
|
333a37349b | ||
|
|
eff9a0ba25 | ||
|
|
0c6bf35f22 | ||
|
|
b3d3569719 | ||
|
|
44af27f400 | ||
|
|
b0b08fa9d2 | ||
|
|
b82cf843e4 | ||
|
|
db42b46218 | ||
|
|
233bf01a4b | ||
|
|
1a80c283f5 | ||
|
|
52db21955e | ||
|
|
fb78106dc6 | ||
|
|
cc2466b1f8 | ||
|
|
a331244923 | ||
|
|
06d42778fa | ||
|
|
232667d2c6 | ||
|
|
815c117088 | ||
|
|
e171bca9e7 | ||
|
|
704dac7a7f | ||
|
|
34dda10f9e | ||
|
|
bd35f64cf7 | ||
|
|
5ab508f652 | ||
|
|
c24fb71a10 | ||
|
|
2e59a14213 | ||
|
|
45f9c7aa4a | ||
|
|
f946dbd876 | ||
|
|
2b88d967c1 | ||
|
|
68ef5c51e2 | ||
|
|
c9b8f0a211 | ||
|
|
445fddeb28 | ||
|
|
ffbcd9291c | ||
|
|
24fb2fe522 | ||
|
|
bd2150aaaf | ||
|
|
8ff3b3d186 | ||
|
|
1a23a5a6e9 | ||
|
|
cef216bc55 | ||
|
|
3e1a3934c9 | ||
|
|
f600eb8550 | ||
|
|
6067f4d5d7 | ||
|
|
aa9434e002 | ||
|
|
b9cafa3ffa | ||
|
|
4ba6852aea | ||
|
|
5373966e7d | ||
|
|
bc7ea4a8d9 | ||
|
|
7415f395b2 | ||
|
|
7879d2e3bc | ||
|
|
001f9e024a | ||
|
|
dba0fcf7e6 | ||
|
|
b4ebb527a1 | ||
|
|
8d07de9708 | ||
|
|
82eb2a635a | ||
|
|
ccbc101dd8 | ||
|
|
df73ff2a7a | ||
|
|
79b6504d63 | ||
|
|
d39dd0e576 | ||
|
|
ec955496ed | ||
|
|
a2b868833c | ||
|
|
3dc73f6d04 | ||
|
|
5de1c76f6c | ||
|
|
62969122f1 | ||
|
|
e5e8d9c38d | ||
|
|
b37b438c4f | ||
|
|
e88bc9b35b | ||
|
|
5d27ee7856 | ||
|
|
b9c25acf64 | ||
|
|
d39a598e0b | ||
|
|
841df9b6e6 | ||
|
|
0a62d025cf | ||
|
|
3517c734e5 | ||
|
|
1a1b4a1613 | ||
|
|
70da8e43a6 | ||
|
|
43e703bda1 | ||
|
|
2617bbded6 | ||
|
|
ff6964b6a5 | ||
|
|
816fe6d161 | ||
|
|
3e265ea263 | ||
|
|
3a105a900b | ||
|
|
10658a049b | ||
|
|
b6af863e52 | ||
|
|
c5e82ba49f | ||
|
|
0d217e9b09 | ||
|
|
49d3fab984 | ||
|
|
f0964d2d08 | ||
|
|
0ce391e924 | ||
|
|
6cb5050994 | ||
|
|
56ca3eeef2 | ||
|
|
5a7899dca0 | ||
|
|
1242977d11 | ||
|
|
8a60c695bf | ||
|
|
e64ca1ff38 | ||
|
|
defcf160d8 | ||
|
|
b19e2f7dab | ||
|
|
52f8076dcf | ||
|
|
d6e17abb13 | ||
|
|
08369730b6 | ||
|
|
5b2fe2c19c | ||
|
|
22e5dfdc05 | ||
|
|
f4e67863d9 | ||
|
|
033d295ef9 | ||
|
|
56f801f69d | ||
|
|
2fc32b368c | ||
|
|
9b9bb7b5fa | ||
|
|
d9b4ada06b | ||
|
|
1c4d378d03 | ||
|
|
c7278c448e | ||
|
|
a440bcdb41 | ||
|
|
1537833ae0 | ||
|
|
958fef20d9 | ||
|
|
99285cd5c3 | ||
|
|
8c6582679b | ||
|
|
c24adef38f | ||
|
|
0e1eeec8b4 | ||
|
|
b23b2f034e | ||
|
|
5b550e5c49 | ||
|
|
b98e4f2b12 | ||
|
|
df92b295d6 | ||
|
|
4916ce4d02 | ||
|
|
4a8172325e | ||
|
|
c069f208b2 | ||
|
|
42e51a191e | ||
|
|
d0a84a451c | ||
|
|
eeeba484cf | ||
|
|
d528c9d94d | ||
|
|
3d1a2e5202 | ||
|
|
b8c92b8071 | ||
|
|
aad0fbc7ed | ||
|
|
27b5bcc438 | ||
|
|
44c4de41cc | ||
|
|
1ef814b0be | ||
|
|
9fd8aa80b5 | ||
|
|
b4aff2340d | ||
|
|
653794436b | ||
|
|
b71af75f1a | ||
|
|
c2a29ce574 | ||
|
|
d61b000d73 | ||
|
|
02aa8fd718 | ||
|
|
7f8105a905 | ||
|
|
0023cd85f0 | ||
|
|
6ce6a1a77b | ||
|
|
e6a2ea15a2 | ||
|
|
2b8acfa2ca | ||
|
|
25f26127b9 | ||
|
|
230262cf93 | ||
|
|
73f529e3c3 | ||
|
|
73388624ca | ||
|
|
7fc1d61b4f | ||
|
|
1ef205da28 | ||
|
|
b25b911ba1 | ||
|
|
be2e6b4970 | ||
|
|
326ff3b296 | ||
|
|
324ee19377 | ||
|
|
2855c2c24e | ||
|
|
cd6cdb275e | ||
|
|
bc4468ddcd | ||
|
|
2ba7fd280e | ||
|
|
0fd854758c | ||
|
|
b4048ebb59 | ||
|
|
d152a3c75a | ||
|
|
4dfedf4f71 | ||
|
|
bd2dc6564d | ||
|
|
4d35ef51e1 | ||
|
|
2472bcab98 | ||
|
|
27fc37ce50 | ||
|
|
91901e38b8 | ||
|
|
e47c2f8c25 | ||
|
|
bee498792c | ||
|
|
bb0ebf642b | ||
|
|
667636d098 | ||
|
|
eeaad206e8 | ||
|
|
c0f80e65f9 | ||
|
|
06d9f72f97 | ||
|
|
de98c40271 | ||
|
|
9bae00b698 | ||
|
|
f925e83b95 | ||
|
|
bb733a88f1 | ||
|
|
b61077697d | ||
|
|
0322b8782e | ||
|
|
82355a741d | ||
|
|
97b8c68dd3 | ||
|
|
b1d6135dca | ||
|
|
27412551a5 | ||
|
|
df3fd8fe28 | ||
|
|
42343d9194 | ||
|
|
eb32358873 | ||
|
|
e2b671d2f5 | ||
|
|
ee9b0e0390 | ||
|
|
941e94fb98 | ||
|
|
084df38c31 | ||
|
|
ce46e436fd | ||
|
|
89dc9bea2e | ||
|
|
0ace4a731c | ||
|
|
df16ed8e96 | ||
|
|
e99d8976f5 | ||
|
|
13b44b7897 | ||
|
|
a575ad20f0 | ||
|
|
115f2cf210 | ||
|
|
116c8619d5 | ||
|
|
45d4bec43f | ||
|
|
a30e96b553 | ||
|
|
a44a1b3c34 | ||
|
|
0f70de7c0f | ||
|
|
49b5dae073 | ||
|
|
6a800e8085 | ||
|
|
81b401a83d | ||
|
|
bec3657a81 | ||
|
|
45566e126e | ||
|
|
bc0eebfb3e | ||
|
|
4a2289d1ca | ||
|
|
1c1e4faab9 | ||
|
|
7ce7b4878b | ||
|
|
cbb2b46de2 | ||
|
|
1136d26353 | ||
|
|
c8418332de | ||
|
|
79e084542c | ||
|
|
2f38ac7c55 | ||
|
|
2925d2c30e | ||
|
|
b2791a7b62 | ||
|
|
c3ecbcc8d8 | ||
|
|
f751aba86f | ||
|
|
ee13ae744a | ||
|
|
0f3b2b4a92 | ||
|
|
f38e057397 | ||
|
|
89b9e2afa5 | ||
|
|
10f45c321e | ||
|
|
eb1b723ea7 | ||
|
|
d9eb6d74f8 | ||
|
|
6c2f9ea939 | ||
|
|
5f7e2354b5 | ||
|
|
f4f448db0a | ||
|
|
52b357089d | ||
|
|
20e41c85eb | ||
|
|
edac767fd9 | ||
|
|
6f83885de3 | ||
|
|
e1143de850 | ||
|
|
fa2dc6c02e | ||
|
|
2f6a7f752e | ||
|
|
dea45c84e7 | ||
|
|
f9b3f88357 | ||
|
|
61e5fc1294 | ||
|
|
846a3d942f | ||
|
|
bd3032a724 | ||
|
|
a1915882c9 | ||
|
|
5a93241919 | ||
|
|
ae56f211af | ||
|
|
5e8d8fa497 | ||
|
|
da5ede794f | ||
|
|
a15fcafb8a | ||
|
|
f27e3495bd | ||
|
|
5847fbf393 | ||
|
|
a328cab53a | ||
|
|
854629f271 | ||
|
|
a8c8a43424 | ||
|
|
d006d5c52e | ||
|
|
f3ae976aa2 | ||
|
|
d35222446d | ||
|
|
b932aff8b9 | ||
|
|
c6a89b315b | ||
|
|
395e7065ac | ||
|
|
59a1f00bba | ||
|
|
ad673ce0b0 | ||
|
|
3c290f433b | ||
|
|
32d15f5546 | ||
|
|
f3c6b20728 | ||
|
|
f2d34f5405 | ||
|
|
4952f619d8 | ||
|
|
7bceca7a29 | ||
|
|
106e3487a0 | ||
|
|
6d3396c013 | ||
|
|
9c747d767a | ||
|
|
387af88ea5 | ||
|
|
dd8b32aaad | ||
|
|
957401d447 | ||
|
|
24c10cea39 | ||
|
|
de9d1793d6 | ||
|
|
5658b91d98 | ||
|
|
d6afc5b8db | ||
|
|
15243b5d43 | ||
|
|
be89a5498f | ||
|
|
2ffa44824f | ||
|
|
642b2d877d | ||
|
|
44d2d2b1dd | ||
|
|
2256a546e3 | ||
|
|
a378767987 | ||
|
|
4b3c3123fc | ||
|
|
0ab22904f5 | ||
|
|
a5d7d1820f | ||
|
|
40b55c4a10 | ||
|
|
14e557a9d1 | ||
|
|
f6707b2317 | ||
|
|
4eadc8f492 | ||
|
|
eeecde2270 | ||
|
|
179b167ccf | ||
|
|
505a093ba7 | ||
|
|
601d16c5ef | ||
|
|
d7d1519be9 | ||
|
|
ce446aadd9 | ||
|
|
6944bb8040 | ||
|
|
65ab27bb55 | ||
|
|
039cf71a38 | ||
|
|
1e07206ed2 | ||
|
|
f35bf31a1a | ||
|
|
4bc7c9710e | ||
|
|
52994f9cad | ||
|
|
e692602042 | ||
|
|
7ce3bd7d51 | ||
|
|
c43cc285dc | ||
|
|
0232759b68 | ||
|
|
37332bb9bd | ||
|
|
278ad29f39 | ||
|
|
80ebc8856c | ||
|
|
e8656a7594 | ||
|
|
da80ed13bf | ||
|
|
9e6bfdc006 | ||
|
|
d226997187 | ||
|
|
a90f8402ba | ||
|
|
12675fd1d4 | ||
|
|
a216e1b799 | ||
|
|
d84c2b7097 | ||
|
|
8928efc40a | ||
|
|
20b42aaf88 | ||
|
|
dcb899dc90 | ||
|
|
31a0416368 | ||
|
|
d37fe551a1 | ||
|
|
c225c8dff1 | ||
|
|
ab458e7143 | ||
|
|
099ed97277 | ||
|
|
73948e2a65 | ||
|
|
57595bc039 | ||
|
|
b3eaadb5d3 | ||
|
|
dc93911566 | ||
|
|
45d45c30af | ||
|
|
b176784f31 | ||
|
|
f9591c9790 | ||
|
|
af50cde104 | ||
|
|
998bd7cd4c | ||
|
|
c073aa7746 | ||
|
|
cc9d3f063c | ||
|
|
ce5bb337aa | ||
|
|
5c24e42e46 | ||
|
|
f0bd28b438 | ||
|
|
577c651871 | ||
|
|
3d7ecfb8d8 | ||
|
|
b991f20c3a | ||
|
|
3d099e7b1d | ||
|
|
db86fa981b | ||
|
|
74744ac337 | ||
|
|
6bff809c13 | ||
|
|
adfd2c8bc8 | ||
|
|
e90876fbd8 | ||
|
|
7a6dd3f38a | ||
|
|
f38e07d4be | ||
|
|
7dd6cd7224 | ||
|
|
54381a2bf7 | ||
|
|
2be7054525 | ||
|
|
01cb5e0d8d | ||
|
|
81afe922d7 | ||
|
|
8e26d12c33 | ||
|
|
c247cae9bf | ||
|
|
b0ffe98011 | ||
|
|
45dd5f168d | ||
|
|
30a9941b61 | ||
|
|
6da79f2e32 | ||
|
|
0aed416dd6 | ||
|
|
ab4e9deabc | ||
|
|
2f0de8bdbe | ||
|
|
cd4e6c62cf | ||
|
|
075ec3afdb | ||
|
|
0b29664545 | ||
|
|
04802e0397 | ||
|
|
d92f165923 | ||
|
|
72666d0b4a | ||
|
|
038e1a78f0 | ||
|
|
e3811cf744 | ||
|
|
e64238dba3 | ||
|
|
bf69273701 | ||
|
|
3c8bcf55e0 | ||
|
|
cb96f08442 | ||
|
|
ddb7597629 | ||
|
|
c43d48163f | ||
|
|
8318d9af67 | ||
|
|
9b1e9bc336 | ||
|
|
f353023b01 | ||
|
|
ea3beb6f45 | ||
|
|
3205e652ee |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ _site
|
||||
test/scratch/scratch.html
|
||||
.DS_Store
|
||||
.vscode
|
||||
/coverage
|
||||
|
||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -1,6 +1,108 @@
|
||||
# Changelog
|
||||
|
||||
## [1.9.11] - 2024-04-15
|
||||
## [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)
|
||||
method for better Web Components support
|
||||
* [Added](https://github.com/bigskysoftware/htmx/commit/83a1449a89b1fcd1c1655039ede02d74d61e4800) `pushURL` option to the `htmx.ajax()` API
|
||||
* [Fixed](https://github.com/bigskysoftware/htmx/commit/cd045c3e0eb31776a80e3a4b4c74e37d0631c1f1) `hx-sync` and `htmx:abort` within the Shadow Dom [Issue 3419](https://github.com/bigskysoftware/htmx/issues/3419)
|
||||
* [Fixed](https://github.com/bigskysoftware/htmx/commit/04d6c7249b7fd7b8518ddca92e7a70fdcc651b34) a long-standing bug in history support with respect to relative paths [Issue 3449](https://github.com/bigskysoftware/htmx/issues/3449)
|
||||
* Once again, this is a release mainly done by @MichaelWest22's heroic work, thank you!
|
||||
|
||||
## [2.0.7] - 2025-09-08
|
||||
|
||||
* Fix not preventing link when inside htmx enabled element (fixes https://github.com/bigskysoftware/htmx/issues/3395)
|
||||
* Implement `reportValidity()` for reporting proper form validation errors behind config flag (fixes https://github.com/bigskysoftware/htmx/issues/2372)
|
||||
* Update indicator style to have `visibility:hidden` for screen readers (fixes https://github.com/bigskysoftware/htmx/issues/3354)
|
||||
* Bugfix: swap="outerHTML" on <div> with style attribute leaves htmx-swapping class behind (see https://github.com/bigskysoftware/htmx/pull/3341)
|
||||
|
||||
## [2.0.6] - 2025-06-27
|
||||
|
||||
* [Fix](https://github.com/bigskysoftware/htmx/pull/3357) a [regression](https://github.com/bigskysoftware/htmx/issues/3356)
|
||||
with htmx-powered links that contain other elements in them issuing full page refreshes
|
||||
|
||||
## [2.0.5] - 2025-06-20
|
||||
|
||||
* 100% test coverage! (Thank you @MichaelWest22!)
|
||||
* The default recommended CDN is now jsDelivr
|
||||
* The `inherit` keyword is now supported by `hx-include`, `hx-indicator` and `hx-disabled-elt` to allow you to inherit
|
||||
the value from a parent and extend it.
|
||||
* `hx-on` listeners are now added before processing nodes so events during processing can be captured
|
||||
* Using `<button hx-verb="/endpoint" type="reset">` will now reset the associated form (after submitting to `/endpoint`)
|
||||
* Using `<button formmethod="dialog">` will no longer submit its associated form
|
||||
* Local history cache now uses `sessionStorage` rather than `localStorage` so cross-tab contamination doesn't occur
|
||||
* History restoration now follows the standard swapping code paths
|
||||
* Many other smaller bug and documentation fixes
|
||||
|
||||
## [2.0.4] - 2024-12-13
|
||||
|
||||
* Calling `htmx.ajax` with no target or source now defaults to body (previously did nothing)
|
||||
* Nested [shadow root](https://github.com/bigskysoftware/htmx/commit/5ab508f6523a37890932176f7dc54be9f7a281ff) fix
|
||||
* The `htmx:trigger` event now properly fires on the synthetic `load` event
|
||||
* The synthetic `load` event will not be re-called when an element is reinitialized via `htmx.process()`
|
||||
* Boosted `<form>` tags that issue a `GET` with no or empty `action` attributes will properly replace all existing query
|
||||
parameters
|
||||
* Events that are triggered on htmx-powered elements located outside a form, but that refer to a form via the`form`
|
||||
attribute, now properly cancel the submission of the referred-to form
|
||||
* Extension Updates
|
||||
* `preload` extension was
|
||||
[completely reworked](https://github.com/bigskysoftware/htmx-extensions/commit/fb68dfb48063505d2d7420d717c24ac9a8dae244)
|
||||
by @marisst to be compatible with `hx-boost`, not cause side effect, etc. Thank you!
|
||||
* `response-targets` was updated to not use deprecated methods
|
||||
* A [small fix](https://github.com/bigskysoftware/htmx-extensions/commit/e87e1c3d0bf728b4e43861c7459f3f937883eb99) to
|
||||
`ws` to avoid an error when closing in some cases
|
||||
* The `head-support` extension was updated to work with the `sse` extension
|
||||
|
||||
## [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.
|
||||
* 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`
|
||||
* Improved shadowRoot support
|
||||
* Many smaller bug fixes
|
||||
* Moved the core extension documentation back to <https://htmx.org/extensions>
|
||||
|
||||
## [2.0.2] - 2024-08-12
|
||||
* no longer boost forms of type `dialog`
|
||||
* properly trigger the `htmx:trigger` event when an event is delayed or throttled
|
||||
* file upload is now fixed
|
||||
* empty templates that are not used for oob swaps are no longer removed from the DOM
|
||||
* request indicators are not removed when a full page redirect or refresh occurs
|
||||
* elements that have been disabled for a request are properly re-enabled before snapshotting for history
|
||||
* you can now trigger events on other elements using the `HX-Trigger` response header
|
||||
* The `.d.ts` file should now work properly
|
||||
|
||||
## [2.0.1] - 2024-07-12
|
||||
|
||||
* Make the `/dist/htmx.esm.js` file the `main` file in `package.json` to make installing htmx smoother
|
||||
* Update `htmx.d.ts` & include it in the distribution
|
||||
* A fix to avoid removing text-only templates on htmx cleanup
|
||||
* A fix for outerHTML swapping of the `body` tag
|
||||
* Many docs fixes
|
||||
|
||||
## [2.0.0] - 2024-06-17
|
||||
|
||||
* 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/)
|
||||
* 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:
|
||||
* ESM Modules: `/dist/htmx.esm.js`
|
||||
* AMD Modules: `/dist/htmx.amd.js`
|
||||
* CJS Modules: `/dist/htmx.cjs.js`
|
||||
* The `/dist/htmx.js` file continues to be browser-loadable
|
||||
* The `hx-on` attribute, with its special syntax, has been removed in favor of the less-hacky `hx-on:` syntax.
|
||||
* See the [Upgrade Guide](https://htmx.org/migration-guide-htmx-1/) for more details on upgrade steps
|
||||
* The `selectAndSwap()` internal API method was replaced with the public (and much better) [`swap()`](/api/#swap) method
|
||||
|
||||
## [1.9.12] - 2024-04-17
|
||||
|
||||
* [IE Fixes](https://github.com/bigskysoftware/htmx/commit/e64238dba3113c2eabe26b1e9e9ba7fe29ba3010)
|
||||
|
||||
## [1.9.11] - 2024-03-15
|
||||
|
||||
* Fix for new issue w/ web sockets & SSE on iOS 17.4 (thanks apple!)
|
||||
* Fix for double script execution issue when using template parsing
|
||||
|
||||
@@ -3,20 +3,43 @@ Thank you for your interest in contributing! Because we're a small team, we have
|
||||
|
||||
## Issues
|
||||
1. Issues are the best place to propose a new feature. Keep in mind that htmx is a small library, so there are lots of great ideas that don't fit in the core; it's always best to check in about an idea before doing a bunch of work on it.
|
||||
1. When proposing a new features, we will often suggest that you implement it as an [extension](https://htmx.org/extensions), so try that first. Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
|
||||
1. When proposing a new feature, we will often suggest that you implement it as an [extension](https://github.com/bigskysoftware/htmx-extensions), so try that first. Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
|
||||
1. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
|
||||
1. We don't have an issue template yet, but the more detailed your description of the issue, the more quickly we'll be able to evaluate it.
|
||||
1. See an issue that you also have? Give it a reaction (and comment, if you have something to add). We note that!
|
||||
1. If you haven't gotten any traction on an issue, feel free to bump it in the #issues-and-pull-requests channel on our Discord.
|
||||
1. Want to contribute but don't know where to start? Look for issues with the "help wanted" tag.
|
||||
|
||||
## Creating a Development Environment
|
||||
### Pre-requisites
|
||||
To create a development environment for htmx, you'll need the following tools on your system:
|
||||
|
||||
- Node.js 20.x or later
|
||||
- Chrome or Chromium
|
||||
|
||||
Additionally, the environment variable `CHROME_PATH` must contain the full path to the Chrome or Chromium binary on your system.
|
||||
|
||||
### Installing Packages
|
||||
To install htmx's required packages, run the following command:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Running Automated Tests
|
||||
To verify that your htmx environment is working correctly, you can run htmx's automated tests with the following command:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Pull Requests
|
||||
### Technical Requirements
|
||||
1. Code, including tests, must be written in ES5 for [IE 11 compatibility](https://stackoverflow.com/questions/39902809/support-for-es6-in-internet-explorer-11).
|
||||
1. Please lint all proposed changes with the `npm run format` command
|
||||
1. All PRs must be made against the `dev` branch, except documentation PRs (that only modify the `www/` directory) which can be made against `master`.
|
||||
1. Please avoid sending the `dist` files along your PR, only include the `src` ones.
|
||||
1. Please include test cases in [`/test`](https://github.com/bigskysoftware/htmx/tree/dev/test) and docs in [`/www`](https://github.com/bigskysoftware/htmx/tree/dev/www).
|
||||
1. We squash all PRs, so you're welcome to submit with as many commits are you like; they will be evaluated as a single, standalone change.
|
||||
1. We squash all PRs, so you're welcome to submit with as many commits as you like; they will be evaluated as a single, standalone change.
|
||||
|
||||
### Review Guidelines
|
||||
1. Open PRs represent issues that we're actively thinking working on merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
|
||||
|
||||
15
README.md
15
README.md
@@ -10,15 +10,14 @@
|
||||
## introduction
|
||||
|
||||
htmx allows you to access [AJAX](https://htmx.org/docs#ajax), [CSS Transitions](https://htmx.org/docs#css_transitions),
|
||||
[WebSockets](https://htmx.org/docs#websockets) and [Server Sent Events](https://htmx.org/docs#sse)
|
||||
[WebSockets](https://htmx.org/extensions/ws/) and [Server Sent Events](https://htmx.org/extensions/sse/)
|
||||
directly in HTML, using [attributes](https://htmx.org/reference#attributes), so you can build
|
||||
[modern user interfaces](https://htmx.org/examples) with the [simplicity](https://en.wikipedia.org/wiki/HATEOAS) and
|
||||
[power](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) of hypertext
|
||||
|
||||
htmx is small ([~14k min.gz'd](https://unpkg.com/htmx.org/dist/)),
|
||||
[dependency-free](https://github.com/bigskysoftware/htmx/blob/master/package.json),
|
||||
[extendable](https://htmx.org/extensions) &
|
||||
IE11 compatible
|
||||
htmx is small ([~14k min.gz'd](https://cdn.jsdelivr.net/npm/htmx.org/dist/)),
|
||||
[dependency-free](https://github.com/bigskysoftware/htmx/blob/master/package.json) &
|
||||
[extendable](https://htmx.org/extensions)
|
||||
|
||||
## motivation
|
||||
|
||||
@@ -33,7 +32,7 @@ By removing these arbitrary constraints htmx completes HTML as a
|
||||
## quick start
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
<!-- have a button POST a click via AJAX -->
|
||||
<button hx-post="/clicked" hx-swap="outerHTML">
|
||||
Click Me
|
||||
@@ -70,8 +69,6 @@ No time? Then [become a sponsor](https://github.com/sponsors/bigskysoftware#spon
|
||||
|
||||
To develop htmx locally, you will need to install the development dependencies.
|
||||
|
||||
__Requires Node 15.__
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
@@ -102,8 +99,6 @@ At this point you can modify `/src/htmx.js` to add features, and then add tests
|
||||
htmx uses the [mocha](https://mochajs.org/) testing framework, the [chai](https://www.chaijs.com/) assertion framework
|
||||
and [sinon](https://sinonjs.org/releases/v9/fake-xhr-and-server/) to mock out AJAX requests. They are all OK.
|
||||
|
||||
You can also run live tests and demo of the WebSockets and Server-Side Events extensions with `npm run ws-tests`
|
||||
|
||||
## haiku
|
||||
|
||||
*javascript fatigue:<br/>
|
||||
|
||||
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x | :white_check_mark: |
|
||||
| 1.9.x | :white_check_mark: |
|
||||
| < 1.9 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you think you've found a vulnerability, please use the _Report a vulnerability_ button found in the [security tab](https://github.com/bigskysoftware/htmx/security) of the project on Github.
|
||||
|
||||
This process is documented in GitHub's _Secure Coding_ guide: [Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).
|
||||
75
TESTING.md
Normal file
75
TESTING.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# HTMX Testing Guide
|
||||
|
||||
This guide outlines how to test htmx, focusing on running tests headlessly or in a browser environment, running individual tests, and other testing concerns.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Ensure you have a currently supported Node.js and npm installed.
|
||||
2. Install dependencies by running:
|
||||
```bash
|
||||
npm install
|
||||
npm run test
|
||||
```
|
||||
During test runs it will auto install playwright
|
||||
|
||||
## Running All Tests
|
||||
|
||||
To run all tests in headless mode, execute:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
This will run all the tests using headless Chrome.
|
||||
|
||||
To run all tests against all browsers in headless mode, execute:
|
||||
```bash
|
||||
npm run test:all
|
||||
```
|
||||
This will run the tests using Playwright’s headless browser setup across Chrome, Firefox, and WebKit (Safari-adjacent).
|
||||
|
||||
To run all tests against a specific browser, execute:
|
||||
```bash
|
||||
npm run test:chrome
|
||||
npm run test:firefox
|
||||
npm run test:webkit
|
||||
```
|
||||
|
||||
## Running Individual Tests
|
||||
|
||||
### Headless Mode
|
||||
To run a specific test file headlessly, for example `test/core/ajax.js`, use the following command:
|
||||
```bash
|
||||
npm test test/core/ajax.js
|
||||
```
|
||||
If you want to run only one specific test, you can temporarily change `it("...` to `it.only("...` in the test file, and then specify the test file as above. Don't forget to undo this before you commit! You will get eslint warnings now to let you know when you have temporary `.only` in place to help avoid commiting these.
|
||||
|
||||
### Browser Mode
|
||||
To run tests directly in the browser, simply `open test/index.html` in a browser.
|
||||
On Ubuntu you can run:
|
||||
|
||||
```bash
|
||||
xdg-open test/index.html
|
||||
```
|
||||
This runs all the tests in the browser using Mocha instead of web-test-runner for easier and faster debugging.
|
||||
|
||||
From the Mocha browser view you can rerun a just a single test file by clicking the header name or you can click on the play icon to re-play a single test. This makes it easy to update this test/code and refresh to re-run this single test. The browser console also now logs the names of the running tests so you can check here to find any errors or logs produced during each test execution. Adding debugger statements in your code or breakpoints in the browser lets you step though the test execution.
|
||||
|
||||
If you really want to open web-test-runner in headed mode, you can run:
|
||||
```bash
|
||||
npm run test:debug
|
||||
```
|
||||
This will start the server, and open the test runner in a browser. From there you can choose a test file to run. Note that all test logs will show up only in dev tools console unlike Mocha.
|
||||
|
||||
## Code Coverage Report
|
||||
Lines of code coverage reporting will only work when running the default chrome headless testing
|
||||
|
||||
After a test run completes, you can open `coverage/lcov-report/index.html` to view the code coverage report. On Ubuntu you can run:
|
||||
```bash
|
||||
xdg-open coverage/lcov-report/index.html
|
||||
```
|
||||
|
||||
## Test Locations
|
||||
- All tests are located in the `test/attribues` and `test/core` directories. Only .js files in these directory will be discovered by the test runner.
|
||||
- The `web-test-runner.config.mjs` file in the root directory contains the boilerplate HTML for the test runs, including `<script>` tags for the test dependencies.
|
||||
|
||||
### Local CI prediction
|
||||
You can run `npm run test:ci` to locally simulate the result of the CI run. This is useful to run before pushing to GitHub to avoid fixup commits and CI reruns.
|
||||
2
dist/ext/README.md
vendored
2
dist/ext/README.md
vendored
@@ -4,6 +4,6 @@ These are legacy extensions for htmx 1.x and are **NOT** actively maintained or
|
||||
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://extensions.htmx.org),
|
||||
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!)
|
||||
|
||||
6
dist/ext/ajax-header.js
vendored
6
dist/ext/ajax-header.js
vendored
@@ -1,7 +1,11 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
6
dist/ext/alpine-morph.js
vendored
6
dist/ext/alpine-morph.js
vendored
@@ -1,3 +1,7 @@
|
||||
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';
|
||||
@@ -13,4 +17,4 @@ htmx.defineExtension('alpine-morph', {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
7
dist/ext/class-tools.js
vendored
7
dist/ext/class-tools.js
vendored
@@ -1,5 +1,10 @@
|
||||
(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+/);
|
||||
}
|
||||
@@ -89,4 +94,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
})();
|
||||
|
||||
4
dist/ext/client-side-templates.js
vendored
4
dist/ext/client-side-templates.js
vendored
@@ -1,3 +1,7 @@
|
||||
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) {
|
||||
|
||||
|
||||
4
dist/ext/debug.js
vendored
4
dist/ext/debug.js
vendored
@@ -1,3 +1,7 @@
|
||||
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) {
|
||||
|
||||
8
dist/ext/disable-element.js
vendored
8
dist/ext/disable-element.js
vendored
@@ -1,5 +1,7 @@
|
||||
"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")
|
||||
}
|
||||
// Disable Submit Button
|
||||
htmx.defineExtension('disable-element', {
|
||||
onEvent: function (name, evt) {
|
||||
@@ -15,4 +17,4 @@ htmx.defineExtension('disable-element', {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
4
dist/ext/event-header.js
vendored
4
dist/ext/event-header.js
vendored
@@ -1,4 +1,8 @@
|
||||
(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) {
|
||||
|
||||
7
dist/ext/head-support.js
vendored
7
dist/ext/head-support.js
vendored
@@ -5,6 +5,11 @@
|
||||
//==========================================================
|
||||
(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() {
|
||||
@@ -138,4 +143,4 @@
|
||||
}
|
||||
});
|
||||
|
||||
})()
|
||||
})()
|
||||
|
||||
4
dist/ext/include-vals.js
vendored
4
dist/ext/include-vals.js
vendored
@@ -1,4 +1,8 @@
|
||||
(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) {
|
||||
|
||||
8
dist/ext/json-enc.js
vendored
8
dist/ext/json-enc.js
vendored
@@ -1,12 +1,16 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
6
dist/ext/loading-states.js
vendored
6
dist/ext/loading-states.js
vendored
@@ -1,4 +1,10 @@
|
||||
;(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) {
|
||||
|
||||
4
dist/ext/method-override.js
vendored
4
dist/ext/method-override.js
vendored
@@ -1,3 +1,7 @@
|
||||
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") {
|
||||
|
||||
4
dist/ext/morphdom-swap.js
vendored
4
dist/ext/morphdom-swap.js
vendored
@@ -1,3 +1,7 @@
|
||||
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';
|
||||
|
||||
7
dist/ext/multi-swap.js
vendored
7
dist/ext/multi-swap.js
vendored
@@ -1,5 +1,10 @@
|
||||
(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;
|
||||
|
||||
@@ -42,4 +47,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
})();
|
||||
|
||||
17
dist/ext/path-deps.js
vendored
17
dist/ext/path-deps.js
vendored
@@ -1,9 +1,12 @@
|
||||
(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;
|
||||
@@ -30,8 +33,8 @@
|
||||
if (dependsOn(elt.getAttribute('path-deps'), path)) {
|
||||
htmx.trigger(elt, "path-deps");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
htmx.defineExtension('path-deps', {
|
||||
onEvent: function (name, evt) {
|
||||
@@ -41,7 +44,7 @@
|
||||
if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
|
||||
refreshPath(config.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,12 +52,12 @@
|
||||
* ********************
|
||||
* Expose functionality
|
||||
* ********************
|
||||
*/
|
||||
*/
|
||||
|
||||
_root.PathDeps = {
|
||||
refresh: function(path) {
|
||||
refreshPath(path);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}).call(this);
|
||||
|
||||
6
dist/ext/path-params.js
vendored
6
dist/ext/path-params.js
vendored
@@ -1,3 +1,7 @@
|
||||
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") {
|
||||
@@ -8,4 +12,4 @@ htmx.defineExtension('path-params', {
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
24
dist/ext/preload.js
vendored
24
dist/ext/preload.js
vendored
@@ -1,5 +1,9 @@
|
||||
// This adds the "preload" extension to htmx. By default, this will
|
||||
// preload the targets of any tags with `href` or `hx-get` attributes
|
||||
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", {
|
||||
@@ -18,8 +22,8 @@ htmx.defineExtension("preload", {
|
||||
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
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -43,7 +47,7 @@ htmx.defineExtension("preload", {
|
||||
}
|
||||
|
||||
// Special handling for HX-GET - use built-in htmx.ajax function
|
||||
// so that headers match other htmx requests, then set
|
||||
// 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")
|
||||
@@ -57,8 +61,8 @@ htmx.defineExtension("preload", {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, perform a standard xhr request, then set
|
||||
// node.preloadState = TRUE so that requests are not duplicated
|
||||
// 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();
|
||||
@@ -83,16 +87,16 @@ htmx.defineExtension("preload", {
|
||||
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
|
||||
|
||||
4
dist/ext/rails-method.js
vendored
4
dist/ext/rails-method.js
vendored
@@ -1,3 +1,7 @@
|
||||
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") {
|
||||
|
||||
4
dist/ext/remove-me.js
vendored
4
dist/ext/remove-me.js
vendored
@@ -1,4 +1,8 @@
|
||||
(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) {
|
||||
|
||||
9
dist/ext/response-targets.js
vendored
9
dist/ext/response-targets.js
vendored
@@ -1,5 +1,10 @@
|
||||
(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;
|
||||
|
||||
@@ -12,7 +17,7 @@
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} respCode
|
||||
* @param {number} respCodeNumber
|
||||
* @returns {HTMLElement | null}
|
||||
*/
|
||||
function getRespCodeTarget(elt, respCodeNumber) {
|
||||
@@ -58,7 +63,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
6
dist/ext/restored.js
vendored
6
dist/ext/restored.js
vendored
@@ -1,3 +1,7 @@
|
||||
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'){
|
||||
@@ -12,4 +16,4 @@ htmx.defineExtension('restored', {
|
||||
}
|
||||
return;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
39
dist/ext/sse.js
vendored
39
dist/ext/sse.js
vendored
@@ -7,6 +7,11 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
(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;
|
||||
|
||||
@@ -14,8 +19,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
/**
|
||||
* Init saves the provided reference to the internal HTMX API.
|
||||
*
|
||||
* @param {import("../htmx").HtmxInternalApi} api
|
||||
*
|
||||
* @param {import("../htmx").HtmxInternalApi} api
|
||||
* @returns void
|
||||
*/
|
||||
init: function(apiRef) {
|
||||
@@ -30,9 +35,9 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
/**
|
||||
* onEvent handles all events passed to this extension.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
* @returns void
|
||||
*/
|
||||
onEvent: function(name, evt) {
|
||||
@@ -64,8 +69,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
@@ -105,7 +110,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* 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
|
||||
*
|
||||
@@ -183,7 +188,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
@@ -269,8 +274,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @returns boolean
|
||||
*/
|
||||
function maybeCloseSSESource(elt) {
|
||||
@@ -287,9 +292,9 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
/**
|
||||
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*/
|
||||
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||
|
||||
@@ -310,7 +315,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} content
|
||||
* @param {string} content
|
||||
*/
|
||||
function swap(elt, content) {
|
||||
|
||||
@@ -340,10 +345,10 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* doSettle mirrors much of the functionality in htmx that
|
||||
* 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
|
||||
* @param {import("../htmx").HtmxSettleInfo} settleInfo
|
||||
* @returns () => void
|
||||
*/
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
5
dist/ext/ws.js
vendored
5
dist/ext/ws.js
vendored
@@ -6,6 +6,11 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
|
||||
(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;
|
||||
|
||||
|
||||
1432
dist/htmx.amd.js
vendored
1432
dist/htmx.amd.js
vendored
File diff suppressed because it is too large
Load Diff
1432
dist/htmx.cjs.js
vendored
1432
dist/htmx.cjs.js
vendored
File diff suppressed because it is too large
Load Diff
219
dist/htmx.esm.d.ts
vendored
Normal file
219
dist/htmx.esm.d.ts
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
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;
|
||||
}
|
||||
1434
dist/htmx.esm.js
vendored
1434
dist/htmx.esm.js
vendored
File diff suppressed because it is too large
Load Diff
1432
dist/htmx.js
vendored
1432
dist/htmx.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/htmx.min.js
vendored
2
dist/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/htmx.min.js.gz
vendored
BIN
dist/htmx.min.js.gz
vendored
Binary file not shown.
46
editors/jetbrains/htmx.svg
Normal file
46
editors/jetbrains/htmx.svg
Normal 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 |
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
||||
"$schema": "https://json.schemastore.org/web-types",
|
||||
"name": "htmx",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.8",
|
||||
"default-icon": "./htmx.svg",
|
||||
"js-types-syntax": "typescript",
|
||||
"description-markup": "markdown",
|
||||
"contributions": {
|
||||
@@ -9,7 +10,7 @@
|
||||
"attributes": [
|
||||
{
|
||||
"name": "hx-boost",
|
||||
"description": "The **hx-boost** attribute allows you to \"boost\" normal anchors and form tags to use AJAX instead. This has the [nice fallback](https://en.wikipedia.org/wiki/Progressive_enhancement) that, if the user does not have javascript enabled, the site will continue to work.",
|
||||
"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": ""
|
||||
},
|
||||
@@ -17,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"name": "hx-confirm",
|
||||
"description": "The **hx-confirm** attribute allows you to confirm an action before issuing a request. This can be useful in cases where the action is destructive and you want to ensure that the user really wants to do it.",
|
||||
"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": ""
|
||||
},
|
||||
@@ -25,227 +26,280 @@
|
||||
},
|
||||
{
|
||||
"name": "hx-delete",
|
||||
"description": "The **hx-delete** attribute will cause an element to issue a **DELETE** to the specified URL and swap the HTML into the DOM using a swap strategy.",
|
||||
"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": ""
|
||||
"Not Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-confirm/"
|
||||
"doc-url": "https://htmx.org/attributes/hx-delete/"
|
||||
},
|
||||
{
|
||||
"name": "hx-disable",
|
||||
"description": "The **hx-disable** attribute disables htmx processing for the given node and any children nodes",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-disable/"
|
||||
},
|
||||
{
|
||||
"name": "hx-encoding",
|
||||
"description": "The **hx-encoding** attribute changes the request encoding type",
|
||||
"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-encoding"
|
||||
},
|
||||
{
|
||||
"name": "hx-ext",
|
||||
"description": "The **hx-ext** attribute enables extensions for an element",
|
||||
"description-sections": {
|
||||
"Not Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-ext"
|
||||
},
|
||||
{
|
||||
"name": "hx-get",
|
||||
"description": "The **hx-get** attribute issues a `GET` to the specified URL",
|
||||
"description-sections": {
|
||||
"Not Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-get"
|
||||
},
|
||||
{
|
||||
"name": "hx-headers",
|
||||
"description": "The **hx-headers** attribute adds to the headers that will be submitted with the request",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-headers"
|
||||
},
|
||||
{
|
||||
"name": "hx-history-elt",
|
||||
"description": "The **hx-history-elt** attribute specifies the element to snapshot and restore during history navigation",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-history-elt"
|
||||
},
|
||||
{
|
||||
"name": "hx-include",
|
||||
"description": "The **hx-include** attribute specifies additional values/inputs to include in AJAX requests",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-include"
|
||||
},
|
||||
{
|
||||
"name": "hx-indicator",
|
||||
"description": "The **hx-indicator** attribute specifies the element to put the `htmx-request` class on during the AJAX request, displaying it as a request indicator",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-indicator"
|
||||
"doc-url": "https://htmx.org/attributes/hx-disabled-elt/"
|
||||
},
|
||||
{
|
||||
"name": "hx-disinherit",
|
||||
"description": "The **hx-disinherit** attribute allows you to control and disable automatic attribute inheritance for child nodes",
|
||||
"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-disinherit"
|
||||
"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 filter the parameters that will be submitted with a request",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-params/"
|
||||
},
|
||||
{
|
||||
"name": "hx-patch",
|
||||
"description": "The **hx-patch** attribute issues a `PATCH` to the specified URL",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-patch/"
|
||||
},
|
||||
{
|
||||
"name": "hx-post",
|
||||
"description": "The **hx-post** attribute issues a `POST` to the specified URL",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-post/"
|
||||
},
|
||||
{
|
||||
"name": "hx-preserve",
|
||||
"description": "The **hx-preserve** attribute preserves an element between requests (requires the `id` be stable)",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-preserve/"
|
||||
},
|
||||
{
|
||||
"name": "hx-prompt",
|
||||
"description": "The **hx-prompt** attribute shows a prompt before submitting a request",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-prompt/"
|
||||
},
|
||||
{
|
||||
"name": "hx-push-url",
|
||||
"description": "The **hx-push-url** attribute pushes the URL into the location bar, creating a new history entry",
|
||||
"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 browser’s 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": {
|
||||
"Not Inherited": ""
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-push-url"
|
||||
"doc-url": "https://htmx.org/attributes/hx-push-url/"
|
||||
},
|
||||
{
|
||||
"name": "hx-put",
|
||||
"description": "The **hx-put** attribute issues a `PUT` to the specified URL",
|
||||
"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"
|
||||
"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 configures various aspects of the 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-request"
|
||||
"doc-url": "https://htmx.org/attributes/hx-select-oob/"
|
||||
},
|
||||
{
|
||||
"name": "hx-select",
|
||||
"description": "The **hx-select** attribute selects a subset of the server response to process",
|
||||
"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-sse",
|
||||
"description": "The **hx-sse** attribute connects the DOM to a SSE source",
|
||||
"description-sections": {
|
||||
"Not Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-sse"
|
||||
"doc-url": "https://htmx.org/attributes/hx-select/"
|
||||
},
|
||||
{
|
||||
"name": "hx-swap-oob",
|
||||
"description": "The **hx-swap-oob** attribute marks content in a response as being \"Out of Band\", i.e. swapped somewhere other than the target",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-swap-oob/"
|
||||
},
|
||||
{
|
||||
"name": "hx-swap",
|
||||
"description": "The **hx-swap** attribute controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeend')",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-swap/"
|
||||
},
|
||||
{
|
||||
"name": "hx-sync",
|
||||
"description": "The **hx-sync** attribute controls requests made by different elements are synchronized with one another",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-sync/"
|
||||
},
|
||||
{
|
||||
"name": "hx-target",
|
||||
"description": "The **hx-target** attribute specifies the target element to be swapped",
|
||||
"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"
|
||||
"doc-url": "https://htmx.org/attributes/hx-target/"
|
||||
},
|
||||
{
|
||||
"name": "hx-trigger",
|
||||
"description": "The **hx-trigger** attribute specifies specifies the event that triggers the request",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-trigger"
|
||||
},
|
||||
{
|
||||
"name": "hx-vals",
|
||||
"description": "The **hx-vals** attribute specifies values to add to the parameters that will be submitted with the request in JSON form",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-vals"
|
||||
},
|
||||
{
|
||||
"name": "hx-vars",
|
||||
"description": "The **hx-vars** attribute specifies computed values to add to the parameters that will be submitted with the request",
|
||||
"description-sections": {
|
||||
"Inherited": ""
|
||||
},
|
||||
"doc-url": "https://htmx.org/attributes/hx-vars"
|
||||
},
|
||||
{
|
||||
"name": "hx-ws",
|
||||
"description": "The **hx-ws** attribute connects the DOM to a Web Socket source",
|
||||
"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-ws"
|
||||
"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/"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -258,25 +312,284 @@
|
||||
},
|
||||
{
|
||||
"name": "htmx-indicator",
|
||||
"description": "A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present.",
|
||||
"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` while a request is ongoing.",
|
||||
"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`.",
|
||||
"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`.",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
editors/jetbrains/htmx_dark.svg
Normal file
46
editors/jetbrains/htmx_dark.svg
Normal 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 |
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"outFile": "src/htmx.d.ts",
|
||||
"target": "es6",
|
||||
"checkJs": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/htmx.js"
|
||||
],
|
||||
"verbose": true
|
||||
}
|
||||
@@ -4,7 +4,7 @@ publish = "public"
|
||||
command = "zola build"
|
||||
|
||||
[build.environment]
|
||||
ZOLA_VERSION = "0.17.1"
|
||||
ZOLA_VERSION = "0.19.1"
|
||||
|
||||
[context.deploy-preview]
|
||||
command = "zola build --base-url $DEPLOY_PRIME_URL"
|
||||
|
||||
7986
package-lock.json
generated
7986
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -5,7 +5,7 @@
|
||||
"AJAX",
|
||||
"HTML"
|
||||
],
|
||||
"version": "2.0.0-beta1",
|
||||
"version": "2.0.8",
|
||||
"homepage": "https://htmx.org/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/bigskysoftware/htmx/issues"
|
||||
@@ -14,33 +14,45 @@
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/htmx.d.ts",
|
||||
"dist/htmx.esm.d.ts",
|
||||
"dist/*.js",
|
||||
"dist/ext/*.js",
|
||||
"dist/*.js.gz",
|
||||
"editors/jetbrains/htmx.web-types.json"
|
||||
],
|
||||
"main": "dist/htmx.min.js",
|
||||
"types": "dist/htmx.d.ts",
|
||||
"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",
|
||||
"lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/",
|
||||
"lint-fix": "eslint src/htmx.js test/attributes/ test/core/ test/util/ --fix",
|
||||
"format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/",
|
||||
"test": "npm run lint && tsc --project ./jsconfig.json --noEmit true --emitDeclarationOnly false && mocha-chrome test/index.html",
|
||||
"type-declarations": "tsc --project ./jsconfig.json",
|
||||
"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",
|
||||
"www": "bash ./scripts/www.sh"
|
||||
"web-types-generate": "node ./scripts/generate-web-types.mjs",
|
||||
"www": "bash ./scripts/www.sh",
|
||||
"sha": "bash ./scripts/sha.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bigskysoftware/htmx.git"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "standard",
|
||||
"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,
|
||||
@@ -57,6 +69,10 @@
|
||||
"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"
|
||||
@@ -64,19 +80,22 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.10",
|
||||
"chai-dom": "^1.12.0",
|
||||
"eslint": "^8.56.0",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/parse5": "^7.0.0",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"@web/test-runner-playwright": "^0.11.0",
|
||||
"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": "10.1.0",
|
||||
"mocha-chrome": "https://github.com/Telroshan/mocha-chrome",
|
||||
"mocha-webdriver": "^0.3.2",
|
||||
"mocha": "^11.7.4",
|
||||
"mock-socket": "^9.3.1",
|
||||
"sinon": "^9.2.4",
|
||||
"typescript": "^4.9.5",
|
||||
"uglify-js": "^3.17.4",
|
||||
"ws": "^8.14.2"
|
||||
"sinon": "^10.0.1",
|
||||
"typescript": "^5.9.3",
|
||||
"uglify-js": "^3.19.3",
|
||||
"ws": "^8.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
# This script is intended to be run from npm, via `npm run dist`
|
||||
set -euo pipefail
|
||||
|
||||
HTMX_SRC=src/htmx.js
|
||||
HTMX_SRC="src/htmx.js"
|
||||
|
||||
# Clean the dist directory
|
||||
rm -rf dist/*
|
||||
rm -rf dist/*.js dist/*.ts dist/*.gz
|
||||
|
||||
# Regular IIFE script
|
||||
cp $HTMX_SRC dist/htmx.js
|
||||
|
||||
# Minified script
|
||||
# Generate minified script
|
||||
uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js
|
||||
|
||||
# Gzipped script
|
||||
# Generate gzipped script
|
||||
gzip -9 -k -f dist/htmx.min.js > dist/htmx.min.js.gz
|
||||
|
||||
# AMD script
|
||||
# Generate AMD script
|
||||
cat > dist/htmx.amd.js << EOF
|
||||
define(() => {
|
||||
$(cat $HTMX_SRC)
|
||||
@@ -24,14 +24,15 @@ return htmx
|
||||
})
|
||||
EOF
|
||||
|
||||
# CJS script
|
||||
# Generate CJS script
|
||||
cat > dist/htmx.cjs.js << EOF
|
||||
$(cat $HTMX_SRC)
|
||||
module.exports = htmx;
|
||||
EOF
|
||||
|
||||
# ESM script
|
||||
# Generate ESM script
|
||||
cat > dist/htmx.esm.js << EOF
|
||||
$(cat $HTMX_SRC)
|
||||
export { htmx }
|
||||
export default htmx
|
||||
EOF
|
||||
|
||||
|
||||
145
scripts/generate-web-types.mjs
Normal file
145
scripts/generate-web-types.mjs
Normal file
@@ -0,0 +1,145 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
echo "htmx.min.js:"
|
||||
cat dist/htmx.min.js | openssl dgst -sha384 -binary | openssl base64 -A
|
||||
echo ""
|
||||
echo ""
|
||||
echo "htmx.js:"
|
||||
cat dist/htmx.js | openssl dgst -sha384 -binary | openssl base64 -A
|
||||
echo ""
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
htmx.defineExtension('ajax-header', {
|
||||
onEvent: function (name, evt) {
|
||||
if (name === "htmx:configRequest") {
|
||||
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
(function () {
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,96 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
(function(){
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,141 +0,0 @@
|
||||
//==========================================================
|
||||
// head-support.js
|
||||
//
|
||||
// An extension to htmx 1.0 to add head tag merging.
|
||||
//==========================================================
|
||||
(function(){
|
||||
|
||||
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;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
})()
|
||||
@@ -1,24 +0,0 @@
|
||||
(function(){
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
@@ -1,183 +0,0 @@
|
||||
;(function () {
|
||||
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()()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})()
|
||||
@@ -1,11 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
(function () {
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,60 +0,0 @@
|
||||
(function(undefined){
|
||||
'use strict';
|
||||
|
||||
// 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);
|
||||
@@ -1,11 +0,0 @@
|
||||
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);
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
(function(){
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,130 +0,0 @@
|
||||
(function(){
|
||||
|
||||
/** @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} respCode
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
313
src/ext/sse.js
313
src/ext/sse.js
@@ -1,313 +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() {
|
||||
|
||||
/** @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) {
|
||||
|
||||
switch (name) {
|
||||
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// 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(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, 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;
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
|
||||
var sseEventNames = sseSwapAttr.split(",");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
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) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var listener = function(event) {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// Trigger events to be handled by the rest of htmx
|
||||
htmx.trigger(child, sseEventName, event);
|
||||
htmx.trigger(child, "htmx:sseMessage", event);
|
||||
}
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(elt).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName.slice(4), 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);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
})();
|
||||
476
src/ext/ws.js
476
src/ext/ws.js
@@ -1,476 +0,0 @@
|
||||
/*
|
||||
WebSockets Extension
|
||||
============================
|
||||
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
|
||||
/** @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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
193
src/htmx.d.ts
vendored
193
src/htmx.d.ts
vendored
@@ -1,193 +0,0 @@
|
||||
declare namespace htmx {
|
||||
const onLoad: (callback: (elt: Node) => void) => EventListener;
|
||||
const process: (elt: string | Element) => void;
|
||||
const on: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener;
|
||||
const off: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener;
|
||||
const trigger: (elt: string | EventTarget, eventName: string, detail?: any) => boolean;
|
||||
const ajax: (verb: HttpVerb, path: string, context: string | Element | HtmxAjaxHelperContext) => Promise<void>;
|
||||
const find: (eltOrSelector: string | ParentNode, selector?: string) => Element;
|
||||
const findAll: (eltOrSelector: string | ParentNode, selector?: string) => NodeListOf<Element>;
|
||||
const closest: (elt: string | Element, selector: string) => Element;
|
||||
function values(elt: Element, type: HttpVerb): any;
|
||||
const remove: (elt: Node, delay?: number) => void;
|
||||
const addClass: (elt: string | Element, clazz: string, delay?: number) => void;
|
||||
const removeClass: (node: string | Node, clazz: string, delay?: number) => void;
|
||||
const toggleClass: (elt: string | Element, clazz: string) => void;
|
||||
const takeClass: (elt: string | Node, clazz: string) => void;
|
||||
const swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void;
|
||||
const defineExtension: (name: string, extension: any) => void;
|
||||
const removeExtension: (name: string) => void;
|
||||
const logAll: () => void;
|
||||
const logNone: () => void;
|
||||
const logger: any;
|
||||
namespace config {
|
||||
const historyEnabled: boolean;
|
||||
const historyCacheSize: number;
|
||||
const refreshOnHistoryMiss: boolean;
|
||||
const defaultSwapStyle: HtmxSwapStyle;
|
||||
const defaultSwapDelay: number;
|
||||
const defaultSettleDelay: number;
|
||||
const includeIndicatorStyles: boolean;
|
||||
const indicatorClass: string;
|
||||
const requestClass: string;
|
||||
const addedClass: string;
|
||||
const settlingClass: string;
|
||||
const swappingClass: string;
|
||||
const allowEval: boolean;
|
||||
const allowScriptTags: boolean;
|
||||
const inlineScriptNonce: string;
|
||||
const attributesToSettle: string[];
|
||||
const withCredentials: boolean;
|
||||
const timeout: number;
|
||||
const wsReconnectDelay: "full-jitter" | ((retryCount: number) => number);
|
||||
const wsBinaryType: BinaryType;
|
||||
const disableSelector: string;
|
||||
const scrollBehavior: 'auto' | 'instant' | 'smooth';
|
||||
const defaultFocusScroll: boolean;
|
||||
const getCacheBusterParam: boolean;
|
||||
const globalViewTransitions: boolean;
|
||||
const methodsThatUseUrlParams: (HttpVerb)[];
|
||||
const selfRequestsOnly: boolean;
|
||||
const ignoreTitle: boolean;
|
||||
const scrollIntoViewOnBoost: boolean;
|
||||
const triggerSpecsCache: any | null;
|
||||
const disableInheritance: boolean;
|
||||
const responseHandling: HtmxResponseHandlingConfig[];
|
||||
}
|
||||
const parseInterval: (str: string) => number;
|
||||
const _: (str: string) => any;
|
||||
const version: string;
|
||||
}
|
||||
type HttpVerb = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch';
|
||||
type SwapOptions = {
|
||||
select?: string;
|
||||
selectOOB?: string;
|
||||
eventInfo?: any;
|
||||
anchor?: string;
|
||||
contextElement?: Element;
|
||||
afterSwapCallback?: swapCallback;
|
||||
afterSettleCallback?: swapCallback;
|
||||
};
|
||||
type swapCallback = () => any;
|
||||
type HtmxSwapStyle = 'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string;
|
||||
type HtmxSwapSpecification = {
|
||||
swapStyle: HtmxSwapStyle;
|
||||
swapDelay: number;
|
||||
settleDelay: number;
|
||||
transition?: boolean;
|
||||
ignoreTitle?: boolean;
|
||||
head?: string;
|
||||
scroll?: 'top' | 'bottom';
|
||||
scrollTarget?: string;
|
||||
show?: string;
|
||||
showTarget?: string;
|
||||
focusScroll?: boolean;
|
||||
};
|
||||
type ConditionalFunction = ((this: Node, evt: Event) => boolean) & {
|
||||
source: string;
|
||||
};
|
||||
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;
|
||||
};
|
||||
type HtmxElementValidationError = {
|
||||
elt: Element;
|
||||
message: string;
|
||||
validity: ValidityState;
|
||||
};
|
||||
type HtmxHeaderSpecification = Record<string, string>;
|
||||
type HtmxAjaxHelperContext = {
|
||||
source?: Element | string;
|
||||
event?: Event;
|
||||
handler?: HtmxAjaxHandler;
|
||||
target: Element | string;
|
||||
swap?: HtmxSwapStyle;
|
||||
values?: any | FormData;
|
||||
headers?: Record<string, string>;
|
||||
select?: string;
|
||||
};
|
||||
type HtmxRequestConfig = {
|
||||
boosted: boolean;
|
||||
useUrlParams: boolean;
|
||||
formData: FormData;
|
||||
/**
|
||||
* formData proxy
|
||||
*/
|
||||
parameters: any;
|
||||
unfilteredFormData: FormData;
|
||||
/**
|
||||
* unfilteredFormData proxy
|
||||
*/
|
||||
unfilteredParameters: any;
|
||||
headers: HtmxHeaderSpecification;
|
||||
target: Element;
|
||||
verb: HttpVerb;
|
||||
errors: HtmxElementValidationError[];
|
||||
withCredentials: boolean;
|
||||
timeout: number;
|
||||
path: string;
|
||||
triggeringEvent: Event;
|
||||
};
|
||||
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;
|
||||
};
|
||||
type HtmxAjaxEtc = {
|
||||
returnPromise?: boolean;
|
||||
handler?: HtmxAjaxHandler;
|
||||
select?: string;
|
||||
targetOverride?: Element;
|
||||
swapOverride?: HtmxSwapStyle;
|
||||
headers?: Record<string, string>;
|
||||
values?: any | FormData;
|
||||
credentials?: boolean;
|
||||
timeout?: number;
|
||||
};
|
||||
type HtmxResponseHandlingConfig = {
|
||||
code?: string;
|
||||
swap: boolean;
|
||||
error?: boolean;
|
||||
ignoreTitle?: boolean;
|
||||
select?: string;
|
||||
target?: string;
|
||||
swapOverride?: string;
|
||||
event?: string;
|
||||
};
|
||||
type HtmxBeforeSwapDetails = HtmxResponseInfo & {
|
||||
shouldSwap: boolean;
|
||||
serverResponse: any;
|
||||
isError: boolean;
|
||||
ignoreTitle: boolean;
|
||||
selectOverride: string;
|
||||
};
|
||||
type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any;
|
||||
type HtmxSettleTask = (() => void);
|
||||
type HtmxSettleInfo = {
|
||||
tasks: HtmxSettleTask[];
|
||||
elts: Element[];
|
||||
title?: string;
|
||||
};
|
||||
type HtmxExtension = any;
|
||||
1432
src/htmx.js
1432
src/htmx.js
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,14 @@ describe('hx-boost attribute', function() {
|
||||
div.innerHTML.should.equal('Boosted')
|
||||
})
|
||||
|
||||
it('does not boost forms with method="dialog"', function() {
|
||||
make('<div hx-boost="true"><form id="f1" action="/test" method="dialog"><button id="b1">close</button></form></div>')
|
||||
var form = byId('f1')
|
||||
|
||||
var internalData = htmx._('getInternalData')(form)
|
||||
should.equal(undefined, internalData.boosted)
|
||||
})
|
||||
|
||||
it('handles basic anchor properly w/ data-* prefix', function() {
|
||||
this.server.respondWith('GET', '/test', 'Boosted')
|
||||
var div = make('<div data-hx-target="this" data-hx-boost="true"><a id="a1" href="/test">Foo</a></div>')
|
||||
@@ -110,4 +118,104 @@ describe('hx-boost attribute', function() {
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
it('form get w/ search params in action property excludes search params', function() {
|
||||
this.server.respondWith('GET', /\/test.*/, function(xhr) {
|
||||
should.equal(undefined, getParameters(xhr).foo)
|
||||
xhr.respond(200, {}, 'Boosted!')
|
||||
})
|
||||
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="get"><button id="b1">Submit</button></form></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
it('form post w/ query params in action property uses full url', function() {
|
||||
this.server.respondWith('POST', /\/test.*/, function(xhr) {
|
||||
should.equal(undefined, getParameters(xhr).foo)
|
||||
xhr.respond(200, {}, 'Boosted!')
|
||||
})
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="post"><button id="b1">Submit</button></form></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
it('form get with an unset action properly submits', function() {
|
||||
this.server.respondWith('GET', /\/*/, function(xhr) {
|
||||
xhr.respond(200, {}, 'Boosted!')
|
||||
})
|
||||
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" method="get"><button id="b1">Submit</button></form></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
it('form get with no action properly clears existing parameters on submit', function() {
|
||||
/// add a foo=bar to the current url
|
||||
var path = location.href
|
||||
if (!path.includes('foo=bar')) {
|
||||
if (!path.includes('?')) {
|
||||
path += '?foo=bar'
|
||||
} else {
|
||||
path += '&foo=bar'
|
||||
}
|
||||
}
|
||||
history.replaceState({ htmx: true }, '', path)
|
||||
|
||||
this.server.respondWith('GET', /\/*/, function(xhr) {
|
||||
// foo should not be present because the form is a get with no action
|
||||
should.equal(undefined, getParameters(xhr).foo)
|
||||
xhr.respond(200, {}, 'Boosted!')
|
||||
})
|
||||
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" method="get"><button id="b1">Submit</button></form></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
it('form get with an empty action properly clears existing parameters on submit', function() {
|
||||
/// add a foo=bar to the current url
|
||||
var path = location.href
|
||||
if (!path.includes('foo=bar')) {
|
||||
if (!path.includes('?')) {
|
||||
path += '?foo=bar'
|
||||
} else {
|
||||
path += '&foo=bar'
|
||||
}
|
||||
}
|
||||
history.replaceState({ htmx: true }, '', path)
|
||||
|
||||
this.server.respondWith('GET', /\/*/, function(xhr) {
|
||||
// foo should not be present because the form is a get with no action
|
||||
should.equal(undefined, getParameters(xhr).foo)
|
||||
xhr.respond(200, {}, 'Boosted!')
|
||||
})
|
||||
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="" method="get"><button id="b1">Submit</button></form></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Boosted!')
|
||||
})
|
||||
|
||||
if (/headlesschrome/i.test(navigator.userAgent)) {
|
||||
it('ctrlKey mouse click does not boost', function() {
|
||||
// Test only works well in playwright with chome for code coverage as otherwise it opens a new tab breaking things
|
||||
this.server.respondWith('GET', '/test', 'Boosted')
|
||||
var div = make('<div hx-target="this" hx-boost="true"><a id="a1" href="/test">Foo</a></div>')
|
||||
var a = byId('a1')
|
||||
var evt = new MouseEvent('click', { ctrlKey: true })
|
||||
a.dispatchEvent(evt)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.not.equal('Boosted')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,23 +68,6 @@ describe('hx-confirm attribute', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow skipping built-in window.confirm when using issueRequest', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
try {
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
var handler = htmx.on('htmx:confirm', function(evt) {
|
||||
evt.detail.question.should.equal('Sure?')
|
||||
evt.preventDefault()
|
||||
evt.detail.issueRequest(true)
|
||||
})
|
||||
btn.click()
|
||||
confirm.called.should.equal(false)
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
} finally {
|
||||
htmx.off('htmx:confirm', handler)
|
||||
}
|
||||
})
|
||||
it('should allow skipping built-in window.confirm when using issueRequest', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
try {
|
||||
|
||||
@@ -80,4 +80,55 @@ describe('hx-disabled-elt attribute', function() {
|
||||
b2.hasAttribute('disabled').should.equal(false)
|
||||
b3.hasAttribute('disabled').should.equal(false)
|
||||
})
|
||||
|
||||
it('load trigger does not prevent disabled element working', function() {
|
||||
this.server.respondWith('GET', '/test', 'Loaded!')
|
||||
var div1 = make('<div id="d1" hx-get="/test" hx-disabled-elt="#b1" hx-trigger="load">Load Me!</div><button id="b1">Demo</button>')
|
||||
var div = byId('d1')
|
||||
var btn = byId('b1')
|
||||
div.innerHTML.should.equal('Load Me!')
|
||||
btn.hasAttribute('disabled').should.equal(true)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Loaded!')
|
||||
btn.hasAttribute('disabled').should.equal(false)
|
||||
})
|
||||
|
||||
it('hx-disabled-elt supports multiple extended selectors', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var form = make('<form hx-get="/test" hx-disabled-elt="find input[type=\'text\'], find button" hx-swap="none"><input id="i1" type="text" placeholder="Type here..."><button id="b2" type="submit">Send</button></form>')
|
||||
var i1 = byId('i1')
|
||||
var b2 = byId('b2')
|
||||
|
||||
i1.hasAttribute('disabled').should.equal(false)
|
||||
b2.hasAttribute('disabled').should.equal(false)
|
||||
|
||||
b2.click()
|
||||
i1.hasAttribute('disabled').should.equal(true)
|
||||
b2.hasAttribute('disabled').should.equal(true)
|
||||
|
||||
this.server.respond()
|
||||
|
||||
i1.hasAttribute('disabled').should.equal(false)
|
||||
b2.hasAttribute('disabled').should.equal(false)
|
||||
})
|
||||
|
||||
it('closest/find/next/previous handle nothing to find without exception', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var btn1 = make('<button hx-get="/test" hx-disabled-elt="closest input">Click Me!</button>')
|
||||
var btn2 = make('<button hx-get="/test" hx-disabled-elt="find input">Click Me!</button>')
|
||||
var btn3 = make('<button hx-get="/test" hx-disabled-elt="next input">Click Me!</button>')
|
||||
var btn4 = make('<button hx-get="/test" hx-disabled-elt="previous input">Click Me!</button>')
|
||||
btn1.click()
|
||||
btn1.hasAttribute('disabled').should.equal(false)
|
||||
this.server.respond()
|
||||
btn2.click()
|
||||
btn2.hasAttribute('disabled').should.equal(false)
|
||||
this.server.respond()
|
||||
btn3.click()
|
||||
btn3.hasAttribute('disabled').should.equal(false)
|
||||
this.server.respond()
|
||||
btn4.click()
|
||||
btn4.hasAttribute('disabled').should.equal(false)
|
||||
this.server.respond()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
describe('hx-ext attribute', function() {
|
||||
var ext1Calls, ext2Calls, ext3Calls, ext4Calls
|
||||
var ext1Calls, ext2Calls, ext3Calls, ext4Calls, ext5Calls
|
||||
|
||||
beforeEach(function() {
|
||||
ext1Calls = ext2Calls = ext3Calls = ext4Calls = 0
|
||||
ext1Calls = ext2Calls = ext3Calls = ext4Calls = ext5Calls = 0
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
htmx.defineExtension('ext-1', {
|
||||
@@ -24,6 +24,12 @@ describe('hx-ext attribute', function() {
|
||||
if (name === 'htmx:afterRequest') {
|
||||
ext3Calls++
|
||||
}
|
||||
},
|
||||
isInlineSwap: function(swapStyle) {
|
||||
if (swapStyle === 'invalid') throw new Error('simulate exception handling in isInlineSwap')
|
||||
},
|
||||
handleSwap: function(swapStyle) {
|
||||
if (swapStyle === 'invalid') throw new Error('simulate exception handling in handleSwap')
|
||||
}
|
||||
})
|
||||
htmx.defineExtension('ext-4', {
|
||||
@@ -31,6 +37,23 @@ describe('hx-ext attribute', function() {
|
||||
if (name === 'namespace:example') {
|
||||
ext4Calls++
|
||||
}
|
||||
},
|
||||
isInlineSwap: function(swapStyle) {
|
||||
return swapStyle === 'inline'
|
||||
},
|
||||
handleSwap: function(swapStyle, target, fragment, settleInfo) {
|
||||
if (swapStyle === 'inline') {
|
||||
const swapOuterHTML = htmx._('swapOuterHTML')
|
||||
swapOuterHTML(target, fragment, settleInfo)
|
||||
}
|
||||
}
|
||||
})
|
||||
htmx.defineExtension('ext-5', {
|
||||
getSelectors: function() { return ['[foo]'] },
|
||||
onEvent: function(name, evt) {
|
||||
if (name === 'htmx:beforeProcessNode' && evt.target.getAttribute('foo')) {
|
||||
ext5Calls++
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -41,6 +64,8 @@ describe('hx-ext attribute', function() {
|
||||
htmx.removeExtension('ext-1')
|
||||
htmx.removeExtension('ext-2')
|
||||
htmx.removeExtension('ext-3')
|
||||
htmx.removeExtension('ext-4')
|
||||
htmx.removeExtension('ext-5')
|
||||
})
|
||||
|
||||
it('A simple extension is invoked properly', function() {
|
||||
@@ -111,11 +136,23 @@ describe('hx-ext attribute', function() {
|
||||
ext4Calls.should.equal(1)
|
||||
})
|
||||
|
||||
it('A simple extension is invoked properly for elements it specified in getSelectors', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': 'namespace:example' }, ''])
|
||||
var btn = make('<div data-hx-ext="ext-5"><div foo="bar">test</div></div>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
ext1Calls.should.equal(0)
|
||||
ext2Calls.should.equal(0)
|
||||
ext3Calls.should.equal(0)
|
||||
ext4Calls.should.equal(0)
|
||||
ext5Calls.should.equal(1)
|
||||
})
|
||||
|
||||
it('Extensions are ignored properly', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
make('<div id="div-AA" hx-ext="ext-1, ext-2"><button id="btn-AA" hx-get="/test">Click Me!</button>' +
|
||||
'<div id="div-BB" hx-ext="ignore:ext-1"><button id="btn-BB" hx-get="/test"></div></div>')
|
||||
make('<div id="div-AA" hx-ext="ext-1,ext-2,ext-5"><button id="btn-AA" hx-get="/test" foo="foo">Click Me!</button>' +
|
||||
'<div id="div-BB" hx-ext="ignore:ext-1,ignore:ext-5"><button id="btn-BB" hx-get="/test" foo="foo"></button></div></div>')
|
||||
|
||||
var btn1 = byId('btn-AA')
|
||||
var btn2 = byId('btn-BB')
|
||||
@@ -131,5 +168,31 @@ describe('hx-ext attribute', function() {
|
||||
ext1Calls.should.equal(1)
|
||||
ext2Calls.should.equal(2)
|
||||
ext3Calls.should.equal(0)
|
||||
|
||||
ext5Calls.should.equal(1)
|
||||
})
|
||||
|
||||
it('oob swap via swap extension uses isInlineSwap correctly', function() {
|
||||
this.server.respondWith(
|
||||
'GET',
|
||||
'/test',
|
||||
'<div id="b1" hx-swap-oob="inline">Bar</div>'
|
||||
)
|
||||
var btn = make('<div hx-get="/test" hx-swap="none" data-hx-ext="ext-4"><div id="b1">Foo</div></div>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
byId('b1').innerHTML.should.equal('Bar')
|
||||
})
|
||||
|
||||
it('isInlineSwap/handleSwap handling catches, logs and ignores exceptions in extension code', function() {
|
||||
this.server.respondWith(
|
||||
'GET',
|
||||
'/test',
|
||||
'<div id="b1" hx-swap-oob="invalid">Bar</div>'
|
||||
)
|
||||
var btn = make('<div hx-get="/test" hx-swap="none" data-hx-ext="ext-3"><div id="b1">Foo</div></div>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
byId('b1').innerHTML.should.equal('Bar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,18 @@ describe('hx-get attribute', function() {
|
||||
form.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('GET on form with anchor works properly and scrolls to anchor id', function() {
|
||||
this.server.respondWith('GET', /\/test.*/, function(xhr) {
|
||||
getParameters(xhr).foo.should.equal('bar')
|
||||
getParameters(xhr).i1.should.equal('value')
|
||||
xhr.respond(200, {}, '<div id="foo">Clicked</div>')
|
||||
})
|
||||
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar#foo"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>')
|
||||
form.click()
|
||||
this.server.respond()
|
||||
form.innerHTML.should.equal('<div id="foo">Clicked</div>')
|
||||
})
|
||||
|
||||
it('issues a GET request on click and swaps content w/ data-* prefix', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('hx-headers attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('multiple hx-headers works', function() {
|
||||
it('multiple hx-headers works with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
xhr.requestHeaders.v1.should.equal('test')
|
||||
xhr.requestHeaders.v2.should.equal('42')
|
||||
@@ -112,7 +112,7 @@ describe('hx-headers attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-headers can be on parents', function() {
|
||||
it('hx-headers can be on parents with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
xhr.requestHeaders.i1.should.equal('test')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
@@ -124,7 +124,7 @@ describe('hx-headers attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-headers can override parents', function() {
|
||||
it('hx-headers can override parents with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
xhr.requestHeaders.i1.should.equal('best')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
@@ -136,7 +136,7 @@ describe('hx-headers attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-headers overrides inputs', function() {
|
||||
it('hx-headers overrides inputs with javascript', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
xhr.requestHeaders.i1.should.equal('best')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
|
||||
38
test/attributes/hx-history-elt.js
Normal file
38
test/attributes/hx-history-elt.js
Normal file
@@ -0,0 +1,38 @@
|
||||
describe('hx-history attribute', function() {
|
||||
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
|
||||
it('content of hx-history-elt is used during history replacment', function() {
|
||||
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
|
||||
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
var workArea = getWorkArea()
|
||||
workArea.textContent.should.equal('test1')
|
||||
|
||||
byId('d2').click()
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
this.server.respondWith('GET', '/test1', '<div>content outside of hx-history-elt not included</div><div id="work-area" hx-history-elt><div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test3</div></div>')
|
||||
// clear cache so it makes a full page request on history restore
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test3')
|
||||
})
|
||||
})
|
||||
@@ -4,12 +4,12 @@ describe('hx-history attribute', function() {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
|
||||
it('history cache should not contain embargoed content', function() {
|
||||
@@ -32,8 +32,8 @@ describe('hx-history attribute', function() {
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test3')
|
||||
|
||||
// embargoed content should NOT be in the localStorage cache
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
// embargoed content should NOT be in the sessionStorage cache
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(2)
|
||||
|
||||
// on history navigation, embargoed content is retrieved from server
|
||||
|
||||
@@ -136,6 +136,104 @@ describe('hx-include attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('Input can be referred to externally and then via a form then it will only be included once', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<form id="f1"><input id="i1" name="i1" value="test"/></form>')
|
||||
var div = make('<div hx-post="/include" hx-include="previous #i1,#f1"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('checkbox can be referred to externally', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('on')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input id="i1" name="i1" type="checkbox" checked/>')
|
||||
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('files input can be referred to externally', function() {
|
||||
// This test is just to make loc coverage complete and does not test that real file values are sent
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input id="i1" name="i1" type="file" multiple/>')
|
||||
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('properly handles multiple select input referred to externally', function() {
|
||||
var values
|
||||
this.server.respondWith('Post', '/include', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
make('<select id="multiSelect" name="multiSelect" multiple="multiple">' +
|
||||
'<option id="m1" value="m1">m1</option>' +
|
||||
'<option id="m2" value="m2">m2</option>' +
|
||||
'<option id="m3" value="m3">m3</option>' +
|
||||
'<option id="m4" value="m4">m4</option>' +
|
||||
'</select>')
|
||||
var div = make('<div hx-post="/include" hx-include="#multiSelect"></div>')
|
||||
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({})
|
||||
|
||||
byId('m1').selected = true
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ multiSelect: 'm1' })
|
||||
|
||||
byId('m1').selected = true
|
||||
byId('m3').selected = true
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ multiSelect: ['m1', 'm3'] })
|
||||
})
|
||||
|
||||
it('properly handles multiple select input referred to externally and then via a form then it will only be included once', function() {
|
||||
var values
|
||||
this.server.respondWith('Post', '/include', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
make('<form id="f1"><select id="multiSelect" name="multiSelect" multiple="multiple">' +
|
||||
'<option id="m1" value="m1">m1</option>' +
|
||||
'<option id="m2" value="m2">m2</option>' +
|
||||
'<option id="m3" value="m3">m3</option>' +
|
||||
'<option id="m4" value="m4">m4</option>' +
|
||||
'</select></form>')
|
||||
var div = make('<div hx-post="/include" hx-include="previous #multiSelect,#f1"></div>')
|
||||
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({})
|
||||
|
||||
byId('m1').selected = true
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ multiSelect: 'm1' })
|
||||
|
||||
byId('m1').selected = true
|
||||
byId('m3').selected = true
|
||||
div.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ multiSelect: ['m1', 'm3'] })
|
||||
})
|
||||
|
||||
it('Two inputs can be referred to externally', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
@@ -224,4 +322,200 @@ describe('hx-include attribute', function() {
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('Multiple extended selectors can be used in hx-include', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
params.i2.should.equal('foo')
|
||||
params.i3.should.equal('bar')
|
||||
params.i4.should.equal('test2')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="closest div, next input, #i4"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-include processes extended selector in between standard selectors', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
should.equal(params.i2, undefined)
|
||||
params.i3.should.equal('bar')
|
||||
params.i4.should.equal('test2')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="#i1, next input, #i4"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-include processes nested standard selectors correctly', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
params.i2.should.equal('foo')
|
||||
params.i3.should.equal('bar')
|
||||
should.equal(params.i4, undefined)
|
||||
should.equal(params.i5, undefined)
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<input name="i5" value="test"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="next input, #i > :is([name=\'i1\'], [name=\'i2\'])"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-include processes wrapped next/previous selectors correctly', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
should.equal(params.i1, undefined)
|
||||
params.i2.should.equal('foo')
|
||||
params.i3.should.equal('bar')
|
||||
should.equal(params.i4, undefined)
|
||||
should.equal(params.i5, undefined)
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="next <#nonexistent, input/>, previous <#i5, [name=\'i2\'], #i4/>"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>' +
|
||||
'<input name="i5" value="test"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-include processes wrapped closest selector correctly', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
should.equal(params.i1, undefined)
|
||||
params.i2.should.equal('bar')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<section>' +
|
||||
'<input name="i1" value="foo"/>' +
|
||||
'<div>' +
|
||||
'<input name="i2" value="bar"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="closest <section, div/>"></button>' +
|
||||
'</div>' +
|
||||
'</section>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('`inherit` can be used to expand parent hx-include', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test1')
|
||||
params.i2.should.equal('test2')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<div hx-include="#i1">' +
|
||||
' <button id="btn" hx-include="inherit, #i2" hx-post="/include"></button>' +
|
||||
'</div>' +
|
||||
'<input id="i1" name="i1" value="test1"/>' +
|
||||
'<input id="i2" name="i2" value="test2"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('`inherit` can be used to expand multiple parents hx-include', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test1')
|
||||
params.i2.should.equal('test2')
|
||||
params.i3.should.equal('test3')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<div hx-include="#i1">' +
|
||||
' <div hx-include="inherit, #i2">' +
|
||||
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
'<input id="i1" name="i1" value="test1"/>' +
|
||||
'<input id="i2" name="i2" value="test2"/>' +
|
||||
'<input id="i3" name="i3" value="test3"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('`inherit` chain breaks properly', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
should.not.exist(params.i1)
|
||||
params.i2.should.equal('test2')
|
||||
params.i3.should.equal('test3')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<div hx-include="#i1">' +
|
||||
' <div hx-include="#i2">' +
|
||||
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
'<input id="i1" name="i1" value="test1"/>' +
|
||||
'<input id="i2" name="i2" value="test2"/>' +
|
||||
'<input id="i3" name="i3" value="test3"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('`inherit` syntax regex properly catches keyword', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test1')
|
||||
params.i2.should.equal('test2')
|
||||
params.i3.should.equal('test3')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
make('<div hx-include="#i1">' +
|
||||
' <div hx-include="#i2, inherit,.nonexistent-class">' +
|
||||
' <button id="btn" hx-include="customtag,inherit , #i3" hx-post="/include"></button>' +
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
'<input id="i1" name="i1" value="test1"/>' +
|
||||
'<input id="i2" name="i2" value="test2"/>' +
|
||||
'<input id="i3" name="i3" value="test3"/>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,4 +123,68 @@ describe('hx-indicator attribute', function() {
|
||||
b2.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(false)
|
||||
})
|
||||
|
||||
it('`inherit` can be used to expand parent hx-indicator', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
make('<div hx-indicator="#a1">' +
|
||||
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a2">Click Me!</button>' +
|
||||
'</div>')
|
||||
var btn = byId('btn')
|
||||
var a1 = make('<a id="a1"></a>')
|
||||
var a2 = make('<a id="a2"></a>')
|
||||
btn.click()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(true)
|
||||
a2.classList.contains('htmx-request').should.equal(true)
|
||||
this.server.respond()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(false)
|
||||
a2.classList.contains('htmx-request').should.equal(false)
|
||||
})
|
||||
|
||||
it('`inherit` can be used to expand multiple parents hx-indicator', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
make('<div hx-indicator="#a1">' +
|
||||
' <div hx-indicator="inherit, #a2">' +
|
||||
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
|
||||
' </div>' +
|
||||
'</div>')
|
||||
var btn = byId('btn')
|
||||
var a1 = make('<a id="a1"></a>')
|
||||
var a2 = make('<a id="a2"></a>')
|
||||
var a3 = make('<a id="a3"></a>')
|
||||
btn.click()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(true)
|
||||
a2.classList.contains('htmx-request').should.equal(true)
|
||||
a3.classList.contains('htmx-request').should.equal(true)
|
||||
this.server.respond()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(false)
|
||||
a2.classList.contains('htmx-request').should.equal(false)
|
||||
a3.classList.contains('htmx-request').should.equal(false)
|
||||
})
|
||||
|
||||
it('`inherit` chain breaks properly', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
make('<div hx-indicator="#a1">' +
|
||||
' <div hx-indicator="#a2">' +
|
||||
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
|
||||
' </div>' +
|
||||
'</div>')
|
||||
var btn = byId('btn')
|
||||
var a1 = make('<a id="a1"></a>')
|
||||
var a2 = make('<a id="a2"></a>')
|
||||
var a3 = make('<a id="a3"></a>')
|
||||
btn.click()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(false)
|
||||
a2.classList.contains('htmx-request').should.equal(true)
|
||||
a3.classList.contains('htmx-request').should.equal(true)
|
||||
this.server.respond()
|
||||
btn.classList.contains('htmx-request').should.equal(false)
|
||||
a1.classList.contains('htmx-request').should.equal(false)
|
||||
a2.classList.contains('htmx-request').should.equal(false)
|
||||
a3.classList.contains('htmx-request').should.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,6 +133,20 @@ describe('hx-on:* attribute', function() {
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('should fire when triggered by load', function() {
|
||||
this.server.respondWith('POST', '/test', 'test')
|
||||
make("<div hx-trigger='load' hx-post='/test' hx-on:htmx:config-request='foo = true'></div>")
|
||||
window.foo.should.equal(true)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('should fire when triggered by revealed', function() {
|
||||
this.server.respondWith('POST', '/test', 'test')
|
||||
make("<div hx-trigger='revealed' hx-post='/test' hx-on:htmx:config-request='foo = true' style='position: fixed; top: 1px; left: 1px; border: 3px solid red'></div>")
|
||||
window.foo.should.equal(true)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('de-initializes hx-on-* content properly', function() {
|
||||
window.tempCount = 0
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
describe('hx-on attribute', function() {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
})
|
||||
|
||||
it('can handle basic events w/ no other attributes', function() {
|
||||
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
|
||||
btn.click()
|
||||
window.foo.should.equal(true)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('can modify a parameter via htmx:configRequest', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
var params = parseParams(xhr.requestBody)
|
||||
xhr.respond(200, {}, params.foo)
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:configRequest: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('bar')
|
||||
})
|
||||
|
||||
it('can cancel an event via preventDefault for htmx:configRequest', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, '<button>Bar</button>')
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:configRequest: event.preventDefault()' hx-post='/test' hx-swap='outerHTML'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('Foo')
|
||||
})
|
||||
|
||||
it('can respond to kebab-case events', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
var params = parseParams(xhr.requestBody)
|
||||
xhr.respond(200, {}, params.foo)
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:config-request: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('bar')
|
||||
})
|
||||
|
||||
it('has the this symbol set to the element', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, 'foo')
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:config-request: window.elt = this' hx-post='/test'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('foo')
|
||||
btn.should.equal(window.elt)
|
||||
delete window.elt
|
||||
})
|
||||
|
||||
it('can handle multi-line JSON', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, 'foo')
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
|
||||
" bar: false}' hx-post='/test'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('foo')
|
||||
var obj = { foo: true, bar: false }
|
||||
obj.should.deep.equal(window.elt)
|
||||
delete window.elt
|
||||
})
|
||||
|
||||
it('can handle multiple event handlers in the presence of multi-line JSON', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, 'foo')
|
||||
})
|
||||
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
|
||||
' bar: false}\n' +
|
||||
" htmx:afterRequest: window.foo = true'" +
|
||||
" hx-post='/test'>Foo</button>")
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerText.should.equal('foo')
|
||||
|
||||
var obj = { foo: true, bar: false }
|
||||
obj.should.deep.equal(window.elt)
|
||||
delete window.elt
|
||||
|
||||
window.foo.should.equal(true)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('de-initializes hx-on content properly', function() {
|
||||
window.tempCount = 0
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, "<button id='foo' hx-on=\"click: window.tempCount++;\">increment</button>")
|
||||
})
|
||||
var div = make("<div hx-post='/test'>Foo</div>")
|
||||
|
||||
// get response
|
||||
div.click()
|
||||
this.server.respond()
|
||||
|
||||
// click button
|
||||
byId('foo').click()
|
||||
window.tempCount.should.equal(1)
|
||||
|
||||
// get second response
|
||||
div.click()
|
||||
this.server.respond()
|
||||
|
||||
// click button again
|
||||
byId('foo').click()
|
||||
window.tempCount.should.equal(2)
|
||||
|
||||
delete window.tempCount
|
||||
})
|
||||
|
||||
it('is not evaluated when allowEval is false', function() {
|
||||
var calledEvent = false
|
||||
var handler = htmx.on('htmx:evalDisallowedError', function() {
|
||||
calledEvent = true
|
||||
})
|
||||
htmx.config.allowEval = false
|
||||
try {
|
||||
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
|
||||
btn.click()
|
||||
should.not.exist(window.foo)
|
||||
} finally {
|
||||
htmx.config.allowEval = true
|
||||
htmx.off('htmx:evalDisallowedError', handler)
|
||||
delete window.foo
|
||||
}
|
||||
calledEvent.should.equal(true)
|
||||
})
|
||||
|
||||
it('can handle event types with dots', function() {
|
||||
var btn = make("<button hx-on='my.custom.event: window.foo = true'>Foo</button>")
|
||||
// IE11 doesn't support `new CustomEvent()` so call htmx' internal utility function
|
||||
btn.dispatchEvent(htmx._('makeEvent')('my.custom.event'))
|
||||
window.foo.should.equal(true)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('can handle being swapped using innerHTML', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, '<button id="bar" hx-on="click: window.bar = true">Bar</button>')
|
||||
})
|
||||
|
||||
make(
|
||||
'<div>' +
|
||||
'<button id="swap" hx-get="/test" hx-target="#baz" hx-swap="innerHTML">Swap</button>' +
|
||||
'<div id="baz"><button id="foo" hx-on="click: window.foo = true">Foo</button></div>' +
|
||||
'</div>'
|
||||
)
|
||||
|
||||
var fooBtn = byId('foo')
|
||||
fooBtn.click()
|
||||
window.foo.should.equal(true)
|
||||
|
||||
var swapBtn = byId('swap')
|
||||
swapBtn.click()
|
||||
this.server.respond()
|
||||
|
||||
var barBtn = byId('bar')
|
||||
barBtn.click()
|
||||
window.bar.should.equal(true)
|
||||
|
||||
delete window.foo
|
||||
delete window.bar
|
||||
})
|
||||
|
||||
it('cleans up all handlers when the DOM updates', function() {
|
||||
// setup
|
||||
window.foo = 0
|
||||
window.bar = 0
|
||||
var div = make("<div hx-on='increment-foo: window.foo++\nincrement-bar: window.bar++'>Foo</div>")
|
||||
make('<div>Another Div</div>') // sole purpose is to update the DOM
|
||||
|
||||
// check there is just one handler against each event
|
||||
htmx.trigger(div, 'increment-foo')
|
||||
htmx.trigger(div, 'increment-bar')
|
||||
window.foo.should.equal(1)
|
||||
window.bar.should.equal(1)
|
||||
|
||||
// teardown
|
||||
delete window.foo
|
||||
delete window.bar
|
||||
})
|
||||
})
|
||||
@@ -34,4 +34,46 @@ describe('hx-preserve attribute', function() {
|
||||
byId('d1').innerHTML.should.equal('Old Content')
|
||||
byId('d2').innerHTML.should.equal('New Content')
|
||||
})
|
||||
|
||||
it('preserved element should not be swapped if it is part of a oob swap', function() {
|
||||
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='true'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
|
||||
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
|
||||
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
byId('d1').innerHTML.should.equal('Normal Content')
|
||||
byId('d3').innerHTML.should.equal('Old Content')
|
||||
byId('d4').innerHTML.should.equal('New oob Content')
|
||||
})
|
||||
|
||||
it('preserved element should not be swapped if it is part of a hx-select-oob swap', function() {
|
||||
this.server.respondWith('GET', '/test', "Normal Content<div id='d2'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
|
||||
var div1 = make("<div id='d1' hx-get='/test' hx-select-oob='#d2'>Click Me!</div>")
|
||||
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
byId('d1').innerHTML.should.equal('Normal Content')
|
||||
byId('d3').innerHTML.should.equal('Old Content')
|
||||
byId('d4').innerHTML.should.equal('New oob Content')
|
||||
})
|
||||
|
||||
it('preserved element should relocated unchanged if it is part of a oob swap targeting a different loction', function() {
|
||||
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='innerHTML:#d5'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
|
||||
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
|
||||
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
|
||||
var div5 = make("<div id='d5'></div>")
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
byId('d1').innerHTML.should.equal('Normal Content')
|
||||
byId('d2').innerHTML.should.equal('')
|
||||
byId('d5').innerHTML.should.equal('<div id="d3" hx-preserve="">Old Content</div><div id="d4">New oob Content</div>')
|
||||
})
|
||||
|
||||
it('when moveBefore is disabled/missing preseved content is copied into fragment instead of pantry', function() {
|
||||
var div = make("<div hx-get='/test'><div id='d1' hx-preserve>Old Content</div><div id='d2'>Old Content</div></div>")
|
||||
var fragment = htmx._('makeFragment')('<div id="d1" hx-preserve>New Content</div>')
|
||||
fragment.firstChild.moveBefore = undefined
|
||||
htmx._('handlePreservedElements')(fragment)
|
||||
fragment.firstChild.innerHTML.should.equal('Old Content')
|
||||
})
|
||||
})
|
||||
|
||||
37
test/attributes/hx-prompt.js
Normal file
37
test/attributes/hx-prompt.js
Normal file
@@ -0,0 +1,37 @@
|
||||
describe('hx-prompt attribute', function() {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
})
|
||||
|
||||
it('hx-prompt should set request header to prompt response', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
should.equal(xhr.requestHeaders['HX-Prompt'], 'foo')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var promptSave = window.prompt
|
||||
window.prompt = function() { return 'foo' }
|
||||
var btn = make('<button hx-get="/test" hx-prompt="test prompt">Click Me!</a>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
window.prompt = promptSave
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-prompt that is cancled returns null and blocks the request', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var promptSave = window.prompt
|
||||
window.prompt = function() { return null }
|
||||
var btn = make('<button hx-get="/test" hx-prompt="test prompt">Click Me!</a>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
window.prompt = promptSave
|
||||
btn.innerHTML.should.equal('Click Me!')
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,16 @@
|
||||
describe('hx-push-url attribute', function() {
|
||||
const chai = window.chai
|
||||
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
|
||||
it('navigation should push an element into the cache when true', function() {
|
||||
@@ -21,20 +22,33 @@ describe('hx-push-url attribute', function() {
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache[cache.length - 1].url.should.equal('/test')
|
||||
})
|
||||
|
||||
it('navigation should push an element into the cache when string', function() {
|
||||
it('navigation should not push an element into the cache when false', function() {
|
||||
this.server.respondWith('GET', '/test', 'second')
|
||||
getWorkArea().innerHTML.should.be.equal('')
|
||||
var div = make('<div hx-push-url="/abc123" hx-get="/test">first</div>')
|
||||
var div = make('<div hx-push-url="false" hx-get="/test">first</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
should.equal(cache, null)
|
||||
})
|
||||
|
||||
it('navigation should push an element into the cache when string', function() {
|
||||
this.server.respondWith('GET', '/test', 'second')
|
||||
getWorkArea().innerHTML.should.be.equal('')
|
||||
var div = make('<div hx-push-url="abc123" hx-get="/test">first</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(2)
|
||||
cache[1].url.should.equal('/abc123')
|
||||
})
|
||||
@@ -54,7 +68,7 @@ describe('hx-push-url attribute', function() {
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
|
||||
cache.length.should.equal(2)
|
||||
htmx._('restoreHistory')('/test1')
|
||||
@@ -92,7 +106,7 @@ describe('hx-push-url attribute', function() {
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
}
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(10) // should only be 10 elements
|
||||
})
|
||||
|
||||
@@ -111,15 +125,26 @@ describe('hx-push-url attribute', function() {
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
|
||||
cache.length.should.equal(2)
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test1')
|
||||
})
|
||||
|
||||
it('cache miss should refresh when refreshOnHistoryMiss true', function() {
|
||||
htmx.config.refreshOnHistoryMiss = true
|
||||
var refresh = false
|
||||
htmx.location = { reload: function() { refresh = true } }
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
htmx._('restoreHistory')('/test3')
|
||||
refresh.should.equal(true)
|
||||
htmx.location = window.location
|
||||
htmx.config.refreshOnHistoryMiss = false
|
||||
})
|
||||
|
||||
it('navigation should push an element into the cache w/ data-* prefix', function() {
|
||||
this.server.respondWith('GET', '/test', 'second')
|
||||
getWorkArea().innerHTML.should.be.equal('')
|
||||
@@ -127,20 +152,20 @@ describe('hx-push-url attribute', function() {
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
})
|
||||
|
||||
it('deals with malformed JSON in history cache when getting', function() {
|
||||
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
|
||||
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
|
||||
var history = htmx._('getCachedHistory')('url')
|
||||
should.equal(history, null)
|
||||
})
|
||||
|
||||
it('deals with malformed JSON in history cache when saving', function() {
|
||||
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
|
||||
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
|
||||
htmx._('saveToHistoryCache')('url', make('<div>'))
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
})
|
||||
|
||||
@@ -149,21 +174,32 @@ describe('hx-push-url attribute', function() {
|
||||
htmx._('saveToHistoryCache')('url2', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url3', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url2', make('<div>'))
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(3)
|
||||
})
|
||||
|
||||
it('setting history cache size to 0 clears cache', function() {
|
||||
htmx._('saveToHistoryCache')('url1', make('<div>'))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
htmx.config.historyCacheSize = 0
|
||||
htmx._('saveToHistoryCache')('url2', make('<div>'))
|
||||
cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
should.equal(cache, null)
|
||||
htmx.config.historyCacheSize = 10
|
||||
})
|
||||
|
||||
it('history cache is LRU', function() {
|
||||
htmx._('saveToHistoryCache')('url1', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url2', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url3', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url2', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('url1', make('<div>'))
|
||||
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(3)
|
||||
cache[0].url.should.equal('url3')
|
||||
cache[1].url.should.equal('url2')
|
||||
cache[2].url.should.equal('url1')
|
||||
cache[0].url.should.equal('/url3')
|
||||
cache[1].url.should.equal('/url2')
|
||||
cache[2].url.should.equal('/url1')
|
||||
})
|
||||
|
||||
it('htmx:afterSettle is called when replacing outerHTML', function() {
|
||||
@@ -204,17 +240,159 @@ describe('hx-push-url attribute', function() {
|
||||
})
|
||||
|
||||
it('saveToHistoryCache should not throw', function() {
|
||||
this.timeout(4000)
|
||||
var bigContent = 'Dummy'
|
||||
for (var i = 0; i < 20; i++) {
|
||||
bigContent += bigContent
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem('htmx-history-cache')
|
||||
sessionStorage.removeItem('htmx-history-cache')
|
||||
htmx._('saveToHistoryCache')('/dummy', make('<div>' + bigContent + '</div>'), 'Foo', 0)
|
||||
should.equal(localStorage.getItem('htmx-history-cache'), null)
|
||||
should.equal(sessionStorage.getItem('htmx-history-cache'), null)
|
||||
} finally {
|
||||
// clear history cache afterwards
|
||||
localStorage.removeItem('htmx-history-cache')
|
||||
sessionStorage.removeItem('htmx-history-cache')
|
||||
}
|
||||
})
|
||||
|
||||
if (/chrome/i.test(navigator.userAgent)) {
|
||||
it('when sessionStorage disabled history not saved fine', function() {
|
||||
var setItem = sessionStorage.setItem
|
||||
sessionStorage.setItem = undefined
|
||||
this.server.respondWith('GET', '/test', 'second')
|
||||
getWorkArea().innerHTML.should.be.equal('')
|
||||
var div = make('<div hx-push-url="true" hx-get="/test">first</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var hist = htmx._('getCachedHistory')('/test')
|
||||
should.equal(hist, null)
|
||||
sessionStorage.setItem = setItem
|
||||
})
|
||||
}
|
||||
|
||||
it.skip('normalizePath falls back to no normalization if path not valid URL', function() {
|
||||
// path normalization has a bug breaking it right now preventing this test
|
||||
htmx._('saveToHistoryCache')('http://', make('<div>'))
|
||||
htmx._('saveToHistoryCache')('http//', make('<div>'))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(2)
|
||||
cache[0].url.should.equal('http://') // no normalization as invalid
|
||||
cache[1].url.should.equal('/http') // can normalize this one
|
||||
})
|
||||
|
||||
it('history cache clears out disabled attribute', function() {
|
||||
htmx._('saveToHistoryCache')('/url1', make('<div><div data-disabled-by-htmx disabled></div></div>'))
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
cache[0].url.should.equal('/url1')
|
||||
cache[0].content.should.equal('<div data-disabled-by-htmx=""></div>')
|
||||
})
|
||||
|
||||
it('ensure cache-busting parameter not pushed to history url', function() {
|
||||
this.server.respondWith('GET', /\/test.*/, function(xhr) {
|
||||
getParameters(xhr)['org.htmx.cache-buster'].should.equal('foo')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
|
||||
try {
|
||||
htmx.config.getCacheBusterParam = true
|
||||
var btn = make('<button hx-push-url="true" hx-get="/test" id="foo">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
} finally {
|
||||
htmx.config.getCacheBusterParam = false
|
||||
}
|
||||
sessionStorage.getItem('htmx-current-path-for-history').should.equal('/test')
|
||||
})
|
||||
|
||||
it('ensure history pushState called', function() {
|
||||
if (!byId('mocha')) { // This test does not work in browser using mocha
|
||||
this.server.respondWith('GET', /\/test.*/, function(xhr) {
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
|
||||
try {
|
||||
htmx.config.historyEnabled = true
|
||||
var btn = make('<button hx-push-url="true" hx-get="/test" id="foo">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
} finally {
|
||||
htmx.config.historyEnabled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle HX-Push response header', function() {
|
||||
var path
|
||||
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
|
||||
path = event.detail.path
|
||||
})
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Push': '/pushpath' }, 'Result'])
|
||||
var div1 = make('<div id="d1" hx-get="/test"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
div1.innerHTML.should.equal('Result')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
path.should.equal('/pushpath')
|
||||
htmx.off('htmx:pushedIntoHistory', handler)
|
||||
})
|
||||
|
||||
it('should handle HX-Push-Url response header', function() {
|
||||
var path
|
||||
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
|
||||
path = event.detail.path
|
||||
})
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Push-Url': '/pushpath' }, 'Result'])
|
||||
var div1 = make('<div id="d1" hx-get="/test"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
div1.innerHTML.should.equal('Result')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
path.should.equal('/pushpath')
|
||||
htmx.off('htmx:pushedIntoHistory', handler)
|
||||
})
|
||||
|
||||
it('should ignore HX-Push-Url=false response header', function() {
|
||||
var path = ''
|
||||
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
|
||||
path = event.detail.path
|
||||
})
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Push-Url': 'false' }, 'Result'])
|
||||
var div1 = make('<div id="d1" hx-get="/test"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
div1.innerHTML.should.equal('Result')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
should.equal(cache, null)
|
||||
path.should.equal('')
|
||||
htmx.off('htmx:pushedIntoHistory', handler)
|
||||
})
|
||||
|
||||
it('pushing url without anchor will retain the page anchor tag', function() {
|
||||
var handler = htmx.on('htmx:configRequest', function(evt) {
|
||||
evt.detail.path = evt.detail.path + '#test'
|
||||
})
|
||||
var path = ''
|
||||
var handler2 = htmx.on('htmx:pushedIntoHistory', function(evt) {
|
||||
path = evt.detail.path
|
||||
})
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var div = make("<div hx-get='/test' hx-push-url='/test'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
path.should.equal('/test#test')
|
||||
} finally {
|
||||
htmx.off('htmx:configRequest', handler)
|
||||
htmx.off('htmx:pushedIntoHistory', handler2)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
43
test/attributes/hx-replace-url.js
Normal file
43
test/attributes/hx-replace-url.js
Normal file
@@ -0,0 +1,43 @@
|
||||
describe('hx-replace-url attribute', function() {
|
||||
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
|
||||
it('navigation should replace an element into the cache when true', function() {
|
||||
this.server.respondWith('GET', '/test', 'second')
|
||||
getWorkArea().innerHTML.should.be.equal('')
|
||||
var div = make('<div hx-replace-url="true" hx-get="/test">first</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.click()
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('second')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache[cache.length - 1].url.should.equal('/test')
|
||||
})
|
||||
|
||||
it('should handle HX-Replace-Url response header', function() {
|
||||
var path
|
||||
var handler = htmx.on('htmx:replacedInHistory', function(event) {
|
||||
path = event.detail.path
|
||||
})
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Replace-Url': '/pushpath' }, 'Result'])
|
||||
var div1 = make('<div id="d1" hx-get="/test"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
div1.innerHTML.should.equal('Result')
|
||||
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
|
||||
cache.length.should.equal(1)
|
||||
path.should.equal('/pushpath')
|
||||
htmx.off('htmx:replacedInHistory', handler)
|
||||
})
|
||||
})
|
||||
@@ -10,18 +10,19 @@ describe('hx-request attribute', function() {
|
||||
|
||||
it('basic hx-request timeout works', function(done) {
|
||||
var timedOut = false
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var div = make("<div hx-post='/vars' hx-request='\"timeout\":1'></div>")
|
||||
htmx.config.selfRequestsOnly = false
|
||||
var div = make("<div hx-post='https://hypermedia.systems/www/test' hx-request='\"timeout\":1'></div>")
|
||||
htmx.on(div, 'htmx:timeout', function() {
|
||||
timedOut = true
|
||||
})
|
||||
this.server.restore() // use real xhrs
|
||||
div.click()
|
||||
setTimeout(function() {
|
||||
htmx.config.selfRequestsOnly = true
|
||||
div.innerHTML.should.equal('')
|
||||
// unfortunately it looks like sinon.js doesn't implement the timeout functionality
|
||||
// timedOut.should.equal(true);
|
||||
timedOut.should.equal(true)
|
||||
done()
|
||||
}, 400)
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('hx-request header works', function() {
|
||||
|
||||
@@ -66,6 +66,28 @@ describe('hx-swap-oob attribute', function() {
|
||||
})
|
||||
}
|
||||
|
||||
it('handles remvoing hx-swap-oob tag', function() {
|
||||
this.server.respondWith('GET', '/test', "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped3</div>")
|
||||
var div = make('<div data-hx-get="/test">click me</div>')
|
||||
make('<div id="d1"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked')
|
||||
byId('d1').innerHTML.should.equal('Swapped3')
|
||||
byId('d1').hasAttribute('hx-swap-oob').should.equal(false)
|
||||
})
|
||||
|
||||
it('handles remvoing data-hx-swap-oob tag', function() {
|
||||
this.server.respondWith('GET', '/test', "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped3</div>")
|
||||
var div = make('<div data-hx-get="/test">click me</div>')
|
||||
make('<div id="d1"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked')
|
||||
byId('d1').innerHTML.should.equal('Swapped3')
|
||||
byId('d1').hasAttribute('data-hx-swap-oob').should.equal(false)
|
||||
})
|
||||
|
||||
it('handles no id match properly', function() {
|
||||
this.server.respondWith('GET', '/test', "Clicked<div id='d1' hx-swap-oob='true'>Swapped2</div>")
|
||||
var div = make('<div hx-get="/test">click me</div>')
|
||||
@@ -155,6 +177,7 @@ describe('hx-swap-oob attribute', function() {
|
||||
|
||||
it('swaps into all targets that match the selector (outerHTML)', function() {
|
||||
var oobSwapContent = '<div class="new-target" hx-swap-oob="outerHTML:.target">Swapped9</div>'
|
||||
var finalContent = '<div class="new-target">Swapped9</div>'
|
||||
this.server.respondWith('GET', '/test', '<div>Clicked</div>' + oobSwapContent)
|
||||
var div = make('<div hx-get="/test">click me</div>')
|
||||
make('<div id="d1"><div>No swap</div></div>')
|
||||
@@ -163,8 +186,8 @@ describe('hx-swap-oob attribute', function() {
|
||||
div.click()
|
||||
this.server.respond()
|
||||
byId('d1').innerHTML.should.equal('<div>No swap</div>')
|
||||
byId('d2').innerHTML.should.equal(oobSwapContent)
|
||||
byId('d3').innerHTML.should.equal(oobSwapContent)
|
||||
byId('d2').innerHTML.should.equal(finalContent)
|
||||
byId('d3').innerHTML.should.equal(finalContent)
|
||||
})
|
||||
|
||||
it('oob swap delete works properly', function() {
|
||||
@@ -176,6 +199,30 @@ describe('hx-swap-oob attribute', function() {
|
||||
should.equal(byId('d1'), null)
|
||||
})
|
||||
|
||||
it('oob swap removes templates used for oob encapsulation only properly', function() {
|
||||
this.server.respondWith('GET', '/test', '' +
|
||||
'Clicked<template><div hx-swap-oob="outerHTML" id="d1">Foo</div></template>')
|
||||
var div = make('<button hx-get="/test" id="b1">Click Me</button>' +
|
||||
'<div id="d1" ></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
should.equal(byId('b1').innerHTML, 'Clicked')
|
||||
should.equal(byId('d1').innerHTML, 'Foo')
|
||||
})
|
||||
|
||||
it('oob swap keeps templates not used for oob swap encapsulation', function() {
|
||||
this.server.respondWith('GET', '/test', '' +
|
||||
'Clicked<template></template>')
|
||||
var div = make('<button hx-get="/test" id="b1">Click Me</button>' +
|
||||
'<div id="d1" ></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
should.equal(byId('b1').innerHTML, 'Clicked<template></template>')
|
||||
should.equal(byId('d1').innerHTML, '')
|
||||
})
|
||||
|
||||
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
|
||||
it('oob swap supports table row in fragment along other oob swap elements with config ' + JSON.stringify(config), function() {
|
||||
Object.assign(htmx.config, config)
|
||||
@@ -213,4 +260,131 @@ describe('hx-swap-oob attribute', function() {
|
||||
byId('td1').innerHTML.should.equal('hey')
|
||||
})
|
||||
}
|
||||
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
|
||||
it('handles oob target in web components with both inside shadow root and config ' + JSON.stringify(config), function() {
|
||||
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
|
||||
class TestElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const root = this.attachShadow({ mode: 'open' })
|
||||
root.innerHTML = `
|
||||
<button hx-get="/test" hx-target="next div">Click me!</button>
|
||||
<div id="main-target"></div>
|
||||
<div id="oob-swap-target">this should get swapped</div>
|
||||
`
|
||||
htmx.process(root) // Tell HTMX about this component's shadow DOM
|
||||
}
|
||||
}
|
||||
var elementName = 'test-oobswap-inside-' + config.allowNestedOobSwaps
|
||||
customElements.define(elementName, TestElement)
|
||||
var div = make(`<div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
|
||||
var badTarget = div.querySelector('#oob-swap-target')
|
||||
var webComponent = div.querySelector(elementName)
|
||||
var btn = webComponent.shadowRoot.querySelector('button')
|
||||
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
|
||||
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
should.equal(mainTarget.textContent, 'Clicked')
|
||||
should.equal(goodTarget.textContent, 'new contents')
|
||||
should.equal(badTarget.textContent, 'this should not get swapped')
|
||||
})
|
||||
}
|
||||
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
|
||||
it('handles oob target in web components with main target outside web component config ' + JSON.stringify(config), function() {
|
||||
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
|
||||
class TestElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const root = this.attachShadow({ mode: 'open' })
|
||||
root.innerHTML = `
|
||||
<button hx-get="/test" hx-target="global #main-target">Click me!</button>
|
||||
<div id="main-target"></div>
|
||||
<div id="oob-swap-target">this should get swapped</div>
|
||||
`
|
||||
htmx.process(root) // Tell HTMX about this component's shadow DOM
|
||||
}
|
||||
}
|
||||
var elementName = 'test-oobswap-global-main-' + config.allowNestedOobSwaps
|
||||
customElements.define(elementName, TestElement)
|
||||
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
|
||||
var badTarget = div.querySelector('#oob-swap-target')
|
||||
var webComponent = div.querySelector(elementName)
|
||||
var btn = webComponent.shadowRoot.querySelector('button')
|
||||
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
|
||||
var mainTarget = div.querySelector('#main-target')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
should.equal(mainTarget.textContent, 'Clicked')
|
||||
should.equal(goodTarget.textContent, 'new contents')
|
||||
should.equal(badTarget.textContent, 'this should not get swapped')
|
||||
})
|
||||
}
|
||||
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
|
||||
it('handles global oob target in web components with main target inside web component config ' + JSON.stringify(config), function() {
|
||||
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:global #oob-swap-target">new contents</div>Clicked')
|
||||
class TestElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const root = this.attachShadow({ mode: 'open' })
|
||||
root.innerHTML = `
|
||||
<button hx-get="/test" hx-target="next div">Click me!</button>
|
||||
<div id="main-target"></div>
|
||||
<div id="oob-swap-target">this should not get swapped</div>
|
||||
`
|
||||
htmx.process(root) // Tell HTMX about this component's shadow DOM
|
||||
}
|
||||
}
|
||||
var elementName = 'test-oobswap-global-oob-' + config.allowNestedOobSwaps
|
||||
customElements.define(elementName, TestElement)
|
||||
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should get swapped</div><${elementName}/></div>`)
|
||||
var webComponent = div.querySelector(elementName)
|
||||
var badTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
|
||||
var btn = webComponent.shadowRoot.querySelector('button')
|
||||
var goodTarget = div.querySelector('#oob-swap-target')
|
||||
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
should.equal(mainTarget.textContent, 'Clicked')
|
||||
should.equal(goodTarget.textContent, 'new contents')
|
||||
should.equal(badTarget.textContent, 'this should not get swapped')
|
||||
})
|
||||
}
|
||||
|
||||
it.skip('triggers htmx:oobErrorNoTarget when no targets found', function(done) {
|
||||
// this test fails right now because when targets not found it returns an empty array which makes it miss the event as it should be if (targets.length)
|
||||
this.server.respondWith('GET', '/test', "Clicked<div id='nonexistent' hx-swap-oob='true'>Swapped</div>")
|
||||
var div = make('<div hx-get="/test">click me</div>')
|
||||
|
||||
// Define the event listener function so it can be removed later
|
||||
var eventListenerFunction = function(event) {
|
||||
event.detail.content.innerHTML.should.equal('Swapped')
|
||||
document.body.removeEventListener('htmx:oobErrorNoTarget', eventListenerFunction)
|
||||
done()
|
||||
}
|
||||
|
||||
document.body.addEventListener('htmx:oobErrorNoTarget', eventListenerFunction)
|
||||
div.click()
|
||||
this.server.respond()
|
||||
})
|
||||
|
||||
it('handles elements with IDs containing special characters properly', function() {
|
||||
this.server.respondWith('GET', '/test', '<div id="foo-/bar/" hx-swap-oob="innerHTML">Swapped10</div>')
|
||||
var div = make('<div hx-get="/test">click me</div>')
|
||||
make('<div id="foo-/bar/">Existing Content</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
var swappedElement = document.querySelector('[id="foo-/bar/"]')
|
||||
swappedElement.innerHTML.should.equal('Swapped10')
|
||||
})
|
||||
|
||||
it('handles one swap into multiple elements with the same ID properly', function() {
|
||||
this.server.respondWith('GET', '/test', '<div id="foo-/bar/" hx-swap-oob="innerHTML">Swapped11</div>')
|
||||
var div = make('<div hx-get="/test">click me</div>')
|
||||
make('<div id="foo-/bar/">Existing Content 1</div>')
|
||||
make('<div id="foo-/bar/">Existing Content 2</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
var swappedElements = document.querySelectorAll('[id="foo-/bar/"]')
|
||||
swappedElements.forEach(function(element) {
|
||||
element.innerHTML.should.equal('Swapped11')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -83,6 +83,15 @@ describe('hx-swap attribute', function() {
|
||||
byId('a1').innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('swap outerHTML on body falls back to innerHTML properly', function() {
|
||||
var fakebody = htmx._('parseHTML')('<body id="b1">Old Content</body>')
|
||||
var wa = getWorkArea()
|
||||
var fragment = htmx._('makeFragment')('<body hx-get="/test" hx-swap="outerHTML">Changed!</body>')
|
||||
wa.append(fakebody.querySelector('body'))
|
||||
htmx._('swapOuterHTML')(byId('b1'), fragment, {})
|
||||
byId('b1').innerHTML.should.equal('Changed!')
|
||||
})
|
||||
|
||||
it('swap beforebegin properly', function() {
|
||||
var i = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -274,6 +283,8 @@ describe('hx-swap attribute', function() {
|
||||
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).swapDelay.should.equal(10)
|
||||
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).settleDelay.should.equal(0)
|
||||
|
||||
swapSpec(make("<div hx-swap='transition:true'/>")).transition.should.equal(true)
|
||||
|
||||
swapSpec(make("<div hx-swap='customstyle settle:11 swap:10'/>")).swapStyle.should.equal('customstyle')
|
||||
})
|
||||
|
||||
@@ -300,6 +311,21 @@ describe('hx-swap attribute', function() {
|
||||
done()
|
||||
})
|
||||
|
||||
if (/chrome/i.test(navigator.userAgent)) {
|
||||
it('works with transition:true', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var div = make(
|
||||
"<div hx-get='/test' hx-swap='innerHTML transition:true'></div>"
|
||||
)
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerText.should.equal('')
|
||||
setTimeout(function() {
|
||||
div.innerText.should.equal('Clicked!')
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
it('works with a settle delay', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>")
|
||||
@@ -330,6 +356,115 @@ describe('hx-swap attribute', function() {
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with scroll:top', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML scroll:#container:top'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML scroll:#container:top'></div>")
|
||||
var container = make('<div id="container" style="overflow: scroll; height: 150px; width: 150px;">' +
|
||||
'<p>' +
|
||||
'Far out in the uncharted backwaters of the unfashionable end of the western' +
|
||||
'spiral arm of the Galaxy lies a small unregarded yellow sun. Orbiting this' +
|
||||
'at a distance of roughly ninety-two million miles is an utterly' +
|
||||
'insignificant little blue green planet whose ape-descended life forms are so' +
|
||||
'amazingly primitive that they still think digital watches are a pretty neat' +
|
||||
'idea.' +
|
||||
'</p>' +
|
||||
'</div>')
|
||||
container.scrollTop = 10
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
container.scrollTop.should.equal(0)
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with scroll:bottom', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML scroll:#container:bottom'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML scroll:#container:bottom'></div>")
|
||||
var container = make('<div id="container" style="overflow: scroll; height: 150px; width: 150px;">' +
|
||||
'<p>' +
|
||||
'Far out in the uncharted backwaters of the unfashionable end of the western' +
|
||||
'spiral arm of the Galaxy lies a small unregarded yellow sun. Orbiting this' +
|
||||
'at a distance of roughly ninety-two million miles is an utterly' +
|
||||
'insignificant little blue green planet whose ape-descended life forms are so' +
|
||||
'amazingly primitive that they still think digital watches are a pretty neat' +
|
||||
'idea.' +
|
||||
'</p>' +
|
||||
'</div>')
|
||||
container.scrollTop = 10
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
container.scrollTop.should.not.equal(10)
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with show:top', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:top'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:#d2:top'></div>")
|
||||
var div2 = make("<div id='d2'></div>")
|
||||
var scrollOptions
|
||||
div2.scrollIntoView = function(options) { scrollOptions = options }
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
scrollOptions.block.should.equal('start')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with show:bottom', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:bottom'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:#d2:bottom'></div>")
|
||||
var div2 = make("<div id='d2'></div>")
|
||||
var scrollOptions
|
||||
div2.scrollIntoView = function(options) { scrollOptions = options }
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
scrollOptions.block.should.equal('end')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with show:window:bottom', function(done) {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:window:bottom'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:window:bottom'></div>")
|
||||
var scrollOptions
|
||||
document.body.scrollIntoView = function(options) { scrollOptions = options }
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
scrollOptions.block.should.equal('end')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('works with focus-scroll:true', function(done) {
|
||||
// no easy way to tell if the scroll worked as expected
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML focus-scroll:true'><input id='i2' type='text'></div>")
|
||||
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML focus-scroll:true'><input id='i2' type='text'></div>")
|
||||
byId('i2').focus()
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
setTimeout(function() {
|
||||
byId('d1').classList.contains('foo').should.equal(true)
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('swap outerHTML properly w/ data-* prefix', function() {
|
||||
this.server.respondWith('GET', '/test', '<a id="a1" data-hx-get="/test2">Click Me</a>')
|
||||
this.server.respondWith('GET', '/test2', 'Clicked!')
|
||||
@@ -402,4 +537,27 @@ describe('hx-swap attribute', function() {
|
||||
btn.innerText.should.equal('Clicked!')
|
||||
window.document.title.should.equal('Test Title')
|
||||
})
|
||||
|
||||
it('swapError fires if swap throws exception', function() {
|
||||
try {
|
||||
// override makeSettleInfo to cause swap function to throw exception
|
||||
htmx._('htmx.backupMakeSettleInfo = makeSettleInfo')
|
||||
htmx._('makeSettleInfo = function() { throw new Error("throw") }')
|
||||
var error = false
|
||||
var handler = htmx.on('htmx:swapError', function(evt) {
|
||||
error = true
|
||||
})
|
||||
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
var div = make("<div hx-get='/test'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
} catch (e) {
|
||||
} finally {
|
||||
div.innerHTML.should.equal('')
|
||||
error.should.equal(true)
|
||||
htmx.off('htmx:swapError', handler)
|
||||
htmx._('makeSettleInfo = htmx.backupMakeSettleInfo')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,7 @@ describe('hx-trigger attribute', function() {
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
})
|
||||
|
||||
// This test and the next one should be kept in sync.
|
||||
it('changed modifier works along from clause with two inputs', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -106,6 +107,92 @@ describe('hx-trigger attribute', function() {
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
})
|
||||
|
||||
// This test and the previous one should be kept in sync.
|
||||
it('changed modifier counts each triggerspec separately', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
requests++
|
||||
xhr.respond(200, {}, 'Requests: ' + requests)
|
||||
})
|
||||
var input1 = make('<input type="text"/>')
|
||||
var input2 = make('<input type="text"/>')
|
||||
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
|
||||
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
|
||||
var div = make('<div id="d1"></div>')
|
||||
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
|
||||
input1.value = 'bar'
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
|
||||
input2.value = 'foo'
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 4')
|
||||
})
|
||||
|
||||
it('separate changed modifier works along from clause with two inputs', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
requests++
|
||||
xhr.respond(200, {}, 'Requests: ' + requests)
|
||||
})
|
||||
var input1 = make('<input type="text"/>')
|
||||
var input2 = make('<input type="text"/>')
|
||||
make('<div hx-trigger="click changed from:input:nth-child(1), click changed from:input:nth-child(2)" hx-target="#d1" hx-get="/test"></div>')
|
||||
var div = make('<div id="d1"></div>')
|
||||
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
|
||||
input1.value = 'bar'
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
|
||||
input2.value = 'foo'
|
||||
input1.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
input2.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
})
|
||||
|
||||
it('once modifier works', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -367,7 +454,7 @@ describe('hx-trigger attribute', function() {
|
||||
div.dispatchEvent(event)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Not Called')
|
||||
foo = true
|
||||
window.foo = true
|
||||
div.dispatchEvent(event)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Called!')
|
||||
@@ -409,6 +496,47 @@ describe('hx-trigger attribute', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('filters properly with true for empty condition', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called!')
|
||||
var form = make('<form hx-get="/test" hx-trigger="evt[]">Not Called</form>')
|
||||
form.click()
|
||||
form.innerHTML.should.equal('Not Called')
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
form.dispatchEvent(event)
|
||||
this.server.respond()
|
||||
form.innerHTML.should.equal('Called!')
|
||||
})
|
||||
|
||||
it('syntax error in condition issues error', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called!')
|
||||
var errorEvent = null
|
||||
var handler = htmx.on('htmx:syntax:error', function(event) {
|
||||
errorEvent = event
|
||||
})
|
||||
var form = make('<form hx-get="/test" hx-trigger="evt[{]">Not Called</form>')
|
||||
try {
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
form.dispatchEvent(event)
|
||||
should.not.equal(null, errorEvent)
|
||||
should.not.equal(null, errorEvent.detail.source)
|
||||
console.log(errorEvent.detail.source)
|
||||
} finally {
|
||||
htmx.off('htmx:syntax:error', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('filters properly with condition containing square backets', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called!')
|
||||
var form = make('<form hx-get="/test" hx-trigger="evt[foo[0]]">Not Called</form>')
|
||||
form.click()
|
||||
form.innerHTML.should.equal('Not Called')
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
event.foo = [true]
|
||||
form.dispatchEvent(event)
|
||||
this.server.respond()
|
||||
form.innerHTML.should.equal('Called!')
|
||||
})
|
||||
|
||||
it('from clause works', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -570,6 +698,26 @@ describe('hx-trigger attribute', function() {
|
||||
div1.innerHTML.should.equal('Requests: 2')
|
||||
})
|
||||
|
||||
it('from clause works with multiple extended selectors', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
requests++
|
||||
xhr.respond(200, {}, 'Requests: ' + requests)
|
||||
})
|
||||
make('<button id="btn" type="button">Click me</button>' +
|
||||
'<div hx-trigger="click from:(previous button, next a)" hx-target="#a1" hx-get="/test"></div>' +
|
||||
'<a id="a1">Requests: 0</a>')
|
||||
var btn = byId('btn')
|
||||
var a1 = byId('a1')
|
||||
a1.innerHTML.should.equal('Requests: 0')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
a1.innerHTML.should.equal('Requests: 1')
|
||||
a1.click()
|
||||
this.server.respond()
|
||||
a1.innerHTML.should.equal('Requests: 2')
|
||||
})
|
||||
|
||||
it('event listeners can filter on target', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -657,7 +805,7 @@ describe('hx-trigger attribute', function() {
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('A throttle of 0 does not multiple requests from happening', function(done) {
|
||||
it('A throttle of 0 does not prevent multiple requests from happening', function(done) {
|
||||
var requests = 0
|
||||
var server = this.server
|
||||
server.respondWith('GET', '/test', function(xhr) {
|
||||
@@ -875,6 +1023,60 @@ describe('hx-trigger attribute', function() {
|
||||
div2.innerHTML.should.equal('test 2')
|
||||
})
|
||||
|
||||
it('scrolling triggers revealed event', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'test')
|
||||
this.server.autoRespond = true
|
||||
this.server.autoRespondAfter = 0
|
||||
var div = make('<div hx-get="/test" hx-trigger="revealed"></div>')
|
||||
div.innerHTML.should.equal('')
|
||||
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
|
||||
htmx.trigger(document.body, 'scroll')
|
||||
|
||||
setTimeout(function() {
|
||||
div.innerHTML.should.equal('test')
|
||||
done()
|
||||
}, 250)
|
||||
})
|
||||
|
||||
if (/headlesschrome/i.test(navigator.userAgent)) {
|
||||
it('scrolling triggers intersect event', function(done) {
|
||||
// test only works reliably with playwright
|
||||
this.server.respondWith('GET', '/test', 'test')
|
||||
this.server.autoRespond = true
|
||||
this.server.autoRespondAfter = 0
|
||||
var div = make('<div hx-get="/test" hx-trigger="intersect"></div>')
|
||||
div.innerHTML.should.equal('')
|
||||
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
|
||||
htmx.trigger(document.body, 'scroll')
|
||||
|
||||
setTimeout(function() {
|
||||
div.innerHTML.should.equal('test')
|
||||
done()
|
||||
}, 250)
|
||||
})
|
||||
}
|
||||
|
||||
it('triggering revealed while component not yet inited still works', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'test')
|
||||
var div = make('<div hx-get="/test" hx-trigger="revealed"></div>')
|
||||
var data = div['htmx-internal-data']
|
||||
delete data.initHash // simulate not inited or revealed yet
|
||||
div.removeAttribute('data-hx-revealed')
|
||||
var server1 = this.server
|
||||
div.innerHTML.should.equal('')
|
||||
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
|
||||
htmx.trigger(document.body, 'scroll')
|
||||
setTimeout(function() {
|
||||
server1.autoRespond = true
|
||||
server1.autoRespondAfter = 0
|
||||
htmx.process(div) // processing the div should also trigger revealed event now
|
||||
setTimeout(function() {
|
||||
div.innerHTML.should.equal('test')
|
||||
done()
|
||||
}, 10)
|
||||
}, 250)
|
||||
})
|
||||
|
||||
it('reveal event works when triggered by window', function() {
|
||||
this.server.respondWith('GET', '/test1', 'test 1')
|
||||
var div = make('<div hx-get="/test1" hx-trigger="revealed" style="position: fixed; top: 1px; left: 1px; border: 3px solid red">foo</div>')
|
||||
@@ -939,6 +1141,45 @@ describe('hx-trigger attribute', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('fires the htmx:trigger event for delayed triggers', function(done) {
|
||||
var param = 'foo'
|
||||
var handler = htmx.on('htmx:trigger', function(evt) {
|
||||
param = 'bar'
|
||||
})
|
||||
var div = make('<button hx-trigger="click delay:10ms">Submit</button>')
|
||||
div.click()
|
||||
setTimeout(function() {
|
||||
try {
|
||||
should.equal(param, 'bar')
|
||||
done()
|
||||
} finally {
|
||||
htmx.off('htmx:trigger', handler)
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('fires the htmx:trigger event when the trigger is a load', function(done) {
|
||||
this.server.respondWith(
|
||||
'GET',
|
||||
'/test',
|
||||
'<div hx-trigger="load delay:50ms" hx-on::trigger="this.innerText = \'Done\'">Response</div>'
|
||||
)
|
||||
|
||||
var div = make('<div hx-get="/test">Submit</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
var response = div.children[0]
|
||||
response.innerText.should.equal('Response')
|
||||
|
||||
setTimeout(function() {
|
||||
try {
|
||||
response.innerText.should.equal('Done')
|
||||
done()
|
||||
} finally {
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
it('filters support "this" reference to the current element', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called!')
|
||||
var form = make('<form hx-get="/test" hx-trigger="click[this.classList.contains(\'bar\')]">Not Called</form>')
|
||||
@@ -984,10 +1225,6 @@ describe('hx-trigger attribute', function() {
|
||||
it('correctly handles CSS descendant combinators in modifier target', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called')
|
||||
|
||||
document.addEventListener('htmx:syntax:error', function(evt) {
|
||||
chai.assert.fail('htmx:syntax:error')
|
||||
})
|
||||
|
||||
make('<div class="d1"><a id="a1" class="a1">Click me</a><a id="a2" class="a2">Click me</a></div>')
|
||||
var div = make('<div hx-trigger="click from:body target:(.d1 .a2)" hx-get="/test">Not Called</div>')
|
||||
|
||||
@@ -1002,12 +1239,52 @@ describe('hx-trigger attribute', function() {
|
||||
|
||||
it('correctly handles CSS descendant combinators in modifier root', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called')
|
||||
|
||||
document.addEventListener('htmx:syntax:error', function(evt) {
|
||||
chai.assert.fail('htmx:syntax:error')
|
||||
var errorEvent = null
|
||||
var handler = htmx.on('htmx:syntax:error', function(event) {
|
||||
errorEvent = event
|
||||
})
|
||||
var form = make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>')
|
||||
try {
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
form.dispatchEvent(event)
|
||||
should.equal(null, errorEvent)
|
||||
} finally {
|
||||
htmx.off('htmx:syntax:error', handler)
|
||||
}
|
||||
})
|
||||
|
||||
make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>')
|
||||
it('correctly handles intersect with modifier threshold', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called')
|
||||
var errorEvent = null
|
||||
var handler = htmx.on('htmx:syntax:error', function(event) {
|
||||
errorEvent = event
|
||||
})
|
||||
var form = make('<div hx-trigger="intersect threshold:0.5" hx-get="/test">Not Called</div>')
|
||||
try {
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
form.dispatchEvent(event)
|
||||
should.equal(null, errorEvent)
|
||||
} finally {
|
||||
htmx.off('htmx:syntax:error', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('issues error with invalid trigger spec', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called')
|
||||
var errorEvent = null
|
||||
var handler = htmx.on('htmx:syntax:error', function(event) {
|
||||
errorEvent = event
|
||||
})
|
||||
var form = make('<div hx-trigger="intersect invalid:0.5" hx-get="/test">Not Called</div>')
|
||||
try {
|
||||
var event = htmx._('makeEvent')('evt')
|
||||
form.dispatchEvent(event)
|
||||
should.not.equal(null, errorEvent)
|
||||
should.not.equal(null, errorEvent.detail.source)
|
||||
console.log(errorEvent.detail.source)
|
||||
} finally {
|
||||
htmx.off('htmx:syntax:error', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses trigger specs cache if defined', function() {
|
||||
@@ -1062,4 +1339,35 @@ describe('hx-trigger attribute', function() {
|
||||
|
||||
htmx.config.triggerSpecsCache = initialCacheConfig
|
||||
})
|
||||
|
||||
it('handles spaces at the end of trigger specs', function() {
|
||||
var requests = 0
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
requests++
|
||||
xhr.respond(200, {}, 'Requests: ' + requests)
|
||||
})
|
||||
var div = make('<div hx-trigger="load , click consume " hx-get="/test">Requests: 0</div>')
|
||||
div.innerHTML.should.equal('Requests: 0')
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 1')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Requests: 2')
|
||||
})
|
||||
|
||||
it('Removing polling trigger and processing node removes timeout', function(complete) {
|
||||
this.server.respondWith('GET', '/test', 'Called!')
|
||||
var div = make('<div hx-get="/test" hx-trigger="every 5ms">Not Called</div>')
|
||||
div.removeAttribute('hx-trigger')
|
||||
should.not.equal(div['htmx-internal-data'].timeout, undefined)
|
||||
htmx.process(div)
|
||||
should.equal(div['htmx-internal-data'].timeout, undefined)
|
||||
this.server.autoRespond = true
|
||||
this.server.autoRespondAfter = 0
|
||||
setTimeout(function() {
|
||||
div.innerHTML.should.equal('Not Called')
|
||||
delete window.foo
|
||||
complete()
|
||||
}, 30)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,6 +45,23 @@ describe('hx-vals attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('Dynamic hx-vals using spread operator works', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.v1.should.equal('test')
|
||||
params.v2.should.equal('42')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
window.foo = function() {
|
||||
return { v1: 'test', v2: 42 }
|
||||
}
|
||||
var div = make("<div hx-post='/vars' hx-vals='js:{...foo()}'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('hx-vals can be on parents', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
@@ -120,7 +137,7 @@ describe('hx-vals attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('multiple hx-vals works', function() {
|
||||
it('multiple hx-vals works with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.v1.should.equal('test')
|
||||
@@ -133,7 +150,7 @@ describe('hx-vals attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-vals can be on parents', function() {
|
||||
it('hx-vals can be on parents with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
@@ -146,7 +163,7 @@ describe('hx-vals attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-vals can override parents', function() {
|
||||
it('hx-vals can override parents with javascript', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('best')
|
||||
@@ -159,7 +176,7 @@ describe('hx-vals attribute', function() {
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-vals overrides inputs', function() {
|
||||
it('hx-vals overrides inputs with javascript', function() {
|
||||
this.server.respondWith('POST', '/include', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('best')
|
||||
@@ -297,4 +314,66 @@ describe('hx-vals attribute', function() {
|
||||
}
|
||||
calledEvent.should.equal(true)
|
||||
})
|
||||
|
||||
it('using js: with hx-vals has event available', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var div = make('<div id="test" hx-post="/vars" hx-vals="js:i1:event.target.id"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('using js: with hx-vals has event available when used with a delay', function(done) {
|
||||
var params = null
|
||||
var div = make('<div id="test" hx-post="/vars" hx-vals="js:{i1:event.target.id}" hx-trigger="click delay:10ms"></div>')
|
||||
htmx.on(div, 'htmx:configRequest', function(evt) {
|
||||
evt.preventDefault()
|
||||
params = evt.detail.parameters
|
||||
}, { once: true })
|
||||
div.click()
|
||||
new Promise(resolve => setTimeout(resolve, 20)).then(function() {
|
||||
params.i1.should.equal('test')
|
||||
done()
|
||||
}).catch(done)
|
||||
})
|
||||
|
||||
it('hx-vals works with null values', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('null')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var div = make("<div hx-post='/vars' hx-vals='{\"i1\": null }'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('hx-vals works with object values', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('{"a":"b"}')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var div = make("<div hx-post='/vars' hx-vals='{\"i1\": { \"a\": \"b\" } }'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('js: this refers to the element with the hx-vals attribute', function() {
|
||||
this.server.respondWith('POST', '/vars', function(xhr) {
|
||||
var params = getParameters(xhr)
|
||||
params.i1.should.equal('test')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
var div = make('<div hx-post="/vars" hx-vals="javascript:{ ...this.dataset }" data-i1="test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -559,6 +559,34 @@ describe('Core htmx AJAX Tests', function() {
|
||||
values.should.deep.equal({ c1: ['cb1', 'cb3'], c2: 'cb5', c3: 'cb6' })
|
||||
})
|
||||
|
||||
it('properly handles radio inputs', function() {
|
||||
var values
|
||||
this.server.respondWith('Post', '/test', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
|
||||
var form = make('<form hx-post="/test" hx-trigger="click">' +
|
||||
'<div role="radiogroup">' +
|
||||
'<input id="rb1" name="r1" value="rb1" type="radio">' +
|
||||
'<input id="rb2" name="r1" value="rb2" type="radio">' +
|
||||
'<input id="rb3" name="r1" value="rb3" type="radio">' +
|
||||
'<input id="rb4" name="r2" value="rb4" type="radio">' +
|
||||
'<input id="rb5" name="r2" value="rb5" type="radio">' +
|
||||
'<input id="rb6" name="r3" value="rb6" type="radio">' +
|
||||
'</div>' +
|
||||
'</form>')
|
||||
|
||||
form.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({})
|
||||
|
||||
byId('rb1').checked = true
|
||||
form.click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ r1: 'rb1' })
|
||||
})
|
||||
|
||||
it('text nodes dont screw up settling via variable capture', function() {
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' hx-trigger='click consume' hx-get='/test2'></div>fooo")
|
||||
this.server.respondWith('GET', '/test2', 'clicked')
|
||||
@@ -936,7 +964,11 @@ describe('Core htmx AJAX Tests', function() {
|
||||
|
||||
it('scripts w/ src attribute are properly loaded', function(done) {
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='setGlobal.js'></script>")
|
||||
if (byId('mocha')) {
|
||||
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='setGlobal.js'></script>")
|
||||
} else {
|
||||
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='/test/setGlobal.js'></script>")
|
||||
}
|
||||
var div = make("<div hx-get='/test'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
@@ -1156,6 +1188,48 @@ describe('Core htmx AJAX Tests', function() {
|
||||
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
|
||||
})
|
||||
|
||||
it('sends referenced form values when a button referencing another form is clicked', function() {
|
||||
var values
|
||||
this.server.respondWith('POST', '/test3', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(205, {}, '')
|
||||
})
|
||||
|
||||
make('<form id="externalForm" hx-post="/test">' +
|
||||
'<input type="text" name="t1" value="textValue">' +
|
||||
'<input type="hidden" name="b1" value="inputValue">' +
|
||||
'</form>' +
|
||||
'<form hx-post="/test2">' +
|
||||
'<input type="text" name="t1" value="checkValue">' +
|
||||
'<button id="submit" form="externalForm" hx-post="/test3" type="submit" name="b1" value="buttonValue">button</button>' +
|
||||
'</form>')
|
||||
|
||||
byId('submit').click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
|
||||
})
|
||||
|
||||
it('sends referenced form values when a submit input referencing another form is clicked', function() {
|
||||
var values
|
||||
this.server.respondWith('POST', '/test3', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
|
||||
make('<form id="externalForm" hx-post="/test">' +
|
||||
'<input type="text" name="t1" value="textValue">' +
|
||||
'<input type="hidden" name="b1" value="inputValue">' +
|
||||
'</form>' +
|
||||
'<form hx-post="/test2">' +
|
||||
'<input type="text" name="t1" value="checkValue">' +
|
||||
'<input id="submit" form="externalForm" hx-post="/test3" type="submit" name="b1" value="buttonValue">' +
|
||||
'</form>')
|
||||
|
||||
byId('submit').click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
|
||||
})
|
||||
|
||||
it('properly handles inputs external to form', function() {
|
||||
var values
|
||||
this.server.respondWith('Post', '/test', function(xhr) {
|
||||
@@ -1178,32 +1252,18 @@ describe('Core htmx AJAX Tests', function() {
|
||||
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'], s1: 'selectValue' })
|
||||
})
|
||||
|
||||
it('handles form post with button formmethod dialog properly', function() {
|
||||
var values
|
||||
it('properly handles buttons with formmethod=dialog', function() {
|
||||
var request = false
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(200, {}, '')
|
||||
request = true
|
||||
xhr.respond(200, {}, '<button>Bar</button>')
|
||||
})
|
||||
|
||||
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">submit</button></form></dialog>')
|
||||
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">Submit</button></form></dialog>')
|
||||
|
||||
byId('submit').click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('handles form get with button formmethod dialog properly', function() {
|
||||
var responded = false
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
responded = true
|
||||
xhr.respond(200, {}, '')
|
||||
})
|
||||
|
||||
make('<dialog><form hx-get="/test"><button id="submit" formmethod="dialog">submit</button></form></dialog>')
|
||||
|
||||
byId('submit').click()
|
||||
this.server.respond()
|
||||
responded.should.equal(true)
|
||||
request.should.equal(false)
|
||||
})
|
||||
|
||||
it('can associate submit buttons from outside a form with the current version of the form after swap', function() {
|
||||
@@ -1234,4 +1294,66 @@ describe('Core htmx AJAX Tests', function() {
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ name: '', outside: '' })
|
||||
})
|
||||
|
||||
it('properly handles form reset behaviour with a htmx enabled reset button inside a form', function() {
|
||||
var values
|
||||
this.server.respondWith('POST', '/reset', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
|
||||
make('<form id="externalForm" hx-post="/test">' +
|
||||
'<input id="t1" type="text" name="t1" value="defaultValue">' +
|
||||
'<button hx-post="/reset" id="reset" type="reset" name="b1" value="buttonValue">reset</button>' +
|
||||
'</form>')
|
||||
byId('t1').value = 'otherValue'
|
||||
byId('reset').click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ b1: 'buttonValue', t1: 'otherValue' })
|
||||
byId('t1').value.should.equal('defaultValue')
|
||||
})
|
||||
|
||||
it('properly handles form reset behaviour with a htmx enabled reset button outside a form', function() {
|
||||
var values
|
||||
this.server.respondWith('POST', '/reset', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(204, {}, '')
|
||||
})
|
||||
|
||||
make('<form id="externalForm" hx-post="/test">' +
|
||||
'<input id="t1" type="text" name="t1" value="defaultValue">' +
|
||||
'</form>' +
|
||||
'<button hx-post="/reset" id="reset" form="externalForm" type="reset" name="b1" value="buttonValue">reset</button>')
|
||||
byId('t1').value = 'otherValue'
|
||||
byId('reset').click()
|
||||
this.server.respond()
|
||||
values.should.deep.equal({ b1: 'buttonValue', t1: 'otherValue' })
|
||||
byId('t1').value.should.equal('defaultValue')
|
||||
})
|
||||
|
||||
it('script tags get swapped in with nonce applied from inlineScriptNonce', function() {
|
||||
var globalWasCalled = false
|
||||
window.callGlobal = function() {
|
||||
globalWasCalled = true
|
||||
}
|
||||
htmx.config.inlineScriptNonce = 'testnonce'
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', "<script id='noncescript'>callGlobal()</script>")
|
||||
var div = make("<div hx-get='/test'></div>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
globalWasCalled.should.equal(true)
|
||||
byId('noncescript').nonce.should.equal('testnonce')
|
||||
} finally {
|
||||
delete window.callGlobal
|
||||
htmx.config.inlineScriptNonce = ''
|
||||
}
|
||||
})
|
||||
|
||||
it('normalizeScriptTags logs error when insertBefore fails', function() {
|
||||
htmx.div = make('<div><script></script></div>')
|
||||
htmx.div.insertBefore = undefined
|
||||
htmx._('normalizeScriptTags(htmx.div)')
|
||||
delete htmx.div
|
||||
})
|
||||
})
|
||||
|
||||
327
test/core/api.js
327
test/core/api.js
@@ -48,6 +48,17 @@ describe('Core htmx API test', function() {
|
||||
div.innerHTML.should.equal('')
|
||||
})
|
||||
|
||||
it('remove element with delay properly', function(done) {
|
||||
var div = make('<div><a></a></div>')
|
||||
var a = htmx.find(div, 'a')
|
||||
htmx.remove(a, 10)
|
||||
div.innerHTML.should.not.equal('')
|
||||
setTimeout(function() {
|
||||
div.innerHTML.should.equal('')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('should remove element properly w/ selector', function() {
|
||||
var div = make("<div><a id='a1'></a></div>")
|
||||
var a = htmx.find(div, 'a')
|
||||
@@ -88,6 +99,10 @@ describe('Core htmx API test', function() {
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
})
|
||||
|
||||
it('should not error if you remove class from invalid element', function() {
|
||||
htmx.removeClass(null, 'foo')
|
||||
})
|
||||
|
||||
it('should remove class properly w/ selector', function() {
|
||||
var div = make("<div id='div1'></div>")
|
||||
htmx.addClass(div, 'foo')
|
||||
@@ -96,7 +111,7 @@ describe('Core htmx API test', function() {
|
||||
div.classList.contains('foo').should.equal(false)
|
||||
})
|
||||
|
||||
it('should add class properly after delay', function(done) {
|
||||
it('should remove class properly after delay', function(done) {
|
||||
var div = make('<div></div>')
|
||||
htmx.addClass(div, 'foo')
|
||||
div.classList.contains('foo').should.equal(true)
|
||||
@@ -213,6 +228,83 @@ describe('Core htmx API test', function() {
|
||||
div.innerHTML.should.equal('foo!')
|
||||
})
|
||||
|
||||
it('ajax api does not fall back to body when target invalid', function() {
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('GET', '/test', '#d2')
|
||||
this.server.respond()
|
||||
document.body.innerHTML.should.not.equal('foo!')
|
||||
})
|
||||
|
||||
it('ajax api fails when target invalid', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('GET', '/test', '#d2').then(
|
||||
(value) => {
|
||||
},
|
||||
(reason) => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
})
|
||||
|
||||
it('ajax api fails when target invalid even if source set', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('GET', '/test', {
|
||||
source: div,
|
||||
target: '#d2'
|
||||
}).then(
|
||||
(value) => {
|
||||
},
|
||||
(reason) => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
})
|
||||
|
||||
it('ajax api fails when source invalid and no target set', function(done) {
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('GET', '/test', {
|
||||
source: '#d2'
|
||||
}).then(
|
||||
(value) => {
|
||||
},
|
||||
(reason) => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
})
|
||||
|
||||
it('ajax api falls back to targeting source if target not set', function() {
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('GET', '/test', {
|
||||
source: div
|
||||
})
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('foo!')
|
||||
})
|
||||
|
||||
it('ajax api falls back to targeting body if target and source not set', function() {
|
||||
var target
|
||||
this.server.respondWith('GET', '/test', 'foo!')
|
||||
htmx.on(document.body, 'htmx:configRequest', function(evt) {
|
||||
target = evt.detail.target
|
||||
return false
|
||||
})
|
||||
htmx.ajax('GET', '/test', { swap: 'none' })
|
||||
this.server.respond()
|
||||
target.should.equal(document.body)
|
||||
})
|
||||
|
||||
it('ajax api works with swapSpec', function() {
|
||||
this.server.respondWith('GET', '/test', "<p class='test'>foo!</p>")
|
||||
var div = make("<div><div id='target'></div></div>")
|
||||
@@ -229,6 +321,16 @@ describe('Core htmx API test', function() {
|
||||
div.innerHTML.should.equal('<div id="d2">bar</div>')
|
||||
})
|
||||
|
||||
it('ajax api works with selectOOB', function() {
|
||||
this.server.respondWith('GET', '/test', "<div id='oob'>OOB Content</div><div>Main Content</div>")
|
||||
var target = make("<div id='target'></div>")
|
||||
var oobDiv = make("<div id='oob'></div>")
|
||||
htmx.ajax('GET', '/test', { target: '#target', selectOOB: '#oob:innerHTML' })
|
||||
this.server.respond()
|
||||
target.innerHTML.should.equal('<div>Main Content</div>')
|
||||
oobDiv.innerHTML.should.equal('OOB Content')
|
||||
})
|
||||
|
||||
it('ajax api works with Hx-Select overrides select', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': '#d2' }, "<div id='d1'>foo</div><div id='d2'>bar</div>"])
|
||||
var div = make("<div id='target'></div>")
|
||||
@@ -318,6 +420,17 @@ describe('Core htmx API test', function() {
|
||||
div.innerHTML.should.equal('delete')
|
||||
})
|
||||
|
||||
it('does not trigger load on re-init of an existing element', function() {
|
||||
this.server.respondWith('GET', '/test', 'test')
|
||||
var div = make('<div hx-get="/test" hx-trigger="load" hx-swap="beforeend"></div>')
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('test')
|
||||
div.setAttribute('hx-swap', 'afterbegin')
|
||||
htmx.process(div)
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('test')
|
||||
})
|
||||
|
||||
it('onLoad is called... onLoad', function() {
|
||||
// also tests on/off
|
||||
this.server.respondWith('GET', '/test', "<div id='d1' hx-get='/test'></div>")
|
||||
@@ -382,6 +495,29 @@ describe('Core htmx API test', function() {
|
||||
output.innerHTML.should.be.equal('<div>Swapped!</div>')
|
||||
})
|
||||
|
||||
it('swap works with a swap delay', function(done) {
|
||||
var div = make("<div hx-get='/test'></div>")
|
||||
div.innerText.should.equal('')
|
||||
htmx.swap(div, 'jsswapped', { swapDelay: 10 })
|
||||
div.innerText.should.equal('')
|
||||
setTimeout(function() {
|
||||
div.innerText.should.equal('jsswapped')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
if (/chrome/i.test(navigator.userAgent)) {
|
||||
it('swap works with a view transition', function(done) {
|
||||
var div = make("<div hx-get='/test'></div>")
|
||||
div.innerText.should.equal('')
|
||||
htmx.swap(div, 'jsswapped', { transition: true })
|
||||
div.innerText.should.equal('')
|
||||
setTimeout(function() {
|
||||
div.innerText.should.equal('jsswapped')
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
it('swaps content properly (with select)', function() {
|
||||
var output = make('<output id="output"/>')
|
||||
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', { swapStyle: 'innerHTML' }, { select: '#select-me' })
|
||||
@@ -403,4 +539,193 @@ describe('Core htmx API test', function() {
|
||||
output.innerHTML.should.be.equal('<div>Swapped!</div>')
|
||||
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
|
||||
})
|
||||
|
||||
it('swap delete works when parent is removed', function() {
|
||||
this.server.respondWith('DELETE', '/test', 'delete')
|
||||
|
||||
var parent = make('<div><div id="d1" hx-swap="delete" hx-delete="/test">click me</div></div>')
|
||||
var div = htmx.find(parent, '#d1')
|
||||
div.click()
|
||||
div.remove()
|
||||
parent.remove()
|
||||
this.server.respond()
|
||||
parent.children.length.should.equal(0)
|
||||
})
|
||||
|
||||
it('swap outerHTML works when parent is removed', function() {
|
||||
this.server.respondWith('GET', '/test', 'delete')
|
||||
|
||||
var parent = make('<div><div id="d1" hx-swap="outerHTML" hx-get="/test">click me</div></div>')
|
||||
var div = htmx.find(parent, '#d1')
|
||||
div.click()
|
||||
div.remove()
|
||||
parent.remove()
|
||||
this.server.respond()
|
||||
parent.children.length.should.equal(0)
|
||||
})
|
||||
|
||||
it('values api returns formDataProxy with correct form data even if clicked button removed', function() {
|
||||
make('<form id="valuesform" hx-post="/test">' +
|
||||
'<input type="text" name="t1" value="textValue">' +
|
||||
'<button id="submit" type="submit" name="b1" value="buttonValue">button</button>' +
|
||||
'</form>')
|
||||
|
||||
var apiValues = htmx.values(byId('valuesform'), 'post')
|
||||
apiValues.get('t1').should.equal('textValue')
|
||||
should.equal(apiValues.get('b1'), null)
|
||||
byId('submit').click()
|
||||
apiValues = htmx.values(byId('valuesform'), 'post')
|
||||
apiValues.get('t1').should.equal('textValue')
|
||||
apiValues.get('b1').should.equal('buttonValue')
|
||||
byId('submit').remove()
|
||||
apiValues = htmx.values(byId('valuesform'), 'post')
|
||||
JSON.stringify(apiValues).should.equal('{"t1":"textValue"}')
|
||||
var assign = Object.assign({}, apiValues)
|
||||
JSON.stringify(assign).should.equal('{"t1":"textValue"}')
|
||||
})
|
||||
|
||||
it('tests for formDataProxy array updating and testing for loc coverage', function() {
|
||||
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>')
|
||||
var vals = htmx.values(form, 'post')
|
||||
vals.foo.should.equal('bar')
|
||||
vals.do.should.deep.equal(['rey', 'rey'])
|
||||
should.equal(vals.do.toString(), undefined)
|
||||
vals.do.push('test')
|
||||
vals.do.should.deep.equal(['rey', 'rey', 'test'])
|
||||
vals.do = ['bob', 'jim']
|
||||
vals.do.should.deep.equal(['bob', 'jim'])
|
||||
vals.do[0] = 'hi'
|
||||
vals.do.should.deep.equal(['hi', 'jim'])
|
||||
var arr = vals.do
|
||||
arr[0] = ['override']
|
||||
arr[0].should.equal('override')
|
||||
vals.do.should.deep.equal(['override', 'jim'])
|
||||
vals[Symbol.toStringTag].should.equal('FormData')
|
||||
try {
|
||||
vals[Symbol.toStringTag] = 'notFormData' // should do nothing
|
||||
} catch (e) {}
|
||||
vals[Symbol.toStringTag].should.equal('FormData')
|
||||
})
|
||||
|
||||
it('logAll() and logNone() run without error', function() {
|
||||
make('<div id="d1"></div>')
|
||||
htmx.logAll()
|
||||
htmx.trigger(byId('d1'), 'test-event')
|
||||
htmx.logNone()
|
||||
})
|
||||
|
||||
it('querySelectorExt internal extension api works with just string', function() {
|
||||
make('<div id="d1">content</div>')
|
||||
var div = htmx._('querySelectorExt("#d1")')
|
||||
div.innerHTML.should.equal('content')
|
||||
})
|
||||
|
||||
it('ajax api with no context works', function() {
|
||||
// request would replace body so prevent ths with 204 response
|
||||
var status
|
||||
var handler = htmx.on('htmx:beforeSwap', function(event) {
|
||||
status = event.detail.xhr.status
|
||||
})
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.respond(204, {}, 'foo!')
|
||||
})
|
||||
var div = make('<div></div>')
|
||||
htmx.ajax('GET', '/test')
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('')
|
||||
status.should.equal(204)
|
||||
htmx.off('htmx:beforeSwap', handler)
|
||||
})
|
||||
|
||||
it('ajax api with can pass in custom handler and handle if it throws error', function() {
|
||||
var onLoadError = false
|
||||
var handler = htmx.on('htmx:onLoadError', function(event) {
|
||||
onLoadError = true
|
||||
})
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.respond(204, {}, 'foo!')
|
||||
})
|
||||
var div = make('<div></div>')
|
||||
try {
|
||||
htmx.ajax('GET', '/test', { handler: function() { throw new Error('throw') } })
|
||||
this.server.respond()
|
||||
} catch (e) {}
|
||||
div.innerHTML.should.equal('')
|
||||
onLoadError.should.equal(true)
|
||||
|
||||
// repeat the error resonse a 2nd time to make sure request lock removed after error
|
||||
onLoadError = false
|
||||
try {
|
||||
htmx.ajax('GET', '/test', { handler: function() { throw new Error('throw') } })
|
||||
this.server.respond()
|
||||
} catch (e) {}
|
||||
div.innerHTML.should.equal('')
|
||||
onLoadError.should.equal(true)
|
||||
htmx.off('htmx:onLoadError', handler)
|
||||
})
|
||||
|
||||
it('process api can process non elements fine', function() {
|
||||
var div = make('<div>textNode</div>')
|
||||
htmx.process(div.firstChild)
|
||||
})
|
||||
|
||||
it('ajax api push Url should push an element into the cache when true', function() {
|
||||
this.server.respondWith('POST', '/test123', 'Clicked!')
|
||||
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('POST', '/test123', {
|
||||
target: '#d1',
|
||||
swap: 'innerHTML',
|
||||
push: 'true'
|
||||
})
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/test123')
|
||||
})
|
||||
|
||||
it('ajax api push Url should push an element into the cache when string', function() {
|
||||
this.server.respondWith('POST', '/test', 'Clicked!')
|
||||
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('POST', '/test', {
|
||||
target: '#d1',
|
||||
swap: 'innerHTML',
|
||||
push: '/abc123'
|
||||
})
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/abc123')
|
||||
})
|
||||
|
||||
it('ajax api replace Url should replace an element into the cache when true', function() {
|
||||
this.server.respondWith('POST', '/test123', 'Clicked!')
|
||||
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('POST', '/test123', {
|
||||
target: '#d1',
|
||||
swap: 'innerHTML',
|
||||
replace: 'true'
|
||||
})
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/test123')
|
||||
})
|
||||
|
||||
it('ajax api replace Url should replace an element into the cache when string', function() {
|
||||
this.server.respondWith('POST', '/test', 'Clicked!')
|
||||
|
||||
var div = make("<div id='d1'></div>")
|
||||
htmx.ajax('POST', '/test', {
|
||||
target: '#d1',
|
||||
swap: 'innerHTML',
|
||||
replace: '/abc123'
|
||||
})
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Clicked!')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/abc123')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -90,6 +90,32 @@ describe('htmx config test', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('non mapped responseHandling config will not swap', function() {
|
||||
var originalResponseHandling = htmx.config.responseHandling
|
||||
try {
|
||||
htmx.config.responseHandling = [{ code: '200', swap: true }]
|
||||
|
||||
var responseCode = null
|
||||
this.server.respondWith('GET', '/test', function(xhr, id) {
|
||||
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
|
||||
})
|
||||
|
||||
responseCode = 400 // 400 should not swap as not found in config
|
||||
var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Click Me!')
|
||||
|
||||
responseCode = 200 // 200 should cause a swap by default
|
||||
var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('200')
|
||||
} finally {
|
||||
htmx.config.responseHandling = originalResponseHandling
|
||||
}
|
||||
})
|
||||
|
||||
it('can change the target of a given response code', function() {
|
||||
var originalResponseHandling = htmx.config.responseHandling
|
||||
try {
|
||||
@@ -113,6 +139,58 @@ describe('htmx config test', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('throws targetError if you the target in responseHandling is invalid', function() {
|
||||
var originalResponseHandling = htmx.config.responseHandling
|
||||
try {
|
||||
var error = false
|
||||
var handler = htmx.on('htmx:targetError', function(evt) {
|
||||
evt.detail.target.should.equal('#a-div')
|
||||
error = true
|
||||
})
|
||||
htmx.config.responseHandling = originalResponseHandling.slice()
|
||||
htmx.config.responseHandling.unshift({ code: '444', swap: true, target: '#a-div' })
|
||||
|
||||
var responseCode = null
|
||||
this.server.respondWith('GET', '/test', function(xhr, id) {
|
||||
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
|
||||
})
|
||||
|
||||
responseCode = 444
|
||||
var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Click Me!')
|
||||
} catch (e) {
|
||||
} finally {
|
||||
htmx.config.responseHandling = originalResponseHandling
|
||||
htmx.off('htmx:targetError', handler)
|
||||
error.should.equal(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('can change the target to "this" in a given response code', function() {
|
||||
var originalResponseHandling = htmx.config.responseHandling
|
||||
try {
|
||||
htmx.config.responseHandling = originalResponseHandling.slice()
|
||||
htmx.config.responseHandling.unshift({ code: '444', swap: true, target: 'this' })
|
||||
|
||||
var responseCode = null
|
||||
this.server.respondWith('GET', '/test', function(xhr, id) {
|
||||
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
|
||||
})
|
||||
|
||||
responseCode = 444
|
||||
var div = make('<div id="a-div">Another Div</div>')
|
||||
var btn = make('<button hx-target="#a-div" hx-get="/test">Click Me!</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('444')
|
||||
div.innerHTML.should.equal('Another Div')
|
||||
} finally {
|
||||
htmx.config.responseHandling = originalResponseHandling
|
||||
}
|
||||
})
|
||||
|
||||
it('can change the swap type of a given response code', function() {
|
||||
var originalResponseHandling = htmx.config.responseHandling
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
describe('Core htmx Events', function() {
|
||||
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
@@ -77,6 +78,21 @@ describe('Core htmx Events', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('events accept an options argument and the result works as expected', function() {
|
||||
var invoked = 0
|
||||
var handler = htmx.on('custom', function() {
|
||||
invoked = invoked + 1
|
||||
}, { once: true })
|
||||
try {
|
||||
var div = make("<div hx-post='/test'></div>")
|
||||
htmx.trigger(div, 'custom')
|
||||
htmx.trigger(div, 'custom')
|
||||
invoked.should.equal(1)
|
||||
} finally {
|
||||
htmx.off('custom', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:configRequest allows attribute removal', function() {
|
||||
var param = 'foo'
|
||||
var handler = htmx.on('htmx:configRequest', function(evt) {
|
||||
@@ -163,6 +179,24 @@ describe('Core htmx Events', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:afterSwap is called when replacing outerHTML, new line content', function() {
|
||||
var called = false
|
||||
var handler = htmx.on('htmx:afterSwap', function(evt) {
|
||||
called = true
|
||||
})
|
||||
try {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.respond(200, {}, '\n<button>Bar</button>')
|
||||
})
|
||||
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>")
|
||||
div.click()
|
||||
this.server.respond()
|
||||
should.equal(called, true)
|
||||
} finally {
|
||||
htmx.off('htmx:afterSwap', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:oobBeforeSwap is called before swap', function() {
|
||||
var called = false
|
||||
var handler = htmx.on('htmx:oobBeforeSwap', function(evt) {
|
||||
@@ -681,4 +715,206 @@ describe('Core htmx Events', function() {
|
||||
htmx.off('htmx:afterSwap', afterSwapHandler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:beforeSwap can override swap style using evt.detail.swapOverride and has final say on it', function() {
|
||||
var swapWasOverriden = false
|
||||
var responseBody = 'look at me. i’m the innerHTML now.'
|
||||
|
||||
var beforeSwapHandler = htmx.on('htmx:beforeSwap', function(evt) {
|
||||
evt.detail.swapOverride = 'innerHTML'
|
||||
})
|
||||
var afterSwapHandler = htmx.on('htmx:afterSwap', function(evt) {
|
||||
console.log('afterSwap', byId('b').innerHTML)
|
||||
swapWasOverriden = byId('b') !== null && byId('b').innerHTML === responseBody
|
||||
})
|
||||
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Reswap': 'afterbegin' }, responseBody])
|
||||
make("<div id='a' hx-get='/test' hx-target='#b' hx-swap='beforeend'></div><div id='b'> – IF YOU CAN READ THIS, IT FAILED – </div>")
|
||||
byId('a').click()
|
||||
this.server.respond()
|
||||
swapWasOverriden.should.equal(true)
|
||||
} finally {
|
||||
htmx.off('htmx:beforeSwap', beforeSwapHandler)
|
||||
htmx.off('htmx:afterSwap', afterSwapHandler)
|
||||
}
|
||||
})
|
||||
|
||||
it('preventDefault() in htmx:historyCacheMiss stops the history request', function() {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var handler = htmx.on('htmx:historyCacheMiss', function(evt) {
|
||||
evt.preventDefault()
|
||||
})
|
||||
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
|
||||
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
|
||||
try {
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
var workArea = getWorkArea()
|
||||
workArea.textContent.should.equal('test1')
|
||||
|
||||
byId('d2').click()
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test2')
|
||||
} finally {
|
||||
htmx.off('htmx:historyCacheMiss', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:historyCacheMissLoad event can update history swap', function() {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var handler = htmx.on('htmx:historyCacheMissLoad', function(evt) {
|
||||
evt.detail.historyElt = byId('hist-re-target')
|
||||
evt.detail.swapSpec.swapStyle = 'outerHTML'
|
||||
evt.detail.response = '<div id="hist-re-target">Updated<div>'
|
||||
evt.detail.path = '/test3'
|
||||
})
|
||||
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
|
||||
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
make('<div id="hist-re-target"></div>')
|
||||
|
||||
try {
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
var workArea = getWorkArea()
|
||||
workArea.textContent.should.equal('test1')
|
||||
|
||||
byId('d2').click()
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test2Updated')
|
||||
byId('hist-re-target').textContent.should.equal('Updated')
|
||||
htmx._('currentPathForHistory').should.equal('/test3')
|
||||
} finally {
|
||||
htmx.off('htmx:historyCacheMissLoad', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:historyCacheMiss event can set custom request headers', function() {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var handler = htmx.on('htmx:historyCacheMiss', function(evt) {
|
||||
evt.detail.xhr.setRequestHeader('CustomHeader', 'true')
|
||||
})
|
||||
this.server.respondWith('GET', '/test1', function(xhr) {
|
||||
should.equal(xhr.requestHeaders.CustomHeader, 'true')
|
||||
xhr.respond(200, {}, '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
})
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test1')
|
||||
} finally {
|
||||
htmx.off('htmx:historyCacheMiss', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('preventDefault() in htmx:historyCacheHit stops the history action', function() {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var handler = htmx.on('htmx:historyCacheHit', function(evt) {
|
||||
evt.preventDefault()
|
||||
})
|
||||
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
|
||||
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
|
||||
try {
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
var workArea = getWorkArea()
|
||||
workArea.textContent.should.equal('test1')
|
||||
|
||||
byId('d2').click()
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
htmx._('restoreHistory')('/test1')
|
||||
getWorkArea().textContent.should.equal('test2')
|
||||
} finally {
|
||||
htmx.off('htmx:historyCacheHit', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:historyCacheHit event can update history swap', function() {
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var handler = htmx.on('htmx:historyCacheHit', function(evt) {
|
||||
evt.detail.historyElt = byId('hist-re-target')
|
||||
evt.detail.swapSpec.swapStyle = 'outerHTML'
|
||||
evt.detail.item.content = '<div id="hist-re-target">Updated<div>'
|
||||
evt.detail.path = '/test3'
|
||||
})
|
||||
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
|
||||
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
|
||||
|
||||
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
|
||||
make('<div id="hist-re-target"></div>')
|
||||
|
||||
try {
|
||||
byId('d1').click()
|
||||
this.server.respond()
|
||||
var workArea = getWorkArea()
|
||||
workArea.textContent.should.equal('test1')
|
||||
|
||||
byId('d2').click()
|
||||
this.server.respond()
|
||||
workArea.textContent.should.equal('test2')
|
||||
|
||||
htmx._('restoreHistory')('/test1')
|
||||
this.server.respond()
|
||||
getWorkArea().textContent.should.equal('test2Updated')
|
||||
byId('hist-re-target').textContent.should.equal('Updated')
|
||||
htmx._('currentPathForHistory').should.equal('/test3')
|
||||
} finally {
|
||||
htmx.off('htmx:historyCacheHit', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:targetError should include the hx-target value', function() {
|
||||
var target = null
|
||||
var handler = htmx.on('htmx:targetError', function(evt) {
|
||||
target = evt.detail.target
|
||||
})
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', '')
|
||||
var div = make('<div hx-post="/test" hx-target="#non-existent"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
target.should.equal('#non-existent')
|
||||
} finally {
|
||||
htmx.off('htmx:targetError', handler)
|
||||
}
|
||||
})
|
||||
|
||||
it('htmx:targetError can include an inherited hx-target value', function() {
|
||||
var target = null
|
||||
var handler = htmx.on('htmx:targetError', function(evt) {
|
||||
target = evt.detail.target
|
||||
})
|
||||
try {
|
||||
this.server.respondWith('GET', '/test', '')
|
||||
make('<div hx-target="#parent-target"><div id="child" hx-post="/test"></div></div>')
|
||||
byId('child').click()
|
||||
this.server.respond()
|
||||
target.should.equal('#parent-target')
|
||||
} finally {
|
||||
htmx.off('htmx:targetError', handler)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,4 +39,49 @@ describe('Core htmx extension tests', function() {
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('Click Me!')
|
||||
})
|
||||
|
||||
it('withExtensions catches and logs any exceptions', function() {
|
||||
htmx.defineExtension('ext-prevent-request', {
|
||||
onEvent: function(name, evt) {
|
||||
if (name === 'htmx:beforeRequest') {
|
||||
evt.preventDefault()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var div = make('<div hx-ext="ext-prevent-request">Foo</div>')
|
||||
htmx._('withExtensions')(div, function(extension) {
|
||||
throw new Error('throw error to catch and log')
|
||||
})
|
||||
})
|
||||
|
||||
it('encodeParameters works as expected', function() {
|
||||
htmx.defineExtension('enc-param', {
|
||||
encodeParameters: function(xhr, parameters, elt) {
|
||||
return 'foo=bar'
|
||||
}
|
||||
})
|
||||
var values
|
||||
this.server.respondWith('Post', '/test', function(xhr) {
|
||||
values = getParameters(xhr)
|
||||
xhr.respond(200, {}, 'clicked!')
|
||||
})
|
||||
this.server.respondWith('GET', '/test', 'clicked!')
|
||||
var div = make('<div hx-post="/test" hx-ext="enc-param">Click Me!</div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('clicked!')
|
||||
values.foo.should.equal('bar')
|
||||
})
|
||||
|
||||
it('extensionBase return expected values', function() {
|
||||
var extBase = htmx._('extensionBase')()
|
||||
should.equal(extBase.init(), null)
|
||||
should.equal(extBase.getSelectors(), null)
|
||||
should.equal(extBase.onEvent(), true)
|
||||
should.equal(extBase.transformResponse('text'), 'text')
|
||||
should.equal(extBase.isInlineSwap(), false)
|
||||
should.equal(extBase.handleSwap(), false)
|
||||
should.equal(extBase.encodeParameters(), null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
describe('Core htmx AJAX headers', function() {
|
||||
const chai = window.chai
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
@@ -131,7 +133,7 @@ describe('Core htmx AJAX headers', function() {
|
||||
invokedEvent.should.equal(true)
|
||||
})
|
||||
|
||||
it('should handle JSON with array arg HX-Trigger response header properly', function() {
|
||||
it('should handle JSON with object arg HX-Trigger response header properly', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{"foo":{"a":1, "b":2}}' }, ''])
|
||||
|
||||
var div = make('<div hx-get="/test"></div>')
|
||||
@@ -147,6 +149,21 @@ describe('Core htmx AJAX headers', function() {
|
||||
invokedEvent.should.equal(true)
|
||||
})
|
||||
|
||||
it('should handle JSON with target array arg HX-Trigger response header properly', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{"foo":{"target":"#testdiv"}}' }, ''])
|
||||
|
||||
var div = make('<div hx-get="/test"></div>')
|
||||
var testdiv = make('<div id="testdiv"></div>')
|
||||
var invokedEvent = false
|
||||
testdiv.addEventListener('foo', function(evt) {
|
||||
invokedEvent = true
|
||||
evt.detail.elt.should.equal(testdiv)
|
||||
})
|
||||
div.click()
|
||||
this.server.respond()
|
||||
invokedEvent.should.equal(true)
|
||||
})
|
||||
|
||||
it('should survive malformed JSON in HX-Trigger response header', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{not: valid}' }, ''])
|
||||
|
||||
@@ -252,6 +269,37 @@ describe('Core htmx AJAX headers', function() {
|
||||
div2.innerHTML.should.equal('Result')
|
||||
})
|
||||
|
||||
it('should handle HX-Retarget override back to this', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Retarget': 'this' }, 'Result'])
|
||||
|
||||
var div1 = make('<div id="d1" hx-get="/test" hx-target="#d2"></div>')
|
||||
var div2 = make('<div id="d2"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
div1.innerHTML.should.equal('Result')
|
||||
div2.innerHTML.should.equal('')
|
||||
})
|
||||
|
||||
it('should handle report target:error when HX-Retarget invalid', function() {
|
||||
try {
|
||||
var error = false
|
||||
var handler = htmx.on('htmx:targetError', function(evt) {
|
||||
evt.detail.target.should.equal('#d2')
|
||||
error = true
|
||||
})
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Retarget': '#d2' }, 'Result'])
|
||||
|
||||
var div1 = make('<div id="d1" hx-get="/test"></div>')
|
||||
div1.click()
|
||||
this.server.respond()
|
||||
} catch (e) {
|
||||
} finally {
|
||||
htmx.off('htmx:targetError', handler)
|
||||
div1.innerHTML.should.equal('')
|
||||
error.should.equal(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle HX-Reswap', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Reswap': 'innerHTML' }, 'Result'])
|
||||
|
||||
@@ -271,6 +319,16 @@ describe('Core htmx AJAX headers', function() {
|
||||
div.innerHTML.should.equal('<div id="d2">bar</div>')
|
||||
})
|
||||
|
||||
it('should handle HX-Reselect unset', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': 'unset' }, 'bar'])
|
||||
|
||||
var div = make('<div hx-get="/test" hx-select="#d2"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
|
||||
div.innerHTML.should.equal('bar')
|
||||
})
|
||||
|
||||
it('should handle simple string HX-Trigger-After-Swap response header properly w/ outerHTML swap', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger-After-Swap': 'foo' }, ''])
|
||||
|
||||
@@ -339,17 +397,91 @@ describe('Core htmx AJAX headers', function() {
|
||||
htmx.off('bar', handlerBar)
|
||||
})
|
||||
|
||||
it('should change body content on HX-Location', function() {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#testdiv"}' }, ''])
|
||||
it('should change body content on HX-Location', function(done) {
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#work-area"}' }, ''])
|
||||
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('<div>Yay! Welcome</div>')
|
||||
setTimeout(function() {
|
||||
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('request to restore history should include the HX-Request header', function() {
|
||||
it('should push new Url on HX-Location', function(done) {
|
||||
sessionStorage.removeItem('htmx-current-path-for-history')
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#work-area"}' }, ''])
|
||||
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
this.server.respond()
|
||||
setTimeout(function() {
|
||||
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/test2')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('should not push new Url on HX-Location if push Url false', function(done) {
|
||||
sessionStorage.setItem('htmx-current-path-for-history', '/old')
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"false", "path":"/test2", "target":"#work-area"}' }, ''])
|
||||
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
this.server.respond()
|
||||
setTimeout(function() {
|
||||
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/old')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('should push different Url on HX-Location if push Url is string', function(done) {
|
||||
sessionStorage.removeItem('htmx-current-path-for-history')
|
||||
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"/abc123", "path":"/test2", "target":"#work-area"}' }, ''])
|
||||
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
this.server.respond()
|
||||
setTimeout(function() {
|
||||
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
|
||||
var path = sessionStorage.getItem('htmx-current-path-for-history')
|
||||
path.should.equal('/abc123')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('should refresh page on HX-Refresh', function() {
|
||||
var refresh = false
|
||||
htmx.location = { reload: function() { refresh = true } }
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Refresh': 'true' }, ''])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
refresh.should.equal(true)
|
||||
htmx.location = window.location
|
||||
})
|
||||
|
||||
it('should update location.href on HX-Redirect', function() {
|
||||
htmx.location = { href: window.location.href }
|
||||
this.server.respondWith('GET', '/test', [200, { 'HX-Redirect': 'https://htmx.org/headers/hx-redirect/' }, ''])
|
||||
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
|
||||
div.click()
|
||||
this.server.respond()
|
||||
htmx.location.href.should.equal('https://htmx.org/headers/hx-redirect/')
|
||||
htmx.location = window.location
|
||||
})
|
||||
|
||||
it('request to restore history should include the HX-Request header when historyRestoreAsHxRequest true', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.requestHeaders['HX-Request'].should.be.equal('true')
|
||||
xhr.respond(200, {}, '')
|
||||
@@ -358,6 +490,32 @@ describe('Core htmx AJAX headers', function() {
|
||||
this.server.respond()
|
||||
})
|
||||
|
||||
it('request to restore history should not include the HX-Request header when historyRestoreAsHxRequest false', function() {
|
||||
htmx.config.historyRestoreAsHxRequest = false
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
should.equal(xhr.requestHeaders['HX-Request'], undefined)
|
||||
xhr.respond(200, {}, '')
|
||||
})
|
||||
htmx._('loadHistoryFromServer')('/test')
|
||||
this.server.respond()
|
||||
htmx.config.historyRestoreAsHxRequest = true
|
||||
})
|
||||
|
||||
it('request history from server with error status code throws error event', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.requestHeaders['HX-Request'].should.be.equal('true')
|
||||
xhr.respond(404, {}, '')
|
||||
})
|
||||
var invokedEvent = false
|
||||
var handler = htmx.on('htmx:historyCacheMissLoadError', function(evt) {
|
||||
invokedEvent = true
|
||||
})
|
||||
htmx._('loadHistoryFromServer')('/test')
|
||||
this.server.respond()
|
||||
invokedEvent.should.equal(true)
|
||||
htmx.off('htmx:historyCacheMissLoadError', handler)
|
||||
})
|
||||
|
||||
it('request to restore history should include the HX-History-Restore-Request header', function() {
|
||||
this.server.respondWith('GET', '/test', function(xhr) {
|
||||
xhr.requestHeaders['HX-History-Restore-Request'].should.be.equal('true')
|
||||
|
||||
@@ -87,22 +87,56 @@ describe('Core htmx internals Tests', function() {
|
||||
var anchorThatShouldCancel = make("<a href='/foo'></a>")
|
||||
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldCancel).should.equal(true)
|
||||
|
||||
var anchorThatShouldCancel = make("<a href='#'></a>")
|
||||
anchorThatShouldCancel = make("<a href='#'></a>")
|
||||
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldCancel).should.equal(true)
|
||||
|
||||
var anchorThatShouldNotCancel = make("<a href='#foo'></a>")
|
||||
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldNotCancel).should.equal(false)
|
||||
|
||||
var divThatShouldNotCancel = make('<div></div>')
|
||||
htmx._('shouldCancel')({ type: 'click' }, divThatShouldNotCancel).should.equal(false)
|
||||
|
||||
var form = make('<form></form>')
|
||||
htmx._('shouldCancel')({ type: 'submit' }, form).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'submit', target: form }, form).should.equal(true)
|
||||
|
||||
var form = make("<form><input id='i1' type='submit'></form>")
|
||||
var input = byId('i1')
|
||||
htmx._('shouldCancel')({ type: 'click' }, input).should.equal(true)
|
||||
// check that events targeting elements that shouldn't cancel don't cancel
|
||||
htmx._('shouldCancel')({ type: 'click', target: divThatShouldNotCancel }, form).should.equal(false)
|
||||
|
||||
var form = make("<form><button id='b1' type='submit'></form>")
|
||||
var button = byId('b1')
|
||||
htmx._('shouldCancel')({ type: 'click' }, button).should.equal(true)
|
||||
// check elements inside links getting click events should cancel parent links
|
||||
var anchorWithButton = make("<a href='/foo'><button></button></a>")
|
||||
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton.firstChild).should.equal(true)
|
||||
|
||||
// check that links inside htmx elements should not cancel
|
||||
var divWithLink = make("<div hx-get='/data'><a href='/page'>Link</a></div>")
|
||||
var link = divWithLink.querySelector('a')
|
||||
htmx._('shouldCancel')({ type: 'click', target: link }, divWithLink).should.equal(false)
|
||||
|
||||
form = make('<form id="f1">' +
|
||||
'<input id="insideInput" type="submit">' +
|
||||
'<button id="insideFormBtn"></button>' +
|
||||
'<button id="insideSubmitBtn" type="submit"></button>' +
|
||||
'<button id="insideResetBtn" type="reset"></button>' +
|
||||
'<button id="insideButtonBtn" type="button"></button>' +
|
||||
'</form>' +
|
||||
'<input id="outsideInput" form="f1" type="submit">' +
|
||||
'<button id="outsideFormBtn" form="f1"></button>' +
|
||||
'<button id="outsideSubmitBtn" form="f1" type="submit"></button>")' +
|
||||
'<button id="outsideButtonBtn" form="f1" type="button"></button>")' +
|
||||
'<button id="outsideResetBtn" form="f1" type="reset"></button>")' +
|
||||
'<button id="outsideNoFormBtn"></button>")')
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('insideInput')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('insideFormBtn')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('insideSubmitBtn')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('insideResetBtn')).should.equal(false)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('insideButtonBtn')).should.equal(false)
|
||||
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideInput')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideFormBtn')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideSubmitBtn')).should.equal(true)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideButtonBtn')).should.equal(false)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideResetBtn')).should.equal(false)
|
||||
htmx._('shouldCancel')({ type: 'click' }, byId('outsideNoFormBtn')).should.equal(false)
|
||||
})
|
||||
|
||||
it('unset properly unsets a given attribute', function() {
|
||||
@@ -128,4 +162,67 @@ describe('Core htmx internals Tests', function() {
|
||||
var value = htmx._('encodeParamsForBody')(null, form, {});
|
||||
(value instanceof FormData).should.equal(true)
|
||||
})
|
||||
|
||||
it('Calling onpopstate to trigger backup and restore of page triggers htmx:restored event', function() {
|
||||
var restored
|
||||
var handler = htmx.on('htmx:restored', function(event) {
|
||||
restored = true
|
||||
})
|
||||
make('<div hx-get="/test" hx-trigger="restored">Not Called</div>')
|
||||
window.onpopstate({ state: { htmx: true } })
|
||||
restored.should.equal(true)
|
||||
htmx.off('htmx:restored', handler)
|
||||
})
|
||||
|
||||
it('scroll position is restored from history restore', function() {
|
||||
make('<div style="height: 1000px;" hx-get="/test" hx-trigger="restored">Not Called</div>')
|
||||
window.scrollTo(0, 50)
|
||||
window.onpopstate({ state: { htmx: true } })
|
||||
parseInt(window.scrollY).should.equal(50)
|
||||
})
|
||||
|
||||
it('calling onpopstate with no htmx state not true calls original popstate', function() {
|
||||
window.onpopstate({ state: { htmx: false } })
|
||||
})
|
||||
|
||||
it('getPathFromResponse returns paths when valid', function() {
|
||||
var path = htmx._('getPathFromResponse')({ responseURL: 'https://htmx.org/somepath?a=b#fragment' })
|
||||
should.equal(path, '/somepath?a=b')
|
||||
path = htmx._('getPathFromResponse')({ responseURL: 'notvalidurl' })
|
||||
should.equal(path, undefined)
|
||||
})
|
||||
|
||||
it('appendParam can process objects', function() {
|
||||
var param = htmx._('appendParam')('a=b', 'jim', 'foo')
|
||||
should.equal(param, 'a=b&jim=foo')
|
||||
param = htmx._('appendParam')('a=b', 'jim', '{"foo":"bar"}')
|
||||
should.equal(param, 'a=b&jim=%7B%22foo%22%3A%22bar%22%7D')
|
||||
param = htmx._('appendParam')('a=b', 'jim', { foo: 'bar' })
|
||||
should.equal(param, 'a=b&jim=%7B%22foo%22%3A%22bar%22%7D')
|
||||
})
|
||||
|
||||
it('handleTitle falls back to setting document.title when no title head element', function() {
|
||||
var oldTitle = window.document.title
|
||||
document.querySelector('title').remove()
|
||||
htmx._('handleTitle')('update title')
|
||||
window.document.title.should.equal('update title')
|
||||
window.document.title = oldTitle
|
||||
})
|
||||
|
||||
it('without meta config getMetaConfig returns null', function() {
|
||||
document.querySelector('meta[name="htmx-config"]').remove()
|
||||
should.equal(htmx._('getMetaConfig')(), null)
|
||||
})
|
||||
|
||||
it('internalAPI settleImmediately completes settle tasks', function() {
|
||||
// settleImmediately is no longer used internally and may no longer be needed at all
|
||||
// as swapping without settleing does not seem via internalAPI
|
||||
const fragment = htmx._('makeFragment')('<div>Content</div>')
|
||||
const historyElement = htmx._('getHistoryElement')()
|
||||
const settleInfo = htmx._('makeSettleInfo')(historyElement)
|
||||
htmx._('swapInnerHTML')(historyElement, fragment, settleInfo)
|
||||
historyElement.firstChild.className.should.equal('htmx-added')
|
||||
htmx._('settleImmediately')(settleInfo.tasks)
|
||||
historyElement.firstChild.className.should.equal('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ describe('Core htmx Parameter Handling', function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
@@ -134,8 +135,10 @@ describe('Core htmx Parameter Handling', function() {
|
||||
vals.do.should.equal('rey')
|
||||
vals.btn.should.equal('bar')
|
||||
done()
|
||||
})
|
||||
}, { once: true })
|
||||
button.focus()
|
||||
// Headless / Hardly-throttled CPU might result in 'focusin' not being fired, double it just in case
|
||||
htmx.trigger(button, 'focusin')
|
||||
})
|
||||
|
||||
it('form includes last focused submit', function(done) {
|
||||
@@ -149,8 +152,10 @@ describe('Core htmx Parameter Handling', function() {
|
||||
vals.do.should.equal('rey')
|
||||
vals.s1.should.equal('bar')
|
||||
done()
|
||||
})
|
||||
}, { once: true })
|
||||
button.focus()
|
||||
// Headless / Hardly-throttled CPU might result in 'focusin' not being fired, double it just in case
|
||||
htmx.trigger(button, 'focusin')
|
||||
})
|
||||
|
||||
it('form does not include button when focus is lost', function() {
|
||||
@@ -276,6 +281,22 @@ describe('Core htmx Parameter Handling', function() {
|
||||
vals.foo.should.equal('bar')
|
||||
})
|
||||
|
||||
it('formdata works with null values', function() {
|
||||
var form = make('<form hx-post="/test"><input name="foo" value="bar"/></form>')
|
||||
var vals = htmx._('getInputValues')(form, 'get').values
|
||||
function updateToNull() { vals.foo = null }
|
||||
updateToNull.should.not.throw()
|
||||
vals.foo.should.equal('null')
|
||||
})
|
||||
|
||||
it('formdata can be used to construct a URLSearchParams instance', function() {
|
||||
var form = make('<input name="foo" value="bar"/>')
|
||||
var vals = htmx._('getInputValues')(form, 'get').values
|
||||
function makeSearchParams() { return new URLSearchParams(vals).toString() }
|
||||
makeSearchParams.should.not.throw()
|
||||
makeSearchParams().should.equal('foo=bar')
|
||||
})
|
||||
|
||||
it('order of parameters follows order of input elements', function() {
|
||||
this.server.respondWith('GET', '/test?foo=bar&bar=foo&foo=bar&foo2=bar2', function(xhr) {
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
@@ -293,4 +314,102 @@ describe('Core htmx Parameter Handling', function() {
|
||||
this.server.respond()
|
||||
form.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('order of parameters follows order of input elements with POST', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
xhr.requestBody.should.equal('foo=bar&bar=foo&foo=bar&foo2=bar2')
|
||||
xhr.respond(200, {}, 'Clicked!')
|
||||
})
|
||||
|
||||
var form = make('<form hx-post="/test">' +
|
||||
'<input name="foo" value="bar">' +
|
||||
'<input name="bar" value="foo">' +
|
||||
'<input name="foo" value="bar">' +
|
||||
'<input name="foo2" value="bar2">' +
|
||||
'<button id="b1">Click Me!</button>' +
|
||||
'</form>')
|
||||
|
||||
byId('b1').click()
|
||||
this.server.respond()
|
||||
form.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
|
||||
it('file is correctly uploaded with file input', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
should.equal(xhr.requestHeaders['Content-Type'], undefined)
|
||||
|
||||
const file = xhr.requestBody.get('file')
|
||||
file.should.instanceOf(File)
|
||||
file.name.should.equal('test.txt')
|
||||
|
||||
xhr.respond(200, {}, 'OK')
|
||||
})
|
||||
|
||||
const form = make('<form hx-post="/test" hx-target="#result" hx-encoding="multipart/form-data">' +
|
||||
'<input type="file" name="file">' +
|
||||
'<button type="submit"></button>' +
|
||||
'</form>')
|
||||
const input = form.querySelector('input')
|
||||
const file = new File(['Test'], 'test.txt', { type: 'text/plain' })
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(file)
|
||||
input.files = dataTransfer.files
|
||||
|
||||
const result = make('<div id="result"></div>')
|
||||
|
||||
form.querySelector('button').click()
|
||||
this.server.respond()
|
||||
result.innerHTML.should.equal('OK')
|
||||
})
|
||||
|
||||
it('file is not uploaded with blank filename', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
should.equal(xhr.requestHeaders['Content-Type'], undefined)
|
||||
|
||||
const file = xhr.requestBody.get('file')
|
||||
should.equal(file, null)
|
||||
|
||||
xhr.respond(200, {}, 'OK')
|
||||
})
|
||||
|
||||
const form = make('<form hx-post="/test" hx-target="#result" hx-encoding="multipart/form-data">' +
|
||||
'<input type="file" name="file">' +
|
||||
'<button type="submit"></button>' +
|
||||
'</form>')
|
||||
const input = form.querySelector('input')
|
||||
const file = new File(['Test'], '', { type: 'text/plain' })
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(file)
|
||||
input.files = dataTransfer.files
|
||||
|
||||
const result = make('<div id="result"></div>')
|
||||
|
||||
form.querySelector('button').click()
|
||||
this.server.respond()
|
||||
result.innerHTML.should.equal('OK')
|
||||
})
|
||||
|
||||
it('file is correctly uploaded with htmx.ajax', function() {
|
||||
this.server.respondWith('POST', '/test', function(xhr) {
|
||||
should.equal(xhr.requestHeaders['Content-Type'], undefined)
|
||||
|
||||
const file = xhr.requestBody.get('file')
|
||||
file.should.instanceOf(File)
|
||||
file.name.should.equal('test.txt')
|
||||
|
||||
xhr.respond(200, {}, 'OK')
|
||||
})
|
||||
|
||||
const div = make('<div hx-encoding="multipart/form-data"></div>')
|
||||
|
||||
htmx.ajax('POST', '/test', {
|
||||
source: div,
|
||||
values: {
|
||||
file: new File(['Test'], 'test.txt', { type: 'text/plain' })
|
||||
}
|
||||
})
|
||||
|
||||
this.server.respond()
|
||||
div.innerHTML.should.equal('OK')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,12 +5,12 @@ describe('Core htmx perf Tests', function() {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
afterEach(function() {
|
||||
this.server.restore()
|
||||
clearWorkArea()
|
||||
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
|
||||
})
|
||||
|
||||
function stringRepeat(str, num) {
|
||||
@@ -39,8 +39,8 @@ describe('Core htmx perf Tests', function() {
|
||||
}
|
||||
var start = performance.now()
|
||||
var string = JSON.stringify(array)
|
||||
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, string)
|
||||
var reReadString = localStorage.getItem(HTMX_HISTORY_CACHE_NAME)
|
||||
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, string)
|
||||
var reReadString = sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME)
|
||||
var finalJson = JSON.parse(reReadString)
|
||||
var end = performance.now()
|
||||
var timeInMs = end - start
|
||||
@@ -57,6 +57,6 @@ describe('Core htmx perf Tests', function() {
|
||||
htmx._('cleanInnerHtmlForHistory')(workArea)
|
||||
var end = performance.now()
|
||||
var timeInMs = end - start
|
||||
chai.assert(timeInMs < 50, 'Should take less than 50ms on most platforms')
|
||||
chai.assert(timeInMs < 80, 'Should take less than 80ms on most platforms')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -100,11 +100,11 @@ describe('Core htmx Regression Tests', function() {
|
||||
it('does not submit with a false condition on a form', function() {
|
||||
this.server.respondWith('POST', '/test', 'Submitted')
|
||||
var defaultPrevented = false
|
||||
htmx.on('click', function(evt) {
|
||||
htmx.on('submit', function(evt) {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
})
|
||||
var form = make('<form hx-post="/test" hx-trigger="click[false]"></form>')
|
||||
form.click()
|
||||
var form = make('<form hx-post="/test" hx-trigger="submit[false]"><button id="b1">submit</button></form>')
|
||||
byId('b1').click()
|
||||
this.server.respond()
|
||||
defaultPrevented.should.equal(true)
|
||||
})
|
||||
@@ -270,4 +270,216 @@ describe('Core htmx Regression Tests', function() {
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('a modified click trigger on a form does not prevent the default behaviour of other elements - https://github.com/bigskysoftware/htmx/issues/2755', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
make('<input type="date" id="datefield">')
|
||||
make('<form hx-trigger="click from:body"></form>')
|
||||
|
||||
htmx.on('#datefield', 'click', function(evt) {
|
||||
// we need to wait so the state of the evt is finalized
|
||||
setTimeout(() => {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
try {
|
||||
defaultPrevented.should.equal(false)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
byId('datefield').click()
|
||||
})
|
||||
|
||||
it('swap=outerHTML clears htmx-swapping class when old node has a style attribute and no class', function(done) {
|
||||
this.server.respondWith('GET', '/test', '<div id="test-div">Test</div>')
|
||||
|
||||
var btn = make('<button hx-get="/test" hx-target="#test-div" hx-swap="outerHTML">Click Me!</button>')
|
||||
var div = make('<div id="test-div" style></div>')
|
||||
btn.click()
|
||||
|
||||
this.server.respond()
|
||||
|
||||
var div = byId('test-div')
|
||||
const isSwappingClassStillThere = div.classList.contains('htmx-swapping')
|
||||
isSwappingClassStillThere.should.equal(false)
|
||||
done()
|
||||
})
|
||||
|
||||
it('swap=outerHTML won\'t carry over user-defined classes when old node has a style attribute before the class attribute', function(done) {
|
||||
this.server.respondWith('GET', '/test', '<div id="test-div">Test</div>')
|
||||
|
||||
var btn = make('<button hx-get="/test" hx-target="#test-div" hx-swap="outerHTML">Click Me!</button>')
|
||||
var div = make('<div id="test-div" style class="my-class"></div>')
|
||||
btn.click()
|
||||
|
||||
this.server.respond()
|
||||
|
||||
var div = byId('test-div')
|
||||
div.classList.length.should.equal(0)
|
||||
done()
|
||||
})
|
||||
|
||||
it('a button clicked inside an htmx enabled link will prevent the link from navigating on click', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var link = make('<a href="/foo" hx-get="/foo"><button>test</button></a>')
|
||||
var button = link.firstChild
|
||||
|
||||
htmx.on(link, 'click', function(evt) {
|
||||
// we need to wait so the state of the evt is finalized
|
||||
setTimeout(() => {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
button.click()
|
||||
})
|
||||
|
||||
it('a htmx enabled button clicked inside a link will prevent the link from navigating on click', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var link = make('<a href="/foo"><button hx-get="/foo">test</button></a>')
|
||||
var button = link.firstChild
|
||||
|
||||
htmx.on(link, 'click', function(evt) {
|
||||
// we need to wait so the state of the evt is finalized
|
||||
setTimeout(() => {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
button.click()
|
||||
})
|
||||
|
||||
it('a htmx enabled button containing sub elements will prevent the button submitting a form', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var form = make('<form><button hx-get="/foo"><span>test</span></button></form>')
|
||||
var button = form.firstChild
|
||||
var span = button.firstChild
|
||||
|
||||
htmx.on(button, 'click', function(evt) {
|
||||
// we need to wait so the state of the evt is finalized
|
||||
setTimeout(() => {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
span.click()
|
||||
})
|
||||
|
||||
it('a htmx enabled element inside a form button will prevent the button submitting a form', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var form = make('<form><button><span hx-get="/foo">test</span></button></form>')
|
||||
var button = form.firstChild
|
||||
var span = button.firstChild
|
||||
|
||||
htmx.on(button, 'click', function(evt) {
|
||||
// we need to wait so the state of the evt is finalized
|
||||
setTimeout(() => {
|
||||
defaultPrevented = evt.defaultPrevented
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
span.click()
|
||||
})
|
||||
|
||||
it('from: trigger on form prevents default form submission', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var form = make('<form id="test-form" action="/submit"><input type="submit" value="Submit"></form>')
|
||||
var div = make('<div hx-post="/test" hx-trigger="submit from:#test-form"></div>')
|
||||
var submitBtn = form.firstChild
|
||||
|
||||
htmx.on(form, 'submit', function(evt) {
|
||||
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
|
||||
evt.preventDefault() // Prevent navigation in case test fails
|
||||
setTimeout(() => {
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
submitBtn.click()
|
||||
})
|
||||
|
||||
it('from: trigger on button prevents default form submission', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var form = make('<form><button id="test-btn" type="submit">Submit</button></form>')
|
||||
var div = make('<div hx-post="/test" hx-trigger="click from:#test-btn"></div>')
|
||||
var button = byId('test-btn')
|
||||
|
||||
htmx.on(button, 'click', function(evt) {
|
||||
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
|
||||
evt.preventDefault() // Prevent form submission in case test fails
|
||||
setTimeout(() => {
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
button.click()
|
||||
})
|
||||
|
||||
it('from: trigger on link prevents default navigation', function(done) {
|
||||
var defaultPrevented = 'unset'
|
||||
var link = make('<a id="test-link" href="/page">Go to page</a>')
|
||||
var div = make('<div hx-get="/test" hx-trigger="click from:#test-link"></div>')
|
||||
|
||||
htmx.on(link, 'click', function(evt) {
|
||||
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
|
||||
evt.preventDefault() // Prevent navigation in case test fails
|
||||
setTimeout(() => {
|
||||
try {
|
||||
defaultPrevented.should.equal(true)
|
||||
done()
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
link.click()
|
||||
})
|
||||
|
||||
it('check deleting button during click does not trigger exception error in getRelatedFormData when button can no longer find form', function() {
|
||||
var defaultPrevented = 'unset'
|
||||
var form = make('<form><button>delete</button></form>')
|
||||
var button = form.firstChild
|
||||
htmx.on(button, 'click', function(evt) {
|
||||
evt.target.remove()
|
||||
})
|
||||
|
||||
button.click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,6 +27,16 @@ describe('security options', function() {
|
||||
btn.innerHTML.should.equal('Initial')
|
||||
})
|
||||
|
||||
it('can disable a child elt', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
var div = make('<div><button id="b1" hx-disable hx-get="/test">Initial</button></div>')
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Initial')
|
||||
})
|
||||
|
||||
it('can disable a single elt dynamically', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
@@ -65,6 +75,28 @@ describe('security options', function() {
|
||||
btn.innerHTML.should.equal('Clicked a second time')
|
||||
})
|
||||
|
||||
it('can disable a single a tag dynamically & enable it back with boost', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
var btn = make('<a id="b1" hx-boost="true" href="/test" hx-target="this">Initial</button>')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
|
||||
this.server.respondWith('GET', '/test', 'Clicked a second time')
|
||||
|
||||
btn.setAttribute('hx-disable', '')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
|
||||
btn.removeAttribute('hx-disable')
|
||||
htmx.process(btn)
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked a second time')
|
||||
})
|
||||
|
||||
it('can disable a single parent elt dynamically', function() {
|
||||
this.server.respondWith('GET', '/test', 'Clicked!')
|
||||
|
||||
@@ -120,7 +152,67 @@ describe('security options', function() {
|
||||
btn.click()
|
||||
})
|
||||
|
||||
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done) {
|
||||
it('can make a real local data uri request when selfRequestOnly false', function(done) {
|
||||
htmx.config.selfRequestsOnly = false
|
||||
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
|
||||
if (evt.detail.sameHost === false && evt.detail.url.protocol !== 'data:') {
|
||||
evt.preventDefault()
|
||||
}
|
||||
})
|
||||
this.server.restore() // use real xhrs
|
||||
var btn = make('<button hx-get="data:,foo">Initial</button>')
|
||||
btn.click()
|
||||
htmx.config.selfRequestsOnly = true
|
||||
setTimeout(function() {
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
btn.innerHTML.should.equal('foo')
|
||||
done()
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('can disable hx-on on a single elt', function() {
|
||||
var btn = make("<button hx-disable hx-on:click='window.foo = true'>Foo</button>")
|
||||
btn.click()
|
||||
should.equal(window.foo, undefined)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('can disable hx-on on a parent elt', function() {
|
||||
var div = make("<div hx-disable><button id='b1' hx-on:click='window.foo = true'>Foo</button></div>")
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
should.equal(window.foo, undefined)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('can disable hx-on on a single elt dynamically', function() {
|
||||
var btn = make("<button hx-on:click='window.foo = true'>Foo</button>")
|
||||
btn.click()
|
||||
should.equal(window.foo, true)
|
||||
delete window.foo
|
||||
|
||||
btn.setAttribute('hx-disable', '')
|
||||
|
||||
btn.click()
|
||||
should.equal(window.foo, undefined)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it('can disable hx-on on a parent elt dynamically', function() {
|
||||
var div = make("<div><button id='b1' hx-on:click='window.foo = true'>Foo</button></div>")
|
||||
var btn = byId('b1')
|
||||
btn.click()
|
||||
should.equal(window.foo, true)
|
||||
delete window.foo
|
||||
|
||||
div.setAttribute('hx-disable', '')
|
||||
|
||||
btn.click()
|
||||
should.equal(window.foo, undefined)
|
||||
delete window.foo
|
||||
})
|
||||
|
||||
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is true", function(done) {
|
||||
this.timeout(4000)
|
||||
// should trigger send error, rather than reject
|
||||
var listener = htmx.on('htmx:invalidPath', function() {
|
||||
@@ -135,31 +227,34 @@ describe('security options', function() {
|
||||
|
||||
it('can cancel egress request based on htmx:validateUrl event', function(done) {
|
||||
this.timeout(4000)
|
||||
htmx.config.selfRequestsOnly = false
|
||||
// should trigger send error, rather than reject
|
||||
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
|
||||
evt.preventDefault()
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
})
|
||||
var listener = htmx.on('htmx:invalidPath', function() {
|
||||
htmx.off('htmx:invalidPath', listener)
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
done()
|
||||
})
|
||||
this.server.restore() // use real xhrs
|
||||
// will 404, but should respond
|
||||
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
|
||||
btn.click()
|
||||
htmx.config.selfRequestsOnly = true
|
||||
})
|
||||
|
||||
it('can cancel egress request based on htmx:validateUrl event, sameHost is false', function(done) {
|
||||
this.timeout(4000)
|
||||
htmx.config.selfRequestsOnly = false
|
||||
// should trigger send error, rather than reject
|
||||
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
|
||||
if (evt.detail.sameHost === false) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
})
|
||||
var listener = htmx.on('htmx:invalidPath', function() {
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
htmx.off('htmx:invalidPath', listener)
|
||||
done()
|
||||
})
|
||||
@@ -167,6 +262,31 @@ describe('security options', function() {
|
||||
// will 404, but should respond
|
||||
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
|
||||
btn.click()
|
||||
htmx.config.selfRequestsOnly = true
|
||||
})
|
||||
|
||||
it('can cancel egress request based on htmx:validateUrl event and then allow a request', function(done) {
|
||||
htmx.config.selfRequestsOnly = false
|
||||
var requestCount = 0
|
||||
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
|
||||
requestCount = requestCount + 1
|
||||
if (requestCount === 1) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
})
|
||||
this.server.restore() // use real xhrs
|
||||
var btn = make('<button hx-get="data:,foo">Initial</button>')
|
||||
btn.click()
|
||||
setTimeout(function() {
|
||||
btn.innerHTML.should.not.equal('foo')
|
||||
btn.click()
|
||||
setTimeout(function() {
|
||||
htmx.off('htmx:validateUrl', pathVerifier)
|
||||
htmx.config.selfRequestsOnly = true
|
||||
btn.innerHTML.should.equal('foo')
|
||||
done()
|
||||
}, 30)
|
||||
}, 30)
|
||||
})
|
||||
|
||||
it('can disable script tag support with htmx.config.allowScriptTags', function() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user