1 | import $ from './jquery';
|
2 | import './form-notification';
|
3 | import {appendDescription, appendErrorMessages, getMessageContainer, setFieldSpinner} from './form-notification';
|
4 | import './form-validation/basic-validators';
|
5 | import amdify from './internal/amdify';
|
6 | import * as deprecate from './internal/deprecation';
|
7 | import globalize from './internal/globalize';
|
8 | import skate from './internal/skate';
|
9 | import validatorRegister from './form-validation/validator-register';
|
10 |
|
11 |
|
12 | const ATTRIBUTE_VALIDATION_OPTION_PREFIX = 'aui-validation-';
|
13 | const ATTRIBUTE_NOTIFICATION_PREFIX = 'data-aui-notification-';
|
14 |
|
15 | const ATTRIBUTE_FIELD_STATE = 'aui-validation-state';
|
16 | const INVALID = 'invalid';
|
17 | const VALID = 'valid';
|
18 | const VALIDATING = 'validating';
|
19 | const UNVALIDATED = 'unvalidated';
|
20 |
|
21 | const ATTRIBUTE_VALIDATION_FIELD_COMPONENT = 'data-aui-validation-field';
|
22 |
|
23 |
|
24 | const CLASS_VALIDATION_INITIALISED = '_aui-form-validation-initialised';
|
25 |
|
26 |
|
27 | const EVENT_FIELD_STATE_CHANGED = '_aui-internal-field-state-changed';
|
28 |
|
29 | function isFieldInitialised($field) {
|
30 | return $field.hasClass(CLASS_VALIDATION_INITIALISED);
|
31 | }
|
32 |
|
33 | function initValidation($field) {
|
34 | if (!isFieldInitialised($field)) {
|
35 | prepareFieldMarkup($field);
|
36 | bindFieldEvents($field);
|
37 | changeFieldState($field, UNVALIDATED);
|
38 | }
|
39 | }
|
40 |
|
41 | function prepareFieldMarkup($field){
|
42 | $field.addClass(CLASS_VALIDATION_INITIALISED);
|
43 | appendDescription($field);
|
44 | }
|
45 |
|
46 | function bindFieldEvents($field) {
|
47 | bindStopTypingEvent($field);
|
48 | bindValidationEvent($field);
|
49 | }
|
50 |
|
51 | function 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 |
|
64 | function 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 |
|
75 | function 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 |
|
86 | function 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 |
|
98 | function 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 |
|
112 | function clearFieldMessages($field) {
|
113 | setFieldNotification(getDisplayField($field), 'none');
|
114 | }
|
115 |
|
116 | function getValidators() {
|
117 | return validatorRegister.validators();
|
118 | }
|
119 |
|
120 | function 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 |
|
133 | function 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 |
|
148 | function 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 |
|
172 | function createArgumentAccessorFunction($field) {
|
173 | return function (arg) {
|
174 | return $field.attr('data-' + ATTRIBUTE_VALIDATION_OPTION_PREFIX + arg) || $field.attr(arg);
|
175 | };
|
176 | }
|
177 |
|
178 | function 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 |
|
204 | function 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 |
|
214 | function 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 |
|
234 | function removeIconOnlyNotifications($field) {
|
235 | removeFieldNotification($field, 'wait');
|
236 | setFieldSpinner($field, false);
|
237 | removeFieldNotification($field, 'success');
|
238 | }
|
239 |
|
240 | function removeFieldNotification($field, type) {
|
241 | $field.removeAttr(ATTRIBUTE_NOTIFICATION_PREFIX + type);
|
242 | if (type === 'error') {
|
243 | getMessageContainer($field, type).remove();
|
244 | }
|
245 | }
|
246 |
|
247 | function isSpinnerVisible($field) {
|
248 | return $field.is('[' + ATTRIBUTE_NOTIFICATION_PREFIX + 'wait]');
|
249 | }
|
250 |
|
251 | function combineJSONMessages(newString, previousString) {
|
252 | const previousStackedMessageList = JSON.parse(previousString);
|
253 | return previousStackedMessageList.concat([newString]);
|
254 | }
|
255 |
|
256 | function getDisplayField($field) {
|
257 | var displayFieldID = getValidationOption($field, 'displayfield');
|
258 | var notifyOnSelf = (displayFieldID === undefined);
|
259 | return notifyOnSelf ? $field : $('#' + displayFieldID);
|
260 | }
|
261 |
|
262 | function getFieldState($field) {
|
263 | return $field.attr('data-' + ATTRIBUTE_FIELD_STATE);
|
264 | }
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 | function validateField($field) {
|
271 | $field = $($field);
|
272 | validationTriggeredHandler($field);
|
273 | }
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
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();
|
301 | }
|
302 | }
|
303 | });
|
304 |
|
305 | function delaySubmitUntilStateChange($form, event) {
|
306 | event.preventDefault();
|
307 | $form.one(EVENT_FIELD_STATE_CHANGED, function () {
|
308 | $form.trigger('submit');
|
309 | });
|
310 | }
|
311 |
|
312 | function 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 |
|
319 | function getFieldCollectionStateNames($fields) {
|
320 | var states = $.map($fields, function (field) {
|
321 | return getFieldState($(field));
|
322 | });
|
323 | return states;
|
324 | }
|
325 |
|
326 | function 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 |
|
342 | function validateUnvalidatedFields($form) {
|
343 | var $unvalidatedElements = getFieldsInFormWithState($form, UNVALIDATED);
|
344 | $unvalidatedElements.each(function (index, el) {
|
345 | validator.validate($(el));
|
346 | });
|
347 | }
|
348 |
|
349 | function selectFirstInvalid($form) {
|
350 | var $firstInvalidField = getFieldsInFormWithState($form, INVALID).first();
|
351 | $firstInvalidField.focus();
|
352 | }
|
353 |
|
354 | function getFieldsInFormWithState($form, state) {
|
355 | var selector = '[data-' + ATTRIBUTE_FIELD_STATE + '=' + state + ']';
|
356 | return $form.find(selector);
|
357 | }
|
358 |
|
359 |
|
360 | const validator = {
|
361 | register: validatorRegister.register,
|
362 | validate: validateField
|
363 | };
|
364 |
|
365 | skate(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);
|
373 | },
|
374 | type: skate.type.ATTRIBUTE
|
375 | });
|
376 |
|
377 | amdify('aui/form-validation', validator);
|
378 | globalize('formValidation', validator);
|
379 | export default validator;
|