Skip to content

Commit 449c8e9

Browse files
committed
prep next release
1 parent cee310e commit 449c8e9

27 files changed

Lines changed: 529 additions & 174 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [2.0.7] - 2025-09-07
4+
5+
* [Fix](https://github.com/bigskysoftware/htmx/pull/3357) a [regression](https://github.com/bigskysoftware/htmx/issues/3356)
6+
with htmx-powered links that contain other elements in them issuing full page refreshes
7+
38
## [2.0.6] - 2025-06-27
49

510
* [Fix](https://github.com/bigskysoftware/htmx/pull/3357) a [regression](https://github.com/bigskysoftware/htmx/issues/3356)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ By removing these arbitrary constraints htmx completes HTML as a
3232
## quick start
3333

3434
```html
35-
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
35+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
3636
<!-- have a button POST a click via AJAX -->
3737
<button hx-post="/clicked" hx-swap="outerHTML">
3838
Click Me

dist/htmx.amd.js

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,14 @@ var htmx = (function() {
279279
* @type boolean
280280
* @default true
281281
*/
282-
historyRestoreAsHxRequest: true
282+
historyRestoreAsHxRequest: true,
283+
/**
284+
* Weather to report input validation errors to the end user and update focus to the first input that fails validation.
285+
* This should always be enabled as this matches default browser form submit behaviour
286+
* @type boolean
287+
* @default false
288+
*/
289+
reportValidityOfForms: false
283290
},
284291
/** @type {typeof parseInterval} */
285292
parseInterval: null,
@@ -290,7 +297,7 @@ var htmx = (function() {
290297
location,
291298
/** @type {typeof internalEval} */
292299
_: null,
293-
version: '2.0.6'
300+
version: '2.0.7'
294301
}
295302
// Tsc madness part 2
296303
htmx.onLoad = onLoadHelper
@@ -1414,7 +1421,7 @@ var htmx = (function() {
14141421
* @param {Element} mergeFrom
14151422
*/
14161423
function cloneAttributes(mergeTo, mergeFrom) {
1417-
forEach(mergeTo.attributes, function(attr) {
1424+
forEach(Array.from(mergeTo.attributes), function(attr) {
14181425
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
14191426
mergeTo.removeAttribute(attr.name)
14201427
}
@@ -2425,21 +2432,22 @@ var htmx = (function() {
24252432
* @returns {boolean}
24262433
*/
24272434
function shouldCancel(evt, elt) {
2428-
if (evt.type === 'submit' || evt.type === 'click') {
2429-
// use elt from event that was submitted/clicked where possible to determining if default form/link behavior should be canceled
2430-
elt = asElement(evt.target) || elt
2431-
if (elt.tagName === 'FORM') {
2432-
return true
2433-
}
2434-
// @ts-ignore Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
2435-
// The properties will resolve to undefined for elements that don't define 'type' or 'form', which is fine
2436-
if (elt.form && elt.type === 'submit') {
2435+
if (evt.type === 'submit' && elt.tagName === 'FORM') {
2436+
return true
2437+
} else if (evt.type === 'click') {
2438+
// find button wrapping the trigger element
2439+
const btn = /** @type {HTMLButtonElement|HTMLInputElement|null} */ (elt.closest('input[type="submit"], button'))
2440+
// Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
2441+
if (btn && btn.form && btn.type === 'submit') {
24372442
return true
24382443
}
2439-
elt = elt.closest('a')
2440-
// @ts-ignore check for a link wrapping the event elt or if elt is a link. elt will be link so href check is fine
2441-
if (elt && elt.href &&
2442-
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {
2444+
2445+
// find link wrapping the trigger element
2446+
const link = elt.closest('a')
2447+
// Allow links with href="#fragment" (anchors with content after #) to perform normal fragment navigation.
2448+
// Cancel default action for links with href="#" (bare hash) to prevent scrolling to top and unwanted URL changes.
2449+
const samePageAnchor = /^#.+/
2450+
if (link && link.href && !samePageAnchor.test(link.getAttribute('href'))) {
24432451
return true
24442452
}
24452453
}
@@ -2516,7 +2524,7 @@ var htmx = (function() {
25162524
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
25172525
return
25182526
}
2519-
if (explicitCancel || shouldCancel(evt, elt)) {
2527+
if (explicitCancel || shouldCancel(evt, eltToListenOn)) {
25202528
evt.preventDefault()
25212529
}
25222530
if (maybeFilterEvent(triggerSpec, elt, evt)) {
@@ -2856,6 +2864,9 @@ var htmx = (function() {
28562864
return
28572865
}
28582866
const form = getRelatedForm(elt)
2867+
if (!form) {
2868+
return
2869+
}
28592870
return getInternalData(form)
28602871
}
28612872

@@ -3547,8 +3558,17 @@ var htmx = (function() {
35473558
if (element.willValidate) {
35483559
triggerEvent(element, 'htmx:validation:validate')
35493560
if (!element.checkValidity()) {
3561+
if (
3562+
triggerEvent(element, 'htmx:validation:failed', {
3563+
message: element.validationMessage,
3564+
validity: element.validity
3565+
}) &&
3566+
!errors.length &&
3567+
htmx.config.reportValidityOfForms
3568+
) {
3569+
element.reportValidity()
3570+
}
35503571
errors.push({ elt: element, message: element.validationMessage, validity: element.validity })
3551-
triggerEvent(element, 'htmx:validation:failed', { message: element.validationMessage, validity: element.validity })
35523572
}
35533573
}
35543574
}
@@ -5061,12 +5081,14 @@ var htmx = (function() {
50615081
function insertIndicatorStyles() {
50625082
if (htmx.config.includeIndicatorStyles !== false) {
50635083
const nonceAttribute = htmx.config.inlineStyleNonce ? ` nonce="${htmx.config.inlineStyleNonce}"` : ''
5084+
const indicator = htmx.config.indicatorClass
5085+
const request = htmx.config.requestClass
50645086
getDocument().head.insertAdjacentHTML('beforeend',
5065-
'<style' + nonceAttribute + '>\
5066-
.' + htmx.config.indicatorClass + '{opacity:0}\
5067-
.' + htmx.config.requestClass + ' .' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
5068-
.' + htmx.config.requestClass + '.' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
5069-
</style>')
5087+
`<style${nonceAttribute}>` +
5088+
`.${indicator}{opacity:0;visibility: hidden} ` +
5089+
`.${request} .${indicator}, .${request}.${indicator}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}` +
5090+
'</style>'
5091+
)
50705092
}
50715093
}
50725094

dist/htmx.cjs.js

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,14 @@ var htmx = (function() {
278278
* @type boolean
279279
* @default true
280280
*/
281-
historyRestoreAsHxRequest: true
281+
historyRestoreAsHxRequest: true,
282+
/**
283+
* Weather to report input validation errors to the end user and update focus to the first input that fails validation.
284+
* This should always be enabled as this matches default browser form submit behaviour
285+
* @type boolean
286+
* @default false
287+
*/
288+
reportValidityOfForms: false
282289
},
283290
/** @type {typeof parseInterval} */
284291
parseInterval: null,
@@ -289,7 +296,7 @@ var htmx = (function() {
289296
location,
290297
/** @type {typeof internalEval} */
291298
_: null,
292-
version: '2.0.6'
299+
version: '2.0.7'
293300
}
294301
// Tsc madness part 2
295302
htmx.onLoad = onLoadHelper
@@ -1413,7 +1420,7 @@ var htmx = (function() {
14131420
* @param {Element} mergeFrom
14141421
*/
14151422
function cloneAttributes(mergeTo, mergeFrom) {
1416-
forEach(mergeTo.attributes, function(attr) {
1423+
forEach(Array.from(mergeTo.attributes), function(attr) {
14171424
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
14181425
mergeTo.removeAttribute(attr.name)
14191426
}
@@ -2424,21 +2431,22 @@ var htmx = (function() {
24242431
* @returns {boolean}
24252432
*/
24262433
function shouldCancel(evt, elt) {
2427-
if (evt.type === 'submit' || evt.type === 'click') {
2428-
// use elt from event that was submitted/clicked where possible to determining if default form/link behavior should be canceled
2429-
elt = asElement(evt.target) || elt
2430-
if (elt.tagName === 'FORM') {
2431-
return true
2432-
}
2433-
// @ts-ignore Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
2434-
// The properties will resolve to undefined for elements that don't define 'type' or 'form', which is fine
2435-
if (elt.form && elt.type === 'submit') {
2434+
if (evt.type === 'submit' && elt.tagName === 'FORM') {
2435+
return true
2436+
} else if (evt.type === 'click') {
2437+
// find button wrapping the trigger element
2438+
const btn = /** @type {HTMLButtonElement|HTMLInputElement|null} */ (elt.closest('input[type="submit"], button'))
2439+
// Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
2440+
if (btn && btn.form && btn.type === 'submit') {
24362441
return true
24372442
}
2438-
elt = elt.closest('a')
2439-
// @ts-ignore check for a link wrapping the event elt or if elt is a link. elt will be link so href check is fine
2440-
if (elt && elt.href &&
2441-
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {
2443+
2444+
// find link wrapping the trigger element
2445+
const link = elt.closest('a')
2446+
// Allow links with href="#fragment" (anchors with content after #) to perform normal fragment navigation.
2447+
// Cancel default action for links with href="#" (bare hash) to prevent scrolling to top and unwanted URL changes.
2448+
const samePageAnchor = /^#.+/
2449+
if (link && link.href && !samePageAnchor.test(link.getAttribute('href'))) {
24422450
return true
24432451
}
24442452
}
@@ -2515,7 +2523,7 @@ var htmx = (function() {
25152523
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
25162524
return
25172525
}
2518-
if (explicitCancel || shouldCancel(evt, elt)) {
2526+
if (explicitCancel || shouldCancel(evt, eltToListenOn)) {
25192527
evt.preventDefault()
25202528
}
25212529
if (maybeFilterEvent(triggerSpec, elt, evt)) {
@@ -2855,6 +2863,9 @@ var htmx = (function() {
28552863
return
28562864
}
28572865
const form = getRelatedForm(elt)
2866+
if (!form) {
2867+
return
2868+
}
28582869
return getInternalData(form)
28592870
}
28602871

@@ -3546,8 +3557,17 @@ var htmx = (function() {
35463557
if (element.willValidate) {
35473558
triggerEvent(element, 'htmx:validation:validate')
35483559
if (!element.checkValidity()) {
3560+
if (
3561+
triggerEvent(element, 'htmx:validation:failed', {
3562+
message: element.validationMessage,
3563+
validity: element.validity
3564+
}) &&
3565+
!errors.length &&
3566+
htmx.config.reportValidityOfForms
3567+
) {
3568+
element.reportValidity()
3569+
}
35493570
errors.push({ elt: element, message: element.validationMessage, validity: element.validity })
3550-
triggerEvent(element, 'htmx:validation:failed', { message: element.validationMessage, validity: element.validity })
35513571
}
35523572
}
35533573
}
@@ -5060,12 +5080,14 @@ var htmx = (function() {
50605080
function insertIndicatorStyles() {
50615081
if (htmx.config.includeIndicatorStyles !== false) {
50625082
const nonceAttribute = htmx.config.inlineStyleNonce ? ` nonce="${htmx.config.inlineStyleNonce}"` : ''
5083+
const indicator = htmx.config.indicatorClass
5084+
const request = htmx.config.requestClass
50635085
getDocument().head.insertAdjacentHTML('beforeend',
5064-
'<style' + nonceAttribute + '>\
5065-
.' + htmx.config.indicatorClass + '{opacity:0}\
5066-
.' + htmx.config.requestClass + ' .' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
5067-
.' + htmx.config.requestClass + '.' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
5068-
</style>')
5086+
`<style${nonceAttribute}>` +
5087+
`.${indicator}{opacity:0;visibility: hidden} ` +
5088+
`.${request} .${indicator}, .${request}.${indicator}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}` +
5089+
'</style>'
5090+
)
50695091
}
50705092
}
50715093

dist/htmx.esm.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ declare namespace htmx {
204204
let responseHandling: HtmxResponseHandlingConfig[];
205205
let allowNestedOobSwaps: boolean;
206206
let historyRestoreAsHxRequest: boolean;
207+
let reportValidityOfForms: boolean;
207208
}
208209
let parseInterval: (str: string) => number | undefined;
209210
let location: Location;

0 commit comments

Comments
 (0)