vgapp 1.5.7 → 1.5.8

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.
Files changed (109) hide show
  1. package/app/langs/en/buttons.json +17 -17
  2. package/app/langs/en/messages.json +36 -36
  3. package/app/langs/ru/buttons.json +17 -17
  4. package/app/langs/ru/messages.json +1 -1
  5. package/app/modules/vgalert/js/vgalert.js +7 -7
  6. package/app/modules/vgalert/scss/_variables.scss +16 -16
  7. package/app/modules/vgcollapse/scss/_variables.scss +7 -7
  8. package/app/modules/vgcollapse/scss/vgcollapse.scss +5 -5
  9. package/app/modules/vgdropdown/scss/_variables.scss +14 -14
  10. package/app/modules/vgdropdown/scss/vgdropdown.scss +5 -5
  11. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  12. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  13. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  14. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  15. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  16. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  17. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  18. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  19. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  20. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  21. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  22. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  23. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  24. package/app/modules/vgfilepreview/readme.md +68 -68
  25. package/app/modules/vgfiles/js/base.js +198 -198
  26. package/app/modules/vgfiles/js/droppable.js +17 -17
  27. package/app/modules/vgfiles/js/render.js +153 -153
  28. package/app/modules/vgfiles/js/sortable.js +52 -52
  29. package/app/modules/vgfiles/js/vgfiles.js +43 -43
  30. package/app/modules/vgfiles/readme.md +123 -123
  31. package/app/modules/vgfiles/scss/_mixins.scss +13 -13
  32. package/app/modules/vgfiles/scss/_variables.scss +10 -10
  33. package/app/modules/vgfiles/scss/vgfiles.scss +28 -28
  34. package/app/modules/vgformsender/js/hideshowpass.js +22 -22
  35. package/app/modules/vgformsender/readme.md +1 -1
  36. package/app/modules/vgformsender/scss/_variables.scss +5 -5
  37. package/app/modules/vgformsender/scss/vgformsender.scss +42 -42
  38. package/app/modules/vglawcookie/js/vglawcookie.js +19 -19
  39. package/app/modules/vglawcookie/scss/_variables.scss +13 -13
  40. package/app/modules/vglawcookie/scss/vglawcookie.scss +5 -5
  41. package/app/modules/vgmodal/js/vgmodal.interaction.js +216 -216
  42. package/app/modules/vgmodal/js/vgmodal.js +9 -9
  43. package/app/modules/vgmodal/js/vgmodal.minimized.js +227 -227
  44. package/app/modules/vgmodal/scss/_variables.scss +17 -17
  45. package/app/modules/vgmodal/scss/vgmodal.scss +77 -77
  46. package/app/modules/vgnav/js/vgnav.js +62 -62
  47. package/app/modules/vgnav/scss/_breakpoints.scss +36 -36
  48. package/app/modules/vgnav/scss/_hamburger.scss +5 -5
  49. package/app/modules/vgnav/scss/_placement.scss +5 -5
  50. package/app/modules/vgnav/scss/_toggle.scss +5 -5
  51. package/app/modules/vgnav/scss/_variables.scss +26 -26
  52. package/app/modules/vgnav/scss/vgnav.scss +41 -41
  53. package/app/modules/vgnestable/README.md +39 -39
  54. package/app/modules/vgrangeslider/js/skins.js +8 -8
  55. package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -29
  56. package/app/modules/vgrangeslider/scss/_variables.scss +55 -55
  57. package/app/modules/vgrangeslider/scss/vgrangeslider.scss +239 -239
  58. package/app/modules/vgrollup/js/vgrollup.js +15 -15
  59. package/app/modules/vgrollup/scss/_variables.scss +15 -15
  60. package/app/modules/vgrollup/scss/vgrollup.scss +6 -6
  61. package/app/modules/vgselect/js/handlers.js +6 -6
  62. package/app/modules/vgselect/js/vgselect.js +167 -123
  63. package/app/modules/vgselect/readme.md +11 -11
  64. package/app/modules/vgselect/scss/_variables.scss +51 -51
  65. package/app/modules/vgselect/scss/vgselect.scss +1 -1
  66. package/app/modules/vgsidebar/js/vgsidebar.js +21 -21
  67. package/app/modules/vgsidebar/scss/_variables.scss +10 -10
  68. package/app/modules/vgsidebar/scss/vgsidebar.scss +5 -5
  69. package/app/modules/vgspy/js/vgspy.js +43 -43
  70. package/app/modules/vgtable/js/_filters.js +10 -6
  71. package/app/modules/vgtable/js/_options.js +16 -16
  72. package/app/modules/vgtable/js/_pagination.js +118 -118
  73. package/app/modules/vgtable/js/_responsive.js +79 -79
  74. package/app/modules/vgtable/js/_sticky-header.js +349 -280
  75. package/app/modules/vgtable/js/vgtable.js +98 -98
  76. package/app/modules/vgtable/scss/_sorting.scss +6 -6
  77. package/app/modules/vgtable/scss/_sticky-header.scss +31 -31
  78. package/app/modules/vgtable/scss/_variables.scss +6 -4
  79. package/app/modules/vgtable/scss/vgtable.scss +8 -4
  80. package/app/modules/vgtabs/js/vgtabs.js +91 -91
  81. package/app/modules/vgtabs/scss/_variables.scss +23 -23
  82. package/app/modules/vgtabs/scss/vgtabs.scss +5 -5
  83. package/app/modules/vgtoast/js/vgtoast.js +4 -4
  84. package/app/modules/vgtoast/scss/_variables.scss +13 -13
  85. package/app/modules/vgtoast/scss/vgtoast.scss +5 -5
  86. package/app/modules/vgtooltip/js/vgtooltip.js +127 -127
  87. package/app/modules/vgtooltip/scss/_variables.scss +10 -10
  88. package/app/modules/vgtooltip/scss/vgtooltip.scss +5 -5
  89. package/app/utils/js/components/audio-metadata.js +240 -240
  90. package/app/utils/js/components/backdrop.js +116 -116
  91. package/app/utils/js/components/file-icon.js +7 -7
  92. package/app/utils/js/components/file-preview.js +304 -304
  93. package/app/utils/js/components/responsive.js +165 -165
  94. package/app/utils/js/components/sanitize.js +150 -150
  95. package/app/utils/js/components/video-metadata.js +140 -140
  96. package/app/utils/scss/_backdrop.scss +5 -5
  97. package/app/utils/scss/_theme-default.scss +12 -12
  98. package/app/utils/scss/_theme.scss +7 -7
  99. package/app/utils/scss/default.scss +9 -9
  100. package/app/utils/scss/functions.scss +21 -21
  101. package/app/utils/scss/index.scss +4 -4
  102. package/app/utils/scss/mixin.scss +16 -16
  103. package/app/utils/scss/variables.scss +147 -147
  104. package/build/vgapp.css +1 -1
  105. package/build/vgapp.css.map +1 -1
  106. package/build/vgapp.js +1 -1
  107. package/build/vgapp.js.map +1 -1
  108. package/index.js +7 -7
  109. package/package.json +1 -1
