diff --git a/src/core/UIHandler.ts b/src/core/UIHandler.ts index 037685e..9d94020 100644 --- a/src/core/UIHandler.ts +++ b/src/core/UIHandler.ts @@ -1,5 +1,5 @@ import {Naja, Options, Payload} from '../Naja'; -import {assert, onDomReady, TypedEventListener} from '../utils'; +import {onDomReady, TypedEventListener} from '../utils'; export class UIHandler extends EventTarget { public selector: string = '.ajax'; @@ -20,41 +20,30 @@ export class UIHandler extends EventTarget { } public bindUI(element: Element): void { - const selectors = [ - `a${this.selector}`, - `input[type="submit"]${this.selector}`, - `input[type="image"]${this.selector}`, - `button[type="submit"]${this.selector}`, - `button[form]:not([type])${this.selector}`, - `form button:not([type])${this.selector}`, - `form${this.selector} input[type="submit"]`, - `form${this.selector} input[type="image"]`, - `form${this.selector} button[type="submit"]`, - `form${this.selector} button:not([type])`, - ].join(', '); - - const bindElement = (element: Element) => { + const selector = `a${this.selector}`; + + const bindElement = (element: HTMLAnchorElement) => { element.removeEventListener('click', this.handler); element.addEventListener('click', this.handler); }; - const elements = element.querySelectorAll(selectors); - elements.forEach((element) => bindElement(element)); - - if (element.matches(selectors)) { - bindElement(element); + if (element.matches(selector)) { + return bindElement(element as HTMLAnchorElement); } + const elements = element.querySelectorAll(selector); + elements.forEach((element) => bindElement(element as HTMLAnchorElement)); + const bindForm = (form: HTMLFormElement) => { form.removeEventListener('submit', this.handler); form.addEventListener('submit', this.handler); }; - if (element.matches(`form${this.selector}`)) { - bindForm(element as HTMLFormElement); + if (element.tagName === 'FORM') { + return bindForm(element as HTMLFormElement); } - const forms = element.querySelectorAll(`form${this.selector}`); + const forms = element.querySelectorAll('form'); forms.forEach((form) => bindForm(form as HTMLFormElement)); } @@ -73,52 +62,49 @@ export class UIHandler extends EventTarget { }; if (event.type === 'submit') { - this.submitForm(element as HTMLFormElement, options, event).catch(ignoreErrors); + const {submitter} = (event as SubmitEvent); + if ((element as HTMLFormElement).matches(this.selector) || submitter?.matches(this.selector)) { + this.submitForm(element as HTMLFormElement, options, event as SubmitEvent).catch(ignoreErrors); + } } else if (event.type === 'click') { - this.clickElement(element as HTMLElement, options, mouseEvent).catch(ignoreErrors); + this.processInteraction(element as HTMLAnchorElement, 'GET', (element as HTMLAnchorElement).href, null, options, mouseEvent).catch(ignoreErrors); + } } public async clickElement(element: HTMLElement, options: Options = {}, event?: MouseEvent): Promise { - let method: string = 'GET', url: string = '', data: any; - if (element.tagName === 'A') { - assert(element instanceof HTMLAnchorElement); + return this.processInteraction(element, 'GET', (element as HTMLAnchorElement).href, null, options, event); - method = 'GET'; - url = element.href; - data = null; + } else if (element.tagName === 'INPUT' || element.tagName === 'BUTTON' && (element as HTMLButtonElement | HTMLInputElement).form) { + return this.submitForm(element, options, event); - } else if (element.tagName === 'INPUT' || element.tagName === 'BUTTON') { - assert(element instanceof HTMLInputElement || element instanceof HTMLButtonElement); + } - const {form} = element; - // eslint-disable-next-line no-nested-ternary,no-extra-parens - method = element.getAttribute('formmethod')?.toUpperCase() ?? form?.getAttribute('method')?.toUpperCase() ?? 'GET'; - url = element.getAttribute('formaction') ?? form?.getAttribute('action') ?? window.location.pathname + window.location.search; - data = new FormData(form ?? undefined); + return {}; + } - if (element.type === 'submit' && element.name !== '') { - data.append(element.name, element.value || ''); + public async submitForm(sender: HTMLFormElement|HTMLElement, options: Options = {}, event?: Event): Promise { + let form: HTMLFormElement|null = sender.tagName === 'FORM' ? sender as HTMLFormElement : null; + let submitter: HTMLElement|null|undefined = null; - } else if (element.type === 'image') { - const coords = element.getBoundingClientRect(); - const prefix = element.name !== '' ? `${element.name}.` : ''; - data.append(`${prefix}x`, Math.max(0, Math.floor(event !== undefined ? event.pageX - coords.left : 0))); - data.append(`${prefix}y`, Math.max(0, Math.floor(event !== undefined ? event.pageY - coords.top : 0))); - } + if (event?.type === 'submit') { + submitter = (event as SubmitEvent)?.submitter; + } else if (sender.tagName === 'INPUT' || sender.tagName === 'BUTTON') { + form = (sender as HTMLButtonElement | HTMLInputElement).form ?? null; + submitter = sender; } - return this.processInteraction(element, method, url, data, options, event); - } + if (form) { + const method = (submitter?.getAttribute('formmethod') ?? form.getAttribute('method') ?? 'GET').toUpperCase(); + const url = submitter?.getAttribute('formaction') ?? form.getAttribute('action') ?? window.location.pathname + window.location.search; + const data = new FormData(form, submitter); - public async submitForm(form: HTMLFormElement, options: Options = {}, event?: Event): Promise { - const method = form.getAttribute('method')?.toUpperCase() ?? 'GET'; - const url = form.getAttribute('action') ?? window.location.pathname + window.location.search; - const data = new FormData(form); + return this.processInteraction(submitter ?? form, method, url, data, options, event); + } - return this.processInteraction(form, method, url, data, options, event); + return {}; } public async processInteraction( diff --git a/tests/Naja.UIHandler.js b/tests/Naja.UIHandler.js index d01b461..1c1a8e1 100644 --- a/tests/Naja.UIHandler.js +++ b/tests/Naja.UIHandler.js @@ -110,12 +110,8 @@ describe('UIHandler', function () { this.a.dispatchEvent(createEvent('click')); this.form.dispatchEvent(createEvent('submit')); - this.input.dispatchEvent(createEvent('click')); - this.image.dispatchEvent(createEvent('click')); - this.submitButton.dispatchEvent(createEvent('click')); - this.externalButton.dispatchEvent(createEvent('click')); - assert.equal(naja.uiHandler.handler.callCount, 6); + assert.equal(naja.uiHandler.handler.callCount, 2); }); it('binds to elements specified by custom selector', function () { @@ -488,8 +484,9 @@ describe('UIHandler', function () { const preventDefault = sinon.spy(); const evt = { - type: 'click', - currentTarget: this.input, + type: 'submit', + currentTarget: this.form2, + submitter: this.input, preventDefault, }; handler.handleUI(evt); @@ -511,8 +508,9 @@ describe('UIHandler', function () { const preventDefault = sinon.spy(); const evt = { - type: 'click', - currentTarget: this.image, + type: 'submit', + currentTarget: this.form3, + submitter: this.image, preventDefault, }; handler.handleUI(evt); @@ -534,8 +532,9 @@ describe('UIHandler', function () { const preventDefault = sinon.spy(); const evt = { - type: 'click', - currentTarget: this.submitButton, + type: 'submit', + currentTarget: this.form4, + submitter: this.submitButton, preventDefault, }; handler.handleUI(evt); @@ -557,8 +556,9 @@ describe('UIHandler', function () { const preventDefault = sinon.spy(); const evt = { - type: 'click', - currentTarget: this.externalButton, + type: 'submit', + currentTarget: this.form5, + submitter: this.externalButton, preventDefault, }; handler.handleUI(evt); @@ -591,6 +591,29 @@ describe('UIHandler', function () { }); }); + it('dispatches request on button[form]|input[form]', function () { + const naja = mockNaja(); + const mock = sinon.mock(naja); + const expectedResult = {answer: 42}; + + mock.expects('makeRequest') + .withExactArgs('POST', '/UIHandler/submitForm', sinon.match.instanceOf(FormData), {}) + .once() + .returns(Promise.resolve(expectedResult)); + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = '/UIHandler/submitForm'; + const input = document.createElement('input'); + input.type = 'submit'; + form.appendChild(input); + + const handler = new UIHandler(naja); + handler.clickElement(input); + + mock.verify(); + }); + it('triggers interaction event', function () { const naja = mockNaja(); const mock = sinon.mock(naja); @@ -618,6 +641,20 @@ describe('UIHandler', function () { mock.verify(); }); + + it('does not trigger interaction event on non-hyperlink|:not([form]) elements', function () { + const naja = mockNaja(); + + const btn = document.createElement('button'); + + const listener = sinon.spy(); + const handler = new UIHandler(naja); + handler.addEventListener('interaction', listener); + + handler.clickElement(btn); + + assert.isFalse(listener.called); + }); }); describe('submitForm()', function () { @@ -672,6 +709,20 @@ describe('UIHandler', function () { mock.verify(); }); + + it('does not trigger interaction event without form element', function () { + const naja = mockNaja(); + + const btn = document.createElement('button'); + + const listener = sinon.spy(); + const handler = new UIHandler(naja); + handler.addEventListener('interaction', listener); + + handler.submitForm(btn); + + assert.isFalse(listener.called); + }); }); describe('processInteraction()', function () {