mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-01-25 05:06:13 +00:00
Improve moprh exact node matching with scan ahead (#3591)
* Improve moprh exact node matching with scan ahead * better inline comments documentation --------- Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
This commit is contained in:
24
src/htmx.js
24
src/htmx.js
@@ -112,6 +112,7 @@ var htmx = (() => {
|
||||
pauseInBackground: false
|
||||
},
|
||||
morphIgnore: ["data-htmx-powered"],
|
||||
morphScanLimit: 10,
|
||||
noSwap: [204, 304],
|
||||
implicitInheritance: false
|
||||
}
|
||||
@@ -1996,26 +1997,35 @@ var htmx = (() => {
|
||||
}
|
||||
|
||||
__findBestMatch(ctx, node, startPoint, endPoint) {
|
||||
let softMatch = null, nextSibling = node.nextSibling, siblingSoftMatchCount = 0, displaceMatchCount = 0;
|
||||
let softMatch = null, nextSibling = node.nextSibling, siblingMatchCount = 0, displaceMatchCount = 0, scanLimit = this.config.morphScanLimit;
|
||||
// Get ID count for this node to prioritize ID-based matches
|
||||
let newSet = ctx.idMap.get(node), nodeMatchCount = newSet?.size || 0;
|
||||
let cursor = startPoint;
|
||||
while (cursor && cursor != endPoint) {
|
||||
let oldSet = ctx.idMap.get(cursor);
|
||||
if (this.__isSoftMatch(cursor, node)) {
|
||||
// Hard match: matching IDs found in both nodes
|
||||
if (oldSet && newSet && [...oldSet].some(id => newSet.has(id))) return cursor;
|
||||
if (softMatch === null && !oldSet) {
|
||||
if (!nodeMatchCount) return cursor;
|
||||
else softMatch = cursor;
|
||||
if (!oldSet) {
|
||||
// Exact match: nodes are identical
|
||||
if (scanLimit > 0 && cursor.isEqualNode(node)) return cursor;
|
||||
// Soft match: same tag/type, save as fallback
|
||||
if (!softMatch) softMatch = cursor;
|
||||
}
|
||||
}
|
||||
// Stop if too many ID elements would be displaced
|
||||
displaceMatchCount += oldSet?.size || 0;
|
||||
if (displaceMatchCount > nodeMatchCount) break;
|
||||
if (softMatch === null && nextSibling && this.__isSoftMatch(cursor, nextSibling)) {
|
||||
siblingSoftMatchCount++;
|
||||
// Look ahead: if next siblings match exactly, abort to let them match instead
|
||||
if (nextSibling && scanLimit > 0 && cursor.isEqualNode(nextSibling)) {
|
||||
siblingMatchCount++;
|
||||
nextSibling = nextSibling.nextSibling;
|
||||
if (siblingSoftMatchCount >= 2) softMatch = undefined;
|
||||
if (siblingMatchCount >= 2) return null;
|
||||
}
|
||||
// Don't move elements containing focus
|
||||
if (cursor.contains(document.activeElement)) break;
|
||||
// Stop scanning if limit reached and no IDs to match
|
||||
if (--scanLimit < 1 && nodeMatchCount === 0) break;
|
||||
cursor = cursor.nextSibling;
|
||||
}
|
||||
return softMatch || null;
|
||||
|
||||
@@ -47,7 +47,25 @@ This approach minimizes DOM changes, which helps preserve:
|
||||
|
||||
## Configuration Options
|
||||
|
||||
htmx provides three configuration options to control morphing behavior:
|
||||
htmx provides four configuration options to control morphing behavior:
|
||||
|
||||
### `htmx.config.morphScanLimit`
|
||||
|
||||
A number that limits how many siblings to scan when looking for matching elements during morphing. The default is `10`.
|
||||
|
||||
```javascript
|
||||
// Increase scan limit for large lists
|
||||
htmx.config.morphScanLimit = 100;
|
||||
```
|
||||
|
||||
**How it works:** When morphing tries to match an element from the new content with an element in the old content, it scans through siblings to find exact matches. This limit prevents excessive scanning in very large DOM trees.
|
||||
|
||||
**Important:** Elements with matching IDs will always be found regardless of the scan limit, as ID-based matches are prioritized and continue scanning even after the limit is reached.
|
||||
|
||||
**Use cases:**
|
||||
- **Large lists**: Increase the limit when morph accuracy is needed with long lists of items without IDs
|
||||
- **Performance tuning**: Decrease the limit to improve performance if morphing is slow
|
||||
- **Default behavior**: Most applications don't need to change this value
|
||||
|
||||
### `htmx.config.morphIgnore`
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ listed below:
|
||||
| `htmx.config.extensions` | defaults to `''`, a comma-separated list of extension names to load (e.g., `'preload,optimistic'`) |
|
||||
| `htmx.config.sse` | configuration for Server-Sent Events (SSE) streams. An object with the following properties: `reconnect` (default: `false`), `reconnectMaxAttempts` (default: `10`), `reconnectDelay` (default: `500`ms), `reconnectMaxDelay` (default: `60000`ms), `reconnectJitter` (default: `0.3`), `pauseInBackground` (default: `false`) |
|
||||
| `htmx.config.morphIgnore` | defaults to `["data-htmx-powered"]`, array of attribute names to ignore when morphing elements (see [Morphing](@/morphing.md)) |
|
||||
| `htmx.config.morphScanLimit` | defaults to `10`, limits how many siblings to scan when matching elements during morphing. Increase for better accuracy with long lists without IDs (see [Morphing](@/morphing.md)) |
|
||||
| `htmx.config.morphSkip` | defaults to `undefined`, CSS selector for elements that should be completely skipped during morphing (see [Morphing](@/morphing.md)) |
|
||||
| `htmx.config.morphSkipChildren` | defaults to `undefined`, CSS selector for elements whose children should not be morphed (see [Morphing](@/morphing.md)) |
|
||||
| `htmx.config.noSwap` | defaults to `[204, 304]`, array of HTTP status codes that should not trigger a swap |
|
||||
|
||||
Reference in New Issue
Block a user