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.3 - 0.8.6 (Январь, 4, 2025)
1
+ # VEGAS-APP 0.8.7 - 0.9.0 (Февраль, 10, 2026)
2
2
  * Исправлены ошибки в разных модулях
3
3
 
4
4
  ---
5
5
 
6
- # VEGAS-APP 0.8.2 (Январь, 4, 2025)
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, 2025)
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
- const value = option.dataset.value;
58
- const realOpt = select.querySelector(`option[value="${CSS.escape(value)}"]`);
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
- instance?.hide();
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
- const value = btn.dataset.value;
175
- const opt = select.querySelector(`option[value="${CSS.escape(value)}"]`);
176
- const item = container.querySelector(`.${CLASS_NAME_OPTION}[data-value="${CSS.escape(value)}"]`);
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 value = lastTag.querySelector('svg')?.dataset.value;
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.querySelector(`option[value="${CSS.escape(value)}"]`);
201
- const listItem = select.closest(`.${CLASS_NAME_CONTAINER}`).querySelector(`.${CLASS_NAME_OPTION}[data-value="${CSS.escape(value)}"]`);
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, i) => {
185
+ [...options].forEach((option) => {
181
186
  if (option.hidden) return;
182
187
 
183
- const value = option.value || '';
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
- if (i === selectedIndex) {
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 showPlaceholder = placeholder && !this.hasSelectedValidOption(selector);
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
- tag.innerHTML = `<span>${opt.textContent}</span><svg class="${CLASS_NAME_TAG_REMOVE}" data-value="${opt.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>`;
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
- const showPlaceholder = placeholder && (!value || this.isPlaceholderValue(select, value) || !text);
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
- this._showLoading(false);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgapp",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "",
5
5
  "author": {
6
6
  "name": "Vegas Studio",