hyperform
Version:
Capture form validation back from the browser
326 lines (275 loc) • 9.95 kB
JavaScript
/**
* Implement constraint checking functionality defined in the HTML5 standard
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-cva-validity
* @return bool true if the test fails [!], false otherwise
*/
import format_date from './format_date';
import get_next_valid from './get_next_valid';
import get_type from './get_type';
import sprintf from './sprintf';
import string_to_number from './string_to_number';
import string_to_date from './string_to_date';
import unicode_string_length from './unicode_string_length';
import custom_messages from '../components/custom_messages';
import _ from '../components/localization';
import { message_store } from '../components/message_store';
import CustomValidatorRegistry from '../components/registry';
import { get_wrapper } from '../components/wrapper';
import test_bad_input from '../validators/bad_input';
import test_max from '../validators/max';
import test_maxlength from '../validators/maxlength';
import test_min from '../validators/min';
import test_minlength from '../validators/minlength';
import test_pattern from '../validators/pattern';
import test_required from '../validators/required';
import test_step from '../validators/step';
import test_type from '../validators/type';
/**
* boilerplate function for all tests but customError
*/
function check(test, react) {
return element => {
const invalid = ! test(element);
if (invalid) {
react(element);
}
return invalid;
};
}
/**
* create a common function to set error messages
*/
function set_msg(element, msgtype, _default) {
message_store.set(element, custom_messages.get(element, msgtype, _default));
}
const badInput = check(test_bad_input, element => set_msg(element, 'badInput',
_('Please match the requested type.')));
function customError(element) {
/* prevent infinite loops when the custom validators call setCustomValidity(),
* which in turn calls this code again. We check, if there is an already set
* custom validity message there. */
if (element.__hf_custom_validation_running) {
const msg = message_store.get(element);
return (msg && msg.is_custom);
}
/* check, if there are custom validators in the registry, and call
* them. */
const custom_validators = CustomValidatorRegistry.get(element);
const cvl = custom_validators.length;
var valid = true;
if (cvl) {
element.__hf_custom_validation_running = true;
for (let i = 0; i < cvl; i++) {
const result = custom_validators[i](element);
if (result !== undefined && ! result) {
valid = false;
/* break on first invalid response */
break;
}
}
delete(element.__hf_custom_validation_running);
}
/* check, if there are other validity messages already */
if (valid) {
const msg = message_store.get(element);
valid = ! (msg.toString() && ('is_custom' in msg));
}
return ! valid;
}
const patternMismatch = check(test_pattern, element => {
set_msg(element, 'patternMismatch',
element.title?
sprintf(_('PatternMismatchWithTitle'), element.title)
:
_('PatternMismatch')
);
});
/**
* TODO: when rangeOverflow and rangeUnderflow are both called directly and
* successful, the inRange and outOfRange classes won't get removed, unless
* element.validityState.valid is queried, too.
*/
const rangeOverflow = check(test_max, element => {
const type = get_type(element);
const wrapper = get_wrapper(element);
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
let msg;
switch (type) {
case 'date':
case 'datetime':
case 'datetime-local':
msg = sprintf(_('DateRangeOverflow'),
format_date(string_to_date(element.getAttribute('max'), type), type));
break;
case 'time':
msg = sprintf(_('TimeRangeOverflow'),
format_date(string_to_date(element.getAttribute('max'), type), type));
break;
// case 'number':
default:
msg = sprintf(_('NumberRangeOverflow'),
string_to_number(element.getAttribute('max'), type));
break;
}
set_msg(element, 'rangeOverflow', msg);
element.classList.add(outOfRangeClass);
element.classList.remove(inRangeClass);
});
const rangeUnderflow = check(test_min, element => {
const type = get_type(element);
const wrapper = get_wrapper(element);
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
let msg;
switch (type) {
case 'date':
case 'datetime':
case 'datetime-local':
msg = sprintf(_('DateRangeUnderflow'),
format_date(string_to_date(element.getAttribute('min'), type), type));
break;
case 'time':
msg = sprintf(_('TimeRangeUnderflow'),
format_date(string_to_date(element.getAttribute('min'), type), type));
break;
// case 'number':
default:
msg = sprintf(_('NumberRangeUnderflow'),
string_to_number(element.getAttribute('min'), type));
break;
}
set_msg(element, 'rangeUnderflow', msg);
element.classList.add(outOfRangeClass);
element.classList.remove(inRangeClass);
});
const stepMismatch = check(test_step, element => {
const list = get_next_valid(element);
const min = list[0];
const max = list[1];
let sole = false;
let msg;
if (min === null) {
sole = max;
} else if (max === null) {
sole = min;
}
if (sole !== false) {
msg = sprintf(_('StepMismatchOneValue'), sole);
} else {
msg = sprintf(_('StepMismatch'), min, max);
}
set_msg(element, 'stepMismatch', msg);
});
const tooLong = check(test_maxlength, element => {
set_msg(element, 'tooLong',
sprintf(_('TextTooLong'), element.getAttribute('maxlength'),
unicode_string_length(element.value)));
});
const tooShort = check(test_minlength, element => {
set_msg(element, 'tooShort',
sprintf(_('Please lengthen this text to %l characters or more (you are currently using %l characters).'),
element.getAttribute('minlength'),
unicode_string_length(element.value)));
});
const typeMismatch = check(test_type, element => {
let msg = _('Please use the appropriate format.');
const type = get_type(element);
if (type === 'email') {
if (element.hasAttribute('multiple')) {
msg = _('Please enter a comma separated list of email addresses.');
} else {
msg = _('InvalidEmail');
}
} else if (type === 'url') {
msg = _('InvalidURL');
} else if (type === 'file') {
msg = _('Please select a file of the correct type.');
}
set_msg(element, 'typeMismatch', msg);
});
const valueMissing = check(test_required, element => {
let msg = _('ValueMissing');
const type = get_type(element);
if (type === 'checkbox') {
msg = _('CheckboxMissing');
} else if (type === 'radio') {
msg = _('RadioMissing');
} else if (type === 'file') {
if (element.hasAttribute('multiple')) {
msg = _('Please select one or more files.');
} else {
msg = _('FileMissing');
}
} else if (element instanceof window.HTMLSelectElement) {
msg = _('SelectMissing');
}
set_msg(element, 'valueMissing', msg);
});
/**
* the "valid" property calls all other validity checkers and returns true,
* if all those return false.
*
* This is the major access point for _all_ other API methods, namely
* (check|report)Validity().
*/
const valid = element => {
const wrapper = get_wrapper(element);
const validClass = wrapper && wrapper.settings.classes.valid || 'hf-valid';
const invalidClass = wrapper && wrapper.settings.classes.invalid || 'hf-invalid';
const userInvalidClass = wrapper && wrapper.settings.classes.userInvalid || 'hf-user-invalid';
const userValidClass = wrapper && wrapper.settings.classes.userValid || 'hf-user-valid';
const inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range';
const outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range';
const validatedClass = wrapper && wrapper.settings.classes.validated || 'hf-validated';
element.classList.add(validatedClass);
for (let checker of [badInput, customError, patternMismatch, rangeOverflow,
rangeUnderflow, stepMismatch, tooLong, tooShort,
typeMismatch, valueMissing]) {
if (checker(element)) {
element.classList.add(invalidClass);
element.classList.remove(validClass);
element.classList.remove(userValidClass);
if ((
(element.type === 'checkbox' || element.type === 'radio') &&
element.checked !== element.defaultChecked
) ||
/* the following test is trivially false for checkboxes/radios */
element.value !== element.defaultValue) {
element.classList.add(userInvalidClass);
} else {
element.classList.remove(userInvalidClass);
}
element.setAttribute('aria-invalid', 'true');
return false;
}
}
message_store.delete(element);
element.classList.remove(invalidClass);
element.classList.remove(userInvalidClass);
element.classList.remove(outOfRangeClass);
element.classList.add(validClass);
element.classList.add(inRangeClass);
if (element.value !== element.defaultValue) {
element.classList.add(userValidClass);
} else {
element.classList.remove(userValidClass);
}
element.setAttribute('aria-invalid', 'false');
return true;
};
export default {
badInput,
customError,
patternMismatch,
rangeOverflow,
rangeUnderflow,
stepMismatch,
tooLong,
tooShort,
typeMismatch,
valueMissing,
valid,
};
;