@@ -1,14 +1,14 @@
1
- /**
2
- * Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
3
- * Возможности: поиск, группы, дерево, теги, AJAX-пагинация, события и освобождение ресурсов.
4
- */
5
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
3
+ * Возможности: поиск, группы, дерево, теги, ограничение высоты, AJAX-пагинация, события и освобождение ресурсов.
4
+ */
5
+ import BaseModule from "../../base-module";
6
6
  import {
7
7
  isDisabled,
8
8
  isEmptyObj,
9
9
  mergeDeepObject,
10
- normalizeData,
11
- transliterate,
10
+ normalizeData,
11
+ transliterate,
12
12
  } from "../../../utils/js/functions";
13
13
  import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
14
14
  import EventHandler from "../../../utils/js/dom/event";
@@ -35,7 +35,9 @@ const CLASS_NAME_TAG = 'vg-select-tag';
35
35
  const CLASS_NAME_TAG_REMOVE = 'vg-select-tag-remove';
36
36
  const CLASS_NAME_LOAD_MORE = 'vg-select-load-more';
37
37
  const CLASS_NAME_LOADING = 'vg-select-loading';
38
- const CLASS_NAME_DROP_UP = 'drop-up';
38
+ const CLASS_NAME_DROP_UP = 'drop-up';
39
+
40
+ const DEFAULT_LIST_MAX_HEIGHT = 300;
39
41
 
