UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

390 lines (323 loc) 11.5 kB
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;