vgapp 0.8.8 → 0.9.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/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
# VEGAS-APP 0.8.
|
|
1
|
+
# VEGAS-APP 0.8.7 - 0.9.0 (Февраль, 10, 2026)
|
|
2
2
|
* Исправлены ошибки в разных модулях
|
|
3
3
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# VEGAS-APP 0.8.
|
|
6
|
+
# VEGAS-APP 0.8.3 - 0.8.6 (Январь, 4, 2026)
|
|
7
|
+
* Исправлены ошибки в разных модулях
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# VEGAS-APP 0.8.2 (Январь, 4, 2026)
|
|
7
12
|
* Оптимизирован модуль VGRollup, см. файл readme.md
|
|
8
13
|
* Оптимизирован модуль VGSidebar, см. файл readme.md
|
|
9
14
|
* Оптимизирован модуль VGTabs, см. файл readme.md
|
|
@@ -13,7 +18,7 @@
|
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
16
|
-
# VEGAS-APP 0.8.1 (Январь, 2,
|
|
21
|
+
# VEGAS-APP 0.8.1 (Январь, 2, 2026)
|
|
17
22
|
* Оптимизирован и дополнен модуль VGLoadMore, см. файл readme.md
|
|
18
23
|
|
|
19
24
|
---
|
|
@@ -54,13 +54,18 @@ const _handlersVGSelect = () => {
|
|
|
54
54
|
const container = option.closest(`.${CLASS_NAME_CONTAINER}`);
|
|
55
55
|
const select = container.previousElementSibling;
|
|
56
56
|
const isMultiple = select.multiple;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
|
|
58
|
+
// value у option может отсутствовать/быть пустым -> работаем по индексу
|
|
59
|
+
const indexStr = option.dataset.index;
|
|
60
|
+
const idx = Number.isFinite(Number(indexStr)) ? parseInt(indexStr, 10) : NaN;
|
|
61
|
+
const realOpt = Number.isInteger(idx) ? select.options[idx] : null;
|
|
59
62
|
if (!realOpt) return;
|
|
60
63
|
|
|
61
64
|
const instance = VGSelect.getInstance(container);
|
|
62
65
|
const wasSelected = realOpt.selected;
|
|
63
66
|
|
|
67
|
+
const value = realOpt.value;
|
|
68
|
+
|
|
64
69
|
if (isMultiple) {
|
|
65
70
|
realOpt.selected = !realOpt.selected;
|
|
66
71
|
option.classList.toggle('selected', realOpt.selected);
|
|
@@ -87,7 +92,9 @@ const _handlersVGSelect = () => {
|
|
|
87
92
|
title: option.textContent,
|
|
88
93
|
...Manipulator.get(option)
|
|
89
94
|
});
|
|
90
|
-
|
|
95
|
+
|
|
96
|
+
const closeOnSelect = instance?._params?.close !== false;
|
|
97
|
+
if (closeOnSelect) instance?.hide();
|
|
91
98
|
}
|
|
92
99
|
});
|
|
93
100
|
|
|
@@ -171,9 +178,14 @@ const _handlersVGSelect = () => {
|
|
|
171
178
|
e.stopPropagation();
|
|
172
179
|
const container = this.closest(`.${CLASS_NAME_CONTAINER}`);
|
|
173
180
|
const select = container.previousElementSibling;
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
181
|
+
|
|
182
|
+
const indexStr = btn.dataset.index;
|
|
183
|
+
const idx = Number.isFinite(Number(indexStr)) ? parseInt(indexStr, 10) : NaN;
|
|
184
|
+
const opt = Number.isInteger(idx) ? select.options[idx] : null;
|
|
185
|
+
const item = Number.isInteger(idx)
|
|
186
|
+
? container.querySelector(`.${CLASS_NAME_OPTION}[data-index="${idx}"]`)
|
|
187
|
+
: null;
|
|
188
|
+
|
|
177
189
|
const instance = VGSelect.getInstance(container);
|
|
178
190
|
|
|
179
191
|
if (opt) {
|
|
@@ -181,10 +193,10 @@ const _handlersVGSelect = () => {
|
|
|
181
193
|
if (item) item.classList.remove('selected');
|
|
182
194
|
VGSelect.updateUI(select);
|
|
183
195
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
184
|
-
EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value } });
|
|
196
|
+
EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value: opt.value } });
|
|
185
197
|
|
|
186
|
-
instance?._triggerEvent(EVENT_KEY_DESELECT, { value });
|
|
187
|
-
instance?._callCallback('onDeselect', { value });
|
|
198
|
+
instance?._triggerEvent(EVENT_KEY_DESELECT, { value: opt.value });
|
|
199
|
+
instance?._callCallback('onDeselect', { value: opt.value });
|
|
188
200
|
}
|
|
189
201
|
});
|
|
190
202
|
|
|
@@ -195,10 +207,16 @@ const _handlersVGSelect = () => {
|
|
|
195
207
|
const tags = tagsContainer.querySelectorAll(`.${CLASS_NAME_TAG}`);
|
|
196
208
|
if (tags.length > 0) {
|
|
197
209
|
const lastTag = tags[tags.length - 1];
|
|
198
|
-
const
|
|
210
|
+
const svg = lastTag.querySelector('svg');
|
|
211
|
+
const indexStr = svg?.dataset.index;
|
|
212
|
+
const idx = Number.isFinite(Number(indexStr)) ? parseInt(indexStr, 10) : NaN;
|
|
213
|
+
|
|
199
214
|
const select = input.closest(`.${CLASS_NAME_CONTAINER}`).previousElementSibling;
|
|
200
|
-
const option = select.
|
|
201
|
-
const listItem =
|
|
215
|
+
const option = Number.isInteger(idx) ? select.options[idx] : null;
|
|
216
|
+
const listItem = Number.isInteger(idx)
|
|
217
|
+
? input.closest(`.${CLASS_NAME_CONTAINER}`).querySelector(`.${CLASS_NAME_OPTION}[data-index="${idx}"]`)
|
|
218
|
+
: null;
|
|
219
|
+
|
|
202
220
|
const instance = VGSelect.getInstance(input.closest(`.${CLASS_NAME_CONTAINER}`));
|
|
203
221
|
|
|
204
222
|
if (option) {
|
|
@@ -206,15 +224,14 @@ const _handlersVGSelect = () => {
|
|
|
206
224
|
if (listItem) listItem.classList.remove('selected');
|
|
207
225
|
VGSelect.updateUI(select);
|
|
208
226
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
209
|
-
EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value } });
|
|
227
|
+
EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value: option.value } });
|
|
210
228
|
|
|
211
|
-
instance?._triggerEvent(EVENT_KEY_DESELECT, { value });
|
|
212
|
-
instance?._callCallback('onDeselect', { value });
|
|
229
|
+
instance?._triggerEvent(EVENT_KEY_DESELECT, { value: option.value });
|
|
230
|
+
instance?._callCallback('onDeselect', { value: option.value });
|
|
213
231
|
}
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
});
|
|
217
|
-
|
|
218
235
|
}
|
|
219
236
|
|
|
220
237
|
export default _handlersVGSelect;
|
|
@@ -80,6 +80,7 @@ class VGSelect extends BaseModule {
|
|
|
80
80
|
perPage: 20,
|
|
81
81
|
loadMoreText: 'Загрузить ещё',
|
|
82
82
|
},
|
|
83
|
+
close: true,
|
|
83
84
|
placeholder: '',
|
|
84
85
|
onInit: null,
|
|
85
86
|
onShow: null,
|
|
@@ -103,6 +104,9 @@ class VGSelect extends BaseModule {
|
|
|
103
104
|
this._params.search.loadMoreText = lang_buttons(this._params.lang, NAME)['load-more'] || 'Загрузить еще'
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
this._remoteSearchRequestId = 0;
|
|
108
|
+
this._remoteSearchAbortController = null;
|
|
109
|
+
|
|
106
110
|
this._initObserver();
|
|
107
111
|
this._initLoadMoreButton();
|
|
108
112
|
|
|
@@ -176,21 +180,26 @@ class VGSelect extends BaseModule {
|
|
|
176
180
|
static _createListItems(options, parent, selector) {
|
|
177
181
|
const frag = document.createDocumentFragment();
|
|
178
182
|
const selectedIndex = selector.selectedIndex;
|
|
183
|
+
const hasExplicitSelected = VGSelect.hasExplicitSelectedOption(selector);
|
|
179
184
|
|
|
180
|
-
[...options].forEach((option
|
|
185
|
+
[...options].forEach((option) => {
|
|
181
186
|
if (option.hidden) return;
|
|
182
187
|
|
|
183
|
-
|
|
188
|
+
// value атрибута может не быть или он может быть пустым -> для маппинга используем индекс
|
|
189
|
+
const rawValueAttr = option.getAttribute('value'); // null если атрибута нет
|
|
190
|
+
const value = rawValueAttr == null ? '' : rawValueAttr;
|
|
184
191
|
const text = option.textContent.trim();
|
|
185
192
|
const isEmptyOption = value === '' && text === '';
|
|
186
193
|
|
|
187
194
|
const li = document.createElement('li');
|
|
188
195
|
li.textContent = text;
|
|
196
|
+
li.dataset.index = String(option.index);
|
|
189
197
|
li.dataset.value = value;
|
|
190
198
|
li.classList.add(CLASS_NAME_OPTION);
|
|
191
199
|
Manipulator.set(li, 'data-vg-toggle', 'select-option');
|
|
192
200
|
|
|
193
|
-
|
|
201
|
+
// Если нет явно выбранных option — ничего не подсвечиваем при открытии списка
|
|
202
|
+
if (hasExplicitSelected && option.index === selectedIndex) {
|
|
194
203
|
li.classList.add('selected');
|
|
195
204
|
}
|
|
196
205
|
|
|
@@ -228,6 +237,15 @@ class VGSelect extends BaseModule {
|
|
|
228
237
|
return attr.split(',').map(v => v.trim()).includes(String(value));
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Есть ли в <select> явно отмеченные selected (именно атрибутом selected, а не браузерным дефолтом)
|
|
242
|
+
* @param {HTMLSelectElement} select
|
|
243
|
+
* @returns {boolean}
|
|
244
|
+
*/
|
|
245
|
+
static hasExplicitSelectedOption(select) {
|
|
246
|
+
return Array.from(select.options).some(opt => opt.hasAttribute('selected'));
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
/**
|
|
232
250
|
* Проверяет, выбрана ли допустимая опция (не placeholder)
|
|
233
251
|
* @param {HTMLSelectElement} select - Элемент <select>
|
|
@@ -298,7 +316,8 @@ class VGSelect extends BaseModule {
|
|
|
298
316
|
const index = selector.selectedIndex;
|
|
299
317
|
const option = index >= 0 ? selector.options[index] : null;
|
|
300
318
|
const text = option?.textContent.trim() || '';
|
|
301
|
-
const
|
|
319
|
+
const hasExplicitSelected = this.hasExplicitSelectedOption(selector);
|
|
320
|
+
const showPlaceholder = placeholder && (!hasExplicitSelected || !this.hasSelectedValidOption(selector));
|
|
302
321
|
|
|
303
322
|
current.innerHTML = showPlaceholder
|
|
304
323
|
? `<span class="${CLASS_NAME_PLACEHOLDER}">${placeholder}</span>`
|
|
@@ -568,7 +587,8 @@ class VGSelect extends BaseModule {
|
|
|
568
587
|
selected.forEach(opt => {
|
|
569
588
|
const tag = document.createElement('div');
|
|
570
589
|
tag.classList.add(CLASS_NAME_TAG);
|
|
571
|
-
|
|
590
|
+
// data-index — основной ключ (работает даже если у option нет value атрибута)
|
|
591
|
+
tag.innerHTML = `<span>${opt.textContent}</span><svg class="${CLASS_NAME_TAG_REMOVE}" data-index="${opt.index}" data-value="${opt.getAttribute('value') ?? ''}" width="14" height="14" viewBox="0 0 14 14"><line x1="2" y1="2" x2="12" y2="12" stroke="currentColor"/><line x1="12" y1="2" x2="2" y2="12" stroke="currentColor"/></svg>`;
|
|
572
592
|
tags.insertBefore(tag, input);
|
|
573
593
|
});
|
|
574
594
|
}
|
|
@@ -577,7 +597,9 @@ class VGSelect extends BaseModule {
|
|
|
577
597
|
const option = index >= 0 ? select.options[index] : null;
|
|
578
598
|
const text = option?.textContent.trim() || '';
|
|
579
599
|
const value = option?.value;
|
|
580
|
-
|
|
600
|
+
|
|
601
|
+
const hasExplicitSelected = this.hasExplicitSelectedOption(select);
|
|
602
|
+
const showPlaceholder = placeholder && (!hasExplicitSelected || !value || this.isPlaceholderValue(select, value) || !text);
|
|
581
603
|
|
|
582
604
|
const oldText = current.textContent;
|
|
583
605
|
const newText = showPlaceholder ? placeholder : text || '-';
|
|
@@ -812,19 +834,38 @@ class VGSelect extends BaseModule {
|
|
|
812
834
|
const searchInput = this._element.querySelector(SELECTOR_SEARCH_INPUT);
|
|
813
835
|
const wasOpen = this._isShown();
|
|
814
836
|
|
|
837
|
+
// Обновляем текущее "желательное" состояние
|
|
815
838
|
this._searchTerm = term;
|
|
816
839
|
this._currentPage = 1;
|
|
817
840
|
this._totalPages = null;
|
|
818
841
|
|
|
842
|
+
// Отменяем предыдущий запрос (если ещё летит)
|
|
843
|
+
if (this._remoteSearchAbortController) {
|
|
844
|
+
this._remoteSearchAbortController.abort();
|
|
845
|
+
}
|
|
846
|
+
this._remoteSearchAbortController = new AbortController();
|
|
847
|
+
|
|
848
|
+
// Метим этот запрос как "последний"
|
|
849
|
+
const requestId = ++this._remoteSearchRequestId;
|
|
850
|
+
const signal = this._remoteSearchAbortController.signal;
|
|
851
|
+
|
|
819
852
|
this._showLoading(true);
|
|
820
853
|
this._hideLoadMoreButton(true);
|
|
821
854
|
|
|
822
855
|
fetch(url, {
|
|
823
856
|
method,
|
|
824
|
-
headers: { 'Content-Type': 'application/json' }
|
|
857
|
+
headers: { 'Content-Type': 'application/json' },
|
|
858
|
+
signal
|
|
825
859
|
})
|
|
826
860
|
.then(res => res.json())
|
|
827
861
|
.then(data => {
|
|
862
|
+
// Если прилетел не самый свежий ответ — игнорируем
|
|
863
|
+
if (requestId !== this._remoteSearchRequestId) return;
|
|
864
|
+
|
|
865
|
+
// Если пользователь уже ввёл другой текст — тоже игнорируем
|
|
866
|
+
const liveTerm = searchInput ? searchInput.value.trim() : '';
|
|
867
|
+
if (liveTerm !== term) return;
|
|
868
|
+
|
|
828
869
|
const select = this._element.previousElementSibling;
|
|
829
870
|
if (!select) return;
|
|
830
871
|
|
|
@@ -850,19 +891,25 @@ class VGSelect extends BaseModule {
|
|
|
850
891
|
this._hideLoadMoreButton(false);
|
|
851
892
|
}
|
|
852
893
|
|
|
894
|
+
// Важно: НЕ перетираем searchInput.value "term"-ом — это и вызывает глюк при гонках.
|
|
853
895
|
if (wasOpen && searchInput) {
|
|
854
|
-
searchInput.value = term;
|
|
855
896
|
searchInput.focus();
|
|
856
897
|
this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_ACTIVE);
|
|
857
898
|
}
|
|
858
899
|
})
|
|
859
900
|
.catch(err => {
|
|
901
|
+
// Abort — нормальная ситуация при быстром вводе
|
|
902
|
+
if (err && (err.name === 'AbortError')) return;
|
|
903
|
+
|
|
860
904
|
console.error('VGSelect: Remote search error', err);
|
|
861
905
|
this._triggerEvent(EVENT_KEY_ERROR, { error: 'Search request failed', term });
|
|
862
906
|
this._hideLoadMoreButton(false);
|
|
863
907
|
})
|
|
864
908
|
.finally(() => {
|
|
865
|
-
|
|
909
|
+
// Лоадер убираем только для последнего актуального запроса
|
|
910
|
+
if (requestId === this._remoteSearchRequestId) {
|
|
911
|
+
this._showLoading(false);
|
|
912
|
+
}
|
|
866
913
|
});
|
|
867
914
|
}
|
|
868
915
|
|
|
@@ -168,6 +168,11 @@ const MILLISECONDS_MULTIPLIER = 1000;
|
|
|
168
168
|
* @param {number} [timeoutMs]
|
|
169
169
|
*/
|
|
170
170
|
const executeAfterTransition = (callback, element, waitForTransition = true, timeoutMs) => {
|
|
171
|
+
if (!element) {
|
|
172
|
+
execute(callback);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
if (!waitForTransition) {
|
|
172
177
|
execute(callback);
|
|
173
178
|
return;
|
|
@@ -213,6 +218,7 @@ const getTransitionDurationFromElement = (element) => {
|
|
|
213
218
|
* @param {Element} element
|
|
214
219
|
*/
|
|
215
220
|
const triggerTransitionEnd = (element) => {
|
|
221
|
+
if (!element) return;
|
|
216
222
|
element.dispatchEvent(new Event(TRANSITION_END));
|
|
217
223
|
};
|
|
218
224
|
|