UNPKG

maz-ui

Version:

A standalone components library for Vue.Js 3 & Nuxt.Js 3

424 lines (423 loc) 13.8 kB
import { nextTick, getCurrentInstance, inject } from "vue"; import { useFreezeValue } from "../composables/useFreezeValue.js"; function isPrimitive(value) { return value == null || typeof value == "string" || typeof value == "number" || typeof value == "boolean" || typeof value == "symbol" || typeof value == "bigint"; } function isEqualArrays(a, b) { if (a.length !== b.length) return !1; for (const [i, element] of a.entries()) if (!isEqual(element, b[i])) return !1; return !0; } function isEqualObjects(a, b) { const keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length !== keysB.length) return !1; for (const key of keysA) if (!keysB.includes(key) || !isEqual(a[key], b[key])) return !1; return !0; } function isEqual(a, b) { return isPrimitive(a) || isPrimitive(b) ? a === b : a instanceof Date && b instanceof Date ? a.getTime() === b.getTime() : typeof a != typeof b || Array.isArray(a) !== Array.isArray(b) ? !1 : Array.isArray(a) && Array.isArray(b) ? isEqualArrays(a, b) : typeof a == "object" && typeof b == "object" ? isEqualObjects(a, b) : !1; } const CONFIG = { mode: "lazy", scrollToErrorSelector: ".has-field-error", debounceTime: 300, throttleTime: 1e3 }; function debounceId(identifier, func, delay) { const debouncedFunctions = {}; return function(...args) { debouncedFunctions[identifier] || (debouncedFunctions[identifier] = { timer: null, promise: null }); const debounced = debouncedFunctions[identifier]; return debounced.timer && clearTimeout(debounced.timer), debounced.promise = new Promise((resolve, reject) => { debounced.timer = setTimeout(async () => { try { resolve(await func(...args)); } catch (error) { reject(error); } finally { delete debouncedFunctions[identifier]; } }, delay); }), debounced.promise; }; } function throttleId(identifier, func, interval) { const state = {}; return async (...args) => { const now = Date.now(); return state[identifier] || (state[identifier] = { promise: null, lastCall: 0, lastArgs: [] }), now - state[identifier].lastCall >= interval ? (state[identifier].lastCall = now, func(...args)) : (state[identifier].lastArgs = args, state[identifier].promise || (state[identifier].promise = new Promise((resolve) => { setTimeout( async () => { state[identifier].lastCall = Date.now(); const result = await func(...state[identifier].lastArgs); state[identifier].promise = null, resolve(result); }, interval - (now - state[identifier].lastCall) ); })), state[identifier].promise); }; } function isEmptyValue(value) { return value == null || value === ""; } const storeValidbot = {}; async function getValibotValidationMethod(methodName) { if (storeValidbot[methodName]) return storeValidbot[methodName]; const valibot = await import("valibot"); return storeValidbot[methodName] = valibot[methodName], valibot[methodName]; } async function getValidationSchema(formSchema) { return (await getValibotValidationMethod("objectAsync"))(formSchema); } async function getFieldValidationResult(name, schema, value) { const fieldSchema = await getValidationSchema(schema), result = await (await getValibotValidationMethod("safeParseAsync"))(fieldSchema.entries[name], value ?? ""); return { result, isValid: result.success }; } async function setFieldValidationState({ name, fieldState, schema, payload, setError = !0, setErrorIfInvalidAndNotEmpty = !1 }) { if (await nextTick(), fieldState.validating) return Promise.resolve(); fieldState.validating = !0; try { if (!schema[name]) { fieldState.valid = !0, fieldState.validating = !1, fieldState.validated = !0, fieldState.errors = [], fieldState.error = !1; return; } const { result, isValid } = await getFieldValidationResult(name, schema, payload[name]); fieldState.valid = isValid; const shouldSetError = setError || setErrorIfInvalidAndNotEmpty && !isValid && !isEmptyValue(payload[name]), shouldPreserveError = fieldState.blurred && !isValid && fieldState.mode && ["eager", "blur", "progressive"].includes(fieldState.mode); (shouldSetError || shouldPreserveError) && (fieldState.error = !isValid), fieldState.errors = result.issues ?? [], fieldState.validated = !0; } finally { fieldState.validating = !1; } } function validateForm({ fieldsStates, payload, setErrors = !0, schema }) { return Promise.all( Object.keys(fieldsStates).map( (name) => setFieldValidationState({ name, setError: setErrors, fieldState: fieldsStates[name], payload, schema, setErrorIfInvalidAndNotEmpty: fieldsStates[name].mode === "lazy" }) ) ); } function getErrorMessages(errors, fieldsStates) { const errorMessages = {}; for (const [name, value] of Object.entries(errors)) { const issues = value; errorMessages[name] = fieldsStates[name].error && issues[0] ? issues[0].message : void 0; } return errorMessages; } function getFieldsErrors(fieldsStates) { const fieldsErrors = {}; for (const [name, fieldState] of Object.entries(fieldsStates)) fieldsErrors[name] = fieldState.errors; return fieldsErrors; } function getValidateFunction({ name, hasValidation, debouncedFields, throttledFields }) { if (!hasValidation) return; const fieldName = String(name); if (debouncedFields?.[fieldName] && throttledFields?.[fieldName]) throw new Error(`The field "${String(fieldName)}" cannot be both debounced and throttled`); return debouncedFields?.[fieldName] ? debounceId( String(fieldName), setFieldValidationState, typeof debouncedFields[fieldName] == "number" ? debouncedFields[fieldName] : CONFIG.debounceTime ) : throttledFields?.[fieldName] ? throttleId( String(fieldName), setFieldValidationState, typeof throttledFields[fieldName] == "number" ? throttledFields[fieldName] : CONFIG.throttleTime ) : setFieldValidationState; } function getFieldState({ name, schema, initialValue, fieldState, options }) { const hasValidation = schema ? fieldHasValidation(name, schema) : !1, validateFunction = getValidateFunction({ name, hasValidation, debouncedFields: options?.debouncedFields, throttledFields: options?.throttledFields }); return { blurred: !1, dirty: !1, errors: [], error: !1, valid: !hasValidation, validating: !1, validated: !1, initialValue: useFreezeValue(initialValue), validateFunction, mode: hasValidation ? options?.mode ?? fieldState?.mode ?? CONFIG.mode : void 0 }; } function fieldHasValidation(field, schema) { return Object.keys(schema).includes(field); } function getFieldsStates({ schema, payload, options }) { const fieldsStates = {}; for (const fieldName in schema) { const name = fieldName; fieldsStates[name] = getFieldState({ name, schema, options, fieldState: fieldsStates[name], initialValue: payload?.[name] }); } return fieldsStates; } function updateFieldsStates({ fieldsStates, payload, schema, options, updateMode = !1 }) { for (const fieldName in schema) { const name = fieldName; fieldsStates[name] = updateFieldState({ name, fieldState: fieldsStates[name], payload, schema, options, updateMode }); } } function updateFieldState({ name, fieldState, payload, schema, options, updateMode = !0 }) { const { initialValue, mode, ...rest } = getFieldState({ name, schema, initialValue: options.defaultValue ?? payload[name], fieldState, options }), newMode = updateMode ? mode ?? fieldState.mode ?? CONFIG.mode : fieldState.mode; return { ...fieldState, initialValue, mode: newMode, ...fieldState?.mode && newMode !== fieldState.mode ? rest : {} }; } function canExecuteValidation({ eventName, fieldState, isSubmitted }) { const { dirty, blurred, mode, valid } = fieldState; return eventName === "blur" && (hasModeIncludes(["lazy", "aggressive"], mode) || valid) || eventName === "input" && mode === "blur" || !mode ? !1 : isSubmitted || mode === "eager" && blurred || mode === "blur" && blurred || mode === "aggressive" && dirty || mode === "lazy" && dirty || mode === "progressive"; } function handleFieldBlur({ name, force = !1, payload, fieldState, schema, isSubmitted }) { const fieldValue = payload[name], isDirty = !isEqual(fieldValue, fieldState.initialValue); if (fieldState.dirty = isDirty, fieldState.blurred = fieldState.blurred || (fieldState.mode === "eager" ? isDirty : !0), !!(force || canExecuteValidation({ eventName: "blur", fieldState, isSubmitted }))) return fieldState.validateFunction?.({ name, fieldState, schema, payload, setError: fieldState.mode === "progressive" ? fieldState.validated || fieldState.blurred : !0 }); } function handleFieldInput({ name, payload, fieldState, schema, isSubmitted, forceValidation = !1 }) { const fieldValue = payload[name]; fieldState.validated = !1; const isDirty = !isEqual(fieldValue, fieldState.initialValue); if (fieldState.dirty = isDirty, !!(forceValidation || canExecuteValidation({ eventName: "input", fieldState, isSubmitted }))) return fieldState.validateFunction?.({ name, fieldState, schema, payload, setError: fieldState.mode === "progressive" ? fieldState.validated || fieldState.blurred : !0 }); } function hasModeIncludes(modes, value) { return value ? modes.includes(value) : !1; } function getInstance(composableName) { const instance = getCurrentInstance(); if (!instance) throw new Error(`${composableName} must be called within setup()`); return instance; } function getContext(identifier, composableName) { const context = getInstance(composableName).formContexts?.get(identifier) ?? inject(identifier); if (!context) throw new Error("useFormField must be used within a form (useFormValidator)"); return context; } function scrollToError(selector = CONFIG.scrollToErrorSelector) { const element = document.querySelector(selector); element && element.scrollIntoView({ behavior: "smooth", block: "center" }); } function findInteractiveElements(el) { if (isInteractiveElement(el)) return [el]; const selector = [ // Standard form elements "input", "select", "textarea", "button", // Focusable elements "a[href]", '[tabindex]:not([tabindex="-1"])', // Elements with interactive ARIA roles '[role="button"]', '[role="textbox"]', '[role="combobox"]', '[role="listbox"]', '[role="slider"]', '[role="spinbutton"]', '[role="switch"]', '[role="checkbox"]', '[role="radio"]', '[role="menuitem"]', '[role="option"]', // Contenteditable elements '[contenteditable="true"]', // Custom interactive elements (common patterns) "[data-interactive]", ".interactive", "[data-clickable]" ].join(", "); return Array.from(el.querySelectorAll(selector)).filter(isInteractiveElement); } function isInteractiveElement(el) { if (el.getAttribute("tabindex") === "-1") return !1; if (el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLButtonElement || el instanceof HTMLAnchorElement && el.href || el.hasAttribute("tabindex") && el.getAttribute("tabindex") !== "-1") return !0; const interactiveRoles = [ "button", "textbox", "combobox", "listbox", "slider", "spinbutton", "switch", "checkbox", "radio", "menuitem", "option" ], role = el.getAttribute("role"); return role && interactiveRoles.includes(role) || el.getAttribute("contenteditable") === "true" ? !0 : el.hasAttribute("data-interactive") || el.hasAttribute("data-clickable") || el.classList.contains("interactive"); } const activeListeners = /* @__PURE__ */ new WeakMap(); function addEventToInteractiveElements({ interactiveElements, onBlur, mode }) { interactiveElements.forEach((element) => { if (hasModeIncludes(["eager", "blur", "progressive"], mode)) { const existingListeners = activeListeners.get(element); existingListeners?.has("blur") && element.removeEventListener("blur", existingListeners.get("blur")), element.addEventListener("blur", onBlur), activeListeners.has(element) || activeListeners.set(element, /* @__PURE__ */ new Map()), activeListeners.get(element).set("blur", onBlur); } }); } function removeEventFromInteractiveElements({ interactiveElements, onBlur }) { interactiveElements.forEach((element) => { const existingListeners = activeListeners.get(element); existingListeners?.has("blur") && (element.removeEventListener("blur", onBlur), existingListeners.delete("blur"), existingListeners.size === 0 && activeListeners.delete(element)); }); } function getValidationEvents({ hasRef, fieldState, onBlur }) { if (!(hasRef || hasModeIncludes(["aggressive", "lazy"], fieldState.mode))) return { onBlur }; } export { CONFIG as C, getFieldsStates as a, getFieldsErrors as b, getErrorMessages as c, handleFieldInput as d, getContext as e, fieldHasValidation as f, getInstance as g, hasModeIncludes as h, updateFieldState as i, isEqual as j, setFieldValidationState as k, getValidationEvents as l, findInteractiveElements as m, addEventToInteractiveElements as n, handleFieldBlur as o, removeEventFromInteractiveElements as r, scrollToError as s, updateFieldsStates as u, validateForm as v };