UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

293 lines (292 loc) • 10.2 kB
/* COPYRIGHT Esri - https://js.arcgis.com/5.1/LICENSE.txt */ import { makeGenericController } from "@arcgis/lumina/controllers"; import { uncapitalize, kebabToPascal } from "@arcgis/toolkit/string"; import { r as isCalciteFocusable } from "./dom.js"; import "lit"; const minMaxStepTypes = ["date", "datetime-local", "month", "number", "range", "time", "week"]; const patternTypes = ["email", "password", "search", "tel", "text", "url"]; const minMaxLengthTypes = ["email", "password", "search", "tel", "text", "textarea", "url"]; function updateConstraintValidation(inputComponent, input, propName, matchesType) { const attributeName = propName.toLowerCase(); const value = inputComponent[propName]; if (matchesType && value != null) { input.setAttribute(attributeName, `${value}`); } else { input.removeAttribute(attributeName); } } function syncInputDelegate(type, inputComponent, input) { input.type = type; const isMinMaxStepType = minMaxStepTypes.includes(type); const numericInputComponent = inputComponent; updateConstraintValidation(numericInputComponent, input, "min", isMinMaxStepType); updateConstraintValidation(numericInputComponent, input, "max", isMinMaxStepType); updateConstraintValidation(numericInputComponent, input, "step", isMinMaxStepType); const isMinMaxLengthType = minMaxLengthTypes.includes(type); const textualInputComponent = inputComponent; updateConstraintValidation(textualInputComponent, input, "minLength", isMinMaxLengthType); updateConstraintValidation(textualInputComponent, input, "maxLength", isMinMaxLengthType); const isPatternType = patternTypes.includes(type); updateConstraintValidation(textualInputComponent, input, "pattern", isPatternType); } function isSupportedType(type) { const effectiveType = type === "textarea" ? "text" : type; return minMaxStepTypes.includes(effectiveType) || patternTypes.includes(effectiveType) || minMaxLengthTypes.includes(effectiveType); } const joinableValueTypes = ["text", "email", "search", "hidden", "tel", "url"]; const allValid = Object.freeze({ validity: {}, validationMessage: "" }); function validate(input, value) { if (!Array.isArray(value)) { if (validateValue(input, value)) { return allValid; } return { validity: getValidityFlags(input.validity), validationMessage: input.validationMessage }; } if (joinableValueTypes.includes(input.type)) { if (validateValue(input, value.join(","))) { return allValid; } return { validity: getValidityFlags(input.validity), validationMessage: input.validationMessage }; } const mergedValidity = {}; const validationMessages = []; for (const item of value) { if (validateValue(input, item)) { continue; } Object.assign(mergedValidity, getValidityFlags(input.validity)); if (input.validationMessage) { validationMessages.push(input.validationMessage); } } return { validity: mergedValidity, validationMessage: validationMessages.join("; ") }; } function validateValue(inputDelegate, valueToValidate) { inputDelegate.value = // file will throw if non-empty string is provided inputDelegate.type === "file" || valueToValidate == null ? "" : String(valueToValidate); return inputDelegate.validity.valid; } function getValidityFlags(validityState) { const validityFlags = {}; for (const key in validityState) { if (key !== "valid" && validityState[key]) { validityFlags[key] = true; } } return validityFlags; } const componentsWithInputEvent = [ "calcite-input", "calcite-input-number", "calcite-input-text", "calcite-text-area" ]; function getClearValidationEventName(componentTag) { const componentTagCamelCase = uncapitalize(kebabToPascal(componentTag)); return `${componentTagCamelCase}${componentsWithInputEvent.includes(componentTag) ? "Input" : "Change"}`; } function isFormComponentEl(el) { return "form" in el && "name" in el && isCalciteFocusable(el); } function displayValidationMessage(component, { status, message, icon }) { if ("status" in component) { component.status = status; } if ("validationIcon" in component && typeof component.validationIcon !== "string") { component.validationIcon = icon; } if ("validationMessage" in component && !component.validationMessage) { component.validationMessage = message; } } function syncInternalInput(component, input) { const { disabled, required } = component; input.disabled = disabled; input.required = !!required; if (isCheckable(component)) { input.checked = component.checked; } else if (isInputComponent(component, input)) { syncInputDelegate(input.type, component, input); } } function isCheckable(component) { return "checked" in component; } function isInputComponent(component, input) { return component && isSupportedType(input.type); } function focusFirstInvalidFormElement(form) { const formElements = Array.from(form.elements); requestAnimationFrame(() => { const invalidEls = formElements.filter( (el) => el.matches("[status=invalid]") && isFormComponentEl(el) ); for (const el of invalidEls) { if (el.validationMessage) { el.setFocus(); break; } } }); } const useForm = (options) => { return makeGenericController((component, controller) => { let customValidityMessage = ""; let inputDelegate; let lastAssociatedForm = null; let effectiveInputType = options.inputType; if (effectiveInputType) { inputDelegate = document.createElement("input"); } function invalidFormHandler(event) { if (event.defaultPrevented) { return; } event.preventDefault(); const form = event.currentTarget; focusFirstInvalidFormElement(form); } function onFormReset() { if ("status" in component) { component.status = "idle"; } if ("validationIcon" in component) { component.validationIcon = false; } if ("validationMessage" in component) { component.validationMessage = ""; } if (isCheckable(component)) { component.checked = component.defaultChecked; } component.value = component.defaultValue; } component.listen("luminaFormResetCallback", () => { onFormReset(); }); component.listen("luminaFormAssociatedCallback", ({ detail: [form] }) => { if (form) { form.addEventListener("invalid", invalidFormHandler, { capture: true }); } else { lastAssociatedForm?.removeEventListener("invalid", invalidFormHandler, { capture: true }); } lastAssociatedForm = form; }); function handleInvalidInput() { const validationMsg = customValidityMessage || inputDelegate?.validationMessage || ""; component.el.dispatchEvent( // allows users to set custom validation messages new CustomEvent("calciteInvalid", { bubbles: true, composed: true }) ); displayValidationMessage(component, { message: validationMsg, icon: true, status: "invalid" }); const clearValidationEvent = getClearValidationEventName(component.el.tagName.toLowerCase()); component.listen( clearValidationEvent, () => { if ("status" in component) { component.status = "idle"; } if ("validationIcon" in component && (!component.validationIcon || component.validationIcon === true)) { component.validationIcon = false; } if ("validationMessage" in component && component.validationMessage === validationMsg) { component.validationMessage = ""; } }, { once: true } ); } controller.onConnected(() => { component.el.addEventListener("invalid", handleInvalidInput); }); controller.onDisconnected(() => { component.el.removeEventListener("invalid", handleInvalidInput); }); controller.onUpdate((changes) => { if (!component.hasUpdated) { component.defaultValue = component.value; if (isCheckable(component)) { component.defaultChecked = component.checked; } } if (changes.has("name") || changes.has("value") || isCheckable(component) && changes.has("checked")) { component.elementInternals.setFormValue(getFormValue()); } updateValidity(); }); function updateValidity() { const { elementInternals } = component; let validity = {}; let validationMessage = ""; if (inputDelegate) { inputDelegate.type = effectiveInputType; syncInternalInput(component, inputDelegate); ({ validity, validationMessage } = validate(inputDelegate, getComponentValue())); } if (customValidityMessage) { validity = { ...validity, customError: true }; validationMessage = customValidityMessage; } elementInternals.setValidity(validity, validationMessage); if ("validity" in component) { component.validity = elementInternals.validity; } } function getComponentValue() { if (options.getValue) { return options.getValue(); } return component.value; } function getFormValue() { const value = getComponentValue(); if (Array.isArray(value)) { const formData = new FormData(); value.forEach((value2) => formData.append(component.name, value2)); return formData; } if (isCheckable(component)) { if (component.checked) { return value || "on"; } return null; } return value; } return { get active() { return !!component.elementInternals.form; }, overrideDefaultValue: (value) => { component.defaultValue = value; }, overrideInputType: (type) => { effectiveInputType = type; updateValidity(); }, requestSubmit: () => { component.elementInternals.form?.requestSubmit(); }, setCustomValidity: (message) => { customValidityMessage = message; updateValidity(); } }; }); }; export { useForm as u };