40
42
  const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
41
43
  const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
@@ -78,11 +80,12 @@ class VGSelect extends BaseModule {
78
80
  autosearch: true,
79
81
  // Dropdown placement behavior:
80
82
  // - none: default CSS positioning (no JS)
81
- // - auto (default): choose top/bottom based on available space in overflow ancestor/viewport
83
+ // - auto (default): choose top/bottom based on available space in overflow ancestor/viewport
82
84
  // - top: force open upwards
83
85
  // - bottom: force open downwards
84
86
  position: 'auto',
85
- search: {
87
+ height: true,
88
+ search: {
86
89
  enabled: false,
87
90
  route: '',
88
91
  remote: false,
@@ -109,11 +112,12 @@ class VGSelect extends BaseModule {
109
112
  }, params));
110
113
 
111
114
  this._observer = null;
112
- this._observerTimeout = null;
113
- this._searchTimeout = null;
114
- this._disposed = false;
115
+ this._observerTimeout = null;
116
+ this._searchTimeout = null;
117
+ this._disposed = false;
115
118
  this._visibilityTransitionId = 0;
116
- this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
119
+ this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
120
+ this._applyListMaxHeight();
117
121
  this._searchTerm = '';
118
122
  this._currentPage = 1;
119
123
  this._totalPages = null;
@@ -159,9 +163,9 @@ class VGSelect extends BaseModule {
159
163
  * @param {HTMLElement} drop - Контейнер выпадающего списка
160
164
  * @returns {HTMLElement} - Обновлённый список
161
165
  */
162
- static buildListOptions(selector, drop, params = {}) {
163
- let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
164
- const controls = list ? [...list.querySelectorAll(`.${CLASS_NAME_LOAD_MORE}, .${CLASS_NAME_LOADING}`)] : [];
166
+ static buildListOptions(selector, drop, params = {}) {
167
+ let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
168
+ const controls = list ? [...list.querySelectorAll(`.${CLASS_NAME_LOAD_MORE}, .${CLASS_NAME_LOADING}`)] : [];
165
169
  if (!list) {
166
170
  list = document.createElement('ul');
167
171
  Classes.add(list, CLASS_NAME_LIST);
@@ -198,8 +202,8 @@ class VGSelect extends BaseModule {
198
202
  });
199
203
  }
200
204
 
201
- controls.forEach(control => list.appendChild(control));
202
- return list;
205
+ controls.forEach(control => list.appendChild(control));
206
+ return list;
203
207
  }
204
208
 
205
209
  /**
@@ -237,9 +241,6 @@ class VGSelect extends BaseModule {
237
241
  li.style.paddingLeft = `${16 + (level * 16)}px`;
238
242
  }
239
243
 
240
- // Раньше подсветка зависела от "явно выбранных" option (атрибут selected),
241
- // из-за этого UI мог показывать placeholder, но DOM считал, что выбрана 1-я опция.
242
- // Подсвечиваем по фактическому состоянию option.selected.
243
244
  if (option.selected) {
244
245
  li.classList.add('selected');
245
246
  }
@@ -468,16 +469,16 @@ class VGSelect extends BaseModule {
468
469
  search.appendChild(searchInput);
469
470
  this._drop.insertBefore(search, this._drop.firstChild);
470
471
 
471
- searchInput.addEventListener('input', (e) => {
472
+ searchInput.addEventListener('input', (e) => {
472
473
  const term = e.target.value.trim();
473
474
  const params = this._params;
474
475
 
475
476
  this._callCallback('onSearch', { term });
476
477
  if (params.search.remote && params.search.route) {
477
- clearTimeout(this._searchTimeout);
478
- this._remoteSearchAbortController?.abort();
479
- if (term.length < (params.search.minterm || 1)) return;
480
- this._searchTimeout = setTimeout(() => {
478
+ clearTimeout(this._searchTimeout);
479
+ this._remoteSearchAbortController?.abort();
480
+ if (term.length < (params.search.minterm || 1)) return;
481
+ this._searchTimeout = setTimeout(() => {
481
482
  this._fetchRemoteData(term);
482
483
  }, params.search.delay || 300);
483
484
  } else {
@@ -528,7 +529,7 @@ class VGSelect extends BaseModule {
528
529
  if (searchInput) searchInput.focus();
529
530
 
530
531
  return this._queueCallback(() => {
531
- if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
532
+ if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
532
533
  this._updateDropdownPlacement();
533
534
  EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
534
535
  this._triggerEvent(EVENT_KEY_OPEN);
@@ -560,7 +561,7 @@ class VGSelect extends BaseModule {
560
561
  this._element.querySelector(SELECTOR_DATA_TOGGLE).setAttribute('aria-expanded', 'false');
561
562
 
562
563
  this._queueCallback(() => {
563
- if (this._disposed || transitionId !== this._visibilityTransitionId) return;
564
+ if (this._disposed || transitionId !== this._visibilityTransitionId) return;
564
565
  this._element.classList.remove(CLASS_NAME_SHOW);
565
566
  EventHandler.trigger(this._element, EVENT_KEY_HIDDEN, relatedTarget);
566
567
  this._triggerEvent(EVENT_KEY_CLOSE);
@@ -654,21 +655,21 @@ class VGSelect extends BaseModule {
654
655
  /**
655
656
  * Освобождает ресурсы (отключает observer, очищает таймеры)
656
657
  */
657
- dispose() {
658
- if (this._disposed) return;
658
+ dispose() {
659
+ if (this._disposed) return;
659
660
  this._visibilityTransitionId++;
660
661
  if (this._observer) {
661
662
  this._observer.disconnect();
662
663
  this._observer = null;
663
664
  }
664
665
  clearTimeout(this._observerTimeout);
665
- this._observerTimeout = null;
666
- clearTimeout(this._searchTimeout);
667
- this._remoteSearchAbortController?.abort();
668
- if (this._isShown() && 'ontouchstart' in document.documentElement) document.body.style.pointerEvents = '';
669
- this._teardownDropdownPlacement();
670
- super.dispose();
671
- this._disposed = true;
666
+ this._observerTimeout = null;
667
+ clearTimeout(this._searchTimeout);
668
+ this._remoteSearchAbortController?.abort();
669
+ if (this._isShown() && 'ontouchstart' in document.documentElement) document.body.style.pointerEvents = '';
670
+ this._teardownDropdownPlacement();
671
+ super.dispose();
672
+ this._disposed = true;
672
673
  }
673
674
 
674
675
  _setupDropdownPlacement() {
@@ -688,7 +689,7 @@ class VGSelect extends BaseModule {
688
689
  this._updateDropdownPlacement();
689
690
  }
690
691
 
691
- _teardownDropdownPlacement() {
692
+ _teardownDropdownPlacement() {
692
693
  window.removeEventListener('resize', this._boundUpdateDropdownPlacement);
693
694
  window.removeEventListener('scroll', this._boundUpdateDropdownPlacement);
694
695
 
@@ -697,16 +698,63 @@ class VGSelect extends BaseModule {
697
698
  }
698
699
  this._positioningScrollParent = null;
699
700
 
700
- // Reset to stylesheet defaults.
701
- this._element.classList.remove(CLASS_NAME_DROP_UP);
702
- this._element.style.removeProperty('--vg-select-list-max-height');
703
- }
701
+ // Restore the configured limit after a temporary viewport clamp.
702
+ this._element.classList.remove(CLASS_NAME_DROP_UP);
703
+ this._applyListMaxHeight();
704
+ }
705
+
706
+ /**
707
+ * Возвращает пользовательское ограничение списка в пикселях.
708
+ * true использует стандартные 300px, false снимает ограничение.
709
+ * @returns {number|null}
710
+ * @private
711
+ */
712
+ _getListMaxHeight() {
713
+ const raw = this._params?.height;
714
+ if (raw === false || raw === null) return null;
715
+ if (raw === true || typeof raw === 'undefined' || raw === '') {
716
+ const cssHeight = parseFloat(window.getComputedStyle(this._element)
717
+ .getPropertyValue('--vg-select-list-max-height'));
718
+ return Number.isFinite(cssHeight) && cssHeight > 0 ? cssHeight : DEFAULT_LIST_MAX_HEIGHT;
719
+ }
720
+
721
+ const height = Number(raw);
722
+ return Number.isFinite(height) && height > 0 ? height : DEFAULT_LIST_MAX_HEIGHT;
723
+ }
724
+
725
+ /**
726
+ * Применяет базовое либо уменьшенное под доступное пространство ограничение.
727
+ * @param {number|null} [available=null] - Доступная высота дропа
728
+ * @private
729
+ */
730
+ _applyListMaxHeight(available = null) {
731
+ if (!this._element) return;
732
+
733
+ const raw = this._params?.height;
734
+ const usesStylesheetDefault = raw === true || typeof raw === 'undefined' || raw === '';
735
+ if (usesStylesheetDefault) {
736
+ this._element.style.removeProperty('--vg-select-list-max-height');
737
+ if (!Number.isFinite(available)) return;
738
+ }
739
+
740
+ const configured = this._getListMaxHeight();
741
+ if (configured === null) {
742
+ this._element.style.setProperty('--vg-select-list-max-height', 'none');
743
+ return;
744
+ }
745
+
746
+ const availableHeight = Number.isFinite(available)
747
+ ? Math.max(80, Math.floor(available - 12))
748
+ : configured;
749
+ const maxHeight = Math.min(configured, availableHeight);
750
+ this._element.style.setProperty('--vg-select-list-max-height', `${maxHeight}px`);
751
+ }
704
752
 
705
753
  _getPositionMode() {
706
754
  const raw = this._params?.position;
707
- const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
755
+ const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
708
756
  if (mode === 'auto' || mode === 'top' || mode === 'bottom' || mode === 'none') return mode;
709
- return 'auto';
757
+ return 'auto';
710
758
  }
711
759
 
712
760
  _getOverflowAncestor(startEl) {
@@ -764,12 +812,8 @@ class VGSelect extends BaseModule {
764
812
 
765
813
  const available = openUp ? spaceAbove : spaceBelow;
766
814
 
767
- // Give the dropdown some breathing room (borders/margins) and clamp.
768
- const listMax = Math.max(80, Math.floor(available - 12));
769
- if (Number.isFinite(listMax) && listMax > 0) {
770
- this._element.style.setProperty('--vg-select-list-max-height', `${listMax}px`);
771
- }
772
- }
815
+ this._applyListMaxHeight(available);
816
+ }
773
817
 
774
818
  /**
775
819
  * Удаляет кастомный UI
@@ -777,28 +821,28 @@ class VGSelect extends BaseModule {
777
821
  */
778
822
  static destroy(select) {
779
823
  const container = select.nextElementSibling;
780
- if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
781
- VGSelect.getInstance(container)?.dispose();
782
- container.remove();
783
- }
784
- delete select.dataset.inited;
824
+ if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
825
+ VGSelect.getInstance(container)?.dispose();
826
+ container.remove();
827
+ }
828
+ delete select.dataset.inited;
785
829
  }
786
830
 
787
831
  /**
788
832
  * Обновляет отображаемое значение (текст, теги)
789
833
  * @param {HTMLSelectElement} select - Исходный <select>
790
834
  */
791
- static updateUI(select) {
835
+ static updateUI(select) {
792
836
  const container = select.nextElementSibling;
793
837
  if (!container || !container.classList.contains(CLASS_NAME_CONTAINER)) return;
794
838
 
795
839
  const current = container.querySelector(SELECTOR_CURRENT);
796
840
  const placeholder = select.dataset.placeholder || '';
797
841
  const isMultiple = select.multiple;
798
- const instance = VGSelect.getInstance(container);
799
- container.querySelectorAll(`.${CLASS_NAME_OPTION}`).forEach(item => {
800
- item.classList.toggle('selected', !!select.options[Number(item.dataset.index)]?.selected);
801
- });
842
+ const instance = VGSelect.getInstance(container);
843
+ container.querySelectorAll(`.${CLASS_NAME_OPTION}`).forEach(item => {
844
+ item.classList.toggle('selected', !!select.options[Number(item.dataset.index)]?.selected);
845
+ });
802
846
 
803
847
  if (isMultiple) {
804
848
  const tags = current.querySelector(`.${CLASS_NAME_TAGS}`);
@@ -815,12 +859,12 @@ class VGSelect extends BaseModule {
815
859
  instance?._callCallback('onClear');
816
860
  }
817
861
 
818
- if (selected.length === 0) {
819
- input.placeholder = placeholder;
820
- const label = document.createElement('span');
821
- label.className = CLASS_NAME_PLACEHOLDER;
822
- label.textContent = placeholder;
823
- tags.insertBefore(label, input);
862
+ if (selected.length === 0) {
863
+ input.placeholder = placeholder;
864
+ const label = document.createElement('span');
865
+ label.className = CLASS_NAME_PLACEHOLDER;
866
+ label.textContent = placeholder;
867
+ tags.insertBefore(label, input);
824
868
  } else {
825
869
  input.placeholder = '';
826
870
  selected.forEach(opt => {
@@ -1005,16 +1049,16 @@ class VGSelect extends BaseModule {
1005
1049
  _initLoadMoreButton() {
1006
1050
  if (!this._params.search?.pagination || !this._params.search.remote) return;
1007
1051
 
1008
- const list = this._element.querySelector(SELECTOR_LIST);
1009
- if (!list) return;
1010
- if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
1052
+ const list = this._element.querySelector(SELECTOR_LIST);
1053
+ if (!list) return;
1054
+ if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
1011
1055
 
1012
1056
  const btn = document.createElement('li');
1013
1057
  btn.className = CLASS_NAME_LOAD_MORE;
1014
1058
  btn.style.textAlign = 'center';
1015
1059
  btn.style.padding = '8px';
1016
1060
  btn.style.cursor = 'pointer';
1017
- btn.style.color = 'var(--vg-primary-color, #007bff)';
1061
+ btn.style.color = 'var(--vg-primary-color, #007bff)';
1018
1062
  btn.style.fontSize = '14px';
1019
1063
  btn.style.fontWeight = '500';
1020
1064
  btn.textContent = this._params.search.loadMoreText;
@@ -1065,10 +1109,10 @@ class VGSelect extends BaseModule {
1065
1109
  * Загружает следующую страницу данных по клику
1066
1110
  * @private
1067
1111
  */
1068
- async _loadNextPage() {
1112
+ async _loadNextPage() {
1069
1113
  const { route, pageParam = 'page', termParam = 'q', perpage = 20 } = this._params.search;
1070
- const nextPage = this._currentPage + 1;
1071
- const requestId = this._remoteSearchRequestId;
1114
+ const nextPage = this._currentPage + 1;
1115
+ const requestId = this._remoteSearchRequestId;
1072
1116
 
1073
1117
  const url = new URL(route, window.location.origin);
1074
1118
  url.searchParams.set(termParam, this._searchTerm);
@@ -1079,11 +1123,11 @@ class VGSelect extends BaseModule {
1079
1123
  this._showLoading(true);
1080
1124
  this._hideLoadMoreButton(true);
1081
1125
 
1082
- try {
1083
- const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
1084
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1085
- const data = await res.json();
1086
- if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1126
+ try {
1127
+ const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
1128
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1129
+ const data = await res.json();
1130
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1087
1131
 
1088
1132
  if (Array.isArray(data.results)) {
1089
1133
  VGSelect.addOptions(this._element.previousElementSibling, data, { preserve: true });
@@ -1098,16 +1142,16 @@ class VGSelect extends BaseModule {
1098
1142
  this._callCallback('onLoadNext', { page: this._currentPage, data });
1099
1143
  this._triggerEvent(EVENT_KEY_LOAD_NEXT, { page: this._currentPage, term: this._searchTerm });
1100
1144
  }
1101
- } catch (err) {
1102
- if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1145
+ } catch (err) {
1146
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1103
1147
  console.error('VGSelect: Failed to load next page', err);
1104
1148
  this._triggerEvent(EVENT_KEY_ERROR, { error: 'Pagination fetch failed', term: this._searchTerm });
1105
1149
  this._hideLoadMoreButton(false); // оставить кнопку при ошибке
1106
- } finally {
1107
- if (!this._disposed && requestId === this._remoteSearchRequestId) {
1108
- this._showLoading(false);
1109
- this._loading = false;
1110
- }
1150
+ } finally {
1151
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1152
+ this._showLoading(false);
1153
+ this._loading = false;
1154
+ }
1111
1155
  }
1112
1156
  }
1113
1157
 
@@ -1129,8 +1173,8 @@ class VGSelect extends BaseModule {
1129
1173
  // Обновляем текущее "желательное" состояние
1130
1174
  this._searchTerm = term;
1131
1175
  this._currentPage = 1;
1132
- this._totalPages = null;
1133
- this._loading = false;
1176
+ this._totalPages = null;
1177
+ this._loading = false;
1134
1178
 
1135
1179
  // Отменяем предыдущий запрос (если ещё летит)
1136
1180
  if (this._remoteSearchAbortController) {
@@ -1150,14 +1194,14 @@ class VGSelect extends BaseModule {
1150
1194
  headers: { 'Content-Type': 'application/json' },
1151
1195
  signal
1152
1196
  })
1153
- .then(res => {
1154
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1155
- return res.json();
1156
- })
1197
+ .then(res => {
1198
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1199
+ return res.json();
1200
+ })
1157
1201
  .then(data => {
1158
1202
  // Если прилетел не самый свежий ответ — игнорируем
1159
- if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1160
- if (!Array.isArray(data.results)) throw new Error('Invalid results');
1203
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1204
+ if (!Array.isArray(data.results)) throw new Error('Invalid results');
1161
1205
 
1162
1206
  // Если пользователь уже ввёл другой текст — тоже игнорируем
1163
1207
  const liveTerm = searchInput ? searchInput.value.trim() : '';
@@ -1206,8 +1250,8 @@ class VGSelect extends BaseModule {
1206
1250
  this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_ACTIVE);
1207
1251
  }
1208
1252
  })
1209
- .catch(err => {
1210
- if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1253
+ .catch(err => {
1254
+ if (this._disposed || requestId !== this._remoteSearchRequestId) return;
1211
1255
  // Abort — нормальная ситуация при быстром вводе
1212
1256
  if (err && (err.name === 'AbortError')) return;
1213
1257
 
@@ -1217,7 +1261,7 @@ class VGSelect extends BaseModule {
1217
1261
  })
1218
1262
  .finally(() => {
1219
1263
  // Лоадер убираем только для последнего актуального запроса
1220
- if (!this._disposed && requestId === this._remoteSearchRequestId) {
1264
+ if (!this._disposed && requestId === this._remoteSearchRequestId) {
1221
1265
  this._showLoading(false);
1222
1266
  }
1223
1267
  });
@@ -1316,9 +1360,9 @@ class VGSelect extends BaseModule {
1316
1360
 
1317
1361
  if (isRebuild) {
1318
1362
  const drop = container.querySelector(`.${CLASS_NAME_DROPDOWN}`);
1319
- VGSelect.buildListOptions(select, drop, instance?._params || {});
1320
- instance?._syncSearch();
1321
- this.updateUI(select);
1363
+ VGSelect.buildListOptions(select, drop, instance?._params || {});
1364
+ instance?._syncSearch();
1365
+ this.updateUI(select);
1322
1366
  instance?._triggerEvent(EVENT_KEY_REBUILD);
1323
1367
  } else {
1324
1368
  this.updateUI(select);
@@ -1330,29 +1374,29 @@ class VGSelect extends BaseModule {
1330
1374
  * @param {string} term - Поисковый запрос
1331
1375
  * @private
1332
1376
  */
1333
- _filterLocalOptions(term) {
1334
- const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
1335
- const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
1336
- term = term.toLowerCase();
1337
- const search = [term, transliterate(term), transliterate(term, true)];
1338
- let results = 0;
1339
- options.forEach(el => {
1340
- const text = el.textContent.toLowerCase();
1341
- const option = this._element.previousElementSibling.options[Number(el.dataset.index)];
1342
- const empty = option?.value === '' && text.trim() === '';
1343
- const visible = !empty && (!term || search.some(value => text.includes(value)));
1344
- el.hidden = !visible;
1345
- el.style.display = visible ? '' : 'none';
1346
- if (visible) results++;
1347
- });
1348
- list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`).forEach(group => {
1349
- const visible = [...group.querySelectorAll(`.${CLASS_NAME_OPTION}`)].some(option => !option.hidden);
1350
- group.hidden = !visible;
1351
- group.style.display = visible ? '' : 'none';
1352
- });
1353
- this._triggerEvent(`${NAME_KEY}.search`, {query: term, results});
1354
- this._callCallback('onSearch', {query: term, results});
1355
- }
1377
+ _filterLocalOptions(term) {
1378
+ const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
1379
+ const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
1380
+ term = term.toLowerCase();
1381
+ const search = [term, transliterate(term), transliterate(term, true)];
1382
+ let results = 0;
1383
+ options.forEach(el => {
1384
+ const text = el.textContent.toLowerCase();
1385
+ const option = this._element.previousElementSibling.options[Number(el.dataset.index)];
1386
+ const empty = option?.value === '' && text.trim() === '';
1387
+ const visible = !empty && (!term || search.some(value => text.includes(value)));
1388
+ el.hidden = !visible;
1389
+ el.style.display = visible ? '' : 'none';
1390
+ if (visible) results++;
1391
+ });
1392
+ list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`).forEach(group => {
1393
+ const visible = [...group.querySelectorAll(`.${CLASS_NAME_OPTION}`)].some(option => !option.hidden);
1394
+ group.hidden = !visible;
1395
+ group.style.display = visible ? '' : 'none';
1396
+ });
1397
+ this._triggerEvent(`${NAME_KEY}.search`, {query: term, results});
1398
+ this._callCallback('onSearch', {query: term, results});
1399
+ }
1356
1400
  }
1357
1401
 
1358
1402
  _handlersVGSelect();
@@ -38,11 +38,11 @@
38
38
  ```js
39
39
  js import VGSelect from './app/modules/vgselect/js/vgselect';
40
40
  // Инициализация
41
- VGSelect.init(document.getElementById('mySelect'), {
42
- lang: 'ru',
43
- placeholder: 'Выберите значение',
44
- autosearch: true,
45
- search: {
41
+ VGSelect.init(document.getElementById('mySelect'), {
42
+ lang: 'ru',
43
+ placeholder: 'Выберите значение',
44
+ autosearch: true,
45
+ search: {
46
46
  enabled: true,
47
47
  remote: true,
48
48
  route: '/api/search',
@@ -62,11 +62,11 @@ VGSelect.init(document.getElementById('mySelect'), {
62
62
 
63
63
  | Параметр | Тип | Описание |
64
64
  |--------|-----|---------|
65
- | `lang` | `string` | Язык интерфейса (поддерживается i18n). По умолчанию: `ru` |
66
- | `placeholder` | `string` | Текст плейсхолдера |
67
- | `autosearch` | `boolean \| number` | Автоматически включить поиск, если опций больше порога. `true` — порог 7 (по умолчанию), число — собственный порог, `false` — отключить автоматику |
68
- | `search.enabled` | `boolean` | Включить поле поиска вручную при любом количестве опций |
69
- | `search.remote` | `boolean` | Поиск через AJAX; при `true` поле поиска включается независимо от количества локальных опций |
65
+ | `lang` | `string` | Язык интерфейса (поддерживается i18n). По умолчанию: `ru` |
66
+ | `placeholder` | `string` | Текст плейсхолдера |
67
+ | `autosearch` | `boolean \| number` | Автоматически включить поиск, если опций больше порога. `true` — порог 7 (по умолчанию), число — собственный порог, `false` — отключить автоматику |
68
+ | `search.enabled` | `boolean` | Включить поле поиска вручную при любом количестве опций |
69
+ | `search.remote` | `boolean` | Поиск через AJAX; при `true` поле поиска включается независимо от количества локальных опций |
70
70
  | `search.route` | `string` | URL для удалённого поиска |
71
71
  | `search.delay` | `number` | Задержка перед запросом (мс) |
72
72
  | `search.minTerm` | `number` | Минимальная длина запроса |
@@ -179,4 +179,4 @@ MIT. Свободно использовать и модифицировать.
179
179
 
180
180
  📌 *Разработано в рамках фронтенд-системы VG Modules.*
181
181
  > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
182
- > 📍 Поддерживается в проектах VEGAS
182
+ > 📍 Поддерживается в проектах VEGAS