UNPKG

axentix

Version:

Axentix is a framework mixing fully customizable components & utility-first classes, leaving the design choice to the developer.

258 lines (211 loc) 7.63 kB
import { getCssVar } from '../../utils/config'; import { validateInput } from './forms-validation'; let isInit = true; /** * Detect attribute & state of all inputs */ const detectAllInputs = (inputElements: NodeListOf<Element>) => { inputElements.forEach(detectInput); }; /** * Delay detection of all inputs */ const delayDetectionAllInputs = (inputElements: NodeListOf<Element>) => { if (isInit) { isInit = false; return; } setTimeout(() => { detectAllInputs(inputElements); }, 10); }; /** * Detect attribute & state of an input */ const detectInput = (input: any) => { const formField = input.closest('.form-field'); const customSelect = formField.querySelector('.form-custom-select'); const isActive = formField.classList.contains('active'); const types = ['date', 'month', 'week', 'time']; let hasContent = customSelect && input.tagName === 'DIV' && input.innerText.length > 0; if (!customSelect) hasContent = input.value.length > 0 || (input.tagName !== 'SELECT' && input.placeholder.length > 0) || input.tagName === 'SELECT' || types.some((type) => input.matches(`[type="${type}"]`)); const isFocused = document.activeElement === input; const isDisabled = input.hasAttribute('disabled') || input.hasAttribute('readonly'); if (input.firstInit) { updateInput(input, isActive, hasContent, isFocused, formField, customSelect); input.firstInit = false; input.isInit = true; } else { if (!isDisabled) updateInput(input, isActive, hasContent, isFocused, formField, customSelect); } }; /** * Update input field */ const updateInput = ( input: any, isActive: boolean, hasContent: boolean, isFocused: boolean, formField, customSelect: HTMLDivElement ) => { const isTextArea = input.type === 'textarea'; const label = formField.querySelector('label:not(.form-check)'); if (!isActive && (hasContent || isFocused)) { formField.classList.add('active'); } else if (isActive && !(hasContent || isFocused)) { formField.classList.remove('active'); } if (!isTextArea) setFormPosition(input, formField, customSelect, label); else if (label) label.style.backgroundColor = getLabelColor(label); if (isFocused && !isTextArea) formField.classList.add('is-focused'); else if (!customSelect) formField.classList.remove('is-focused'); if (isFocused && isTextArea) formField.classList.add('is-textarea-focused'); else formField.classList.remove('is-textarea-focused'); }; /** * Add bottom position variable to form */ const setFormPosition = ( input: HTMLElement, formField: HTMLElement, customSelect: HTMLDivElement, label?: HTMLLabelElement ) => { const inputWidth = input.clientWidth, inputLeftOffset = input.offsetLeft; const topOffset = input.clientHeight + (customSelect ? customSelect.offsetTop : input.offsetTop) + 'px'; const isBordered = input.closest('.form-material').classList.contains('form-material-bordered'); formField.style.setProperty(getCssVar('form-material-position'), topOffset); let offset = inputLeftOffset, side = 'left', width = inputWidth + 'px', labelLeft = 0; if (formField.classList.contains('form-rtl')) { side = 'right'; offset = formField.clientWidth - inputWidth - inputLeftOffset; } formField.style.setProperty(getCssVar(`form-material-${side}-offset`), offset + 'px'); if (offset != 0) labelLeft = inputLeftOffset; formField.style.setProperty(getCssVar('form-material-width'), width); if (label) { label.style.left = labelLeft + 'px'; if (isBordered) label.style.backgroundColor = getLabelColor(label); } }; const extractBgColor = (target: HTMLElement) => { const bg = window.getComputedStyle(target).backgroundColor; if (bg && !['transparent', 'rgba(0, 0, 0, 0)'].includes(bg)) return bg; }; const getLabelColor = (label: HTMLElement) => { label.style.backgroundColor = ''; let target = label; while (target.parentElement) { const bg = extractBgColor(target); if (bg) return bg; target = target.parentElement; } const htmlBg = extractBgColor(document.documentElement); if (htmlBg) return htmlBg; return 'white'; }; const validate = (input: HTMLInputElement, e: Event) => { if (input.hasAttribute(`data-form-validate`)) validateInput(input, e.type); }; /** * Handle listeners */ const handleListeners = (inputs: NodeListOf<Element>, e: Event) => { inputs.forEach((input) => { if (input === e.target) detectInput(input); }); }; /** * Handle form reset event */ const handleResetEvent = (inputs: NodeListOf<Element>, e: any) => { if (e.target.tagName === 'FORM' && e.target.classList.contains('form-material')) delayDetectionAllInputs(inputs); }; /** * Setup forms fields listeners */ const setupFormsListeners = (inputElements: any) => { inputElements.forEach((input) => { input.firstInit = true; input.validateRef = validate.bind(null, input); input.addEventListener('input', input.validateRef); input.addEventListener('change', input.validateRef); }); detectAllInputs(inputElements); const handleListenersRef = handleListeners.bind(null, inputElements); document.addEventListener('focus', handleListenersRef, true); document.addEventListener('blur', handleListenersRef, true); const delayDetectionAllInputsRef = delayDetectionAllInputs.bind(null, inputElements); window.addEventListener('pageshow', delayDetectionAllInputsRef); const handleResetRef = handleResetEvent.bind(null, inputElements); document.addEventListener('reset', handleResetRef); const detectAllInputsRef = detectAllInputs.bind(null, inputElements); window.addEventListener('resize', detectAllInputsRef); }; const handleFileInput = (input: HTMLInputElement, filePath: HTMLElement) => { const files = input.files; if (files.length > 1) { filePath.innerHTML = Array.from(files) .map((file) => file.name) .join(', '); } else if (files[0]) { filePath.innerHTML = files[0].name; } }; const setupFormFile = (element) => { if (element.isInit) return; element.isInit = true; const input = element.querySelector('input[type="file"]'); const filePath = element.querySelector('.form-file-path'); input.handleRef = handleFileInput.bind(null, input, filePath); input.validateRef = validate.bind(null, input); input.addEventListener('change', input.handleRef); input.addEventListener('input', input.validateRef); input.addEventListener('change', input.validateRef); }; const updateInputsFile = () => { const elements = Array.from(document.querySelectorAll('.form-file')); try { elements.forEach(setupFormFile); } catch (error) { console.error('[Axentix] Form file error', error); } }; /** * Update inputs state */ export const updateInputs = ( inputElements: NodeListOf<HTMLElement> | Array<HTMLElement> = document.querySelectorAll( '.form-material .form-field:not(.form-default) .form-control:not(.form-custom-select)' ) ) => { const { setupInputs, detectInputs } = Array.from(inputElements).reduce( (acc, el: any) => { if (el.isInit) acc.detectInputs.push(el); else acc.setupInputs.push(el); return acc; }, { setupInputs: [], detectInputs: [] } ); updateInputsFile(); try { if (setupInputs.length > 0) setupFormsListeners(setupInputs); if (detectInputs.length > 0) detectAllInputs(detectInputs as unknown as NodeListOf<HTMLElement>); } catch (error) { console.error('[Axentix] Material forms error', error); } }; // Init document.addEventListener('DOMContentLoaded', () => updateInputs());