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(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 });
|
||
|
},
|
||
|
};
|
||
|
}
|