diff --git a/src/htmx.js b/src/htmx.js index 9a250abef..7772001ef 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -2431,23 +2431,22 @@ var htmx = (function() { * @returns {boolean} */ function shouldCancel(evt, elt) { - if (evt.type === 'submit' || evt.type === 'click') { - // use elt from event that was submitted/clicked where possible to determining if default form/link behavior should be canceled - elt = asElement(evt.target) || elt - if (elt.tagName === 'FORM') { - return true - } - // find button wrapping the event elt - const btn = elt.closest('input[type="submit"], button') - // @ts-ignore Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'. - // The properties will resolve to undefined for elements that don't define 'type' or 'form', which is fine + if (evt.type === 'submit' && elt.tagName === 'FORM') { + return true + } else if (evt.type === 'click') { + // find button wrapping the trigger element + const btn = /** @type {HTMLButtonElement|HTMLInputElement|null} */ (elt.closest('input[type="submit"], button')) + // Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'. if (btn && btn.form && btn.type === 'submit') { return true } - elt = elt.closest('a') - // @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 - if (elt && elt.href && - (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) { + + // find link wrapping the trigger element + const link = elt.closest('a') + // Allow links with href="#fragment" (anchors with content after #) to perform normal fragment navigation. + // Cancel default action for links with href="#" (bare hash) to prevent scrolling to top and unwanted URL changes. + const samePageAnchor = /^#.+/ + if (link && link.href && !samePageAnchor.test(link.getAttribute('href'))) { return true } } @@ -2524,7 +2523,7 @@ var htmx = (function() { if (ignoreBoostedAnchorCtrlClick(elt, evt)) { return } - if (explicitCancel || shouldCancel(evt, elt)) { + if (explicitCancel || shouldCancel(evt, eltToListenOn)) { evt.preventDefault() } if (maybeFilterEvent(triggerSpec, elt, evt)) { diff --git a/test/core/internals.js b/test/core/internals.js index 9b6d0b97c..d515ffac1 100644 --- a/test/core/internals.js +++ b/test/core/internals.js @@ -98,13 +98,8 @@ describe('Core htmx internals Tests', function() { var form = make('
') htmx._('shouldCancel')({ type: 'submit', target: form }, form).should.equal(true) - htmx._('shouldCancel')({ type: 'click', target: form }, form).should.equal(true) - - // falls back to check elt tag when target is not an element - htmx._('shouldCancel')({ type: 'click', target: null }, form).should.equal(true) // check that events targeting elements that shouldn't cancel don't cancel - htmx._('shouldCancel')({ type: 'submit', target: anchorThatShouldNotCancel }, form).should.equal(false) htmx._('shouldCancel')({ type: 'click', target: divThatShouldNotCancel }, form).should.equal(false) // check elements inside links getting click events should cancel parent links @@ -112,6 +107,11 @@ describe('Core htmx internals Tests', function() { htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton).should.equal(true) htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton.firstChild).should.equal(true) + // check that links inside htmx elements should not cancel + var divWithLink = make("
Link
") + var link = divWithLink.querySelector('a') + htmx._('shouldCancel')({ type: 'click', target: link }, divWithLink).should.equal(false) + form = make('
' + '' + '' + diff --git a/test/core/regressions.js b/test/core/regressions.js index 336a66d56..79d109b8e 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -100,11 +100,11 @@ describe('Core htmx Regression Tests', function() { it('does not submit with a false condition on a form', function() { this.server.respondWith('POST', '/test', 'Submitted') var defaultPrevented = false - htmx.on('click', function(evt) { + htmx.on('submit', function(evt) { defaultPrevented = evt.defaultPrevented }) - var form = make('
') - form.click() + var form = make('
') + byId('b1').click() this.server.respond() defaultPrevented.should.equal(true) }) @@ -385,6 +385,93 @@ describe('Core htmx Regression Tests', function() { span.click() }) + it('a htmx enabled element inside a form button will prevent the button submitting a form', function(done) { + var defaultPrevented = 'unset' + var form = make('
') + var button = form.firstChild + var span = button.firstChild + + htmx.on(button, 'click', function(evt) { + // we need to wait so the state of the evt is finalized + setTimeout(() => { + defaultPrevented = evt.defaultPrevented + try { + defaultPrevented.should.equal(true) + done() + } catch (err) { + done(err) + } + }, 0) + }) + + span.click() + }) + + it('from: trigger on form prevents default form submission', function(done) { + var defaultPrevented = 'unset' + var form = make('
') + var div = make('
') + var submitBtn = form.firstChild + + htmx.on(form, 'submit', function(evt) { + defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault + evt.preventDefault() // Prevent navigation in case test fails + setTimeout(() => { + try { + defaultPrevented.should.equal(true) + done() + } catch (err) { + done(err) + } + }, 0) + }) + + submitBtn.click() + }) + + it('from: trigger on button prevents default form submission', function(done) { + var defaultPrevented = 'unset' + var form = make('
') + var div = make('
') + var button = byId('test-btn') + + htmx.on(button, 'click', function(evt) { + defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault + evt.preventDefault() // Prevent form submission in case test fails + setTimeout(() => { + try { + defaultPrevented.should.equal(true) + done() + } catch (err) { + done(err) + } + }, 0) + }) + + button.click() + }) + + it('from: trigger on link prevents default navigation', function(done) { + var defaultPrevented = 'unset' + var link = make('Go to page') + var div = make('
') + + htmx.on(link, 'click', function(evt) { + defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault + evt.preventDefault() // Prevent navigation in case test fails + setTimeout(() => { + try { + defaultPrevented.should.equal(true) + done() + } catch (err) { + done(err) + } + }, 0) + }) + + link.click() + }) + it('check deleting button during click does not trigger exception error in getRelatedFormData when button can no longer find form', function() { var defaultPrevented = 'unset' var form = make('
')