@brandup/ui-textbox 1.0.38 → 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
@@ -37,10 +37,10 @@ textbox.on(CHANGE_EVENT, (data: ChangeEventData) => {
37
37
  | Тип | Поведение |
38
38
  | --- | --- |
39
39
  | `text` | Обычный текст |
40
- | `email` | Фильтрует недопустимые символы, ограничивает длину до 256 символов (RFC 5321) |
40
+ | `email` | Оставляет буквы, цифры и `. - _ + @`; ограничивает длину до 256 символов (RFC 5321) — свой `maxlength` применяется, если он строже |
41
41
  | `url` | URL-адрес |
42
42
  | `tel` | Телефон |
43
- | `number` | Только цифры, вставка фильтруется |
43
+ | `number` | Только цифры (целые неотрицательные), вставка фильтруется |
44
44
 
45
45
  Для многострочного ввода передайте `<textarea>`.
46
46
 
package/package.json CHANGED
@@ -24,15 +24,15 @@
24
24
  "email": "it@brandup.online"
25
25
  },
26
26
  "license": "Apache-2.0",
27
- "version": "1.0.38",
27
+ "version": "1.0.39",
28
28
  "main": "source/index.ts",
29
29
  "types": "source/index.ts",
30
30
  "dependencies": {
31
31
  "@brandup/ui": "^2.0.7",
32
32
  "@brandup/ui-helpers": "^2.0.7",
33
- "@brandup/ui-input": "^1.0.38",
34
- "@brandup/ui-kit": "^1.0.38",
35
- "@brandup/ui-richeditor": "^1.0.38"
33
+ "@brandup/ui-input": "^1.0.39",
34
+ "@brandup/ui-kit": "^1.0.39",
35
+ "@brandup/ui-richeditor": "^1.0.39"
36
36
  },
37
37
  "files": [
38
38
  "source",
package/source/textbox.ts CHANGED
@@ -24,6 +24,11 @@ export const MAX_EMAIL_LENGTH = 256; // https://www.rfc-editor.org/rfc/rfc5321#s
24
24
 
25
25
  export type TextBoxType = "text" | "email" | "url" | "tel" | "number";
26
26
 
27
+ // Атрибуты поля, которые компонент подменяет под себя (фокус уходит на редактируемый элемент,
28
+ // ограничения типа нормализуются). При destroy возвращаем их ровно в исходное состояние —
29
+ // иначе после снятия компонента поле остаётся с чужими ограничениями.
30
+ const ATTRS_TO_RESTORE = ["tabindex", "maxlength", "step"];
31
+
27
32
  type TextBoxEvents = {
28
33
  [CHANGE_EVENT]: (data: ChangeEventData) => void;
29
34
  };
@@ -49,6 +54,11 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
49
54
  readonly formatMarkers: FormatMarkers;
50
55
 
51
56
  constructor(valueElem: HTMLInputElement | HTMLTextAreaElement) {
57
+ // исходные атрибуты запоминаем до любых правок — компонент их подменяет, а destroy возвращает
58
+ const originalAttrs = ATTRS_TO_RESTORE.map(
59
+ (name) => [name, valueElem.getAttribute(name)] as [string, string | null]
60
+ );
61
+
52
62
  // определяем тип ввода и нормализуем валидационные атрибуты до super()
53
63
  let type: TextBoxType = "text";
54
64
  if (valueElem instanceof HTMLInputElement) {
@@ -58,7 +68,9 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
58
68
  break;
59
69
  case "email":
60
70
  type = "email";
61
- if (!valueElem.maxLength || valueElem.maxLength > MAX_EMAIL_LENGTH)
71
+ // у поля без атрибута maxlength свойство равно -1, а не 0, поэтому проверять
72
+ // нужно именно «не задан положительный предел», иначе ограничение RFC не применялось бы
73
+ if (valueElem.maxLength <= 0 || valueElem.maxLength > MAX_EMAIL_LENGTH)
62
74
  valueElem.maxLength = MAX_EMAIL_LENGTH;
63
75
  break;
64
76
  case "url":
@@ -76,8 +88,6 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
76
88
  }
77
89
  }
78
90
 
79
- valueElem.classList.add(INPUT_CLASS);
80
-
81
91
  const maxlength = valueElem.maxLength;
82
92
  const symbolCounter = valueElem.hasAttribute("data-symbolcounter");
83
93
  const autoFocus = valueElem.hasAttribute("data-autofocus");
@@ -110,13 +120,13 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
110
120
  const actionsElem = DOM.tag("div", { class: "actions" });
111
121
  const symbolsCountElem = DOM.tag("div", { class: "symbols" });
112
122
 
113
- const container = DOM.tag("div", { class: [ROOT_CLASS].concat(Array.from(valueElem.classList)) }, [
123
+ const container = DOM.tag("div", { class: ROOT_CLASS }, [
114
124
  DOM.tag("div", { class: "decorator" }),
115
125
  DOM.tag("div", { class: "editor" }, [inputElem, symbolsCountElem]),
116
126
  actionsElem,
117
127
  ]);
118
128
 
119
- container.classList.remove(INPUT_CLASS);
129
+ TextBox.prepareValueElem(valueElem, container, INPUT_CLASS);
120
130
 
121
131
  inputElem.tabIndex = disabled ? -1 : valueElem.tabIndex;
122
132
  valueElem.tabIndex = -1;
@@ -144,7 +154,8 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
144
154
  valueElem.insertAdjacentElement("afterend", container);
145
155
  container.insertAdjacentElement("afterbegin", valueElem);
146
156
 
147
- super("BrandUp.TextBox", container, valueElem);
157
+ // класс и подменённые атрибуты вернёт базовый класс при destroy
158
+ super("BrandUp.TextBox", container, valueElem, { class: INPUT_CLASS, attrs: originalAttrs });
148
159
 
149
160
  this.type = type;
150
161
  this.maxlength = maxlength;
@@ -181,10 +192,12 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
181
192
  onEnter: () => this.__submitForm(),
182
193
  };
183
194
 
184
- // допустим ли вводимый символ по типу
195
+ // допустим ли вводимый символ по типу.
196
+ // `+` в адресе — обычное дело (подадреса вида user+tag@example.com), без него такие
197
+ // адреса нельзя было бы набрать; остальные разрешённые в local-part символы редки
185
198
  const typeAllowsChar = (char: string) => {
186
199
  if (type === "number") return /\d/.test(char);
187
- if (type === "email") return /[a-zA-Z\d.\-_@]/.test(char);
200
+ if (type === "email") return /[a-zA-Z\d.\-_+@]/.test(char);
188
201
  return true;
189
202
  };
190
203
 
@@ -213,7 +226,10 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
213
226
  if (maxlength > 0) {
214
227
  const selectionLength = window.getSelection()?.toString().length ?? 0;
215
228
  const left = maxlength - this.__editor.getLength() + selectionLength;
216
- if (pasted.length > left) pasted = pasted.substring(0, Math.max(0, left));
229
+ // не влезает ни одного символа это отказ, а не пустая вставка: иначе
230
+ // вставка молча не делала бы ничего, тогда как ввод символа на пределе мигает ошибкой
231
+ if (left <= 0) return null;
232
+ if (pasted.length > left) pasted = pasted.substring(0, left);
217
233
  }
218
234
 
219
235
  return pasted;
@@ -252,6 +268,10 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
252
268
  this.__onChange();
253
269
  });
254
270
 
271
+ // Счётчик — на каждый ввод, а не по change: длина считается по textContent и стоит копейки,
272
+ // тогда как значение поля синхронизируется реже (см. RichEditor.flushChange).
273
+ editable.addEventListener("input", () => this.__refreshSymbolsCount(), { signal });
274
+
255
275
  // состояние фокуса контрола (рамка/заливка) — на корневом элементе
256
276
  editable.addEventListener("focus", () => !this.disabled && this.element.classList.add("focused"), { signal });
257
277
  editable.addEventListener("blur", () => !this.disabled && this.element.classList.remove("focused"), { signal });
@@ -280,7 +300,9 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
280
300
  }
