@brandup/ui-dropdown 1.0.20

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 ADDED
@@ -0,0 +1,21 @@
1
+ # brandup-ui-dropdown
2
+
3
+ [![Build Status](https://dev.azure.com/brandup/BrandUp%20Core/_apis/build/status%2FBrandUp%2Fbrandup-ui-kit?branchName=master)]()
4
+
5
+ ## Installation
6
+
7
+ Install NPM package [@brandup/ui](https://www.npmjs.com/package/@brandup/ui-kit).
8
+
9
+ ```
10
+ npm i @brandup/ui-kit@latest
11
+ ```
12
+
13
+ ## DropDown
14
+
15
+ `DropDown` - это классовый компонент, который расширяет возможности стандартных элементов `select`.
16
+ Наследуется от `InputControl`.
17
+
18
+ ```JS
19
+ const dropDownElem = document.getElementById("name");
20
+ const dropdown = new DropDown(dropDownElem);
21
+ ```
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@brandup/ui-dropdown",
3
+ "description": "DropDown control type and styles.",
4
+ "keywords": [
5
+ "brandup",
6
+ "javascript",
7
+ "typescript",
8
+ "ui"
9
+ ],
10
+ "author": {
11
+ "name": "Dmitry Kovyazin",
12
+ "email": "it@brandup.online"
13
+ },
14
+ "homepage": "https://github.com/brandup-online/brandup-ui/npm/brandup-ui-dropdown",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/brandup-online/brandup-ui.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/brandup-online/brandup-ui/issues",
21
+ "email": "it@brandup.online"
22
+ },
23
+ "license": "Apache-2.0",
24
+ "version": "1.0.20",
25
+ "main": "source/index.ts",
26
+ "types": "source/index.ts",
27
+ "dependencies": {
28
+ "@brandup/ui": "^1.0.37",
29
+ "@brandup/ui-dom": "^1.0.37",
30
+ "@brandup/ui-helpers": "^1.0.37",
31
+ "@brandup/ui-input": "^1.0.20",
32
+ "@brandup/ui-kit": "^1.0.20"
33
+ },
34
+ "files": [
35
+ "source",
36
+ "README.md"
37
+ ]
38
+ }
@@ -0,0 +1,444 @@
1
+ @import "@brandup/ui-kit/source/adaptive.less";
2
+
3
+ .ui-dropdown {
4
+ position: relative;
5
+ box-sizing: border-box;
6
+ user-select: none;
7
+ width: var(--input-width);
8
+
9
+ & .view {
10
+ --svg-size: 12px;
11
+ --svg-fill: var(--input-color);
12
+
13
+ display: flex;
14
+ height: var(--input-height);
15
+ flex-flow: row nowrap;
16
+ align-items: center;
17
+ justify-content: flex-start;
18
+ box-sizing: border-box;
19
+ color: var(--input-color);
20
+ white-space: nowrap;
21
+ border-radius: var(--input-border-radius);
22
+ padding: 10px var(--input-padding-lr);
23
+ background-color: var(--input-fill);
24
+ transition: background-color ease 100ms;
25
+ gap: 10px;
26
+ line-height: var(--line-height);
27
+ text-decoration: none !important;
28
+ outline: none;
29
+ border: none;
30
+ cursor: pointer;
31
+ width: 100%;
32
+ text-align: left;
33
+
34
+ &:before {
35
+ content: '';
36
+ position: absolute;
37
+ left: 0;
38
+ top: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ border: var(--input-border-type) var(--input-border-width) var(--input-border-color);
42
+ border-radius: var(--input-border-radius);
43
+ box-sizing: border-box;
44
+ transition: border-color ease 100ms;
45
+ }
46
+
47
+ & span {
48
+ flex: 1 1 auto;
49
+ display: block;
50
+ overflow: hidden;
51
+ white-space: nowrap;
52
+ text-overflow: ellipsis;
53
+ }
54
+
55
+ & svg {
56
+ transition: fill ease 100ms, transform ease 150ms;
57
+ }
58
+
59
+ &:hover {
60
+ --input-color: var(--hover--input-color);
61
+ --input-fill: var(--hover--input-fill);
62
+ --input-border-color: var(--hover--input-border-color);
63
+ }
64
+
65
+ &:focus {
66
+ --input-color: var(--focus--input-color);
67
+ --input-fill: var(--focus--input-fill);
68
+ --input-border-color: var(--focus--input-border-color);
69
+ }
70
+ }
71
+
72
+ & .popup {
73
+ z-index: 1000;
74
+ display: none;
75
+ position: absolute;
76
+ top: 100%;
77
+ width: 100%;
78
+ transform: translateY(8px);
79
+ font-weight: 400;
80
+ outline: none;
81
+ min-width: 200px;
82
+ box-sizing: border-box;
83
+
84
+ & .content {
85
+ --svg-fill: var(--popup-color);
86
+
87
+ display: flex;
88
+ flex-flow: column nowrap;
89
+ gap: var(--input-padding-lr);
90
+ background: var(--popup-fill);
91
+ color: var(--popup-color);
92
+ border-style: var(--popup-border-style);
93
+ border-color: var(--popup-border-color);
94
+ border-width: var(--popup-border-width);
95
+ border-radius: var(--popup-border-radius);
96
+ box-shadow: var(--popup-box-shadow);
97
+ padding: var(--input-padding-lr);
98
+ box-sizing: border-box;
99
+ max-height: 370px;
100
+
101
+ & .header {
102
+ padding: 0;
103
+ display: none;
104
+ flex-flow: row nowrap;
105
+ align-items: center;
106
+
107
+ & span {
108
+ flex: 1 1 auto;
109
+ display: block;
110
+ font-weight: 500;
111
+ font-size: var(--svg-size);
112
+ }
113
+
114
+ & button {
115
+ flex: 0 0 auto;
116
+ appearance: none;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ background: none;
121
+ border: none;
122
+ cursor: pointer;
123
+ position: relative;
124
+ padding: 0;
125
+
126
+ &:after {
127
+ content: '';
128
+ display: block;
129
+ position: absolute;
130
+ left: -10px;
131
+ top: -10px;
132
+ right: -10px;
133
+ bottom: -10px;
134
+ }
135
+ }
136
+ }
137
+
138
+ & ul {
139
+ flex: 1 1 100%;
140
+ height: 100%;
141
+ position: relative;
142
+ overflow-y: scroll;
143
+ padding-right: 5px;
144
+ scrollbar-width: 6px;
145
+
146
+ &::-webkit-scrollbar {
147
+ width: 6px;
148
+ height: 5px;
149
+ background: none;
150
+ }
151
+
152
+ &::-webkit-scrollbar-track {
153
+ border-radius: 3px;
154
+ background-color: transparent;
155
+ }
156
+
157
+ &::-webkit-scrollbar-thumb {
158
+ background-color: #8696a0;
159
+ border-radius: 3px;
160
+ }
161
+
162
+ & li {
163
+ padding: calc(var(--input-padding-lr) / 2) var(--input-padding-lr);
164
+ display: flex;
165
+ justify-content: space-between;
166
+ align-items: center;
167
+ border-radius: var(--input-border-radius);
168
+ min-height: unset;
169
+ cursor: pointer;
170
+
171
+ & svg {
172
+ display: none;
173
+ }
174
+
175
+ &:hover {
176
+ --input-color: var(--hover--input-color);
177
+ --input-fill: var(--hover--input-fill);
178
+ }
179
+
180
+ &:focus {
181
+ --input-color: var(--focus--input-color);
182
+ --input-fill: var(--focus--input-fill);
183
+ }
184
+
185
+ &.hasvalue {
186
+ --input-color: var(--hasvalue--input-color);
187
+ --input-fill: var(--hasvalue--input-fill);
188
+
189
+ & svg {
190
+ display: block;
191
+
192
+ --svg-fill: var(--hasvalue--input-color);
193
+ }
194
+
195
+ &:focus {
196
+ --input-color: var(--focus--input-color);
197
+ --input-fill: var(--focus--input-fill);
198
+ }
199
+ }
200
+ }
201
+
202
+ &.result {
203
+ & li {
204
+ display: none;
205
+
206
+ &.ok {
207
+ display: flex;
208
+ }
209
+ }
210
+ }
211
+
212
+ &.notfound {
213
+ display: none;
214
+
215
+ & ~ .empty {
216
+ flex: 1 1 auto;
217
+ display: block;
218
+ }
219
+ }
220
+ }
221
+
222
+ & .empty {
223
+ display: none;
224
+ }
225
+
226
+ & .cancel {
227
+ display: none;
228
+ }
229
+
230
+ &.top {
231
+ top: auto;
232
+ bottom: 100%;
233
+ transform: translateY(-8px);
234
+ }
235
+
236
+ &.right {
237
+ left: 0;
238
+ right: auto;
239
+ }
240
+ }
241
+ }
242
+
243
+ & .search {
244
+ flex: 0 0 auto;
245
+ display: none;
246
+ flex-flow: row nowrap;
247
+ position: relative;
248
+
249
+ & svg {
250
+ display: block;
251
+ position: absolute;
252
+ left: var(--input-padding-lr);
253
+ top: 50%;
254
+ transform: translateY(-50%);
255
+ }
256
+
257
+ & input {
258
+ --input-width: 100%;
259
+ padding-left: calc(var(--input-padding-lr) + var(--svg-size) + var(--input-padding-lr) / 2);
260
+ }
261
+ }
262
+
263
+ &.searchable {
264
+ & .search {
265
+ display: flex;
266
+ }
267
+ }
268
+
269
+ &.empty {
270
+ & .popup {
271
+ & .content {
272
+ & ul {
273
+ display: none;
274
+ }
275
+
276
+ & .empty {
277
+ display: block;
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ &.hasvalue {
284
+ --input-color: var(--hasvalue--input-color);
285
+ --input-fill: var(--hasvalue--input-fill);
286
+ --svg-fill: var(--hasvalue--input-color);
287
+
288
+ & .view {
289
+ &:hover,
290
+ &:focus {
291
+ --input-color: var(--hover--input-color);
292
+ --input-fill: var(--hover--input-fill);
293
+ --svg-fill: var(--hover--input-color);
294
+ }
295
+ }
296
+ }
297
+
298
+ &.expanded {
299
+ --input-color: var(--expanded--input-color);
300
+ --input-fill: var(--expanded--input-fill);
301
+ --svg-fill: var(--expanded--input-color);
302
+
303
+ & .view {
304
+ & svg {
305
+ transform: rotate(180deg);
306
+ }
307
+ }
308
+
309
+ & .popup {
310
+ display: block;
311
+ }
312
+ }
313
+
314
+ &.invalid {
315
+ --input-color: var(--invalid--input-color);
316
+ --input-fill: var(--invalid--input-fill);
317
+ --input-border-color: var(--invalid--input-border-color);
318
+ --svg-fill: var(--invalid--input-color);
319
+ }
320
+
321
+ &.disabled {
322
+ --input-color: var(--disabled--input-color);
323
+ --input-fill: var(--disabled--input-fill);
324
+ --svg-fill: var(--disabled--input-color);
325
+ --input-border-color: var(--disabled--input-border-color);
326
+
327
+ & .view {
328
+ cursor: default;
329
+
330
+ &:focus,
331
+ &:hover {
332
+ --input-color: var(--disabled--input-color);
333
+ --input-fill: var(--disabled--input-fill);
334
+ --input-border-color: var(--disabled--input-border-color);
335
+ }
336
+ }
337
+ }
338
+
339
+ .adaptive-notebook({
340
+ & .popup .content {
341
+ max-height: 340px;
342
+ }
343
+ });
344
+
345
+ .adaptive-notebook-small({
346
+ & .popup .content {
347
+ max-height: 320px;
348
+ }
349
+ });
350
+
351
+ .adaptive-tablet({
352
+ & .popup {
353
+ position: fixed;
354
+ background: rgba(0, 0, 0, 0.5);
355
+ top: 0;
356
+ left: 0;
357
+ right: 0;
358
+ bottom: 0;
359
+ transform: none;
360
+ padding: var(--content-padding-lr);
361
+ min-width: auto;
362
+
363
+ & .content {
364
+ padding: 32px;
365
+ max-width: 540px;
366
+ max-height: 550px;
367
+ width: 100%;
368
+ height: 100%;
369
+
370
+ & .header {
371
+ display: flex;
372
+ }
373
+ }
374
+
375
+ &.top {
376
+ top: 0;
377
+ bottom: 0;
378
+ transform: none;
379
+ }
380
+
381
+ &.right {
382
+ left: 0;
383
+ right: 0;
384
+ }
385
+ }
386
+
387
+ &.expanded {
388
+ & .popup {
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+
393
+ & .content {
394
+ flex: 1 1 auto;
395
+ }
396
+ }
397
+ }
398
+
399
+ & body.ui-dropdown-opened {
400
+ overflow: hidden;
401
+ }
402
+ });
403
+
404
+ .adaptive-mobile({
405
+ & .popup {
406
+ padding: 0;
407
+
408
+ & .content {
409
+ padding: 28px;
410
+ max-width: none;
411
+ max-height: none;
412
+
413
+ & .cancel {
414
+ display: block;
415
+ }
416
+ }
417
+ }
418
+ });
419
+
420
+ .adaptive-mobile-small({
421
+ & .popup {
422
+ & .content {
423
+ padding: 20px;
424
+ }
425
+ }
426
+ });
427
+ }
428
+
429
+ .ui-dropdown-input {
430
+ opacity: 0;
431
+ position: absolute;
432
+ width: 1px;
433
+ height: 1px;
434
+ outline: none;
435
+ visibility: collapse;
436
+ }
437
+
438
+ .ui-dropdown-miniature {
439
+ border: var(--input-border-type) var(--input-border-width) var(--input-border-color);
440
+ background: #F6F7F8;
441
+ border-radius: var(--input-border-radius);
442
+ height: var(--input-height);
443
+ box-sizing: border-box;
444
+ }
@@ -0,0 +1,498 @@
1
+ import { InputControl } from "@brandup/ui-input";
2
+ import { DOM } from "@brandup/ui-dom";
3
+ import { detectLanguage, transcriptText } from "./utils/utilities";
4
+
5
+ import "./dropdown.less"; // стили компонента
6
+
7
+ import arrowBottomIcon from "./svg/arrow-down.svg";
8
+ import checkIcon from "./svg/tick.svg";
9
+ import searchIcon from "./svg/search.svg";
10
+ import closeIcon from "./svg/cancel.svg";
11
+
12
+ export const ROOT_CLASS = "ui-dropdown";
13
+ export const INPUT_CLASS = "ui-dropdown-input";
14
+ export const MINIATURE_CLASS = "ui-dropdown-miniature";
15
+ export const CHANGE_EVENT = "dropdown-change";
16
+
17
+ const TABLET_WIDTH = 1030;
18
+ const BODY_EXPANDED = "ui-dropdown-opened";
19
+
20
+ class DropDown extends InputControl<HTMLSelectElement> {
21
+ private __container: HTMLElement;
22
+ private __popupElem: HTMLElement;
23
+ private __listElem: HTMLElement;
24
+ private __textElem: HTMLElement;
25
+ private __emptyElem: HTMLElement;
26
+ private __searchInput: HTMLInputElement;
27
+ private __closePopupFunc: (e: MouseEvent) => void;
28
+ private __invalidTimeout?: number;
29
+ private __hasEmptyValue: boolean = false;
30
+
31
+ readonly placeholder: string;
32
+ readonly emptyText: string;
33
+ readonly searchPlaceholder: string;
34
+ readonly searchEmpty: string;
35
+ readonly searchOn: number | boolean = 15;
36
+
37
+ get typeName(): string { return "BrandUp.DropDown"; }
38
+
39
+ constructor(selectElem: HTMLSelectElement) {
40
+ super(selectElem);
41
+
42
+ this.__valueElem.classList.add(INPUT_CLASS);
43
+
44
+ this.placeholder = this.__valueElem.getAttribute("data-placeholder") || "Select";
45
+ this.emptyText = this.__valueElem.getAttribute("data-emptytext") || "Empty list";
46
+ this.searchPlaceholder = this.__valueElem.getAttribute("data-search-placeholder") || "Search";
47
+ this.searchEmpty = this.__valueElem.getAttribute("data-search-empty") || "Not found";
48
+
49
+ const se = this.__valueElem.getAttribute("data-search-on");
50
+ if (se) {
51
+ switch (se.toLowerCase()) {
52
+ case "true":
53
+ this.searchOn = true;
54
+ break;
55
+ case "false":
56
+ this.searchOn = false;
57
+ default:
58
+ this.searchOn = parseInt(se);
59
+ break;
60
+ }
61
+ }
62
+
63
+ // __renderUI
64
+ this.__container = DOM.tag("div", { class: [ROOT_CLASS].concat(Array.from(this.__valueElem.classList)) }, [
65
+ DOM.tag("button", { class: "view", command: "open-popup" }, [this.__textElem = DOM.tag("span", null, this.placeholder ?? ''), arrowBottomIcon]),
66
+ this.__popupElem = DOM.tag("div", { class: "popup", tabindex: 0 }, [
67
+ DOM.tag("div", "content", [
68
+ DOM.tag("div", "header", [
69
+ DOM.tag("span", null, this.placeholder ?? ''),
70
+ DOM.tag("button", { command: "close-popup" }, closeIcon)]
71
+ ),
72
+ DOM.tag("div", { class: "search" }, [
73
+ searchIcon,
74
+ this.__searchInput = <HTMLInputElement>DOM.tag("input", { type: "search", maxlength: 50, placeholder: this.searchPlaceholder })
75
+ ]),
76
+ this.__listElem = DOM.tag("ul"),
77
+ this.__emptyElem = DOM.tag("div", { class: "empty" }, this.emptyText),
78
+ DOM.tag("button", { class: "cancel", command: "close-popup" }, "Отмена")
79
+ ])
80
+ ])
81
+ ]);
82
+
83
+ this.__container.classList.remove(INPUT_CLASS);
84
+
85
+ this.setElement(this.__container);
86
+
87
+ if (this.__valueElem.nextElementSibling) {
88
+ const nextElem = <HTMLElement>this.__valueElem.nextElementSibling;
89
+ if (nextElem.classList.contains(MINIATURE_CLASS)) nextElem.remove();
90
+ }
91
+
92
+ this.__valueElem.insertAdjacentElement("beforebegin", this.__container);
93
+ this.__container.insertAdjacentElement("beforeend", this.__valueElem);
94
+
95
+ this.__closePopupFunc = (e: MouseEvent) => {
96
+ const t = <HTMLElement>e.target;
97
+ const dd = t.closest(`.${ROOT_CLASS}`);
98
+ if (!dd || (dd === this.__container && !t.closest("li[data-index]") && t !== this.__searchInput && !t.closest(".search"))) {
99
+ this.__closePopup();
100
+ this.__clearSearch();
101
+ }
102
+ };
103
+
104
+ this.__renderItems();
105
+ this.__initLogic();
106
+ }
107
+
108
+ // рендер элементов и выбор текущего
109
+ private __renderItems() {
110
+ const optionsCount = this.__valueElem.options.length;
111
+ const selectedIndex = this.__valueElem.selectedIndex;
112
+
113
+ if (!optionsCount)
114
+ this.__textElem.innerText = this.placeholder;
115
+
116
+ if (!optionsCount) {
117
+ this.element?.classList.add("empty");
118
+ return;
119
+ }
120
+
121
+ // определяем можно ли делать поиск по элементам в списке
122
+ let isSearchable = false;
123
+ if (this.searchOn === true || optionsCount >= <number>this.searchOn) {
124
+ this.element?.classList.add("searchable");
125
+ isSearchable = true;
126
+ }
127
+
128
+ // вставляем элементы меню в фрагмент, чтобы не нагружать процессор
129
+ const popupItemsFragment = document.createDocumentFragment();
130
+
131
+ let elemCount = 0;
132
+ for (let i = 0; i < optionsCount; i++) {
133
+ const optionElem = this.__valueElem.options.item(i);
134
+ if (!optionElem)
135
+ continue;
136
+
137
+ const itemText = optionElem.textContent?.trim() || "";
138
+ const itemValue = optionElem.value;
139
+
140
+ if (i === 0 && !itemValue) {
141
+ // Если первый элемент, это пустое значение
142
+
143
+ this.__hasEmptyValue = true;
144
+ continue;
145
+ }
146
+
147
+ let itemElem = DOM.tag("li", { command: "select", dataset: { value: itemValue, index: i.toString() } }, [
148
+ DOM.tag("span", { tabindex: "0" }, itemText),
149
+ checkIcon
150
+ ]
151
+ );
152
+
153
+ const transcript = (<any>itemElem)['wsdd_transcript'] = transcriptText(itemText);
154
+
155
+ const isSelected = selectedIndex === i;
156
+ if (isSelected)
157
+ itemElem.classList.add("hasvalue");
158
+
159
+ if (isSearchable) {
160
+ // дублируем быстрый элемент в общем списке, чтобы находить его при поиске
161
+
162
+ itemElem = itemElem.cloneNode(true) as HTMLLIElement;
163
+ (<any>itemElem)['wsdd_transcript'] = transcript;
164
+ popupItemsFragment.append(itemElem);
165
+ }
166
+
167
+ popupItemsFragment.append(itemElem);
168
+
169
+ if (isSelected) {
170
+ this.__container.classList.add("hasvalue");
171
+ this.__textElem.innerText = itemText;
172
+ }
173
+
174
+ elemCount++;
175
+ }
176
+
177
+ if (this.__hasEmptyValue && !elemCount)
178
+ this.element?.classList.add("empty");
179
+
180
+ this.__listElem.append(popupItemsFragment);
181
+ }
182
+
183
+ private __initLogic() {
184
+ this.registerCommand("open-popup", () => this.__togglePopup());
185
+ this.registerCommand("close-popup", () => this.__closePopup());
186
+
187
+ this.registerCommand("select", context => {
188
+ if (!this.element)
189
+ return;
190
+
191
+ const newIndex = context.target.dataset.index;
192
+
193
+ this.element.classList.remove("invalid");
194
+ this.__clearSearch();
195
+ this.__closePopup();
196
+
197
+ const currentSelect = this.__getSelectedElem();
198
+ if (currentSelect && newIndex === currentSelect.own.dataset.index)
199
+ return; // если выбор остался таким же
200
+
201
+ DOM.removeClass(this.element, '.hasvalue', 'hasvalue');
202
+
203
+ if (currentSelect && currentSelect.own.closest(`.${ROOT_CLASS}`))
204
+ this.__textElem.innerText = this.placeholder ?? '';
205
+
206
+ // делаем новый выбор
207
+ this.__valueElem.value = context.target.dataset.value || "";
208
+
209
+ const newSelected = this.__getElemsByIndex(Number(newIndex));
210
+
211
+ if (newSelected) {
212
+ newSelected.own.classList.add("hasvalue");
213
+
214
+ const newDropDown = newSelected.own.closest(`.${ROOT_CLASS}`);
215
+
216
+ if (newDropDown) {
217
+ this.__textElem.innerText = newSelected.own.innerText.trim();
218
+ newDropDown.classList.add("hasvalue");
219
+ this.__closePopup();
220
+ this.__focusPopup();
221
+ }
222
+ }
223
+
224
+ this.__onChange();
225
+ });
226
+
227
+ this.__searchInput.addEventListener("input", () => {
228
+ const query = this.__searchInput.value;
229
+
230
+ this.__search(query);
231
+ });
232
+
233
+ this.__popupElem.addEventListener("keydown", (e: KeyboardEvent) => {
234
+ const target = <HTMLElement>e.target;
235
+ const isSpan = target.tagName == "SPAN";
236
+
237
+ switch (e.key) {
238
+ case "ArrowUp": {
239
+ if (isSpan) {
240
+ // Если есть предыдущий элемент, то переводим фокус на него
241
+ const prevItemElem = target.parentElement?.previousElementSibling;
242
+ if (prevItemElem)
243
+ (<HTMLElement>prevItemElem.firstElementChild).focus();
244
+
245
+ e.preventDefault();
246
+ }
247
+ break;
248
+ }
249
+ case "ArrowDown": {
250
+ if (isSpan) {
251
+ // Если есть следующий элемент, то переводим фокус на него
252
+ const nextItemElem = target.parentElement?.nextElementSibling;
253
+ if (nextItemElem)
254
+ (<HTMLElement>nextItemElem.firstElementChild).focus();
255
+
256
+ e.preventDefault();
257
+ }
258
+ break;
259
+ }
260
+ case "Escape": {
261
+ e.preventDefault();
262
+ this.__closePopup();
263
+ this.__focusPopup();
264
+ break;
265
+ }
266
+ case "Tab": {
267
+ if (target == this.__popupElem && this.element?.classList.contains("empty")) {
268
+ // если список пустой, то фокус уйдёт от компанента на следующий и нужно закрыть popup
269
+ this.__closePopup();
270
+ }
271
+ else if (target == this.__searchInput && this.__listElem.classList.contains("notfound")) {
272
+ // если не найдено, то фокус уйдёт от компанента на следующий и нужно закрыть popup
273
+ this.__closePopup();
274
+ }
275
+ else if (isSpan && !target.parentElement?.nextElementSibling) {
276
+ // если фокус на последнем элементе списка, то фокус уйдёт от компанента на следующий и нужно закрыть popup
277
+ this.__closePopup();
278
+ }
279
+ break;
280
+ }
281
+ case "Enter": { // так как теперь мы обрабатываем не <a>
282
+ target.click();
283
+ this.__closePopup();
284
+ break;
285
+ }
286
+ }
287
+ });
288
+ }
289
+
290
+ private __focusPopup() {
291
+ (<HTMLElement>this.__container.firstElementChild).focus();
292
+ }
293
+
294
+ private __onChange() {
295
+ this.trigger(CHANGE_EVENT, {
296
+ dropdown: this,
297
+ index: this.getSelectedIndex(),
298
+ value: this.getValue(),
299
+ title: this.getSelectedTitle()
300
+ });
301
+ }
302
+
303
+ private __togglePopup() {
304
+ if (this.element?.classList.contains("disabled"))
305
+ return;
306
+
307
+ if (!this.element?.classList.toggle("expanded"))
308
+ return;
309
+
310
+ // закрываем все открытые попапы, кроме текущего
311
+ document.querySelectorAll('.ui-dropdown.expanded').forEach((dropdown) => {
312
+ if (dropdown !== this.element) {
313
+ dropdown.classList.remove('expanded');
314
+ }
315
+ });
316
+
317
+ this.__popupElem.focus({ preventScroll: true });
318
+
319
+ this.__positionPopup();
320
+
321
+ document.body.classList.add(BODY_EXPANDED);
322
+
323
+ let top = 0;
324
+ const selectedElem = this.__getSelectedElem();
325
+ const itemHeight = selectedElem?.own?.clientHeight || 0;
326
+ const itemTop = selectedElem?.own?.offsetTop || 0;
327
+
328
+ top = itemTop - this.__listElem.clientHeight / 2 + itemHeight;
329
+
330
+ if (top !== null)
331
+ this.__listElem.scrollTo({ left: 0, top: top, behavior: 'instant' });
332
+
333
+ document.body.addEventListener("mouseup", this.__closePopupFunc);
334
+ }
335
+
336
+ private __positionPopup() {
337
+ this.__popupElem.classList.remove("top", "right");
338
+
339
+ if (document.body.clientWidth <= TABLET_WIDTH)
340
+ return;
341
+
342
+ const bodyHeight = document.body.clientHeight;
343
+ const popupRect = this.__popupElem.getBoundingClientRect();
344
+
345
+ if (popupRect.y + popupRect.height > bodyHeight) {
346
+ this.__popupElem.classList.add("top");
347
+ }
348
+
349
+ if (popupRect.x < 0) {
350
+ this.__popupElem.classList.add("right");
351
+ }
352
+ }
353
+
354
+ private __closePopup() {
355
+ document.body.classList.remove(BODY_EXPANDED);
356
+ this.element?.classList.remove("expanded");
357
+ document.body.removeEventListener("mouseup", this.__closePopupFunc);
358
+ }
359
+
360
+ private __search(query: string) {
361
+ if (!query) {
362
+ this.__clearSearch();
363
+ return;
364
+ }
365
+
366
+ query = query.toLowerCase();
367
+
368
+ this.__listElem.classList.add("result");
369
+ const items = DOM.queryElements(this.__listElem, "li span");
370
+
371
+
372
+ let findedCount = 0;
373
+ items.forEach(it => {
374
+ const item = it.parentElement;
375
+ if (!item)
376
+ return;
377
+
378
+ if (it.innerText.toLowerCase().startsWith(query)) {
379
+ item.classList.add("ok");
380
+ findedCount++;
381
+ }
382
+ else
383
+ item.classList.remove("ok");
384
+ });
385
+
386
+ if (!findedCount) {
387
+ // если ничего не найдено по оригинальному тексту, то ищем в других раскладках клавиатуры
388
+
389
+ const queryLang = detectLanguage(query);
390
+
391
+ if (queryLang) {
392
+ for (let i = 0; i < this.__listElem.children.length; i++) {
393
+ const item = this.__listElem.children.item(i);
394
+ if (!item)
395
+ continue;
396
+ const transcript = (<any>item)['wsdd_transcript'];
397
+ if (transcript && transcript[queryLang] && transcript[queryLang].startsWith(query)) {
398
+
399
+ item.classList.add("ok");
400
+ findedCount++;
401
+ }
402
+ else
403
+ item.classList.remove("ok");
404
+ }
405
+ }
406
+ }
407
+
408
+ if (findedCount > 0) {
409
+ this.__emptyElem.innerText = this.emptyText;
410
+ this.__listElem.classList.remove("notfound");
411
+ }
412
+ else {
413
+ this.__emptyElem.innerText = this.searchEmpty;
414
+ this.__listElem.classList.add("notfound");
415
+ }
416
+ }
417
+
418
+ private __clearSearch() {
419
+ if (!this.__listElem.classList.contains("result"))
420
+ return;
421
+
422
+ this.__emptyElem.innerText = this.emptyText;
423
+ this.__listElem.classList.remove("result", "notfound");
424
+ DOM.removeClass(this.__listElem, "li.ok", "ok");
425
+ this.__searchInput.value = '';
426
+ }
427
+
428
+ private __getElems(selector: string): { own: HTMLElement } | null {
429
+ if (!this.element)
430
+ return null;
431
+
432
+ const elems = DOM.queryElements(this.element, selector);
433
+ if (elems.length === 2)
434
+ return { own: elems.item(0) };
435
+ else if (elems.length === 1)
436
+ return { own: elems.item(0) };
437
+ return null;
438
+ }
439
+
440
+ private __getElemsByIndex(index: number) {
441
+ return this.__getElems(`[data-index="${index}"]`);
442
+ }
443
+
444
+ private __getSelectedElem() {
445
+ return this.__getElems(".hasvalue[data-index]");
446
+ }
447
+
448
+ getValue(): string | null {
449
+ return this.__valueElem.value || null;
450
+ }
451
+
452
+ getSelectedIndex(): number {
453
+ return this.__valueElem.selectedIndex;
454
+ }
455
+
456
+ getSelectedTitle(): string | null {
457
+ const selected = this.__getSelectedElem();
458
+ return (selected && selected.own.firstElementChild?.textContent?.trim()) || null;
459
+ }
460
+
461
+ override validate(): boolean {
462
+ window.clearTimeout(this.__invalidTimeout);
463
+
464
+ let value = this.getValue();
465
+ let isInvalid = !this.__valueElem.validity.valid;
466
+
467
+ if (this.required && !value)
468
+ isInvalid = true;
469
+
470
+ const clearInvalid = () => this.element?.classList.remove("invalid");
471
+
472
+ if (isInvalid) {
473
+ this.element?.classList.add("invalid");
474
+ }
475
+ else
476
+ clearInvalid();
477
+
478
+ return !isInvalid;
479
+ }
480
+
481
+ override destroy(): void {
482
+ this.__closePopup();
483
+
484
+ this.element?.insertAdjacentElement("afterend", this.__valueElem);
485
+ this.element?.remove();
486
+
487
+ super.destroy();
488
+ }
489
+ }
490
+
491
+ export interface ChangeEventData {
492
+ dropdown: DropDown;
493
+ index: number;
494
+ value: string | null;
495
+ title: string | null;
496
+ }
497
+
498
+ export default DropDown;
@@ -0,0 +1,2 @@
1
+ export { default } from './dropdown';
2
+ export * from './utils/utilities';
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 12 7">
2
+ <path d="M12 1.387L10.5174 0L6 4.226L1.48263 0L0 1.387L6 7L12 1.387Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 18 18" >
2
+ <path d="M1.65129 0L0 1.65129L7.34871 9L0 16.3487L1.65129 18L9 10.6513L16.3487 18L18 16.3487L10.6513 9L18 1.65129L16.3487 0L9 7.34871L1.65129 0Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 26 26">
2
+ <path d="M10 3C6.14585 3 3 6.14585 3 10C3 13.8541 6.14585 17 10 17C11.748 17 13.345 16.348 14.5742 15.2812L15 15.707V17L21 23L23 21L17 15H15.707L15.2812 14.5742C16.348 13.345 17 11.748 17 10C17 6.14585 13.8541 3 10 3ZM10 5C12.7733 5 15 7.22673 15 10C15 12.7733 12.7733 15 10 15C7.22673 15 5 12.7733 5 10C5 7.22673 7.22673 5 10 5Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 42 42" >
2
+ <path d="M37.2371 8.00075C36.7922 8.01416 36.37 8.20224 36.0599 8.52514L15.0108 29.8187L5.94773 20.6504C5.78995 20.4841 5.60097 20.3514 5.39185 20.26C5.18273 20.1686 4.95768 20.1203 4.72988 20.1179C4.50207 20.1156 4.2761 20.1592 4.06518 20.2463C3.85427 20.3334 3.66266 20.4623 3.50157 20.6252C3.34048 20.7882 3.21315 20.982 3.12704 21.1954C3.04093 21.4087 2.99777 21.6373 3.00009 21.8678C3.00241 22.0982 3.05015 22.3259 3.14054 22.5375C3.23092 22.749 3.36212 22.9402 3.52645 23.0998L13.8002 33.4929C14.1213 33.8176 14.5568 34 15.0108 34C15.4648 34 15.9003 33.8176 16.2214 33.4929L38.4811 10.9745C38.7286 10.7312 38.8976 10.418 38.966 10.0759C39.0344 9.73372 38.9991 9.37865 38.8646 9.05718C38.7301 8.73571 38.5028 8.46284 38.2123 8.27433C37.9219 8.08582 37.582 7.99046 37.2371 8.00075Z" />
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ declare module "*.svg" {
2
+ const content: any;
3
+ export default content;
4
+ }
@@ -0,0 +1,72 @@
1
+ const REGEX_CHAR = /\p{L}/u
2
+
3
+ const languages: { [keys in Language]: string } = {
4
+ english: "qwertyuiopasdfghjklzxcvbnm",
5
+ russian: "йцукенгшщзхъфывапролджэячсмитьбю",
6
+ };
7
+
8
+ // prettier-ignore
9
+ const english_russian_dict: ICharMap = {
10
+ q: "й", w: "ц", e: "у", r: "к", t: "е", y: "н", u: "г", i: "ш", o: "щ", p: "з", "[": "х", "]": "ъ",
11
+ a: "ф", s: "ы", d: "в", f: "а", g: "п", h: "р", j: "о", k: "л", l: "д", ";": "ж", "'": "э",
12
+ z: "я", x: "ч", c: "с", v: "м", b: "и", n: "т", m: "ь", ",": "б", ".": "ю", "/": ".",
13
+ "{": "Х", "}": "Ъ", ":": "Ж", '"': "Э", "<": "Б", ">": "Ю", "?": ","
14
+ };
15
+
16
+ // prettier-ignore
17
+ const russian_english_dict: ICharMap = {
18
+ й: "q", ц: "w", у: "e", к: "r", е: "t", н: "y", г: "u", ш: "i", щ: "o", з: "p", х: "[", ъ: "]",
19
+ ф: "a", ы: "s", в: "d", а: "f", п: "g", р: "h", о: "j", л: "k", д: "l", ж: ";", э: "'",
20
+ я: "z", ч: "x", с: "c", м: "v", и: "b", т: "n", ь: "m", б: ",", ю: ".", ".": "/",
21
+ "Х": "{", "Ъ": "}", "Ж": ":", "Э": '"', "Б": "<", "Ю": ">", ",": "?"
22
+ };
23
+
24
+ interface ICharMap {
25
+ [keys: string]: string;
26
+ }
27
+
28
+ type ITranscriptMap = {
29
+ // {целевой язык: {язык ввода: словарь}}
30
+ [keys in Language]: { [keys: string]: ICharMap };
31
+ };
32
+
33
+ const dictionariesMap: ITranscriptMap = {
34
+ russian: { english: russian_english_dict },
35
+ english: { russian: english_russian_dict },
36
+ };
37
+
38
+ export type Language = "english" | "russian";
39
+
40
+ export const detectLanguage = (text: string) => {
41
+ if (!text)
42
+ return null;
43
+
44
+ const match = text.match(REGEX_CHAR);
45
+ if (!match)
46
+ return null;
47
+
48
+ const firstChar = match[0].toLocaleLowerCase();
49
+ for (const lang in languages) {
50
+ if (languages[<Language>lang].includes(firstChar))
51
+ return lang as Language;
52
+ }
53
+
54
+ return null;
55
+ };
56
+
57
+ export const transcriptText = (text: string) => {
58
+ const transcriptVariants: { [keys: string]: string; } = {};
59
+
60
+ const textLanguage = detectLanguage(text);
61
+ if (textLanguage) {
62
+ const textArr: string[] = Array.from(text.toLowerCase());
63
+ for (const lang in dictionariesMap[textLanguage]) {
64
+ const langVariant = dictionariesMap[textLanguage][lang];
65
+
66
+ const transcript = textArr.reduce((prev, current) => prev + (langVariant[current] || current), "");
67
+ transcriptVariants[lang] = transcript;
68
+ }
69
+ }
70
+
71
+ return transcriptVariants;
72
+ };