204 lines
6.2 KiB
JavaScript
204 lines
6.2 KiB
JavaScript
import { __ } from '~/locale';
|
|
import { getInstanceFromDirective } from '~/lib/utils/vue3compat/get_instance_from_directive';
|
|
|
|
/**
|
|
* Validation messages will take priority based on the property order.
|
|
*
|
|
* For example:
|
|
* { valueMissing: {...}, urlTypeMismatch: {...} }
|
|
*
|
|
* `valueMissing` will be displayed the user has entered a value
|
|
* after that, if the input is not a valid URL then `urlTypeMismatch` will show
|
|
*/
|
|
const defaultFeedbackMap = {
|
|
valueMissing: {
|
|
isInvalid: (el) => el.validity?.valueMissing,
|
|
message: __('Please fill out this field.'),
|
|
},
|
|
urlTypeMismatch: {
|
|
isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch,
|
|
message: __('Please enter a valid URL format, ex: http://www.example.com/home'),
|
|
},
|
|
};
|
|
|
|
const getFeedbackForElement = (feedbackMap, el) => {
|
|
const field = Object.values(feedbackMap).find((f) => f.isInvalid(el));
|
|
let elMessage = null;
|
|
if (field) {
|
|
elMessage = el.getAttribute('validation-message');
|
|
}
|
|
|
|
return field?.message || elMessage || el.validationMessage;
|
|
};
|
|
|
|
const focusFirstInvalidInput = (e) => {
|
|
const { target: formEl } = e;
|
|
const invalidInput = formEl.querySelector('input:invalid');
|
|
|
|
if (invalidInput) {
|
|
invalidInput.focus();
|
|
}
|
|
};
|
|
|
|
const getInputElement = (el) => {
|
|
return el.querySelector('input') || el;
|
|
};
|
|
|
|
const isEveryFieldValid = (form) => Object.values(form.fields).every(({ state }) => state === true);
|
|
|
|
const createValidator = (feedbackMap) => ({ el, form, reportInvalidInput = false }) => {
|
|
const { name } = el;
|
|
|
|
if (!name) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
'[gitlab] the validation directive requires the given input to have "name" attribute',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const formField = form.fields[name];
|
|
const isValid = el.checkValidity();
|
|
|
|
// This makes sure we always report valid fields - this can be useful for cases where the consuming
|
|
// component's logic depends on certain fields being in a valid state.
|
|
// Invalid input, on the other hand, should only be reported once we want to display feedback to the user.
|
|
// (eg.: After a field has been touched and moved away from, a submit-button has been clicked, ...)
|
|
formField.state = reportInvalidInput ? isValid : isValid || null;
|
|
formField.feedback = reportInvalidInput ? getFeedbackForElement(feedbackMap, el) : '';
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
form.state = isEveryFieldValid(form);
|
|
};
|
|
|
|
/**
|
|
* Takes an object that allows to add or change custom feedback messages.
|
|
* See possibilities here: https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
|
|
*
|
|
* The passed in object will be merged with the built-in feedback
|
|
* so it is possible to override a built-in message.
|
|
*
|
|
* @example
|
|
* validate({
|
|
* tooLong: {
|
|
* isInvalid: el => el.validity.tooLong === true,
|
|
* message: 'Your custom feedback'
|
|
* }
|
|
* })
|
|
*
|
|
* @example
|
|
* validate({
|
|
* valueMissing: {
|
|
* message: 'Your custom feedback'
|
|
* }
|
|
* })
|
|
*
|
|
* @param {Object<string, { message: string, isValid: ?function}>} customFeedbackMap
|
|
* @returns {{ inserted: function, update: function }} validateDirective
|
|
*/
|
|
export default function initValidation(customFeedbackMap = {}) {
|
|
const feedbackMap = { ...defaultFeedbackMap, ...customFeedbackMap };
|
|
const elDataMap = new WeakMap();
|
|
|
|
return {
|
|
inserted(element, binding, vnode) {
|
|
const { arg: showGlobalValidation } = binding;
|
|
const el = getInputElement(element);
|
|
const { form: formEl } = el;
|
|
const instance = getInstanceFromDirective({ binding, vnode });
|
|
|
|
const validate = createValidator(feedbackMap);
|
|
const elData = { validate, isTouched: false, isBlurred: false };
|
|
|
|
elDataMap.set(el, elData);
|
|
|
|
el.addEventListener('input', function markAsTouched() {
|
|
elData.isTouched = true;
|
|
// once the element has been marked as touched we can stop listening on the 'input' event
|
|
el.removeEventListener('input', markAsTouched);
|
|
});
|
|
|
|
el.addEventListener('blur', function markAsBlurred({ target }) {
|
|
if (elData.isTouched) {
|
|
elData.isBlurred = true;
|
|
validate({ el: target, form: instance.form, reportInvalidInput: true });
|
|
// this event handler can be removed, since the live-feedback in `update` takes over
|
|
el.removeEventListener('blur', markAsBlurred);
|
|
}
|
|
});
|
|
|
|
if (formEl) {
|
|
formEl.addEventListener('submit', focusFirstInvalidInput);
|
|
}
|
|
|
|
validate({ el, form: instance.form, reportInvalidInput: showGlobalValidation });
|
|
},
|
|
update(element, binding, vnode) {
|
|
const el = getInputElement(element);
|
|
const { arg: showGlobalValidation } = binding;
|
|
const { validate, isTouched, isBlurred } = elDataMap.get(el);
|
|
const showValidationFeedback = showGlobalValidation || (isTouched && isBlurred);
|
|
|
|
const instance = getInstanceFromDirective({ binding, vnode });
|
|
validate({ el, form: instance.form, reportInvalidInput: showValidationFeedback });
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This is a helper that initialize the form fields structure to be used in initForm
|
|
* @param {*} fieldValues
|
|
* @returns formObject
|
|
*/
|
|
const initFormField = ({ value, required = true, skipValidation = false }) => ({
|
|
value,
|
|
required,
|
|
state: skipValidation ? true : null,
|
|
feedback: null,
|
|
});
|
|
|
|
/**
|
|
* This is a helper that initialize the form structure that is compliant to be used with the validation directive
|
|
*
|
|
* @example
|
|
* const form initForm = initForm({
|
|
* fields: {
|
|
* name: {
|
|
* value: 'lorem'
|
|
* },
|
|
* description: {
|
|
* value: 'ipsum',
|
|
* required: false,
|
|
* skipValidation: true
|
|
* }
|
|
* }
|
|
* })
|
|
*
|
|
* @example
|
|
* const form initForm = initForm({
|
|
* state: true, // to override
|
|
* foo: { // something custom
|
|
* bar: 'lorem'
|
|
* },
|
|
* fields: {...}
|
|
* })
|
|
*
|
|
* @param {*} formObject
|
|
* @returns form
|
|
*/
|
|
export const initForm = ({ fields = {}, ...rest } = {}) => {
|
|
const initFields = Object.fromEntries(
|
|
Object.entries(fields).map(([fieldName, fieldValues]) => {
|
|
return [fieldName, initFormField(fieldValues)];
|
|
}),
|
|
);
|
|
|
|
return {
|
|
state: false,
|
|
showValidation: false,
|
|
...rest,
|
|
fields: initFields,
|
|
};
|
|
};
|