133 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			133 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import { merge } from 'lodash';
 | |
| import { s__ } from '~/locale';
 | |
| 
 | |
| /**
 | |
|  * 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: s__('Please fill out this field.'),
 | |
|   },
 | |
|   urlTypeMismatch: {
 | |
|     isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch,
 | |
|     message: s__('Please enter a valid URL format, ex: http://www.example.com/home'),
 | |
|   },
 | |
| };
 | |
| 
 | |
| const getFeedbackForElement = (feedbackMap, el) =>
 | |
|   Object.values(feedbackMap).find((f) => f.isInvalid(el))?.message || el.validationMessage;
 | |
| 
 | |
| const focusFirstInvalidInput = (e) => {
 | |
|   const { target: formEl } = e;
 | |
|   const invalidInput = formEl.querySelector('input:invalid');
 | |
| 
 | |
|   if (invalidInput) {
 | |
|     invalidInput.focus();
 | |
|   }
 | |
| };
 | |
| 
 | |
| const isEveryFieldValid = (form) => Object.values(form.fields).every(({ state }) => state === true);
 | |
| 
 | |
| const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = false }) => {
 | |
|   const { form } = context;
 | |
|   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) : '';
 | |
| 
 | |
|   form.state = isEveryFieldValid(form);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Takes an object that allows to add or change custom feedback messages.
 | |
|  *
 | |
|  * 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: {
 | |
|  *     check: 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 = merge(defaultFeedbackMap, customFeedbackMap);
 | |
|   const elDataMap = new WeakMap();
 | |
| 
 | |
|   return {
 | |
|     inserted(el, binding, { context }) {
 | |
|       const { arg: showGlobalValidation } = binding;
 | |
|       const { form: formEl } = el;
 | |
| 
 | |
|       const validate = createValidator(context, 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, 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, reportInvalidInput: showGlobalValidation });
 | |
|     },
 | |
|     update(el, binding) {
 | |
|       const { arg: showGlobalValidation } = binding;
 | |
|       const { validate, isTouched, isBlurred } = elDataMap.get(el);
 | |
|       const showValidationFeedback = showGlobalValidation || (isTouched && isBlurred);
 | |
| 
 | |
|       validate({ el, reportInvalidInput: showValidationFeedback });
 | |
|     },
 | |
|   };
 | |
| }
 |