form-father 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +77 -2
- package/demos/index.html +141 -0
- package/demos/main.js +92 -0
- package/demos/styles.css +221 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +158 -7
- package/dist/index.js +641 -130
- package/dist/index.js.map +1 -1
- package/dist/types/adapters.d.ts +28 -0
- package/dist/types/index.d.ts +128 -4
- package/dist/types/validators.d.ts +3 -2
- package/docs/en/README.md +192 -0
- package/docs/recipes/README.md +89 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,6 +15,18 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
15
15
|
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
function __rest(s, e) {
|
|
19
|
+
var t = {};
|
|
20
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
21
|
+
t[p] = s[p];
|
|
22
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
23
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
24
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
25
|
+
t[p[i]] = s[p[i]];
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
19
31
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
20
32
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -363,6 +375,65 @@ registerBuiltinValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный ф
|
|
|
363
375
|
registerBuiltinValidator('url', v => isUrlValid(v), 'Неверный формат');
|
|
364
376
|
registerBuiltinValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
|
|
365
377
|
|
|
378
|
+
function readFirstIssueMessage(error) {
|
|
379
|
+
var _a;
|
|
380
|
+
if (!error || typeof error !== 'object')
|
|
381
|
+
return undefined;
|
|
382
|
+
const issues = error.issues || error.errors;
|
|
383
|
+
if (Array.isArray(issues)) {
|
|
384
|
+
const firstMessage = (_a = issues.find(issue => typeof (issue === null || issue === void 0 ? void 0 : issue.message) === 'string')) === null || _a === void 0 ? void 0 : _a.message;
|
|
385
|
+
if (firstMessage)
|
|
386
|
+
return firstMessage;
|
|
387
|
+
}
|
|
388
|
+
return typeof error.message === 'string' ? error.message : undefined;
|
|
389
|
+
}
|
|
390
|
+
function resolveMessage(error, message) {
|
|
391
|
+
if (typeof message === 'function')
|
|
392
|
+
return message(error);
|
|
393
|
+
return message || readFirstIssueMessage(error);
|
|
394
|
+
}
|
|
395
|
+
function normalizeResult(result, message) {
|
|
396
|
+
if (typeof result === 'object' && result !== null)
|
|
397
|
+
return result;
|
|
398
|
+
if (result === undefined)
|
|
399
|
+
return true;
|
|
400
|
+
return result ? true : { valid: false, message };
|
|
401
|
+
}
|
|
402
|
+
function createSchemaValidator(schema, options = {}) {
|
|
403
|
+
return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
|
|
404
|
+
const mappedValue = options.mapValue ? options.mapValue(value, $input, $form, params) : value;
|
|
405
|
+
try {
|
|
406
|
+
if (typeof schema === 'function') {
|
|
407
|
+
return normalizeResult(yield schema(mappedValue), resolveMessage(undefined, options.message));
|
|
408
|
+
}
|
|
409
|
+
if (typeof schema.safeParse === 'function') {
|
|
410
|
+
const result = yield schema.safeParse(mappedValue);
|
|
411
|
+
if (result.success)
|
|
412
|
+
return true;
|
|
413
|
+
return { valid: false, message: resolveMessage(result.error || result, options.message) };
|
|
414
|
+
}
|
|
415
|
+
if (typeof schema.parse === 'function') {
|
|
416
|
+
yield schema.parse(mappedValue);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return { valid: false, message: resolveMessage(error, options.message) };
|
|
422
|
+
}
|
|
423
|
+
return { valid: false, message: resolveMessage(undefined, options.message) };
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
|
|
427
|
+
const { override } = options, adapterOptions = __rest(options, ["override"]);
|
|
428
|
+
registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
|
|
429
|
+
}
|
|
430
|
+
function createFieldValidator(predicate, message) {
|
|
431
|
+
return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
|
|
432
|
+
const valid = yield predicate(value, $input, $form, params);
|
|
433
|
+
return valid ? true : { valid: false, message };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
366
437
|
var _a;
|
|
367
438
|
// --- shared cross-file state (singleton via globalThis) ---
|
|
368
439
|
const FORM_GLOBAL_KEY = Symbol.for('formfather.shared');
|
|
@@ -375,6 +446,7 @@ const __shared =
|
|
|
375
446
|
defaultValidationSchema: undefined,
|
|
376
447
|
});
|
|
377
448
|
let loaderIdCounter = 0;
|
|
449
|
+
const FORM_INSTANCES = new WeakMap();
|
|
378
450
|
/** Изначальная дефолтная схема. */
|
|
379
451
|
const INITIAL_DEFAULT_SCHEMA = {
|
|
380
452
|
tel: {
|
|
@@ -427,6 +499,11 @@ class Form {
|
|
|
427
499
|
this.$submits = [];
|
|
428
500
|
this.waitResponse = false;
|
|
429
501
|
this.inputs = null;
|
|
502
|
+
this.fieldEventHandlers = [];
|
|
503
|
+
this.liveValidationTimers = new WeakMap();
|
|
504
|
+
this.fieldValidationTokens = new WeakMap();
|
|
505
|
+
this.fieldValidationTokenCounter = 0;
|
|
506
|
+
this.errors = [];
|
|
430
507
|
/** Общие настройки анимаций (можно подправить под вкус) */
|
|
431
508
|
this._anim = {
|
|
432
509
|
duration: 220,
|
|
@@ -445,29 +522,27 @@ class Form {
|
|
|
445
522
|
loaderColor: '#fff',
|
|
446
523
|
showLoaderButton: true,
|
|
447
524
|
scrollToFirstErroredInput: true,
|
|
525
|
+
focusFirstErroredInput: false,
|
|
448
526
|
logging: false,
|
|
449
527
|
inputSelector: '.input',
|
|
450
528
|
inputWrapperSelector: '.input-primary',
|
|
529
|
+
validateOn: 'submit',
|
|
530
|
+
revalidateOn: ['input', 'change'],
|
|
531
|
+
validationDebounce: 0,
|
|
532
|
+
errorContainerAttribute: 'data-error-container',
|
|
533
|
+
validationStateAttribute: 'data-form-father-state',
|
|
534
|
+
observeMutations: false,
|
|
451
535
|
};
|
|
452
536
|
/* Слияние параметров: глобальные параметры → пользовательские параметры */
|
|
453
537
|
this.config = Object.assign({}, defaultConfig, Form.defaultParams, options);
|
|
454
538
|
if (this.$el) {
|
|
455
539
|
this.$submits = this.findSubmitElements();
|
|
456
|
-
this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
457
|
-
var _a, _b, _c, _d;
|
|
458
|
-
e.preventDefault();
|
|
459
|
-
(_b = (_a = this.config).onBeforeValidate) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
460
|
-
const isValid = yield this.validate();
|
|
461
|
-
(_d = (_c = this.config).onAfterValidate) === null || _d === void 0 ? void 0 : _d.call(_c, isValid, this);
|
|
462
|
-
if (isValid) {
|
|
463
|
-
yield this.submit();
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
540
|
if (this.isCorrectArguments()) {
|
|
467
|
-
this
|
|
468
|
-
$submit.addEventListener('click', this._onSubmitClickHandler);
|
|
469
|
-
});
|
|
541
|
+
this.bindSubmitHandlers();
|
|
470
542
|
this.initialization();
|
|
543
|
+
this.bindFieldValidationEvents();
|
|
544
|
+
this.setupMutationObserver();
|
|
545
|
+
FORM_INSTANCES.set(this.$el, this);
|
|
471
546
|
}
|
|
472
547
|
}
|
|
473
548
|
else {
|
|
@@ -477,6 +552,100 @@ class Form {
|
|
|
477
552
|
getOptions() {
|
|
478
553
|
return this.options;
|
|
479
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Инициализирует сразу несколько форм по CSS-селектору. Если форма уже инициализирована, её настройки обновятся.
|
|
557
|
+
*
|
|
558
|
+
* @param selector - CSS-селектор форм. По умолчанию `form[data-form-father]`.
|
|
559
|
+
* @param options - Общие параметры для найденных форм.
|
|
560
|
+
*/
|
|
561
|
+
static initAll(selector = 'form[data-form-father]', options = {}) {
|
|
562
|
+
/* istanbul ignore next -- SSR guard: jsdom does not allow document to be unset cleanly. */
|
|
563
|
+
if (typeof document === 'undefined')
|
|
564
|
+
return [];
|
|
565
|
+
return Array.from(document.querySelectorAll(selector))
|
|
566
|
+
.filter($form => $form.tagName === 'FORM')
|
|
567
|
+
.map($form => {
|
|
568
|
+
const existing = FORM_INSTANCES.get($form);
|
|
569
|
+
if (existing) {
|
|
570
|
+
existing.updateOptions(options);
|
|
571
|
+
return existing;
|
|
572
|
+
}
|
|
573
|
+
return new Form($form, options);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/** Обновляет настройки конкретного инстанса формы без пересоздания обработчиков вручную. */
|
|
577
|
+
updateOptions(options) {
|
|
578
|
+
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
579
|
+
this.config = Object.assign(Object.assign({}, this.config), options);
|
|
580
|
+
this.refreshControls();
|
|
581
|
+
this.bindSubmitHandlers();
|
|
582
|
+
this.bindFieldValidationEvents();
|
|
583
|
+
this.setupMutationObserver();
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
/** Возвращает текущий список ошибок формы. */
|
|
587
|
+
getErrors() {
|
|
588
|
+
return this.errors.map(error => (Object.assign({}, error)));
|
|
589
|
+
}
|
|
590
|
+
/** Возвращает значения формы обычным объектом. */
|
|
591
|
+
getValues() {
|
|
592
|
+
return serializeFormToJSON(this.$el);
|
|
593
|
+
}
|
|
594
|
+
normalizeValueList(value) {
|
|
595
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
596
|
+
}
|
|
597
|
+
/** Проставляет значения полей по `name` и уведомляет UI через `input`/`change` события. */
|
|
598
|
+
setValues(values) {
|
|
599
|
+
this.getAllFields().forEach($field => {
|
|
600
|
+
if (!$field.name || !Object.prototype.hasOwnProperty.call(values, $field.name))
|
|
601
|
+
return;
|
|
602
|
+
const value = values[$field.name];
|
|
603
|
+
if ($field instanceof HTMLInputElement && $field.type === 'file')
|
|
604
|
+
return;
|
|
605
|
+
if ($field instanceof HTMLInputElement && $field.type === 'checkbox') {
|
|
606
|
+
if (typeof value === 'boolean') {
|
|
607
|
+
$field.checked = value;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
$field.checked = this.normalizeValueList(value).includes($field.value);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if ($field instanceof HTMLInputElement && $field.type === 'radio') {
|
|
614
|
+
$field.checked = String(value) === $field.value;
|
|
615
|
+
}
|
|
616
|
+
else if ($field instanceof HTMLSelectElement && $field.multiple) {
|
|
617
|
+
const valuesList = this.normalizeValueList(value);
|
|
618
|
+
Array.from($field.options).forEach(option => {
|
|
619
|
+
option.selected = valuesList.includes(option.value);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
$field.value = value == null ? '' : String(value);
|
|
624
|
+
}
|
|
625
|
+
$field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
626
|
+
$field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
627
|
+
});
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
631
|
+
clearErrors() {
|
|
632
|
+
this.errors = [];
|
|
633
|
+
this.getAllFields().forEach($field => {
|
|
634
|
+
this.hideError($field);
|
|
635
|
+
this.clearFieldValidationState($field);
|
|
636
|
+
});
|
|
637
|
+
this.hideErrorForm();
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
641
|
+
reset(options = {}) {
|
|
642
|
+
this.$el.reset();
|
|
643
|
+
this.refreshControls();
|
|
644
|
+
if (options.clearErrors !== false) {
|
|
645
|
+
this.clearErrors();
|
|
646
|
+
}
|
|
647
|
+
return this;
|
|
648
|
+
}
|
|
480
649
|
/**
|
|
481
650
|
* Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
|
|
482
651
|
* параметрами по умолчанию.
|
|
@@ -487,21 +656,123 @@ class Form {
|
|
|
487
656
|
static setDefaultParams(params) {
|
|
488
657
|
this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
|
|
489
658
|
}
|
|
659
|
+
refreshControls() {
|
|
660
|
+
this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
|
|
661
|
+
}
|
|
490
662
|
findSubmitElements() {
|
|
491
663
|
return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'));
|
|
492
664
|
}
|
|
665
|
+
bindSubmitHandlers() {
|
|
666
|
+
if (!this._onSubmitClickHandler) {
|
|
667
|
+
this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
668
|
+
yield this.handleSubmitEvent(e);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
this.$submits.forEach($submit => {
|
|
672
|
+
$submit.removeEventListener('click', this._onSubmitClickHandler);
|
|
673
|
+
});
|
|
674
|
+
this.$submits = this.findSubmitElements();
|
|
675
|
+
this.$submits.forEach($submit => {
|
|
676
|
+
$submit.addEventListener('click', this._onSubmitClickHandler);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
493
679
|
initialization() {
|
|
494
|
-
this.
|
|
680
|
+
this.refreshControls();
|
|
495
681
|
this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
496
|
-
|
|
682
|
+
yield this.handleSubmitEvent(e);
|
|
683
|
+
});
|
|
684
|
+
this.$el.addEventListener('submit', this._onSubmitHandler);
|
|
685
|
+
}
|
|
686
|
+
handleSubmitEvent(e) {
|
|
687
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
688
|
+
var _a, _b, _c, _d, _e, _f;
|
|
497
689
|
e.preventDefault();
|
|
498
690
|
(_b = (_a = this.config).onBeforeValidate) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
499
691
|
const isValid = yield this.validate();
|
|
500
692
|
(_d = (_c = this.config).onAfterValidate) === null || _d === void 0 ? void 0 : _d.call(_c, isValid, this);
|
|
501
|
-
if (isValid)
|
|
502
|
-
this.
|
|
693
|
+
if (!isValid) {
|
|
694
|
+
(_f = (_e = this.config).onValidationError) === null || _f === void 0 ? void 0 : _f.call(_e, this.getErrors(), this);
|
|
695
|
+
return {
|
|
696
|
+
success: false,
|
|
697
|
+
error: this.getErrors(),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return this.submit();
|
|
503
701
|
});
|
|
504
|
-
|
|
702
|
+
}
|
|
703
|
+
normalizeTriggers(value, fallback) {
|
|
704
|
+
const triggers = value == null ? fallback : Array.isArray(value) ? value : [value];
|
|
705
|
+
return Array.from(new Set(triggers));
|
|
706
|
+
}
|
|
707
|
+
clearFieldValidationEvents() {
|
|
708
|
+
this.fieldEventHandlers.forEach(({ $input, event, handler }) => {
|
|
709
|
+
$input.removeEventListener(event, handler);
|
|
710
|
+
});
|
|
711
|
+
this.fieldEventHandlers = [];
|
|
712
|
+
}
|
|
713
|
+
bindFieldValidationEvents() {
|
|
714
|
+
this.clearFieldValidationEvents();
|
|
715
|
+
const validateOn = this.normalizeTriggers(this.config.validateOn, ['submit']).filter((event) => event !== 'submit');
|
|
716
|
+
const revalidateOn = this.normalizeTriggers(this.config.revalidateOn, ['input', 'change']).filter((event) => event !== 'submit');
|
|
717
|
+
const events = Array.from(new Set([...validateOn, ...revalidateOn]));
|
|
718
|
+
if (events.length === 0)
|
|
719
|
+
return;
|
|
720
|
+
const fields = this.getAllFields();
|
|
721
|
+
fields.forEach($input => {
|
|
722
|
+
events.forEach(event => {
|
|
723
|
+
const handler = () => {
|
|
724
|
+
const shouldValidate = validateOn.includes(event) || this.hasFieldError($input);
|
|
725
|
+
if (shouldValidate)
|
|
726
|
+
this.scheduleFieldValidation($input);
|
|
727
|
+
};
|
|
728
|
+
$input.addEventListener(event, handler);
|
|
729
|
+
this.fieldEventHandlers.push({ $input, event, handler });
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
scheduleFieldValidation($input) {
|
|
734
|
+
const currentTimer = this.liveValidationTimers.get($input);
|
|
735
|
+
if (currentTimer)
|
|
736
|
+
clearTimeout(currentTimer);
|
|
737
|
+
const delay = Math.max(0, Number(this.config.validationDebounce) || 0);
|
|
738
|
+
const run = () => {
|
|
739
|
+
void this.validateField($input);
|
|
740
|
+
};
|
|
741
|
+
if (delay === 0) {
|
|
742
|
+
run();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const timer = setTimeout(run, delay);
|
|
746
|
+
this.liveValidationTimers.set($input, timer);
|
|
747
|
+
}
|
|
748
|
+
setupMutationObserver() {
|
|
749
|
+
var _a;
|
|
750
|
+
(_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
751
|
+
this.mutationObserver = undefined;
|
|
752
|
+
if (!this.config.observeMutations || typeof MutationObserver === 'undefined')
|
|
753
|
+
return;
|
|
754
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
755
|
+
this.refreshControls();
|
|
756
|
+
this.bindSubmitHandlers();
|
|
757
|
+
this.bindFieldValidationEvents();
|
|
758
|
+
});
|
|
759
|
+
this.mutationObserver.observe(this.$el, {
|
|
760
|
+
childList: true,
|
|
761
|
+
subtree: true,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
getErrorContainer($input) {
|
|
765
|
+
const attrName = this.config.errorContainerAttribute || 'data-error-container';
|
|
766
|
+
const selector = $input.getAttribute(attrName);
|
|
767
|
+
if (!selector)
|
|
768
|
+
return null;
|
|
769
|
+
try {
|
|
770
|
+
return document.querySelector(selector);
|
|
771
|
+
}
|
|
772
|
+
catch (_a) {
|
|
773
|
+
console.warn(`[FormFather] Invalid error container selector "${selector}"`);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
505
776
|
}
|
|
506
777
|
/**
|
|
507
778
|
* Показывает ошибку для поля ввода.
|
|
@@ -510,6 +781,15 @@ class Form {
|
|
|
510
781
|
* @param {String} text - Текст ошибки.
|
|
511
782
|
*/
|
|
512
783
|
showError($input, text) {
|
|
784
|
+
$input.setAttribute('aria-invalid', 'true');
|
|
785
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
786
|
+
if ($errorContainer) {
|
|
787
|
+
$errorContainer.textContent = text;
|
|
788
|
+
if (!$errorContainer.hasAttribute('role'))
|
|
789
|
+
$errorContainer.setAttribute('role', 'alert');
|
|
790
|
+
$errorContainer.removeAttribute('hidden');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
513
793
|
const $inputWrapper = closest($input, this.config.inputWrapperSelector);
|
|
514
794
|
if (!$inputWrapper) {
|
|
515
795
|
console.warn(`Не найдена обёртка для поля с именем: ${$input.name || $input.type}`);
|
|
@@ -520,7 +800,6 @@ class Form {
|
|
|
520
800
|
customShowError.call($inputWrapper, text);
|
|
521
801
|
return;
|
|
522
802
|
}
|
|
523
|
-
$input.setAttribute('aria-invalid', 'true');
|
|
524
803
|
$inputWrapper.classList.add('input__wrapper--error');
|
|
525
804
|
let $error = $inputWrapper.querySelector('[data-form-father-error]');
|
|
526
805
|
if (!$error) {
|
|
@@ -586,7 +865,12 @@ class Form {
|
|
|
586
865
|
hideError($input) {
|
|
587
866
|
var _a;
|
|
588
867
|
const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
|
|
868
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
589
869
|
$input.removeAttribute('aria-invalid');
|
|
870
|
+
if ($errorContainer) {
|
|
871
|
+
$errorContainer.textContent = '';
|
|
872
|
+
$errorContainer.setAttribute('hidden', '');
|
|
873
|
+
}
|
|
590
874
|
(_a = $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.querySelector('[data-form-father-error]')) === null || _a === void 0 ? void 0 : _a.remove();
|
|
591
875
|
$inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.classList.remove('input__wrapper--error');
|
|
592
876
|
if ($inputWrapper && $inputWrapper.classList.contains('input__wrapper')) {
|
|
@@ -600,7 +884,7 @@ class Form {
|
|
|
600
884
|
*/
|
|
601
885
|
scrollToFirstErroredInput(inputsList) {
|
|
602
886
|
if (inputsList.length > 0) {
|
|
603
|
-
const allInputInFormList = Array.from(this.$el.querySelectorAll('input'));
|
|
887
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
604
888
|
let $firstInputOnForm = null;
|
|
605
889
|
let minIndex = Infinity;
|
|
606
890
|
inputsList.forEach($input => {
|
|
@@ -625,125 +909,325 @@ class Form {
|
|
|
625
909
|
}
|
|
626
910
|
}
|
|
627
911
|
}
|
|
912
|
+
focusFirstErroredInput(inputsList) {
|
|
913
|
+
var _a;
|
|
914
|
+
if (inputsList.length === 0)
|
|
915
|
+
return;
|
|
916
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
917
|
+
const $firstInputOnForm = inputsList
|
|
918
|
+
.slice()
|
|
919
|
+
.sort((a, b) => allInputInFormList.indexOf(a) - allInputInFormList.indexOf(b))[0];
|
|
920
|
+
(_a = $firstInputOnForm === null || $firstInputOnForm === void 0 ? void 0 : $firstInputOnForm.focus) === null || _a === void 0 ? void 0 : _a.call($firstInputOnForm, { preventScroll: true });
|
|
921
|
+
}
|
|
922
|
+
getValidationSchema() {
|
|
923
|
+
return Object.assign(Object.assign({}, (Form.defaultValidationSchema || {})), (this.config.validationSchema || {}));
|
|
924
|
+
}
|
|
925
|
+
getAllFields($block = this.$el) {
|
|
926
|
+
return Array.from($block.querySelectorAll('input, textarea, select'));
|
|
927
|
+
}
|
|
928
|
+
getFieldKey($input) {
|
|
929
|
+
return $input.name || $input.id || $input.getAttribute('data-field') || $input.type || 'field';
|
|
930
|
+
}
|
|
931
|
+
hasFieldError($input) {
|
|
932
|
+
const field = this.getFieldKey($input);
|
|
933
|
+
return $input.getAttribute('aria-invalid') === 'true' || this.errors.some(error => error.field === field);
|
|
934
|
+
}
|
|
935
|
+
clearFieldErrorRecord($input) {
|
|
936
|
+
const field = this.getFieldKey($input);
|
|
937
|
+
this.errors = this.errors.filter(error => error.field !== field);
|
|
938
|
+
}
|
|
939
|
+
getValidationStateAttribute() {
|
|
940
|
+
return this.config.validationStateAttribute || 'data-form-father-state';
|
|
941
|
+
}
|
|
942
|
+
setFieldValidationState($input, state) {
|
|
943
|
+
const attr = this.getValidationStateAttribute();
|
|
944
|
+
if (state) {
|
|
945
|
+
$input.setAttribute(attr, state);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
$input.removeAttribute(attr);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
startFieldValidation($input) {
|
|
952
|
+
const token = ++this.fieldValidationTokenCounter;
|
|
953
|
+
this.fieldValidationTokens.set($input, token);
|
|
954
|
+
$input.setAttribute('aria-busy', 'true');
|
|
955
|
+
this.setFieldValidationState($input, 'validating');
|
|
956
|
+
return token;
|
|
957
|
+
}
|
|
958
|
+
isLatestFieldValidation($input, token) {
|
|
959
|
+
return token == null || this.fieldValidationTokens.get($input) === token;
|
|
960
|
+
}
|
|
961
|
+
finishFieldValidation($input, token, isValid) {
|
|
962
|
+
if (!this.isLatestFieldValidation($input, token))
|
|
963
|
+
return;
|
|
964
|
+
$input.removeAttribute('aria-busy');
|
|
965
|
+
this.setFieldValidationState($input, isValid ? 'valid' : 'invalid');
|
|
966
|
+
}
|
|
967
|
+
clearFieldValidationState($input) {
|
|
968
|
+
this.fieldValidationTokens.delete($input);
|
|
969
|
+
$input.removeAttribute('aria-busy');
|
|
970
|
+
this.setFieldValidationState($input, null);
|
|
971
|
+
}
|
|
972
|
+
setValidationError($input, rule, message, source) {
|
|
973
|
+
this.clearFieldErrorRecord($input);
|
|
974
|
+
this.errors.push({
|
|
975
|
+
field: this.getFieldKey($input),
|
|
976
|
+
rule,
|
|
977
|
+
message,
|
|
978
|
+
source,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
resolveField(field) {
|
|
982
|
+
if (typeof field !== 'string')
|
|
983
|
+
return field;
|
|
984
|
+
return this.findFieldByName(field);
|
|
985
|
+
}
|
|
986
|
+
parseRuleString(ruleString) {
|
|
987
|
+
const [rule, ...params] = ruleString.split(':').map(s => s.trim());
|
|
988
|
+
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
989
|
+
}
|
|
990
|
+
parseValidationAttribute(value) {
|
|
991
|
+
return (value !== null && value !== void 0 ? value : '')
|
|
992
|
+
.split(/[,\s|]+/)
|
|
993
|
+
.map(s => s.trim())
|
|
994
|
+
.filter(Boolean)
|
|
995
|
+
.map(ruleString => this.parseRuleString(ruleString));
|
|
996
|
+
}
|
|
997
|
+
getRuleName(rule) {
|
|
998
|
+
return typeof rule === 'string' ? rule : rule.rule;
|
|
999
|
+
}
|
|
1000
|
+
getRuleIdentity(rule) {
|
|
1001
|
+
return JSON.stringify(rule);
|
|
1002
|
+
}
|
|
1003
|
+
collectRules($input, fromSchema = [], schemaKeys = new Set()) {
|
|
1004
|
+
const list = [];
|
|
1005
|
+
if ($input.hasAttribute('required'))
|
|
1006
|
+
list.push('required');
|
|
1007
|
+
list.push(...fromSchema);
|
|
1008
|
+
const dataValidateRules = this.parseValidationAttribute($input.getAttribute('data-validate')).filter(rule => {
|
|
1009
|
+
const ruleName = this.getRuleName(rule);
|
|
1010
|
+
return getValidator(ruleName) || !schemaKeys.has(ruleName);
|
|
1011
|
+
});
|
|
1012
|
+
list.push(...dataValidateRules);
|
|
1013
|
+
list.push(...this.parseValidationAttribute($input.getAttribute('data-custom-validate')));
|
|
1014
|
+
const filtered = [];
|
|
1015
|
+
const seen = new Set();
|
|
1016
|
+
for (const rule of list) {
|
|
1017
|
+
const ruleName = this.getRuleName(rule);
|
|
1018
|
+
if (!getValidator(ruleName)) {
|
|
1019
|
+
console.warn(`[FormFather] Unknown validation rule "${ruleName}"`);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const identity = this.getRuleIdentity(rule);
|
|
1023
|
+
if (seen.has(identity))
|
|
1024
|
+
continue;
|
|
1025
|
+
seen.add(identity);
|
|
1026
|
+
filtered.push(rule);
|
|
1027
|
+
}
|
|
1028
|
+
return filtered;
|
|
1029
|
+
}
|
|
1030
|
+
getRuleMessage($input, rule, messages, defaultMessage, overrideMessage) {
|
|
1031
|
+
const attrMessage = $input.getAttribute(`data-error-${rule}`);
|
|
1032
|
+
return overrideMessage || attrMessage || messages[rule] || defaultMessage;
|
|
1033
|
+
}
|
|
1034
|
+
fieldMatchesSchemaDefinition($input, key, def) {
|
|
1035
|
+
if (def.selector) {
|
|
1036
|
+
try {
|
|
1037
|
+
return $input.matches(def.selector);
|
|
1038
|
+
}
|
|
1039
|
+
catch (_a) {
|
|
1040
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key);
|
|
1045
|
+
}
|
|
1046
|
+
getFieldValidationConfig($input, schema = this.getValidationSchema()) {
|
|
1047
|
+
const rules = [];
|
|
1048
|
+
const messages = {};
|
|
1049
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1050
|
+
if (!this.fieldMatchesSchemaDefinition($input, key, def))
|
|
1051
|
+
continue;
|
|
1052
|
+
rules.push(...def.rules);
|
|
1053
|
+
Object.assign(messages, def.messages || {});
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
rules: this.collectRules($input, rules, new Set(Object.keys(schema))),
|
|
1057
|
+
messages,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
getValidationFields($block, schema) {
|
|
1061
|
+
const fields = new Set();
|
|
1062
|
+
const addFields = (nodeList) => {
|
|
1063
|
+
Array.from(nodeList).forEach($input => {
|
|
1064
|
+
if ($input instanceof HTMLInputElement ||
|
|
1065
|
+
$input instanceof HTMLTextAreaElement ||
|
|
1066
|
+
$input instanceof HTMLSelectElement) {
|
|
1067
|
+
fields.add($input);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1072
|
+
if (def.selector) {
|
|
1073
|
+
try {
|
|
1074
|
+
addFields($block.querySelectorAll(def.selector));
|
|
1075
|
+
}
|
|
1076
|
+
catch (_a) {
|
|
1077
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1078
|
+
}
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
this.getAllFields($block)
|
|
1082
|
+
.filter($input => this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key))
|
|
1083
|
+
.forEach($input => fields.add($input));
|
|
1084
|
+
}
|
|
1085
|
+
addFields($block.querySelectorAll('[required], [data-validate], [data-custom-validate]'));
|
|
1086
|
+
return Array.from(fields);
|
|
1087
|
+
}
|
|
1088
|
+
validateInput($input_1, rules_1) {
|
|
1089
|
+
return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el, validationToken) {
|
|
1090
|
+
if ($input.hasAttribute('data-no-validate'))
|
|
1091
|
+
return true;
|
|
1092
|
+
const isCheckable = $input instanceof HTMLInputElement && ($input.type === 'checkbox' || $input.type === 'radio');
|
|
1093
|
+
const empty = isCheckable ? !$input.checked : $input.value.trim().length === 0;
|
|
1094
|
+
const hasRequiredRule = rules.some(rule => this.getRuleName(rule) === 'required');
|
|
1095
|
+
if (empty && !hasRequiredRule) {
|
|
1096
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1097
|
+
return !this.hasFieldError($input);
|
|
1098
|
+
this.clearFieldErrorRecord($input);
|
|
1099
|
+
this.hideError($input);
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
for (const validationRule of rules) {
|
|
1103
|
+
const { rule, params } = typeof validationRule === 'string'
|
|
1104
|
+
? { rule: validationRule, params: undefined }
|
|
1105
|
+
: { rule: validationRule.rule, params: validationRule.params };
|
|
1106
|
+
const validator = getValidator(rule);
|
|
1107
|
+
if (!validator)
|
|
1108
|
+
continue;
|
|
1109
|
+
const raw = yield validator.fn($input.value, $input, $block, params);
|
|
1110
|
+
const isObjectResult = typeof raw === 'object' && raw !== null;
|
|
1111
|
+
const passed = isObjectResult ? raw.valid === true : !!raw;
|
|
1112
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1113
|
+
return !this.hasFieldError($input);
|
|
1114
|
+
if (isObjectResult && raw.effect) {
|
|
1115
|
+
yield raw.effect({
|
|
1116
|
+
value: $input.value,
|
|
1117
|
+
$input,
|
|
1118
|
+
$form: $block,
|
|
1119
|
+
params,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1123
|
+
return !this.hasFieldError($input);
|
|
1124
|
+
if (!passed) {
|
|
1125
|
+
const result = raw;
|
|
1126
|
+
const message = this.getRuleMessage($input, rule, messages, validator.defaultMessage, isObjectResult && typeof result.message === 'string' ? result.message : undefined);
|
|
1127
|
+
this.setValidationError($input, rule, message, 'client');
|
|
1128
|
+
this.showError($input, message);
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
if (isObjectResult && raw.stopOthers) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1136
|
+
return !this.hasFieldError($input);
|
|
1137
|
+
this.clearFieldErrorRecord($input);
|
|
1138
|
+
this.hideError($input);
|
|
1139
|
+
return true;
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
runFieldValidation($input_1, rules_1) {
|
|
1143
|
+
return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el) {
|
|
1144
|
+
const validationToken = this.startFieldValidation($input);
|
|
1145
|
+
try {
|
|
1146
|
+
const isValid = yield this.validateInput($input, rules, messages, $block, validationToken);
|
|
1147
|
+
this.finishFieldValidation($input, validationToken, isValid);
|
|
1148
|
+
return isValid;
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
if (this.isLatestFieldValidation($input, validationToken)) {
|
|
1152
|
+
this.clearFieldValidationState($input);
|
|
1153
|
+
}
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
getRadioGroup($input, $block) {
|
|
1159
|
+
if (!($input instanceof HTMLInputElement) || $input.type !== 'radio' || !$input.name)
|
|
1160
|
+
return [];
|
|
1161
|
+
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
1162
|
+
}
|
|
1163
|
+
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
1164
|
+
showFieldError(field, message, source = 'manual') {
|
|
1165
|
+
const $field = this.resolveField(field);
|
|
1166
|
+
if (!$field) {
|
|
1167
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
this.setValidationError($field, source, message, source);
|
|
1171
|
+
this.showError($field, message);
|
|
1172
|
+
$field.removeAttribute('aria-busy');
|
|
1173
|
+
this.setFieldValidationState($field, 'invalid');
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
1177
|
+
validateField(field) {
|
|
1178
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1179
|
+
const $field = this.resolveField(field);
|
|
1180
|
+
if (!$field) {
|
|
1181
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
const { rules, messages } = this.getFieldValidationConfig($field);
|
|
1185
|
+
return this.runFieldValidation($field, rules, messages);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
628
1188
|
/**
|
|
629
1189
|
* Проверяет все поля ввода.
|
|
630
1190
|
*
|
|
631
|
-
* Порядок для
|
|
1191
|
+
* Порядок для каждого поля:
|
|
632
1192
|
*
|
|
633
1193
|
* 1. `required` — если атрибут `required`
|
|
634
1194
|
* 2. правила из `validationSchema`
|
|
635
|
-
* 3. правила из `data-
|
|
1195
|
+
* 3. правила из `data-validate`
|
|
1196
|
+
* 4. правила из `data-custom-validate`
|
|
636
1197
|
*
|
|
637
1198
|
* Для одного поля показывается только первая ошибка. Неизвестные правила фиксируются `console.warn` и исключаются ДО
|
|
638
|
-
*
|
|
1199
|
+
* валидации.
|
|
639
1200
|
*/
|
|
640
1201
|
validate() {
|
|
641
1202
|
return __awaiter(this, arguments, void 0, function* ($block = this.$el) {
|
|
642
|
-
|
|
643
|
-
const schema = Object.assign(Object.assign({}, (Form.defaultValidationSchema || {})), (this.config.validationSchema || {}));
|
|
644
|
-
const warnUnknown = (r) => console.warn(`[FormFather] Unknown validation rule "${r}"`);
|
|
1203
|
+
const schema = this.getValidationSchema();
|
|
645
1204
|
const erroredInputs = [];
|
|
646
1205
|
const processed = new WeakSet();
|
|
647
1206
|
let ok = true;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
list.push(...fromSchema); // 2
|
|
656
|
-
const custom = ((_a = $input.getAttribute('data-custom-validate')) !== null && _a !== void 0 ? _a : '')
|
|
657
|
-
.split(',')
|
|
658
|
-
.map(s => s.trim())
|
|
659
|
-
.filter(Boolean);
|
|
660
|
-
list.push(...custom.map(ruleString => {
|
|
661
|
-
const [rule, ...params] = ruleString.split(':').map(s => s.trim());
|
|
662
|
-
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
663
|
-
})); // 3
|
|
664
|
-
/* —–– предварительная фильтрация unknown ––– */
|
|
665
|
-
const filtered = [];
|
|
666
|
-
for (const r of list) {
|
|
667
|
-
const ruleName = typeof r === 'string' ? r : r.rule;
|
|
668
|
-
if (getValidator(ruleName))
|
|
669
|
-
filtered.push(r);
|
|
670
|
-
else
|
|
671
|
-
warnUnknown(ruleName);
|
|
672
|
-
}
|
|
673
|
-
return filtered;
|
|
674
|
-
};
|
|
675
|
-
const validateInput = ($input_1, rules_1, ...args_1) => __awaiter(this, [$input_1, rules_1, ...args_1], void 0, function* ($input, rules, msgs = {}) {
|
|
676
|
-
if ($input.hasAttribute('data-no-validate'))
|
|
677
|
-
return;
|
|
678
|
-
const empty = $input.type === 'checkbox' || $input.type === 'radio' ? !$input.checked : $input.value.trim().length === 0;
|
|
679
|
-
const hasRequiredRule = rules.some(r => (typeof r === 'string' ? r === 'required' : r.rule === 'required'));
|
|
680
|
-
if (empty && !hasRequiredRule) {
|
|
681
|
-
this.hideError($input);
|
|
682
|
-
return;
|
|
1207
|
+
this.errors = [];
|
|
1208
|
+
for (const $input of this.getValidationFields($block, schema)) {
|
|
1209
|
+
if (processed.has($input))
|
|
1210
|
+
continue;
|
|
1211
|
+
const radioGroup = this.getRadioGroup($input, $block);
|
|
1212
|
+
if (radioGroup.length > 0) {
|
|
1213
|
+
radioGroup.forEach($radio => processed.add($radio));
|
|
683
1214
|
}
|
|
684
|
-
|
|
685
|
-
const { rule, params } = typeof r === 'string' ? { rule: r, params: undefined } : r;
|
|
686
|
-
const vd = getValidator(rule); // гарантированно существует
|
|
687
|
-
const raw = yield vd.fn($input.value, $input, $block, params);
|
|
688
|
-
const isObj = typeof raw === 'object' && raw !== null;
|
|
689
|
-
const passed = isObj ? raw.valid === true : !!raw;
|
|
690
|
-
// сайд-эффект (если есть)
|
|
691
|
-
if (isObj && raw.effect) {
|
|
692
|
-
yield raw.effect({
|
|
693
|
-
value: $input.value,
|
|
694
|
-
$input,
|
|
695
|
-
$form: $block,
|
|
696
|
-
params,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
if (!passed) {
|
|
700
|
-
ok = false;
|
|
701
|
-
const r = raw;
|
|
702
|
-
const msg = (isObj && typeof r.message === 'string' && r.message) || msgs[rule] || vd.defaultMessage;
|
|
703
|
-
this.showError($input, msg);
|
|
704
|
-
erroredInputs.push($input);
|
|
705
|
-
return; // первую ошибку отобразили
|
|
706
|
-
}
|
|
707
|
-
// хотим прервать цепочку правил, даже если успех
|
|
708
|
-
if (isObj && raw.stopOthers) {
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
this.hideError($input);
|
|
713
|
-
});
|
|
714
|
-
const getRadioGroup = ($input) => {
|
|
715
|
-
if ($input.type !== 'radio' || !$input.name)
|
|
716
|
-
return [];
|
|
717
|
-
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
718
|
-
};
|
|
719
|
-
/* ---------- 1. inputs из schema (config + defaults) ---------- */
|
|
720
|
-
for (const [key, def] of Object.entries(schema)) {
|
|
721
|
-
const { selector, rules = [], messages = {} } = def;
|
|
722
|
-
const nodeList = selector
|
|
723
|
-
? $block.querySelectorAll(selector) // явный CSS-селектор
|
|
724
|
-
: $block.querySelectorAll(`[data-validate="${key}"]`); // fallback по name
|
|
725
|
-
for (const $input of Array.from(nodeList)) {
|
|
726
|
-
if (processed.has($input))
|
|
727
|
-
continue;
|
|
728
|
-
const radioGroup = getRadioGroup($input);
|
|
729
|
-
if (radioGroup.length > 0) {
|
|
730
|
-
radioGroup.forEach($radio => processed.add($radio));
|
|
731
|
-
yield validateInput($input, collectRules($input, rules), messages);
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
1215
|
+
else {
|
|
734
1216
|
processed.add($input);
|
|
735
|
-
|
|
1217
|
+
}
|
|
1218
|
+
const { rules, messages } = this.getFieldValidationConfig($input, schema);
|
|
1219
|
+
const fieldValid = yield this.runFieldValidation($input, rules, messages, $block);
|
|
1220
|
+
if (!fieldValid) {
|
|
1221
|
+
ok = false;
|
|
1222
|
+
erroredInputs.push($input);
|
|
736
1223
|
}
|
|
737
1224
|
}
|
|
738
|
-
|
|
739
|
-
const rest = Array.from($block.querySelectorAll('[data-custom-validate]')).filter($i => !processed.has($i));
|
|
740
|
-
for (const $input of rest) {
|
|
741
|
-
processed.add($input);
|
|
742
|
-
yield validateInput($input, collectRules($input, []));
|
|
743
|
-
}
|
|
744
|
-
/* ---------- scroll ---------- */
|
|
1225
|
+
const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
|
|
745
1226
|
if (!ok && this.config.scrollToFirstErroredInput) {
|
|
746
|
-
this.scrollToFirstErroredInput(
|
|
1227
|
+
this.scrollToFirstErroredInput(visibleErroredInputs);
|
|
1228
|
+
}
|
|
1229
|
+
if (!ok && this.config.focusFirstErroredInput) {
|
|
1230
|
+
this.focusFirstErroredInput(visibleErroredInputs);
|
|
747
1231
|
}
|
|
748
1232
|
return ok;
|
|
749
1233
|
});
|
|
@@ -949,6 +1433,7 @@ class Form {
|
|
|
949
1433
|
}
|
|
950
1434
|
/** Полностью снимает слушатели и чистит артефакты формы. */
|
|
951
1435
|
destroy() {
|
|
1436
|
+
var _a;
|
|
952
1437
|
// 1) Снять submit-слушатель
|
|
953
1438
|
if (this._onSubmitHandler) {
|
|
954
1439
|
this.$el.removeEventListener('submit', this._onSubmitHandler);
|
|
@@ -961,6 +1446,9 @@ class Form {
|
|
|
961
1446
|
});
|
|
962
1447
|
this._onSubmitClickHandler = undefined;
|
|
963
1448
|
}
|
|
1449
|
+
this.clearFieldValidationEvents();
|
|
1450
|
+
(_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
1451
|
+
this.mutationObserver = undefined;
|
|
964
1452
|
// 2) Удалить формовый error-блок под инпутами (если есть)
|
|
965
1453
|
const formErrorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
|
|
966
1454
|
if (formErrorWrapper) {
|
|
@@ -976,13 +1464,18 @@ class Form {
|
|
|
976
1464
|
});
|
|
977
1465
|
// 4) Сбросить ошибки у инпутов (если разметка позволяет)
|
|
978
1466
|
const inputs = this.$el.querySelectorAll('input, textarea, select');
|
|
979
|
-
inputs.forEach($i =>
|
|
1467
|
+
inputs.forEach($i => {
|
|
1468
|
+
this.hideError($i);
|
|
1469
|
+
this.clearFieldValidationState($i);
|
|
1470
|
+
});
|
|
980
1471
|
// 5) Убрать возможный флаг скролла
|
|
981
1472
|
document.documentElement.removeAttribute('data-now-scrolling');
|
|
982
1473
|
// 6) Очистить ссылки на элементы, чтобы помочь GC
|
|
983
1474
|
this.inputs = null;
|
|
984
1475
|
this.$submits = [];
|
|
1476
|
+
this.errors = [];
|
|
985
1477
|
this.waitResponse = false;
|
|
1478
|
+
FORM_INSTANCES.delete(this.$el);
|
|
986
1479
|
}
|
|
987
1480
|
/**
|
|
988
1481
|
* Проверяет входные данные на их корректность.
|
|
@@ -1099,7 +1592,10 @@ class Form {
|
|
|
1099
1592
|
const $field = this.findFieldByName(error.name);
|
|
1100
1593
|
if ($field) {
|
|
1101
1594
|
fieldsList.push($field);
|
|
1595
|
+
this.setValidationError($field, 'server', error['error-msg'], 'server');
|
|
1102
1596
|
this.showError($field, error['error-msg']);
|
|
1597
|
+
$field.removeAttribute('aria-busy');
|
|
1598
|
+
this.setFieldValidationState($field, 'invalid');
|
|
1103
1599
|
}
|
|
1104
1600
|
else {
|
|
1105
1601
|
console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
|
|
@@ -1107,6 +1603,8 @@ class Form {
|
|
|
1107
1603
|
});
|
|
1108
1604
|
if (this.config.scrollToFirstErroredInput === true)
|
|
1109
1605
|
this.scrollToFirstErroredInput(fieldsList);
|
|
1606
|
+
if (this.config.focusFirstErroredInput === true)
|
|
1607
|
+
this.focusFirstErroredInput(fieldsList);
|
|
1110
1608
|
}
|
|
1111
1609
|
}
|
|
1112
1610
|
sendData() {
|
|
@@ -1167,12 +1665,8 @@ class Form {
|
|
|
1167
1665
|
}
|
|
1168
1666
|
submit() {
|
|
1169
1667
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1170
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1668
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
1171
1669
|
if (!this.waitResponse) {
|
|
1172
|
-
if (this.config.showLoaderButton === true)
|
|
1173
|
-
this.showLoader();
|
|
1174
|
-
(_b = (_a = this.config).onSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
1175
|
-
this.waitResponse = true;
|
|
1176
1670
|
let loadingFinished = false;
|
|
1177
1671
|
const finishLoading = () => {
|
|
1178
1672
|
if (loadingFinished)
|
|
@@ -1183,27 +1677,44 @@ class Form {
|
|
|
1183
1677
|
loadingFinished = true;
|
|
1184
1678
|
};
|
|
1185
1679
|
try {
|
|
1680
|
+
const beforeSubmitResult = yield ((_b = (_a = this.config).onBeforeSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this));
|
|
1681
|
+
if (beforeSubmitResult === false) {
|
|
1682
|
+
return {
|
|
1683
|
+
success: false,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
if (this.config.showLoaderButton === true)
|
|
1687
|
+
this.showLoader();
|
|
1688
|
+
(_d = (_c = this.config).onSubmit) === null || _d === void 0 ? void 0 : _d.call(_c, this);
|
|
1689
|
+
this.waitResponse = true;
|
|
1186
1690
|
const response = yield this.sendData();
|
|
1187
1691
|
const responseBody = yield this.parseResponseBody(response);
|
|
1188
1692
|
finishLoading();
|
|
1189
|
-
(
|
|
1693
|
+
(_f = (_e = this.config).onResponse) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
|
|
1190
1694
|
if (response.ok && responseBody.success === true) {
|
|
1191
|
-
(
|
|
1695
|
+
(_h = (_g = this.config).onResponseSuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
|
|
1192
1696
|
}
|
|
1193
1697
|
else {
|
|
1194
|
-
(
|
|
1698
|
+
(_k = (_j = this.config).onResponseUnsuccess) === null || _k === void 0 ? void 0 : _k.call(_j, responseBody, this);
|
|
1195
1699
|
this.handleUnsuccessfulResponse(responseBody);
|
|
1196
1700
|
}
|
|
1197
1701
|
parseCommonResponseProperties(responseBody);
|
|
1702
|
+
return {
|
|
1703
|
+
success: response.ok && responseBody.success === true,
|
|
1704
|
+
response,
|
|
1705
|
+
responseBody,
|
|
1706
|
+
};
|
|
1198
1707
|
}
|
|
1199
1708
|
catch (error) {
|
|
1200
1709
|
finishLoading();
|
|
1710
|
+
(_m = (_l = this.config).onSubmitError) === null || _m === void 0 ? void 0 : _m.call(_l, error, this);
|
|
1201
1711
|
throw error;
|
|
1202
1712
|
}
|
|
1203
1713
|
}
|
|
1714
|
+
return undefined;
|
|
1204
1715
|
});
|
|
1205
1716
|
}
|
|
1206
1717
|
}
|
|
1207
1718
|
|
|
1208
|
-
export { blockScrollBody, closest, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1719
|
+
export { blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1209
1720
|
//# sourceMappingURL=index.js.map
|