UNPKG

hyperform

Version:

Capture form validation back from the browser

324 lines (278 loc) 9.63 kB
'use strict'; import trigger_event, { create_event } from './trigger_event'; import matches from './matches'; import { get_validated_elements } from './get_validated_elements'; import reportValidity from '../polyfills/reportValidity'; import { text as text_types } from '../components/types'; import { get_wrapper } from '../components/wrapper'; /** * submit a form, because `element` triggered it * * This method also dispatches a submit event on the form prior to the * submission. The event contains the trigger element as `submittedVia`. * * If the element is a button with a name, the name=value pair will be added * to the submitted data. */ function submit_form_via(element) { /* apparently, the submit event is not triggered in most browsers on * the submit() method, so we do it manually here to model a natural * submit as closely as possible. * Now to the fun fact: If you trigger a submit event from a form, what * do you think should happen? * 1) the form will be automagically submitted by the browser, or * 2) nothing. * And as you already suspected, the correct answer is: both! Firefox * opts for 1), Chrome for 2). Yay! */ var event_got_cancelled; var submit_event = create_event('submit', { cancelable: true }); /* force Firefox to not submit the form, then fake preventDefault() */ submit_event.preventDefault(); Object.defineProperty(submit_event, 'defaultPrevented', { value: false, writable: true, }); Object.defineProperty(submit_event, 'preventDefault', { value: () => submit_event.defaultPrevented = event_got_cancelled = true, writable: true, }); trigger_event(element.form, submit_event, {}, { submittedVia: element }); if (! event_got_cancelled) { add_submit_field(element); window.HTMLFormElement.prototype.submit.call(element.form); window.setTimeout(() => remove_submit_field(element)); } } /** * if a submit button was clicked, add its name=value by means of a type=hidden * input field */ function add_submit_field(button) { if (['image', 'submit'].indexOf(button.type) > -1 && button.name) { const wrapper = get_wrapper(button.form) || {}; var submit_helper = wrapper.submit_helper; if (submit_helper) { if (submit_helper.parentNode) { submit_helper.parentNode.removeChild(submit_helper); } } else { submit_helper = document.createElement('input'); submit_helper.type = 'hidden'; wrapper.submit_helper = submit_helper; } submit_helper.name = button.name; submit_helper.value = button.value; button.form.appendChild(submit_helper); } } /** * remove a possible helper input, that was added by `add_submit_field` */ function remove_submit_field(button) { if (['image', 'submit'].indexOf(button.type) > -1 && button.name) { const wrapper = get_wrapper(button.form) || {}; const submit_helper = wrapper.submit_helper; if (submit_helper && submit_helper.parentNode) { submit_helper.parentNode.removeChild(submit_helper); } } } /** * check a form's validity and submit it * * The method triggers a cancellable `validate` event on the form. If the * event is cancelled, form submission will be aborted, too. * * If the form is found to contain invalid fields, focus the first field. */ function check(button) { /* trigger a "validate" event on the form to be submitted */ const val_event = trigger_event(button.form, 'validate', { cancelable: true }); if (val_event.defaultPrevented) { /* skip the whole submit thing, if the validation is canceled. A user * can still call form.submit() afterwards. */ return; } var valid = true; var first_invalid; button.form.__hf_form_validation = true; get_validated_elements(button.form).map(element => { if (! reportValidity(element)) { valid = false; if (! first_invalid && ('focus' in element)) { first_invalid = element; } } }); delete(button.form.__hf_form_validation); if (valid) { submit_form_via(button); } else if (first_invalid) { /* focus the first invalid element, if validation went south */ first_invalid.focus(); /* tell the tale, if anyone wants to react to it */ trigger_event(button.form, 'forminvalid'); } } /** * test if node is a submit button */ function is_submit_button(node) { return ( /* must be an input or button element... */ (node.nodeName === 'INPUT' || node.nodeName === 'BUTTON') && /* ...and have a submitting type */ (node.type === 'image' || node.type === 'submit') ); } /** * test, if the click event would trigger a submit */ function is_submitting_click(event, button) { return ( /* prevented default: won't trigger a submit */ ! event.defaultPrevented && /* left button or middle button (submits in Chrome) */ (! ('button' in event) || event.button < 2) && /* must be a submit button... */ is_submit_button(button) && /* the button needs a form, that's going to be submitted */ button.form && /* again, if the form should not be validated, we're out of the game */ ! button.form.hasAttribute('novalidate') ); } /** * test, if the keypress event would trigger a submit */ function is_submitting_keypress(event) { return ( /* prevented default: won't trigger a submit */ ! event.defaultPrevented && ( ( /* ...and <Enter> was pressed... */ event.keyCode === 13 && /* ...on an <input> that is... */ event.target.nodeName === 'INPUT' && /* ...a standard text input field (not checkbox, ...) */ text_types.indexOf(event.target.type) > -1 ) || ( /* or <Enter> or <Space> was pressed... */ (event.keyCode === 13 || event.keyCode === 32) && /* ...on a submit button */ is_submit_button(event.target) ) ) && /* there's a form... */ event.target.form && /* ...and the form allows validation */ ! event.target.form.hasAttribute('novalidate') ); } /** * catch clicks to children of <button>s */ function get_clicked_button(element) { if (is_submit_button(element)) { return element; } else if (matches(element, 'button:not([type]) *, button[type="submit"] *')) { return get_clicked_button(element.parentNode); } else { return null; } } /** * return event handler to catch explicit submission by click on a button */ function get_click_handler(ignore=false) { return function(event) { const button = get_clicked_button(event.target); if (button && is_submitting_click(event, button)) { event.preventDefault(); if (ignore || button.hasAttribute('formnovalidate')) { /* if validation should be ignored, we're not interested in any checks */ submit_form_via(button); } else { check(button); } } }; } const click_handler = get_click_handler(); const ignored_click_handler = get_click_handler(true); /** * catch implicit submission by pressing <Enter> in some situations */ function get_keypress_handler(ignore) { return function keypress_handler(event) { if (is_submitting_keypress(event)) { event.preventDefault(); const wrapper = get_wrapper(event.target.form) || { settings: {} }; if (wrapper.settings.preventImplicitSubmit) { /* user doesn't want an implicit submit. Cancel here. */ return; } /* check, that there is no submit button in the form. Otherwise * that should be clicked. */ const el = event.target.form.elements.length; var submit; for (let i = 0; i < el; i++) { if (['image', 'submit'].indexOf(event.target.form.elements[i].type) > -1) { submit = event.target.form.elements[i]; break; } } /* trigger an "implicit_submit" event on the form to be submitted */ const implicit_event = trigger_event(event.target.form, 'implicit_submit', { cancelable: true }, { trigger: event.target, submittedVia: submit || event.target, }); if (implicit_event.defaultPrevented) { /* skip the submit, if implicit submit is canceled */ return; } if (submit) { submit.click(); } else if (ignore) { submit_form_via(event.target); } else { check(event.target); } } }; } const keypress_handler = get_keypress_handler(); const ignored_keypress_handler = get_keypress_handler(true); /** * catch all relevant events _prior_ to a form being submitted * * @param bool ignore bypass validation, when an attempt to submit the * form is detected. True, when the wrapper's revalidate * setting is 'never'. */ export function catch_submit(listening_node, ignore=false) { if (ignore) { listening_node.addEventListener('click', ignored_click_handler); listening_node.addEventListener('keypress', ignored_keypress_handler); } else { listening_node.addEventListener('click', click_handler); listening_node.addEventListener('keypress', keypress_handler); } } /** * decommission the event listeners from catch_submit() again */ export function uncatch_submit(listening_node) { listening_node.removeEventListener('click', ignored_click_handler); listening_node.removeEventListener('keypress', ignored_keypress_handler); listening_node.removeEventListener('click', click_handler); listening_node.removeEventListener('keypress', keypress_handler); }