281
301
 
282
302
  this.registerCommand("copy-text", async (context) => {
283
- if (!window.navigator.clipboard || this.disabled) return;
303
+ // повторный клик, пока показана галочка, запомнил бы её как исходную иконку —
304
+ // после возврата кнопка так и осталась бы с галочкой
305
+ if (!window.navigator.clipboard || this.disabled || context.target.classList.contains("success")) return;
284
306
 
285
307
  await window.navigator.clipboard.writeText(this.__valueElem.value);
286
308
 
@@ -288,14 +310,26 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
288
310
  context.target.innerHTML = doneIcon;
289
311
  context.target.classList.add("success");
290
312
 
291
- const abort = new AbortController();
292
- await FuncHelper.delay(2000, abort.signal);
313
+ // возврат иконки отменяем вместе с компонентом: иначе таймер переживает destroy
314
+ // и дописывает в уже отсоединённую кнопку
315
+ try {
316
+ await FuncHelper.delay(2000, signal);
317
+ } catch {
318
+ return;
319
+ }
293
320
 
294
321
  context.target.innerHTML = prevHtml;
295
322
  context.target.classList.remove("success");
296
323
  });
297
324
  }
298
325
 
326
+ // Редактор откладывает событие изменения при печати, поэтому копия значения в поле формы
327
+ // отстаёт. Базовый класс зовёт этот хук перед каждым чтением значения снаружи —
328
+ // валидация, отправка формы, сбор FormData.
329
+ protected override __syncValue(): void {
330
+ this.__editor.flushChange();
331
+ }
332
+
299
333
  private __toIncorrect() {
300
334
  this.element.classList.add("incorrect");
301
335
  window.setTimeout(() => this.element.classList.remove("incorrect"), 200);
@@ -342,6 +376,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
342
376
  }
343
377
 
344
378
  getValue(): string {
379
+ this.__syncValue(); // значение читают снаружи — отложенное изменение сюда обязано попасть
345
380
  return this.__valueElem.value.trim();
346
381
  }
347
382
 
@@ -351,7 +386,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
351
386
  }
352
387
 
353
388
  override validate(): boolean {
354
- let isValid = super.validate();
389
+ let isValid = super.validate(); // super синхронизирует значение сам, через __syncValue
355
390
  if (isValid) {
356
391
  const value = this.getValue();
357
392
 
@@ -372,11 +407,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
372
407
  this.__listenerAbort.abort();
373
408
  this.__editor.destroy();
374
409
 
375
- this.__valueElem.tabIndex = this.__inputElem.tabIndex;
376
- this.element.insertAdjacentElement("afterend", this.__valueElem);
377
- this.element.remove();
378
-
379
- super.destroy();
410
+ super.destroy(); // снимет слушатели формы и вернёт поле-носитель в исходный вид
380
411
  }
381
412
  }
382
413