UNPKG

@oruga-ui/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

1 lines 20.8 kB
{"version":3,"file":"useInputHandler-Cv7NmM5J.mjs","sources":["../../src/composables/useInputHandler.ts"],"sourcesContent":["import {\n nextTick,\n ref,\n computed,\n triggerRef,\n watch,\n watchEffect,\n type ExtractPropTypes,\n type MaybeRefOrGetter,\n type Component,\n} from \"vue\";\nimport { injectField } from \"@/components/field/fieldInjection\";\nimport { unrefElement } from \"./unrefElement\";\nimport { getOption } from \"@/utils/config\";\nimport { isSSR } from \"@/utils/ssr\";\nimport { isDefined } from \"@/utils/helpers\";\n\n// This should cover all types of HTML elements that have properties related to\n// HTML constraint validation, e.g. .form and .validity.\nconst validatableFormElementTypes = isSSR\n ? []\n : [\n HTMLButtonElement,\n HTMLFieldSetElement,\n HTMLInputElement,\n HTMLObjectElement,\n HTMLOutputElement,\n HTMLSelectElement,\n HTMLTextAreaElement,\n ];\n\nexport type ValidatableFormElement = InstanceType<\n (typeof validatableFormElementTypes)[number]\n>;\n\nfunction asValidatableFormElement(el: unknown): ValidatableFormElement | null {\n return validatableFormElementTypes.some((t) => el instanceof t)\n ? (el as ValidatableFormElement)\n : null;\n}\n\nconst constraintValidationAttributes = [\n \"disabled\",\n \"required\",\n \"pattern\",\n \"maxlength\",\n \"minlength\",\n \"max\",\n \"min\",\n \"step\",\n];\n\n/**\n * Form input handler functionalities\n */\nexport function useInputHandler<T extends ValidatableFormElement>(\n /** input ref element - can be a html element or a vue component*/\n inputRef: Readonly<MaybeRefOrGetter<T | Component>>,\n /** emitted input events */\n emits: {\n /** on input focus event */\n (e: \"focus\", value: Event): void;\n /** on input blur event */\n (e: \"blur\", value: Event): void;\n /** on input invalid event */\n (e: \"invalid\", value: Event): void;\n },\n /** validation configuration props */\n props: Readonly<\n ExtractPropTypes<{\n modelValue?: unknown;\n useHtml5Validation?: boolean;\n customValidity?:\n | string\n | ((currentValue: any, v: ValidityState) => string);\n }>\n >,\n) {\n // inject parent field component if used inside one\n const { parentField } = injectField();\n\n /// Allows access to the native element in cases where it might be missing,\n /// e.g. because the component hasn't been mounted yet or has been suspended\n /// by a <KeepAlive>\n const maybeElement = computed<T | undefined>(() => {\n const el = unrefElement<Component | HTMLElement>(inputRef);\n if (!el) return undefined;\n\n if (el.getAttribute(\"data-oruga-input\"))\n // if element is the input element\n return el as T;\n\n const inputs = el.querySelector(\"[data-oruga-input]\");\n\n if (!inputs) {\n console.warn(\n \"useInputHandler: Underlaying Oruga input component not found\",\n );\n return undefined;\n }\n // return underlaying the input element\n return inputs as T;\n });\n\n /// Should be used for most accesses to the native element; we generally\n /// expect it to be present, especially in event handlers.\n const element = computed(() => {\n const el = maybeElement.value;\n if (!el) console.warn(\"useInputHandler: inputRef contains no element\");\n return el;\n });\n\n // --- Input Focus Feature ---\n\n const isFocused = ref(false);\n\n /** Focus the underlaying input element. */\n function setFocus(): void {\n nextTick(() => {\n if (element.value) element.value.focus();\n });\n }\n\n /** Click the underlaying input element. */\n function doClick(): void {\n nextTick(() => {\n if (element.value) element.value.click();\n });\n }\n\n /** Unset focused and emit blur event. */\n function onBlur(event?: Event): void {\n isFocused.value = false;\n if (parentField?.value) parentField.value.setFocus(false);\n emits(\"blur\", event ? event : new Event(\"blur\"));\n checkHtml5Validity();\n }\n\n /** Set focused and emit focus event. */\n function onFocus(event?: Event): void {\n isFocused.value = true;\n if (parentField?.value) parentField.value.setFocus(true);\n emits(\"focus\", event ? event : new Event(\"focus\"));\n }\n\n // --- Validation Feature ---\n\n const isValid = ref(true);\n\n function setFieldValidity(variant, message): void {\n nextTick(() => {\n if (parentField?.value) {\n // Set type only if not defined\n if (!parentField.value.props.variant)\n parentField.value.setVariant(variant);\n\n // Set message only if not defined\n if (!parentField.value.props.message)\n parentField.value.setMessage(message);\n }\n });\n }\n\n /**\n * Check HTML5 validation, set isValid property.\n * If validation fail, send 'danger' type,\n * and error message to parent if it's a Field.\n */\n function checkHtml5Validity(): void {\n if (!props.useHtml5Validation) return;\n if (!element.value) return;\n\n if (element.value.validity.valid) {\n setFieldValidity(null, null);\n isValid.value = true;\n } else {\n setInvalid();\n isValid.value = false;\n }\n }\n\n function setInvalid(): void {\n const variant = \"danger\";\n const message = element.value?.validationMessage;\n setFieldValidity(variant, message);\n }\n\n function onInvalid(event: Event): void {\n checkHtml5Validity();\n const validatable = asValidatableFormElement(event.target);\n\n if (validatable && parentField?.value && props.useHtml5Validation) {\n // We provide our own error message on the field, so we should suppress the browser's default tooltip.\n // We still want to focus the form's first invalid input, though.\n event.preventDefault();\n\n let isFirstInvalid = false;\n\n if (validatable.form != null) {\n const formElements = validatable.form.elements;\n for (let i = 0; i < formElements.length; ++i) {\n const element = asValidatableFormElement(\n formElements.item(i),\n );\n if (element?.willValidate && !element.validity.valid) {\n isFirstInvalid = validatable === element;\n break;\n }\n }\n }\n\n if (isFirstInvalid) {\n const fieldElement = parentField.value.$el;\n const invalidHandler = getOption(\"invalidHandler\");\n\n if (invalidHandler instanceof Function) {\n invalidHandler(validatable, fieldElement ?? undefined);\n } else {\n // We'll scroll to put the whole field in view, not just the element that triggered the event,\n // which should mean that the message will be visible onscreen.\n // scrollIntoViewIfNeeded() is a non-standard method (but a very common extension).\n // If we can't use it, we'll just fall back to focusing the field.\n const canScrollToField =\n fieldElement?.scrollIntoView != undefined;\n validatable.focus({ preventScroll: canScrollToField });\n if (canScrollToField && fieldElement) {\n fieldElement.scrollIntoView({ block: \"nearest\" });\n }\n }\n }\n }\n emits(\"invalid\", event);\n }\n\n if (!isSSR) {\n /**\n * Provides a way to force the watcher on `updateCustomValidationMessage` to re-run\n *\n * There are some cases (e.g. changes to the element's validation attributes) that can\n * force changes to the element's `validityState`, which isn't a reactive property.\n * Note that just calling the watcher's internal function directly (outside the watcher)\n * wouldn't be a complete solution; the watcher would then miss any new reactive dependencies\n * that show up, e.g. because `props.customValidity` starts taking a branch that the watcher\n * hasn't seen before.\n */\n const forceValidationUpdate = ref(null);\n\n // Propagate any custom constraint validation message to the underlying DOM element.\n // Note that using watchEffect will implicitly pick up any reactive dependencies used\n // inside props.customValidity, which should help the computed message stay up to date.\n watchEffect((): void => {\n // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n forceValidationUpdate.value;\n if (!(props.useHtml5Validation ?? true)) return;\n\n const element = maybeElement.value;\n if (!isDefined(element)) return;\n\n const validity = props.customValidity ?? \"\";\n if (typeof validity === \"string\") {\n element.setCustomValidity(validity);\n } else {\n // The custom validation message may depend on `element.validity`,\n // which isn't a reactive property. `element.validity` depends on\n // the element's current value and the native constraint validation\n // attributes. We can use `props.modelValue` as a reasonable proxy\n // for the DOM element's value, and `props.modelValue` _is_ reactive,\n // so we can read it to help solve that reactivity problem.\n element.setCustomValidity(\n validity(props.modelValue, element.validity),\n );\n }\n\n // Updates the user-visible validation message if necessary\n if (!isValid.value) checkHtml5Validity();\n });\n\n // Clean up validation state if we stop controlling it.\n watch(\n [maybeElement, (): boolean => props.useHtml5Validation ?? true],\n (newItems, oldItems) => {\n const newElement = newItems[0];\n const newUseValidation = newItems[1];\n const oldElement = oldItems[0];\n const oldUseValidation = oldItems[1];\n if (newElement !== oldElement) {\n // Since we're no longer managing the element, we might\n // as well clean up any custom validity we set up.\n oldElement?.setCustomValidity(\"\");\n } else if (oldUseValidation && !newUseValidation) {\n newElement?.setCustomValidity(\"\");\n }\n },\n );\n\n // Respond to attribute changes that could affect validation messages.\n //\n // Technically, having the `required` attribute on one element in a radio button\n // group affects the validity of the entire group.\n // See https://html.spec.whatwg.org/multipage/input.html#radio-button-group.\n // We're not checking for that here because it would require more expensive logic.\n // Because of that, this will only work properly if the `required` attributes of all radio\n // buttons in the group are synchronized with each other, which is likely anyway.\n // (We're also expecting the use of radio buttons with our default validation message handling\n // to be fairly uncommon because the overall visual experience is clunky with such a configuration.)\n const onAttributeChange = (): void => {\n triggerRef(forceValidationUpdate);\n };\n\n let validationAttributeObserver: MutationObserver | null = null;\n\n watch(\n [\n maybeElement,\n isValid,\n (): boolean => props.useHtml5Validation ?? true,\n ():\n | string\n | ((s: ValidityState, v: any) => string)\n | undefined => props.customValidity,\n ],\n (newData, oldData) => {\n // Not using destructuring assignment because browser support is just a little too weak at the moment\n const el = newData[0];\n const valid = newData[1];\n const useValidation = newData[2];\n const functionalValidation = newData[3] instanceof Function;\n const oldEl = oldData[0];\n\n const needWatcher =\n isDefined(el) &&\n useValidation &&\n // For inputs known to be invalid, changes in constraint validation properties\n // may make it so the field is now valid and the message needs to be hidden.\n // For browser-implemented constraint validation (e.g. the `required` attribute),\n // we just care about the message displayed to the user, which is hidden for valid inputs\n // until the next interaction with the control.\n (!valid ||\n // For inputs with complex custom validation, any changes to validation-related attributes\n // may affect the results of `props.customValidity`.\n functionalValidation);\n\n // Clean up previous state.\n if (\n (!needWatcher || el !== oldEl) &&\n validationAttributeObserver != null\n ) {\n // Process any pending events.\n if (validationAttributeObserver.takeRecords().length > 0)\n onAttributeChange();\n validationAttributeObserver.disconnect();\n validationAttributeObserver = null;\n }\n\n // Update the watcher.\n // Note that this branch is also used for the initial setup of the watcher.\n // We're assuming that `maybeElement` will start out null when the watcher is created, which will\n // cause the watcher to be triggered (with `oldEl == undefined`) once the component is mounted.\n if (\n needWatcher &&\n isDefined(el) &&\n (validationAttributeObserver == null || el !== oldEl)\n ) {\n if (validationAttributeObserver == null) {\n validationAttributeObserver = new MutationObserver(\n onAttributeChange,\n );\n }\n validationAttributeObserver.observe(el, {\n attributeFilter: constraintValidationAttributes,\n });\n\n // Note that this doesn't react to changes in the list of ancestors.\n // Based on testing, Vue seems to rarely, if ever, re-parent DOM nodes;\n // it generally prefers to create new ones under the new parent.\n // That means this simpler solution is likely good enough for now.\n let ancestor: Node | null = el;\n while ((ancestor = ancestor.parentNode)) {\n // Form controls can be disabled by their ancestor fieldsets.\n if (ancestor instanceof HTMLFieldSetElement) {\n validationAttributeObserver.observe(ancestor, {\n attributeFilter: [\"disabled\"],\n });\n }\n }\n }\n },\n );\n }\n\n return {\n input: element,\n isFocused,\n isValid,\n setFocus,\n doClick,\n onFocus,\n onBlur,\n onInvalid,\n checkHtml5Validity,\n };\n}\n"],"names":["element"],"mappings":"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"}