UNPKG

11.3 kBJavaScriptView Raw
1import $ from './jquery';
2import './form-notification';
3import {appendDescription, appendErrorMessages, getMessageContainer, setFieldSpinner} from './form-notification';
4import './form-validation/basic-validators';
5import amdify from './internal/amdify';
6import * as deprecate from './internal/deprecation';
7import globalize from './internal/globalize';
8import skate from './internal/skate';
9import validatorRegister from './form-validation/validator-register';
10
11//Attributes
12const ATTRIBUTE_VALIDATION_OPTION_PREFIX = 'aui-validation-';
13const ATTRIBUTE_NOTIFICATION_PREFIX = 'data-aui-notification-';
14
15const ATTRIBUTE_FIELD_STATE = 'aui-validation-state';
16const INVALID = 'invalid';
17const VALID = 'valid';
18const VALIDATING = 'validating';
19const UNVALIDATED = 'unvalidated';
20
21const ATTRIBUTE_VALIDATION_FIELD_COMPONENT = 'data-aui-validation-field';
22
23//Classes
24const CLASS_VALIDATION_INITIALISED = '_aui-form-validation-initialised';
25
26//Events
27const EVENT_FIELD_STATE_CHANGED = '_aui-internal-field-state-changed';
28
29function isFieldInitialised($field) {
30 return $field.hasClass(CLASS_VALIDATION_INITIALISED);
31}
32
33function initValidation($field) {
34 if (!isFieldInitialised($field)) {
35 prepareFieldMarkup($field);
36 bindFieldEvents($field);
37 changeFieldState($field, UNVALIDATED);
38 }
39}
40
41function prepareFieldMarkup($field){
42 $field.addClass(CLASS_VALIDATION_INITIALISED);
43 appendDescription($field);
44}
45
46function bindFieldEvents($field) {
47 bindStopTypingEvent($field);
48 bindValidationEvent($field);
49}
50
51function bindStopTypingEvent($field){
52 var keyUpTimer;
53
54 var triggerStopTypingEvent = function () {
55 $field.trigger('aui-stop-typing');
56 };
57
58 $field.on('keyup', function () {
59 clearTimeout(keyUpTimer);
60 keyUpTimer = setTimeout(triggerStopTypingEvent, 1500);
61 });
62}
63
64function bindValidationEvent($field) {
65 var validateWhen = getValidationOption($field, 'when');
66 var watchedFieldID = getValidationOption($field, 'watchfield');
67
68 var elementsToWatch = watchedFieldID ? $field.add('#' + watchedFieldID) : $field;
69
70 elementsToWatch.on(validateWhen, function startValidation () {
71 validationTriggeredHandler($field);
72 });
73}
74
75function validationTriggeredHandler($field) {
76 var noValidate = getValidationOption($field, 'novalidate');
77
78 if (noValidate) {
79 changeFieldState($field, VALID);
80 return;
81 }
82
83 return startValidating($field);
84}
85
86function getValidationOption($field, option) {
87 var defaults = {
88 'when': 'change'
89 };
90 var optionValue = $field.attr('data-' + ATTRIBUTE_VALIDATION_OPTION_PREFIX + option);
91 if (!optionValue) {
92 optionValue = defaults[option];
93 }
94
95 return optionValue;
96}
97
98function startValidating($field) {
99 clearFieldMessages($field);
100
101 var validatorsToRun = getActivatedValidators($field);
102
103 changeFieldState($field, VALIDATING);
104 var deferreds = runValidatorsAndGetDeferred($field, validatorsToRun);
105 var fieldValidators = $.when.apply($, deferreds);
106 fieldValidators.done(function () {
107 changeFieldState($field, VALID);
108 });
109 return fieldValidators;
110}
111
112function clearFieldMessages($field) {
113 setFieldNotification(getDisplayField($field), 'none');
114}
115
116function getValidators() {
117 return validatorRegister.validators();
118}
119
120function getActivatedValidators($field) {
121 var callList = [];
122 getValidators().forEach(function (validator, index) {
123 var validatorTrigger = validator.validatorTrigger;
124 var runThisValidator = $field.is(validatorTrigger);
125 if (runThisValidator) {
126 callList.push(index);
127 }
128 });
129
130 return callList;
131}
132
133function runValidatorsAndGetDeferred($field, validatorsToRun) {
134 var allDeferreds = [];
135
136 validatorsToRun.forEach(function (validatorIndex) {
137 var validatorFunction = getValidators()[validatorIndex].validatorFunction;
138 var deferred = new $.Deferred();
139 var validatorContext = createValidatorContext($field, deferred);
140 validatorFunction(validatorContext);
141
142 allDeferreds.push(deferred);
143 });
144
145 return allDeferreds;
146}
147
148function createValidatorContext($field, validatorDeferred) {
149 var context = {
150 validate: function () {
151 validatorDeferred.resolve();
152 },
153 invalidate: function (message) {
154 changeFieldState($field, INVALID, message);
155 validatorDeferred.reject();
156 },
157 args: createArgumentAccessorFunction($field),
158 el: $field[0],
159 $el: $field
160 };
161
162 deprecate.prop(context, '$el', {
163 sinceVersion: '5.9.0',
164 removeInVersion: '10.0.0',
165 alternativeName: 'el',
166 extraInfo: 'See https://ecosystem.atlassian.net/browse/AUI-3263.'
167 });
168
169 return context;
170}
171
172function createArgumentAccessorFunction($field) {
173 return function (arg) {
174 return $field.attr('data-' + ATTRIBUTE_VALIDATION_OPTION_PREFIX + arg) || $field.attr(arg);
175 };
176}
177
178function changeFieldState($field, state, message) {
179 $field.attr('data-' + ATTRIBUTE_FIELD_STATE, state);
180
181 if (state === UNVALIDATED) {
182 return;
183 }
184
185 $field.trigger($.Event(EVENT_FIELD_STATE_CHANGED));
186
187 var $displayField = getDisplayField($field);
188
189 var stateToNotificationTypeMap = {};
190 stateToNotificationTypeMap[VALIDATING] = 'wait';
191 stateToNotificationTypeMap[INVALID] = 'error';
192 stateToNotificationTypeMap[VALID] = 'success';
193
194 var notificationType = stateToNotificationTypeMap[state];
195
196 if (state === VALIDATING) {
197 showSpinnerIfSlow($field);
198 } else {
199 setFieldNotification($displayField, notificationType, message);
200 }
201
202}
203
204function showSpinnerIfSlow($field) {
205 setTimeout(function () {
206 let stillValidating = getFieldState($field) === VALIDATING;
207 if (stillValidating) {
208 setFieldNotification($field, 'wait');
209 setFieldSpinner($field, true)
210 }
211 }, 500);
212}
213
214function setFieldNotification($field, type, message) {
215 const spinnerWasVisible = isSpinnerVisible($field);
216 removeIconOnlyNotifications($field);
217 const skipShowingSuccessNotification = (type === 'success') && !spinnerWasVisible;
218 if (skipShowingSuccessNotification) {
219 return;
220 }
221
222 if (type === 'none') {
223 removeFieldNotification($field, 'error');
224 } else {
225 const previousMessage = $field.attr(ATTRIBUTE_NOTIFICATION_PREFIX + type) || '[]';
226 const newMessages = message ? combineJSONMessages(message, previousMessage) : [];
227 $field.attr(ATTRIBUTE_NOTIFICATION_PREFIX + type, JSON.stringify(newMessages));
228 if (type === 'error') {
229 appendErrorMessages($field, newMessages);
230 }
231 }
232}
233
234function removeIconOnlyNotifications($field) {
235 removeFieldNotification($field, 'wait');
236 setFieldSpinner($field, false);
237 removeFieldNotification($field, 'success');
238}
239
240function removeFieldNotification($field, type) {
241 $field.removeAttr(ATTRIBUTE_NOTIFICATION_PREFIX + type);
242 if (type === 'error') {
243 getMessageContainer($field, type).remove();
244 }
245}
246
247function isSpinnerVisible($field) {
248 return $field.is('[' + ATTRIBUTE_NOTIFICATION_PREFIX + 'wait]');
249}
250
251function combineJSONMessages(newString, previousString) {
252 const previousStackedMessageList = JSON.parse(previousString);
253 return previousStackedMessageList.concat([newString]);
254}
255
256function getDisplayField($field) {
257 var displayFieldID = getValidationOption($field, 'displayfield');
258 var notifyOnSelf = (displayFieldID === undefined);
259 return notifyOnSelf ? $field : $('#' + displayFieldID);
260}
261
262function getFieldState($field) {
263 return $field.attr('data-' + ATTRIBUTE_FIELD_STATE);
264}
265
266/**
267 * Trigger validation on a field manually
268 * @param $field the field that validation should be triggered for
269 */
270function validateField($field) {
271 $field = $($field);
272 validationTriggeredHandler($field);
273}
274
275/**
276 * Form scrolling and submission prevent based on validation state
277 * -If the form is unvalidated, validate all fields
278 * -If the form is invalid, go to the first invalid element
279 * -If the form is validating, wait for them to validate and then try submitting again
280 * -If the form is valid, allow form submission
281 */
282$(document).on('submit', function (e) {
283 var form = e.target;
284 var $form = $(form);
285
286 var formState = getFormStateName($form);
287 if (formState === UNVALIDATED) {
288 delaySubmitUntilStateChange($form, e);
289 validateUnvalidatedFields($form);
290 } else if (formState === VALIDATING) {
291 delaySubmitUntilStateChange($form, e);
292 } else if (formState === INVALID) {
293 e.preventDefault();
294 selectFirstInvalid($form);
295 } else if (formState === VALID) {
296 var validSubmitEvent = $.Event('aui-valid-submit');
297 $form.trigger(validSubmitEvent);
298 var preventNormalSubmit = validSubmitEvent.isDefaultPrevented();
299 if (preventNormalSubmit) {
300 e.preventDefault(); //users can bind to aui-valid-submit for ajax forms
301 }
302 }
303});
304
305function delaySubmitUntilStateChange($form, event) {
306 event.preventDefault();
307 $form.one(EVENT_FIELD_STATE_CHANGED, function () {
308 $form.trigger('submit');
309 });
310}
311
312function getFormStateName($form) {
313 var $fieldCollection = $form.find('.' + CLASS_VALIDATION_INITIALISED);
314 var fieldStates = getFieldCollectionStateNames($fieldCollection);
315 var wholeFormState = mergeStates(fieldStates);
316 return wholeFormState;
317}
318
319function getFieldCollectionStateNames($fields) {
320 var states = $.map($fields, function (field) {
321 return getFieldState($(field));
322 });
323 return states;
324}
325
326function mergeStates(stateNames) {
327 var containsInvalidState = stateNames.indexOf(INVALID) !== -1;
328 var containsUnvalidatedState = stateNames.indexOf(UNVALIDATED) !== -1;
329 var containsValidatingState = stateNames.indexOf(VALIDATING) !== -1;
330
331 if (containsInvalidState) {
332 return INVALID;
333 } else if (containsUnvalidatedState) {
334 return UNVALIDATED;
335 } else if (containsValidatingState) {
336 return VALIDATING;
337 } else {
338 return VALID;
339 }
340}
341
342function validateUnvalidatedFields($form) {
343 var $unvalidatedElements = getFieldsInFormWithState($form, UNVALIDATED);
344 $unvalidatedElements.each(function (index, el) {
345 validator.validate($(el));
346 });
347}
348
349function selectFirstInvalid($form) {
350 var $firstInvalidField = getFieldsInFormWithState($form, INVALID).first();
351 $firstInvalidField.focus();
352}
353
354function getFieldsInFormWithState($form, state) {
355 var selector = '[data-' + ATTRIBUTE_FIELD_STATE + '=' + state + ']';
356 return $form.find(selector);
357}
358
359
360const validator = {
361 register: validatorRegister.register,
362 validate: validateField
363};
364
365skate(ATTRIBUTE_VALIDATION_FIELD_COMPONENT, {
366 attached: function (field) {
367 if (field.form) {
368 field.form.setAttribute('novalidate', 'novalidate');
369 }
370 var $field = $(field);
371 initValidation($field);
372 skate.init(field); //needed to kick off form notification skate initialisation
373 },
374 type: skate.type.ATTRIBUTE
375});
376
377amdify('aui/form-validation', validator);
378globalize('formValidation', validator);
379export default validator;