@brandup/ui-input 1.0.36 → 1.0.39

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Установка
8
8
 
9
- ```
9
+ ```bash
10
10
  npm i @brandup/ui-input
11
11
  ```
12
12
 
@@ -17,7 +17,7 @@ npm i @brandup/ui-input
17
17
  ### Свойства
18
18
 
19
19
  | Свойство | Тип | Описание |
20
- |---|---|---|
20
+ | --- | --- | --- |
21
21
  | `form` | `HTMLFormElement \| null` | Форма, к которой привязан элемент |
22
22
  | `disabled` | `boolean` | Элемент отключён |
23
23
  | `required` | `boolean` | Элемент обязателен для заполнения |
@@ -26,7 +26,7 @@ npm i @brandup/ui-input
26
26
  ### Методы
27
27
 
28
28
  | Метод | Описание |
29
- |---|---|
29
+ | --- | --- |
30
30
  | `validate(): boolean` | Проверяет значение через нативный `checkValidity()`. Возвращает `true`, если значение валидно |
31
31
  | `focus(): void` | Устанавливает фокус и прокручивает элемент в видимую область |
32
32
  | `destroy(): void` | Снимает все обработчики и освобождает ресурсы |
package/package.json CHANGED
@@ -24,12 +24,12 @@
24
24
  "email": "it@brandup.online"
25
25
  },
26
26
  "license": "Apache-2.0",
27
- "version": "1.0.36",
27
+ "version": "1.0.39",
28
28
  "main": "source/index.ts",
29
29
  "types": "source/index.ts",
30
30
  "dependencies": {
31
- "@brandup/ui": "^2.0.5",
32
- "@brandup/ui-kit": "^1.0.36"
31
+ "@brandup/ui": "^2.0.7",
32
+ "@brandup/ui-kit": "^1.0.39"
33
33
  },
34
34
  "files": [
35
35
  "source",
package/source/input.ts CHANGED
@@ -12,13 +12,16 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
12
12
  {
13
13
  protected __valueElem: FormInput<T>;
14
14
  protected __submitEvent?: (e: SubmitEvent) => void;
15
+ private __submitCaptureEvent?: (e: Event) => void;
15
16
  private __invalidEvent?: (e: Event) => void;
17
+ private __overrides?: ValueElemOverrides;
16
18
  private __isValidating?: boolean; // true, когда выполняется checkValidity в validate.
17
19
 
18
- constructor(typeName: string, elem: HTMLElement, valueElem: FormInput<T>) {
20
+ constructor(typeName: string, elem: HTMLElement, valueElem: FormInput<T>, overrides?: ValueElemOverrides) {
19
21
  super(typeName, elem);
20
22
 
21
23
  this.__valueElem = valueElem;
24
+ this.__overrides = overrides;
22
25
 
23
26
  // то, что раньше делал _onRenderElement-override; теперь применяем после super, чтобы видеть valueElem
24
27
  elem.classList.add(INPUT_CSS_CLASS);
@@ -29,6 +32,20 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
29
32
  this.__initForm();
30
33
  }
31
34
 
35
+ /**
36
+ * Готовит поле-носитель к обёртке контейнером контрола: класс-скрыватель переезжает на поле,
37
+ * а собственные классы поля — на контейнер, чтобы оформление из разметки применялось к тому,
38
+ * что видно. Статический, потому что вызывается до `super(...)`.
39
+ *
40
+ * Саму вставку в DOM делает контрол: поле встаёт в контейнер первым или последним
41
+ * в зависимости от вёрстки, и на этот порядок завязаны соседские селекторы в стилях.
42
+ */
43
+ protected static prepareValueElem(valueElem: HTMLElement, container: HTMLElement, inputClass: string) {
44
+ container.classList.add(...Array.from(valueElem.classList));
45
+ container.classList.remove(inputClass);
46
+ valueElem.classList.add(inputClass);
47
+ }
48
+
32
49
  get form(): HTMLFormElement | null {
33
50
  return this.__valueElem.form;
34
51
  }
@@ -42,6 +59,14 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
42
59
  return this.__valueElem.hasAttribute("readonly") || this.__valueElem.hasAttribute("data-readonly");
43
60
  }
44
61
 
62
+ /**
63
+ * Довести значение до `__valueElem`, если контрол держит его отдельно и обновляет не мгновенно
64
+ * (например, редактор с отложенной сериализацией). Вызывается перед каждым чтением значения
65
+ * снаружи: валидация, отправка формы, сбор `FormData`. По умолчанию ничего не делает —
66
+ * у контролов, пишущих в поле сразу, синхронизировать нечего.
67
+ */
68
+ protected __syncValue(): void {}
69
+
45
70
  private __initForm() {
46
71
  this.__invalidEvent = (e: Event) => {
47
72
  e.preventDefault();
@@ -51,6 +76,11 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
51
76
  this.__valueElem.addEventListener("invalid", this.__invalidEvent);
52
77
 
53
78
  this.__submitEvent = (e: SubmitEvent) => {
79
+ // Значение синхронизируем до любых проверок и независимо от них: при отключённой
80
+ // валидации обработчик выходит ниже, а форма всё равно отправится — уже с этим значением.
81
+ // Событие submit приходит до действия по умолчанию, поэтому успеваем.
82
+ this.__syncValue();
83
+
54
84
  if ((e.submitter as HTMLButtonElement | null)?.formNoValidate || (<HTMLFormElement>e.target).noValidate)
55
85
  return; // Не делаем валидацию, если она отключена в форме или в инициаторе события submit
56
86
 
@@ -67,7 +97,23 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
67
97
  }
68
98
  };
69
99
 
70
- if (this.form) this.form.addEventListener("submit", this.__submitEvent);
100
+ if (!this.form) return;
101
+
102
+ this.form.addEventListener("submit", this.__submitEvent);
103
+
104
+ // Тот же сброс, но гарантированно раньше любого обработчика самой формы: в фазе перехвата
105
+ // на документе событие приходит до цели, в каком бы порядке ни вешали слушатели. Иначе
106
+ // обработчик submit, повешенный приложением раньше контрола, успел бы собрать FormData
107
+ // со старым значением. Для формы вне документа перехвата не будет — там работает
108
+ // __submitEvent выше.
109
+ //
110
+ // Документ берём у самого поля, а не глобальный: destroy может случиться в любой момент
111
+ // (UIElement разрушает контрол сам, заметив удаление элемента через MutationObserver),
112
+ // и тогда обращаться к глобальному окружению уже небезопасно.
113
+ this.__submitCaptureEvent = (e: Event) => {
114
+ if (e.target === this.form) this.__syncValue();
115
+ };
116
+ this.__valueElem.ownerDocument.addEventListener("submit", this.__submitCaptureEvent, true);
71
117
  }
72
118
 
73
119
  protected __submitForm() {
@@ -79,6 +125,8 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
79
125
  validate(): boolean {
80
126
  if (this.__isValidating) return true;
81
127
 
128
+ this.__syncValue(); // checkValidity читает поле напрямую
129
+
82
130
  this.__isValidating = true;
83
131
  const result = this.__valueElem.checkValidity();
84
132
  this.__isValidating = false;
@@ -91,15 +139,53 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
91
139
  this.element.scrollIntoView({ block: "center", inline: "center" });
92
140
  }
93
141
 
142
+ /**
143
+ * Возвращает поле-носитель в исходное состояние: снимает класс, которым контрол увёл его
144
+ * с экрана, возвращает подменённые атрибуты и вынимает поле из контейнера, а сам контейнер
145
+ * удаляет. Одинаково для всех контролов: поле остаётся в форме, а UI над ним — временный.
146
+ */
147
+ private __restoreValueElem() {
148
+ if (this.__overrides?.class) this.__valueElem.classList.remove(this.__overrides.class);
149
+
150
+ for (const [name, value] of this.__overrides?.attrs ?? []) {
151
+ if (value === null) this.__valueElem.removeAttribute(name);
152
+ else this.__valueElem.setAttribute(name, value);
153
+ }
154
+
155
+ // контрол мог и не оборачивать поле (элемент контрола — само поле)
156
+ if (this.element === this.__valueElem || !this.element.parentElement) return;
157
+
158
+ this.element.insertAdjacentElement("afterend", this.__valueElem);
159
+ this.element.remove();
160
+ }
161
+
94
162
  override destroy() {
95
163
  if (this.form && this.__submitEvent) this.form.removeEventListener("submit", this.__submitEvent);
96
164
 
165
+ if (this.__submitCaptureEvent)
166
+ this.__valueElem.ownerDocument.removeEventListener("submit", this.__submitCaptureEvent, true);
167
+
97
168
  if (this.__invalidEvent) this.__valueElem.removeEventListener("invalid", this.__invalidEvent);
98
169
 
170
+ this.__restoreValueElem();
171
+
99
172
  super.destroy();
100
173
  }
101
174
  }
102
175
 
176
+ /**
177
+ * Что контрол навязал полю-носителю и что нужно вернуть при `destroy`.
178
+ *
179
+ * Снимок делается до правок, то есть до `super(...)`, поэтому передаётся снаружи, а не
180
+ * собирается базовым классом.
181
+ */
182
+ export interface ValueElemOverrides {
183
+ /** Класс, добавленный полю контролом (обычно уводит его с экрана). */
184
+ class?: string;
185
+ /** Подменённые атрибуты: имя и исходное значение (`null` — атрибута не было). */
186
+ attrs?: [name: string, value: string | null][];
187
+ }
188
+
103
189
  export interface IInputControl {
104
190
  get form(): HTMLFormElement | null;
105
191
  get disabled(): boolean;