@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
293 lines (292 loc) • 10.2 kB
JavaScript
/* 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
};