UNPKG

natura11y

Version:

Natura11y is an open-source design system for creating beautiful web interfaces with a focus on accessible interaction design and front-end development.

230 lines (183 loc) 7.56 kB
import { delegateEvent } from './utilities/eventDelegation'; ////////////////////////////////////////////// // A. Shared Methods ////////////////////////////////////////////// const isEmpty = (value) => !value?.trim(); const setFieldValidity = (field, isValid, invalidClasses = ['is-invalid']) => { const entryRoot = field.closest('.form-entry'); entryRoot.classList.toggle(invalidClasses[0], !isValid); field.setAttribute('aria-invalid', !isValid); }; ////////////////////////////////////////////// // B. Form Input ////////////////////////////////////////////// export default class FormInput { #formEntryList = document.querySelectorAll('.form-entry'); #invalidClasses = ['is-invalid']; #formSubmitAttempted = false; // Private methods #checkIfEmpty(field) { const isFieldEmpty = isEmpty(field.value); setFieldValidity(field, !isFieldEmpty, this.#invalidClasses); return isFieldEmpty; } #handleInputChange(formEntryInput, isRequired) { // Check if empty or not if submission has already been attempted if (this.#formSubmitAttempted && isRequired) { this.#checkIfEmpty(formEntryInput); } // Toggle 'has-value' class based on input content formEntryInput.closest('.form-entry').classList.toggle('has-value', formEntryInput.value !== ''); } // Attach input event listener to dynamically validate while typing #addDynamicValidation(formEntryInput) { formEntryInput.addEventListener('input', () => { this.#checkIfEmpty(formEntryInput); // Remove error if the field becomes valid }); } #processFormEntryInput(formEntryInput, isRequired) { const isInputText = formEntryInput.closest('.form-entry').querySelector('.form-entry__field__input'); if (isRequired) { formEntryInput.setAttribute('required', 'true'); formEntryInput.setAttribute('aria-required', 'true'); } // Add dynamic validation on input event (removes error class when user types) this.#addDynamicValidation(formEntryInput); // Handle input change on 'change' event formEntryInput.addEventListener('change', () => this.#handleInputChange(formEntryInput, isRequired)); // Handle click events on input text spans if (isInputText) { isInputText.addEventListener('click', this.#handleClickOnInputText); } } #handleClickOnInputText(event) { const clickInput = event.target.closest('.form-entry__field__input').querySelector('input'); if (event.target.tagName === 'SPAN') clickInput.focus(); } // Public methods init() { this.#formEntryList.forEach((formEntry) => { const isRequired = formEntry.hasAttribute('data-required'); const formEntryInputList = formEntry.querySelectorAll('input, select, textarea'); // Process each form entry input formEntryInputList.forEach((formEntryInput) => this.#processFormEntryInput(formEntryInput, isRequired)); // Scoped Event Delegation for focusin and focusout (within each formEntry) delegateEvent(formEntry, 'focusin', 'input, select, textarea', (event) => { this.#toggleFocusClass(event, true); }); delegateEvent(formEntry, 'focusout', 'input, select, textarea', (event) => { this.#toggleFocusClass(event, false); }); }); } #toggleFocusClass(event, isFocused = true) { event.target.closest('.form-entry').classList.toggle('is-focused', isFocused); } } ////////////////////////////////////////////// // C. Form Submission ////////////////////////////////////////////// export class FormSubmission { #formList = document.querySelectorAll('form[novalidate]'); #invalidClasses = ['is-invalid']; #formSubmitAttempted = false; #processFormErrors(formErrorsList, errorsArray) { formErrorsList.forEach((formError) => { const formEntry = formError.closest('.form-entry'); const formEntryLabel = formEntry.querySelector('.form-entry__field__label'); const errorMessage = formEntry.getAttribute('data-error-message') || 'This field is required'; const helpText = formEntry.querySelector('.form-entry__help')?.innerHTML || ''; errorsArray.push([errorMessage, helpText]); if (!formEntry.querySelector('.form-entry__feedback')) { formEntryLabel.insertAdjacentHTML('afterend', this.#createErrorMessage(errorMessage, helpText)); } }); } #createErrorMessage(desc, inst) { return ` <small class="form-entry__feedback" role="alert"> <span class="icon icon-warn" aria-hidden="true"></span> <span class="message"> <strong>${desc}</strong> ${inst || ''} </span> </small> `; } #scrollToFirstError(form) { const firstError = form.querySelector('.is-invalid, [data-alert]'); if (firstError) { window.scrollTo({ top: firstError.offsetTop - 16, behavior: 'smooth' }); } } #checkIfEmpty(field) { const isFieldEmpty = isEmpty(field.value); setFieldValidity(field, !isFieldEmpty, this.#invalidClasses); return isFieldEmpty; } #handleFormSubmission(form) { form.addEventListener('submit', (event) => { event.preventDefault(); this.#formSubmitAttempted = true; const errorsArray = []; const inputFields = form.querySelectorAll('input, select, textarea'); inputFields.forEach((field) => { if (field.hasAttribute('required')) { this.#checkIfEmpty(field); } }); const formErrorsList = form.querySelectorAll(':invalid'); this.#processFormErrors(formErrorsList, errorsArray); if (errorsArray.length > 0) { event.preventDefault(); this.#scrollToFirstError(form); } }); } init() { this.#formList.forEach((form) => this.#handleFormSubmission(form)); } } ////////////////////////////////////////////// // D. Form File Upload ////////////////////////////////////////////// export class FormFileUpload { #fileUploadList = document.querySelectorAll('.file-upload'); #handleFileChange(fileUpload) { return (event) => { const [file] = event.target.files; if (!file) return; const { name: fileName, size } = file; const fileSize = size >= 1e6 ? `${(size / 1e6).toFixed(2)} MB` : `${(size / 1e3).toFixed(2)} KB`; const fileUploadData = fileUpload.querySelector('.file-upload__data'); if (fileUploadData) fileUploadData.remove(); fileUpload.insertAdjacentHTML('beforeend', ` <span class="file-upload__data"> <span class="file-name">${fileName}</span> <span class="file-size">${fileSize}</span> </span> `); }; } dragOver(event) { event.preventDefault(); event.target.closest('.form-entry').classList.add('is-focused'); } dragOff(event) { event.target.closest('.form-entry').classList.remove('is-focused'); } dropped(event) { event.preventDefault(); event.target.closest('.form-entry').classList.remove('is-focused'); } #handleFileUpload(fileUpload) { const fileUploadInput = fileUpload.querySelector('input[type="file"]'); fileUploadInput.addEventListener('change', this.#handleFileChange(fileUpload)); fileUpload.addEventListener('dragenter', this.dragOver.bind(this)); fileUpload.addEventListener('dragleave', this.dragOff.bind(this)); fileUpload.addEventListener('dragend', this.dragOff.bind(this)); fileUpload.addEventListener('drop', this.dropped.bind(this)); } init() { this.#fileUploadList.forEach((fileUpload) => this.#handleFileUpload(fileUpload)); } }