@atlassian/aui
Version:
Atlassian User Interface library
390 lines (323 loc) • 11.5 kB
JavaScript
import $ from './jquery';
import './form-notification';
import {
appendDescription,
appendErrorMessages,
getMessageContainer,
setFieldSpinner,
updateAriaInfo,
} from './form-notification';
import './form-validation/basic-validators';
import amdify from './internal/amdify';
import * as deprecate from './internal/deprecation';
import globalize from './internal/globalize';
import skate from './internal/skate';
import validatorRegister from './form-validation/validator-register';
//Attributes
const ATTRIBUTE_VALIDATION_OPTION_PREFIX = 'aui-validation-';
const ATTRIBUTE_NOTIFICATION_PREFIX = 'data-aui-notification-';
const ATTRIBUTE_FIELD_STATE = 'aui-validation-state';
const INVALID = 'invalid';
const VALID = 'valid';
const VALIDATING = 'validating';
const UNVALIDATED = 'unvalidated';
const ATTRIBUTE_VALIDATION_FIELD_COMPONENT = 'data-aui-validation-field';
//Classes
const CLASS_VALIDATION_INITIALISED = '_aui-form-validation-initialised';
//Events
const EVENT_FIELD_STATE_CHANGED = '_aui-internal-field-state-changed';
function isFieldInitialised($field) {
return $field.hasClass(CLASS_VALIDATION_INITIALISED);
}
function initValidation($field) {
if (!isFieldInitialised($field)) {
prepareFieldMarkup($field);
bindFieldEvents($field);
changeFieldState($field, UNVALIDATED);
}
}
function prepareFieldMarkup($field) {
$field.addClass(CLASS_VALIDATION_INITIALISED);
appendDescription($field);
}
function bindFieldEvents($field) {
bindStopTypingEvent($field);
bindValidationEvent($field);
}
function bindStopTypingEvent($field) {
var keyUpTimer;
var triggerStopTypingEvent = function () {
$field.trigger('aui-stop-typing');
};
$field.on('keyup', function () {
clearTimeout(keyUpTimer);
keyUpTimer = setTimeout(triggerStopTypingEvent, 1500);
});
}
function bindValidationEvent($field) {
var validateWhen = getValidationOption($field, 'when');
var watchedFieldID = getValidationOption($field, 'watchfield');
var elementsToWatch = watchedFieldID ? $field.add('#' + watchedFieldID) : $field;
elementsToWatch.on(validateWhen, function startValidation() {
validationTriggeredHandler($field);
});
}
function validationTriggeredHandler($field) {
var noValidate = getValidationOption($field, 'novalidate');
if (noValidate) {
changeFieldState($field, VALID);
return;
}
return startValidating($field);
}
function getValidationOption($field, option) {
var defaults = {
when: 'change',
};
var optionValue = $field.attr('data-' + ATTRIBUTE_VALIDATION_OPTION_PREFIX + option);
if (!optionValue) {
optionValue = defaults[option];
}
return optionValue;
}
function startValidating($field) {
clearFieldMessages($field);
var validatorsToRun = getActivatedValidators($field);
changeFieldState($field, VALIDATING);
var deferreds = runValidatorsAndGetDeferred($field, validatorsToRun);
var fieldValidators = $.when.apply($, deferreds);
fieldValidators.done(function () {
changeFieldState($field, VALID);
});
return fieldValidators;
}
function clearFieldMessages($field) {
setFieldNotification(getDisplayField($field), 'none');
}
function getValidators() {
return validatorRegister.validators();
}
function getActivatedValidators($field) {
var callList = [];
getValidators().forEach(function (validator, index) {
var validatorTrigger = validator.validatorTrigger;
var runThisValidator = $field.is(validatorTrigger);
if (runThisValidator) {
callList.push(index);
}
});
return callList;
}
function runValidatorsAndGetDeferred($field, validatorsToRun) {
var allDeferreds = [];
validatorsToRun.forEach(function (validatorIndex) {
var validatorFunction = getValidators()[validatorIndex].validatorFunction;
var deferred = new $.Deferred();
var validatorContext = createValidatorContext($field, deferred);
validatorFunction(validatorContext);
allDeferreds.push(deferred);
});
return allDeferreds;
}
function createValidatorContext($field, validatorDeferred) {
var context = {
validate: function () {
validatorDeferred.resolve();
},
invalidate: function (message) {
changeFieldState($field, INVALID, message);
validatorDeferred.reject();
},
args: createArgumentAccessorFunction($field),
el: $field[0],
$el: $field,
};
deprecate.prop(context, '$el', {
sinceVersion: '5.9.0',
removeInVersion: '10.0.0',
alternativeName: 'el',
extraInfo: 'See https://ecosystem.atlassian.net/browse/AUI-3263.',
});
return context;
}
function createArgumentAccessorFunction($field) {
return function (arg) {
return $field.attr('data-' + ATTRIBUTE_VALIDATION_OPTION_PREFIX + arg) || $field.attr(arg);
};
}
function changeFieldState($field, state, message) {
$field.attr('data-' + ATTRIBUTE_FIELD_STATE, state);
$field.attr('aria-invalid', false);
if (state === UNVALIDATED) {
return;
}
$field.trigger($.Event(EVENT_FIELD_STATE_CHANGED));
var $displayField = getDisplayField($field);
var stateToNotificationTypeMap = {};
stateToNotificationTypeMap[VALIDATING] = 'wait';
stateToNotificationTypeMap[INVALID] = 'error';
stateToNotificationTypeMap[VALID] = 'success';
var notificationType = stateToNotificationTypeMap[state];
if (state === VALIDATING) {
showSpinnerIfSlow($field);
} else {
setFieldNotification($displayField, notificationType, message);
}
if (state === INVALID) {
$field.attr('aria-invalid', true);
}
}
function showSpinnerIfSlow($field) {
setTimeout(function () {
let stillValidating = getFieldState($field) === VALIDATING;
if (stillValidating) {
setFieldNotification($field, 'wait');
setFieldSpinner($field, true);
}
}, 500);
}
function setFieldNotification($field, type, message) {
const spinnerWasVisible = isSpinnerVisible($field);
removeIconOnlyNotifications($field);
const skipShowingSuccessNotification = type === 'success' && !spinnerWasVisible;
if (skipShowingSuccessNotification) {
return;
}
if (type === 'none') {
removeFieldNotification($field, 'error');
} else {
const previousMessage = $field.attr(ATTRIBUTE_NOTIFICATION_PREFIX + type) || '[]';
const newMessages = message ? combineJSONMessages(message, previousMessage) : [];
$field.attr(ATTRIBUTE_NOTIFICATION_PREFIX + type, JSON.stringify(newMessages));
if (type === 'error') {
appendErrorMessages($field, newMessages);
}
}
}
function removeIconOnlyNotifications($field) {
removeFieldNotification($field, 'wait');
setFieldSpinner($field, false);
removeFieldNotification($field, 'success');
}
function removeFieldNotification($field, type) {
$field.removeAttr(ATTRIBUTE_NOTIFICATION_PREFIX + type);
if (type === 'error') {
getMessageContainer($field, type).remove();
updateAriaInfo($field);
}
}
function isSpinnerVisible($field) {
return $field.is('[' + ATTRIBUTE_NOTIFICATION_PREFIX + 'wait]');
}
function combineJSONMessages(newString, previousString) {
const previousStackedMessageList = JSON.parse(previousString);
return previousStackedMessageList.concat([newString]);
}
function getDisplayField($field) {
var displayFieldID = getValidationOption($field, 'displayfield');
var notifyOnSelf = displayFieldID === undefined;
return notifyOnSelf ? $field : $('#' + displayFieldID);
}
function getFieldState($field) {
return $field.attr('data-' + ATTRIBUTE_FIELD_STATE);
}
/**
* Trigger validation on a field manually
* @param $field the field that validation should be triggered for
*/
function validateField($field) {
$field = $($field);
validationTriggeredHandler($field);
}
/**
* Form scrolling and submission prevent based on validation state
* -If the form is unvalidated, validate all fields
* -If the form is invalid, go to the first invalid element
* -If the form is validating, wait for them to validate and then try submitting again
* -If the form is valid, allow form submission
*/
$(document).on('submit', function (e) {
var form = e.target;
var $form = $(form);
var formState = getFormStateName($form);
if (formState === UNVALIDATED) {
delaySubmitUntilStateChange($form, e);
validateUnvalidatedFields($form);
} else if (formState === VALIDATING) {
delaySubmitUntilStateChange($form, e);
} else if (formState === INVALID) {
e.preventDefault();
selectFirstInvalid($form);
} else if (formState === VALID) {
var validSubmitEvent = $.Event('aui-valid-submit');
$form.trigger(validSubmitEvent);
var preventNormalSubmit = validSubmitEvent.isDefaultPrevented();
if (preventNormalSubmit) {
e.preventDefault(); //users can bind to aui-valid-submit for ajax forms
}
}
});
function delaySubmitUntilStateChange($form, event) {
event.preventDefault();
$form.one(EVENT_FIELD_STATE_CHANGED, function () {
$form.trigger('submit');
});
}
function getFormStateName($form) {
var $fieldCollection = $form.find('.' + CLASS_VALIDATION_INITIALISED);
var fieldStates = getFieldCollectionStateNames($fieldCollection);
var wholeFormState = mergeStates(fieldStates);
return wholeFormState;
}
function getFieldCollectionStateNames($fields) {
var states = $.map($fields, function (field) {
return getFieldState($(field));
});
return states;
}
function mergeStates(stateNames) {
var containsInvalidState = stateNames.indexOf(INVALID) !== -1;
var containsUnvalidatedState = stateNames.indexOf(UNVALIDATED) !== -1;
var containsValidatingState = stateNames.indexOf(VALIDATING) !== -1;
if (containsInvalidState) {
return INVALID;
} else if (containsUnvalidatedState) {
return UNVALIDATED;
} else if (containsValidatingState) {
return VALIDATING;
} else {
return VALID;
}
}
function validateUnvalidatedFields($form) {
var $unvalidatedElements = getFieldsInFormWithState($form, UNVALIDATED);
$unvalidatedElements.each(function (index, el) {
validator.validate($(el));
});
}
function selectFirstInvalid($form) {
var $firstInvalidField = getFieldsInFormWithState($form, INVALID).first();
$firstInvalidField.focus();
}
function getFieldsInFormWithState($form, state) {
var selector = '[data-' + ATTRIBUTE_FIELD_STATE + '=' + state + ']';
return $form.find(selector);
}
const validator = {
register: validatorRegister.register,
validate: validateField,
};
skate(ATTRIBUTE_VALIDATION_FIELD_COMPONENT, {
attached: function (field) {
if (field.form) {
field.form.setAttribute('novalidate', 'novalidate');
}
var $field = $(field);
initValidation($field);
skate.init(field); //needed to kick off form notification skate initialisation
},
type: skate.type.ATTRIBUTE,
});
amdify('aui/form-validation', validator);
globalize('formValidation', validator);
export default validator;