@brandup/ui-dropdown 1.0.52 → 1.0.54
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 +12 -1
- package/package.json +3 -3
- package/source/dropdown.ts +95 -43
package/README.md
CHANGED
|
@@ -42,15 +42,26 @@ dropdown.on(CHANGE_EVENT, (data: ChangeEventData) => {
|
|
|
42
42
|
| `data-search-empty` | `"Not found"` | Текст при отсутствии результатов поиска |
|
|
43
43
|
| `data-cancel` | `"Cancel"` | Текст кнопки закрытия (адаптивный режим) |
|
|
44
44
|
| `data-search-on` | `15` | Порог отображения поиска: число (минимальное кол-во опций), `"true"` — всегда, `"false"` — никогда |
|
|
45
|
+
| `data-autofocus` | — | Автофокус при инициализации; то же делает нативный `autofocus`. Условия отмены — см. [@brandup/ui-input](../brandup-ui-input/README.md#автофокус) |
|
|
46
|
+
| `data-readonly` | — | Значение показывается, но сменить его нельзя: список не открывается. У `<select>` нативного `readonly` нет, поэтому режим объявляется этим атрибутом |
|
|
45
47
|
|
|
46
48
|
## API
|
|
47
49
|
|
|
50
|
+
### Собственные методы
|
|
51
|
+
|
|
52
|
+
| Метод | Описание |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `getValue(): string \| null` | Значение выбранного `<option>`; `null`, если не выбрано ничего |
|
|
55
|
+
| `getSelectedIndex(): number` | Индекс выбранного `<option>`; `-1`, если не выбрано ничего |
|
|
56
|
+
| `getSelectedTitle(): string \| null` | Текст выбранного пункта списка |
|
|
57
|
+
| `setValue(value: string \| null): void` | Выбирает пункт с таким значением и показывает его. Значения без своего пункта в списке (в том числе пустое) сбрасывают выбор в плейсхолдер. `dropdown-change` поднимается только при действительной смене показанного пункта |
|
|
58
|
+
|
|
48
59
|
### Методы (унаследованы от InputControl)
|
|
49
60
|
|
|
50
61
|
| Метод | Описание |
|
|
51
62
|
| --- | --- |
|
|
52
63
|
| `validate(): boolean` | Проверяет значение через нативный `checkValidity()` |
|
|
53
|
-
| `focus(): void` |
|
|
64
|
+
| `focus(): void` | Ведёт фокус в кнопку показа списка и прокручивает контрол в видимую область |
|
|
54
65
|
| `destroy(): void` | Восстанавливает исходный `<select>` и освобождает ресурсы |
|
|
55
66
|
|
|
56
67
|
### Свойства
|
package/package.json
CHANGED
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"email": "it@brandup.online"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|
|
27
|
-
"version": "1.0.
|
|
27
|
+
"version": "1.0.54",
|
|
28
28
|
"main": "source/index.ts",
|
|
29
29
|
"types": "source/index.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@brandup/ui": "^2.0.9",
|
|
32
32
|
"@brandup/ui-helpers": "^2.0.9",
|
|
33
|
-
"@brandup/ui-input": "^1.0.
|
|
34
|
-
"@brandup/ui-kit": "^1.0.
|
|
33
|
+
"@brandup/ui-input": "^1.0.54",
|
|
34
|
+
"@brandup/ui-kit": "^1.0.54"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"source",
|
package/source/dropdown.ts
CHANGED
|
@@ -145,12 +145,21 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
145
145
|
|
|
146
146
|
this.__renderItems();
|
|
147
147
|
this.__initLogic();
|
|
148
|
+
|
|
149
|
+
this.__applyAutoFocus(); // автофокус — вместе с прокруткой к контролу; условия у базового класса
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Поле-носитель уведено с экрана и фокус не принимает — ведём его в кнопку показа списка:
|
|
154
|
+
* с неё же начинается работа с клавиатуры (пробел и Enter открывают список).
|
|
155
|
+
*/
|
|
156
|
+
protected override __focusValue(): void {
|
|
157
|
+
this.__focusView();
|
|
148
158
|
}
|
|
149
159
|
|
|
150
|
-
// рендер
|
|
160
|
+
// рендер элементов; текущий выбор отмечает __renderSelection, когда список уже в DOM
|
|
151
161
|
private __renderItems() {
|
|
152
162
|
const optionsCount = this.__valueElem.options.length;
|
|
153
|
-
const selectedIndex = this.__valueElem.selectedIndex;
|
|
154
163
|
|
|
155
164
|
if (!optionsCount) this.__textElem.innerText = this.placeholder;
|
|
156
165
|
|
|
@@ -200,22 +209,16 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
200
209
|
|
|
201
210
|
itemTranscripts.set(itemElem, transcriptText(itemText));
|
|
202
211
|
|
|
203
|
-
const isSelected = selectedIndex === i;
|
|
204
|
-
if (isSelected) itemElem.classList.add("hasvalue");
|
|
205
|
-
|
|
206
212
|
popupItemsFragment.append(itemElem);
|
|
207
213
|
|
|
208
|
-
if (isSelected) {
|
|
209
|
-
this.__container.classList.add("hasvalue");
|
|
210
|
-
this.__textElem.innerText = itemText;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
214
|
elemCount++;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
if (this.__hasEmptyValue && !elemCount) this.element.classList.add("empty");
|
|
217
218
|
|
|
218
219
|
this.__listElem.append(popupItemsFragment);
|
|
220
|
+
|
|
221
|
+
this.__renderSelection(); // список уже в DOM — отметку и текст ставит общий с setValue путь
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
private __initLogic() {
|
|
@@ -223,39 +226,14 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
223
226
|
this.registerCommand("close-popup", () => this.__closePopup());
|
|
224
227
|
|
|
225
228
|
this.registerCommand("select", (context) => {
|
|
226
|
-
const newIndex = context.target.dataset.index;
|
|
227
|
-
|
|
228
|
-
this.element.classList.remove("invalid");
|
|
229
229
|
this.__clearSearch();
|
|
230
230
|
this.__closePopup();
|
|
231
|
+
this.__focusView();
|
|
231
232
|
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
233
|
+
const index = Number(context.target.dataset.index);
|
|
234
|
+
if (!Number.isInteger(index)) return; // пункт без своего индекса выбрать нечем
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (currentSelect && currentSelect.own.closest(`.${ROOT_CLASS}`))
|
|
238
|
-
this.__textElem.innerText = this.placeholder ?? "";
|
|
239
|
-
|
|
240
|
-
// делаем новый выбор
|
|
241
|
-
this.__valueElem.value = context.target.dataset.value || "";
|
|
242
|
-
|
|
243
|
-
const newSelected = this.__getElemsByIndex(Number(newIndex));
|
|
244
|
-
|
|
245
|
-
if (newSelected) {
|
|
246
|
-
newSelected.own.classList.add("hasvalue");
|
|
247
|
-
|
|
248
|
-
const newDropDown = newSelected.own.closest(`.${ROOT_CLASS}`);
|
|
249
|
-
|
|
250
|
-
if (newDropDown) {
|
|
251
|
-
this.__textElem.innerText = newSelected.own.innerText.trim();
|
|
252
|
-
newDropDown.classList.add("hasvalue");
|
|
253
|
-
this.__closePopup();
|
|
254
|
-
this.__focusPopup();
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
this.__onChange();
|
|
236
|
+
this.__applySelection(index);
|
|
259
237
|
});
|
|
260
238
|
|
|
261
239
|
this.__searchInput.addEventListener("input", () => {
|
|
@@ -292,7 +270,7 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
292
270
|
case "Escape": {
|
|
293
271
|
e.preventDefault();
|
|
294
272
|
this.__closePopup();
|
|
295
|
-
this.
|
|
273
|
+
this.__focusView();
|
|
296
274
|
break;
|
|
297
275
|
}
|
|
298
276
|
case "Tab": {
|
|
@@ -319,7 +297,8 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
319
297
|
});
|
|
320
298
|
}
|
|
321
299
|
|
|
322
|
-
|
|
300
|
+
/** Фокус в кнопку показа списка — она первым элементом контейнера, поле-носитель последним. */
|
|
301
|
+
private __focusView() {
|
|
323
302
|
(<HTMLElement>this.__container.firstElementChild).focus();
|
|
324
303
|
}
|
|
325
304
|
|
|
@@ -332,9 +311,23 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
332
311
|
});
|
|
333
312
|
}
|
|
334
313
|
|
|
335
|
-
|
|
336
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Выключенный контрол не работает целиком, а только-для-чтения — показывает значение, но
|
|
316
|
+
* менять его не даёт: гейт закрывает открытие списка и выбор пункта. Состояние читаем
|
|
317
|
+
* с поля-носителя, а не по классу-отражению: атрибут могли переключить после инициализации.
|
|
318
|
+
*
|
|
319
|
+
* Закрытие списка не запрещаем никогда: гейт в `@brandup/ui` общий на все команды элемента,
|
|
320
|
+
* а поле могли выключить уже с открытым списком (например, форма гасит поля на время отправки) —
|
|
321
|
+
* тогда запрет запер бы открытый список, и на узком экране, где это лист во весь экран,
|
|
322
|
+
* выхода с клавиатуры не осталось бы вовсе.
|
|
323
|
+
*/
|
|
324
|
+
protected override _onCanExecCommand(name: string): boolean {
|
|
325
|
+
if (name.toLowerCase() === "close-popup") return true;
|
|
326
|
+
|
|
327
|
+
return !this.disabled && !this.readonly;
|
|
328
|
+
}
|
|
337
329
|
|
|
330
|
+
private __togglePopup() {
|
|
338
331
|
if (this.element.classList.contains("expanded")) {
|
|
339
332
|
// уже открыт — закрываем чисто, чтобы и body-класс, и mouseup-листенер ушли
|
|
340
333
|
this.__closePopup();
|
|
@@ -474,10 +467,69 @@ class DropDown extends InputControl<HTMLSelectElement, DropDownEvents> {
|
|
|
474
467
|
return this.__getElems(".hasvalue[data-index]");
|
|
475
468
|
}
|
|
476
469
|
|
|
470
|
+
/** Индекс пункта, отмеченного сейчас в списке; -1 — контрол показывает placeholder. */
|
|
471
|
+
private __shownIndex(): number {
|
|
472
|
+
const index = this.__getSelectedElem()?.own.dataset.index;
|
|
473
|
+
return index === undefined ? -1 : Number(index);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Переносит выбор в поле-носитель и показывает его; событие изменения поднимается только
|
|
478
|
+
* при действительной смене показанного пункта.
|
|
479
|
+
*
|
|
480
|
+
* Работаем индексом, а не значением: значение в списке может повторяться — пустой пункт-подсказка
|
|
481
|
+
* и свой вариант вроде «Не указано» оба с пустым value, — а присваивание `value` выбрало бы ПЕРВЫЙ
|
|
482
|
+
* совпавший option, то есть не тот пункт, что нажали.
|
|
483
|
+
*
|
|
484
|
+
* С показанным пунктом сравниваем, а не с полем: значение могли записать в поле напрямую
|
|
485
|
+
* (так делает восстановление черновика формы), и сравнение с полем сделало бы такой вызов пустым.
|
|
486
|
+
*/
|
|
487
|
+
private __applySelection(index: number) {
|
|
488
|
+
// Пункта в списке может и не быть: пустой пункт-подсказка своего <li> не получает, как и
|
|
489
|
+
// дубликат уже добавленного значения. На экране это то же самое, что «не выбрано ничего»,
|
|
490
|
+
// — приводим к одному виду, иначе повторная установка выглядела бы сменой выбора.
|
|
491
|
+
const target = this.__getElemsByIndex(index) ? index : -1;
|
|
492
|
+
if (target === this.__shownIndex()) return; // показанный выбор остался таким же
|
|
493
|
+
|
|
494
|
+
this.__valueElem.selectedIndex = index;
|
|
495
|
+
|
|
496
|
+
this.element.classList.remove("invalid");
|
|
497
|
+
this.__renderSelection();
|
|
498
|
+
this.__onChange();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Отражает текущее значение поля-носителя в контроле: отметка в списке и текст на кнопке. */
|
|
502
|
+
private __renderSelection() {
|
|
503
|
+
DOM.removeClass(this.element, ".hasvalue", "hasvalue"); // removeClass обходит потомков — класс контейнера снимаем отдельно
|
|
504
|
+
this.__container.classList.remove("hasvalue");
|
|
505
|
+
this.__textElem.innerText = this.placeholder;
|
|
506
|
+
|
|
507
|
+
const selected = this.__getElemsByIndex(this.__valueElem.selectedIndex);
|
|
508
|
+
if (!selected) return; // пустое или неизвестное значение — контрол показывает placeholder
|
|
509
|
+
|
|
510
|
+
selected.own.classList.add("hasvalue");
|
|
511
|
+
this.__container.classList.add("hasvalue");
|
|
512
|
+
this.__textElem.innerText = (selected.own.firstElementChild?.textContent ?? "").trim();
|
|
513
|
+
}
|
|
514
|
+
|
|
477
515
|
getValue(): string | null {
|
|
478
516
|
return this.__valueElem.value || null;
|
|
479
517
|
}
|
|
480
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Программная установка значения — например, восстановление черновика формы. Пишет значение
|
|
521
|
+
* в поле-носитель и показывает его в контроле; значение без своего пункта в списке даёт
|
|
522
|
+
* пустой выбор. Событие изменения поднимается, только когда показанный выбор действительно
|
|
523
|
+
* сменился: сравниваем с UI, а не с полем — вызывающий мог записать значение в поле сам.
|
|
524
|
+
*/
|
|
525
|
+
setValue(value: string | null): void {
|
|
526
|
+
// значение ищем средствами самого поля: оно встанет на первый подходящий option,
|
|
527
|
+
// а дальше выбор переносится и показывается уже по индексу
|
|
528
|
+
this.__valueElem.value = value ?? "";
|
|
529
|
+
|
|
530
|
+
this.__applySelection(this.__valueElem.selectedIndex);
|
|
531
|
+
}
|
|
532
|
+
|
|
481
533
|
getSelectedIndex(): number {
|
|
482
534
|
return this.__valueElem.selectedIndex;
|
|
483
535
|
}
|