Compare commits

..

26 Commits
rm ... master

Author SHA1 Message Date
Rens
31430d995f
Include target selector in htmx:oobErrorNoTarget event and error log (#3644) 2026-02-03 12:48:58 -07:00
Damien Alexandre
a5fd180db6
fix(essay): Fix typo in Symfony mention in Paris 2024 essay (#3633) 2026-01-29 16:53:50 -07:00
Carson Gross
580549355a fix date 2026-01-20 10:20:30 -07:00
Carson Gross
709512c1ac formatting & a bit of editorial work 2026-01-20 10:19:48 -07:00
Carson Gross
5a374d546b formatting 2026-01-20 10:13:01 -07:00
Carson Gross
1a30b9130e formatting 2026-01-20 10:12:15 -07:00
Carson Gross
ad65bc77ce formatting 2026-01-20 10:11:05 -07:00
Carson Gross
381449089d remove double title 2026-01-20 10:08:00 -07:00
Carson Gross
2e229462e3 add 2024 olympics to the essays page 2026-01-20 10:06:48 -07:00
Rodolphe Trujillo
6b214f11e7
Add essay: Building Critical Infrastructure with htmx for Paris 2024 Olympics (#3627)
* Add essay: Building Critical Infrastructure with htmx for Paris 2024 Olympics

* Refine essay: clarify wording, add note on Tour de France 2025 reuse

* add comma and "the"

---------

Co-authored-by: Rodolphe Trujillo <rodolphe.trujillo@arolo-solutions.com>
2026-01-20 10:04:51 -07:00
Carson Gross
58dc1e247d add sponsor 2026-01-19 15:20:13 -07:00
Alexander Petros
749d5f2f4c
Fix REST links (#3611) 2025-12-31 16:53:41 -07:00
Carson Gross
fcfca903af Merge remote-tracking branch 'origin/master' 2025-12-24 12:46:27 -07:00
Carson Gross
563fff67db add sponsor 2025-12-24 12:46:19 -07:00
Alexander van Saase
9c1297c5f3
website: add Askama to the list of template engines that support template fragments (#3576)
Add Askama to the list of template engines that support fragments
2025-12-11 11:04:26 -07:00
Loren Stewart
e495b68dc3
Add optimistic extension to extensions index (#3474) 2025-11-16 07:37:32 -07:00
Carson Gross
3abaf7eb3f Merge remote-tracking branch 'origin/master' 2025-11-10 12:26:21 -07:00
Carson Gross
e9f2ee94e3 update the-fetchening.md 2025-11-08 20:01:08 -07:00
raven.so.900
bced397c28
Add Nomini to htmx alternatives (#3497)
Add Nomini to alternatives.md
2025-11-08 16:19:33 -07:00
Carson Gross
7a0086fceb improve 2025-11-03 12:49:22 -07:00
Carson Gross
2a1339287e typo 2025-11-03 12:48:52 -07:00
Carson Gross
a275707f4a typo 2025-11-03 12:48:27 -07:00
Carson Gross
f1e0b926d8 typo 2025-11-03 12:42:58 -07:00
Carson Gross
e71f746bad typo 2025-11-03 12:21:19 -07:00
Carson Gross
8b249b1544 correct tag chars 2025-11-03 12:09:23 -07:00
Carson Gross
b7f833b6d5 add article on the fetch()ening 2025-11-03 11:50:52 -07:00
847 changed files with 93039 additions and 60805 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

9
dist/ext/README.md vendored Normal file
View File

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

View File

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

11
dist/ext/ajax-header.js vendored Normal file
View File

@ -0,0 +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';
}
}
});

20
dist/ext/alpine-morph.js vendored Normal file
View File

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

97
dist/ext/class-tools.js vendored Normal file
View File

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

100
dist/ext/client-side-templates.js vendored Normal file
View File

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

15
dist/ext/debug.js vendored Normal file
View File

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

20
dist/ext/disable-element.js vendored Normal file
View File

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

41
dist/ext/event-header.js vendored Normal file
View File

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

146
dist/ext/head-support.js vendored Normal file
View File

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

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

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

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

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

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

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

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

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

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

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

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

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

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

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

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

28
dist/ext/include-vals.js vendored Normal file
View File

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

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

@ -0,0 +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));
}
});

189
dist/ext/loading-states.js vendored Normal file
View File

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

15
dist/ext/method-override.js vendored Normal file
View File

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

21
dist/ext/morphdom-swap.js vendored Normal file
View File

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

50
dist/ext/multi-swap.js vendored Normal file
View File

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

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

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

15
dist/ext/path-params.js vendored Normal file
View File

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

151
dist/ext/preload.js vendored Normal file
View File

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

14
dist/ext/rails-method.js vendored Normal file
View File

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

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

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

135
dist/ext/response-targets.js vendored Normal file
View File

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

19
dist/ext/restored.js vendored Normal file
View File

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

374
dist/ext/sse.js vendored Normal file
View File

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

481
dist/ext/ws.js vendored Normal file
View File

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

5347
dist/htmx.amd.js vendored Normal file

File diff suppressed because it is too large Load Diff

5345
dist/htmx.cjs.js vendored Normal file

File diff suppressed because it is too large Load Diff

52
dist/htmx.d.ts vendored
View File

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

219
dist/htmx.esm.d.ts vendored Normal file
View 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;
}

7490
dist/htmx.esm.js vendored

File diff suppressed because it is too large Load Diff

BIN
dist/htmx.esm.js.br vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

7488
dist/htmx.js vendored

File diff suppressed because it is too large Load Diff

BIN
dist/htmx.js.br vendored

Binary file not shown.

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.br vendored

Binary file not shown.

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

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

3692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

38
scripts/dist.sh Executable file
View File

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

View File

@ -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)
}
}

View File

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

View File

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

View File

@ -1,46 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

View File

@ -1,46 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

View File

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

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