205 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			205 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,
 | |
|   };
 | |
| };
 |