UNPKG

@conform-to/dom

Version:

A set of opinionated helpers built on top of the Constraint Validation API

454 lines (434 loc) 14.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var util = require('./util.js'); /** * Element that user can interact with, * includes `<input>`, `<select>` and `<textarea>`. */ /** * Form Control element. It can either be a submit button or a submit input. */ function isInputElement(element) { return element.tagName === 'INPUT'; } function isSelectElement(element) { return element.tagName === 'SELECT'; } function isTextAreaElement(element) { return element.tagName === 'TEXTAREA'; } /** * A type guard to check if the provided element is a field element, which * is a form control excluding submit, button and reset type. */ function isFieldElement(element) { if (element instanceof Element) { if (isInputElement(element)) { return element.type !== 'submit' && element.type !== 'button' && element.type !== 'reset'; } if (isSelectElement(element) || isTextAreaElement(element)) { return true; } } return false; } /** * Resolves the action from the submit event * with respect to the submitter `formaction` attribute. */ function getFormAction(event) { var _ref, _submitter$getAttribu; var form = event.target; var submitter = event.submitter; return (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search); } /** * Resolves the encoding type from the submit event * with respect to the submitter `formenctype` attribute. */ function getFormEncType(event) { var _submitter$getAttribu2; var form = event.target; var submitter = event.submitter; var encType = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.enctype; if (encType === 'multipart/form-data') { return encType; } return 'application/x-www-form-urlencoded'; } /** * Resolves the method from the submit event * with respect to the submitter `formmethod` attribute. */ function getFormMethod(event) { var _ref2, _submitter$getAttribu3; var form = event.target; var submitter = event.submitter; var method = (_ref2 = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.getAttribute('method')) === null || _ref2 === void 0 ? void 0 : _ref2.toUpperCase(); switch (method) { case 'POST': case 'PUT': case 'PATCH': case 'DELETE': return method; } return 'GET'; } /** * Trigger a form submit event with an optional submitter. * If the submitter is not mounted, it will be appended to the form and removed after submission. */ function requestSubmit(form, submitter) { util.invariant(!!form, 'Failed to submit the form. The element provided is null or undefined.'); if (typeof form.requestSubmit === 'function') { form.requestSubmit(submitter); } else { var _event = new SubmitEvent('submit', { bubbles: true, cancelable: true, submitter }); form.dispatchEvent(_event); } } function createFileList(value) { var dataTransfer = new DataTransfer(); if (Array.isArray(value)) { for (var file of value) { dataTransfer.items.add(file); } } else { dataTransfer.items.add(value); } return dataTransfer.files; } function createGlobalFormsObserver() { var inputListeners = new Set(); var formListeners = new Set(); var cleanup = null; function initialize() { var observer = new MutationObserver(handleMutation); observer.observe(document.body, { subtree: true, childList: true, attributeFilter: ['form', 'name', 'data-conform'] }); document.addEventListener('input', handleInput); document.addEventListener('reset', handleReset); document.addEventListener('submit', handleSubmit, true); return () => { document.removeEventListener('input', handleInput); document.removeEventListener('reset', handleReset); document.removeEventListener('submit', handleSubmit, true); observer.disconnect(); }; } function handleInput(event) { var target = event.target; if (isFieldElement(target)) { inputListeners.forEach(callback => callback({ type: 'input', target })); var form = target.form; if (form) { formListeners.forEach(callback => callback({ type: 'input', target: form })); } } } function handleReset(event) { var form = event.target; if (form instanceof HTMLFormElement) { // Reset event is fired before the form is reset, so we need to wait for the next tick setTimeout(() => { formListeners.forEach(callback => { callback({ type: 'reset', target: form }); }); var _loop = function _loop(target) { if (isFieldElement(target)) { inputListeners.forEach(callback => { callback({ type: 'reset', target }); }); } }; for (var target of form.elements) { _loop(target); } }); } } function handleSubmit(event) { var target = event.target; var submitter = event.submitter; if (target instanceof HTMLFormElement) { formListeners.forEach(callback => callback({ type: 'submit', target, submitter })); } } function handleMutation(mutations) { var seenForms = new Set(); var seenInputs = new Set(); var collectInputs = node => { if (isFieldElement(node)) { return [node]; } return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : []; }; for (var mutation of mutations) { switch (mutation.type) { case 'childList': { var nodes = [...mutation.addedNodes, ...mutation.removedNodes]; for (var node of nodes) { for (var input of collectInputs(node)) { seenInputs.add(input); if (input.form) { seenForms.add(input.form); } } } break; } case 'attributes': { if (isFieldElement(mutation.target)) { seenInputs.add(mutation.target); if (mutation.target.form) { seenForms.add(mutation.target.form); } } break; } } } var _loop2 = function _loop2(target) { formListeners.forEach(callback => { callback({ type: 'mutation', target }); }); }; for (var target of seenForms) { _loop2(target); } var _loop3 = function _loop3(_target) { inputListeners.forEach(callback => { callback({ type: 'mutation', target: _target }); }); }; for (var _target of seenInputs) { _loop3(_target); } } return { onFieldUpdate(callback) { var _cleanup; cleanup = (_cleanup = cleanup) !== null && _cleanup !== void 0 ? _cleanup : initialize(); inputListeners.add(callback); return () => { inputListeners.delete(callback); }; }, onFormUpdate(callback) { var _cleanup2; cleanup = (_cleanup2 = cleanup) !== null && _cleanup2 !== void 0 ? _cleanup2 : initialize(); formListeners.add(callback); return () => { formListeners.delete(callback); }; }, dispose() { var _cleanup3; (_cleanup3 = cleanup) === null || _cleanup3 === void 0 || _cleanup3(); cleanup = null; inputListeners.clear(); formListeners.clear(); } }; } function change(element, value) { // The value should be set to the element before dispatching the event updateField(element, { value }); // Dispatch input event with the updated input value element.dispatchEvent(new InputEvent('input', { bubbles: true })); // Dispatch change event (necessary for select to update the selected option) element.dispatchEvent(new Event('change', { bubbles: true })); } function focus(element) { // Only focusin event will be bubbled element.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); element.dispatchEvent(new FocusEvent('focus')); } function blur(element) { // Only focusout event will be bubbled element.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); element.dispatchEvent(new FocusEvent('blur')); } function normalizeFieldValue(value) { if (typeof value === 'undefined') { return [null, null]; } if (value === null) { return [[], createFileList([])]; } if (typeof value === 'string') { return [[value], null]; } if (Array.isArray(value)) { if (value.every(item => typeof item === 'string')) { return [Array.from(value), null]; } if (value.every(item => item instanceof File)) { return [null, createFileList(value)]; } } if (value instanceof FileList) { return [null, value]; } if (value instanceof File) { return [null, createFileList([value])]; } return [null, null]; } /** * Updates the DOM element with the provided value and defaultValue. */ function updateField(element, options) { var _value$; var [value, file] = normalizeFieldValue(options.value); var [defaultValue] = normalizeFieldValue(options.defaultValue); if (isInputElement(element)) { switch (element.type) { case 'file': { element.files = file; return; } case 'checkbox': case 'radio': { if (value) { var checked = value.includes(element.value); if (element.type === 'checkbox' ? checked !== element.checked : checked) { // Simulate a click to update the checked state element.click(); } element.checked = checked; } if (defaultValue) { element.defaultChecked = defaultValue.includes(element.value); } return; } } } else if (isSelectElement(element)) { var shouldUnselect = value && value.length === 0; for (var option of element.options) { if (value) { var index = value.indexOf(option.value); var selected = index > -1; // Update the selected state of the option if (option.selected !== selected) { option.selected = selected; } // Remove the option from the value array if (selected) { value.splice(index, 1); } } if (defaultValue) { var _index = defaultValue.indexOf(option.value); var _selected = _index > -1; // Update the selected state of the option if (option.defaultSelected !== _selected) { option.defaultSelected = _selected; } // Remove the option from the defaultValue array if (_selected) { defaultValue.splice(_index, 1); } } } // We have already removed all selected options from the value and defaultValue array at this point var missingOptions = new Set([...(value !== null && value !== void 0 ? value : []), ...(defaultValue !== null && defaultValue !== void 0 ? defaultValue : [])]); for (var optionValue of missingOptions) { element.options.add(new Option(optionValue, optionValue, defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.includes(optionValue), value === null || value === void 0 ? void 0 : value.includes(optionValue))); } // If the select element is not multiple and the value is an empty array, unset the selected index // This is to prevent the select element from showing the first option as selected if (shouldUnselect) { element.selectedIndex = -1; } return; } var inputValue = (_value$ = value === null || value === void 0 ? void 0 : value[0]) !== null && _value$ !== void 0 ? _value$ : ''; if (element.value !== inputValue) { /** * Triggering react custom change event * Solution based on dom-testing-library * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776 * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123 */ var { set: valueSetter } = Object.getOwnPropertyDescriptor(element, 'value') || {}; var prototype = Object.getPrototypeOf(element); var { set: prototypeValueSetter } = Object.getOwnPropertyDescriptor(prototype, 'value') || {}; if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { prototypeValueSetter.call(element, inputValue); } else { if (valueSetter) { valueSetter.call(element, inputValue); } else { throw new Error('The given element does not have a value setter'); } } } if (defaultValue) { var _defaultValue$; element.defaultValue = (_defaultValue$ = defaultValue[0]) !== null && _defaultValue$ !== void 0 ? _defaultValue$ : ''; } } exports.blur = blur; exports.change = change; exports.createFileList = createFileList; exports.createGlobalFormsObserver = createGlobalFormsObserver; exports.focus = focus; exports.getFormAction = getFormAction; exports.getFormEncType = getFormEncType; exports.getFormMethod = getFormMethod; exports.isFieldElement = isFieldElement; exports.isInputElement = isInputElement; exports.isSelectElement = isSelectElement; exports.isTextAreaElement = isTextAreaElement; exports.normalizeFieldValue = normalizeFieldValue; exports.requestSubmit = requestSubmit; exports.updateField = updateField;