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/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.$submits.forEach($submit => {
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.inputs = this.$el.querySelectorAll(this.config.inputSelector);
680
+ this.refreshControls();
495
681
  this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
496
- var _a, _b, _c, _d;
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.submit();
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
- this.$el.addEventListener('submit', this._onSubmitHandler);
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
- * Порядок для КАЖДОГО `<input>`:
1191
+ * Порядок для каждого поля:
632
1192
  *
633
1193
  * 1. `required` — если атрибут `required`
634
1194
  * 2. правила из `validationSchema`
635
- * 3. правила из `data-custom-validate`
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
- /* ---------- финальная схема (defaults + config) ---------- */
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
- /* ---------- helpers ---------- */
649
- /** Собирает и тут же фильтрует unknown-rules */
650
- const collectRules = ($input, fromSchema = []) => {
651
- var _a;
652
- const list = [];
653
- if ($input.hasAttribute('required'))
654
- list.push('required'); // 1
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
- for (const r of rules) {
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
- yield validateInput($input, collectRules($input, rules), messages);
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
- /* ---------- 2. inputs только с data-custom ---------- */
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(erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll')));
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 => this.hideError($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
- (_d = (_c = this.config).onResponse) === null || _d === void 0 ? void 0 : _d.call(_c, responseBody, this);
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
- (_f = (_e = this.config).onResponseSuccess) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
1695
+ (_h = (_g = this.config).onResponseSuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
1192
1696
  }
1193
1697
  else {
1194
- (_h = (_g = this.config).onResponseUnsuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
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