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.
- package/app/langs/en/buttons.json +17 -17
- package/app/langs/en/messages.json +36 -36
- package/app/langs/ru/buttons.json +17 -17
- package/app/langs/ru/messages.json +1 -1
- package/app/modules/vgalert/js/vgalert.js +7 -7
- package/app/modules/vgalert/scss/_variables.scss +16 -16
- package/app/modules/vgcollapse/scss/_variables.scss +7 -7
- package/app/modules/vgcollapse/scss/vgcollapse.scss +5 -5
- package/app/modules/vgdropdown/scss/_variables.scss +14 -14
- package/app/modules/vgdropdown/scss/vgdropdown.scss +5 -5
- package/app/modules/vgfilepreview/js/i18n.js +56 -56
- package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
- package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
- package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
- package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
- package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
- package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
- package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
- package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
- package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
- package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
- package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
- package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
- package/app/modules/vgfilepreview/readme.md +68 -68
- package/app/modules/vgfiles/js/base.js +198 -198
- package/app/modules/vgfiles/js/droppable.js +17 -17
- package/app/modules/vgfiles/js/render.js +153 -153
- package/app/modules/vgfiles/js/sortable.js +52 -52
- package/app/modules/vgfiles/js/vgfiles.js +43 -43
- package/app/modules/vgfiles/readme.md +123 -123
- package/app/modules/vgfiles/scss/_mixins.scss +13 -13
- package/app/modules/vgfiles/scss/_variables.scss +10 -10
- package/app/modules/vgfiles/scss/vgfiles.scss +28 -28
- package/app/modules/vgformsender/js/hideshowpass.js +22 -22
- package/app/modules/vgformsender/readme.md +1 -1
- package/app/modules/vgformsender/scss/_variables.scss +5 -5
- package/app/modules/vgformsender/scss/vgformsender.scss +42 -42
- package/app/modules/vglawcookie/js/vglawcookie.js +19 -19
- package/app/modules/vglawcookie/scss/_variables.scss +13 -13
- package/app/modules/vglawcookie/scss/vglawcookie.scss +5 -5
- package/app/modules/vgmodal/js/vgmodal.interaction.js +216 -216
- package/app/modules/vgmodal/js/vgmodal.js +9 -9
- package/app/modules/vgmodal/js/vgmodal.minimized.js +227 -227
- package/app/modules/vgmodal/scss/_variables.scss +17 -17
- package/app/modules/vgmodal/scss/vgmodal.scss +77 -77
- package/app/modules/vgnav/js/vgnav.js +62 -62
- package/app/modules/vgnav/scss/_breakpoints.scss +36 -36
- package/app/modules/vgnav/scss/_hamburger.scss +5 -5
- package/app/modules/vgnav/scss/_placement.scss +5 -5
- package/app/modules/vgnav/scss/_toggle.scss +5 -5
- package/app/modules/vgnav/scss/_variables.scss +26 -26
- package/app/modules/vgnav/scss/vgnav.scss +41 -41
- package/app/modules/vgnestable/README.md +39 -39
- package/app/modules/vgrangeslider/js/skins.js +8 -8
- package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -29
- package/app/modules/vgrangeslider/scss/_variables.scss +55 -55
- package/app/modules/vgrangeslider/scss/vgrangeslider.scss +239 -239
- package/app/modules/vgrollup/js/vgrollup.js +15 -15
- package/app/modules/vgrollup/scss/_variables.scss +15 -15
- package/app/modules/vgrollup/scss/vgrollup.scss +6 -6
- package/app/modules/vgselect/js/handlers.js +6 -6
- package/app/modules/vgselect/js/vgselect.js +167 -123
- package/app/modules/vgselect/readme.md +11 -11
- package/app/modules/vgselect/scss/_variables.scss +51 -51
- package/app/modules/vgselect/scss/vgselect.scss +1 -1
- package/app/modules/vgsidebar/js/vgsidebar.js +21 -21
- package/app/modules/vgsidebar/scss/_variables.scss +10 -10
- package/app/modules/vgsidebar/scss/vgsidebar.scss +5 -5
- package/app/modules/vgspy/js/vgspy.js +43 -43
- package/app/modules/vgtable/js/_filters.js +10 -6
- package/app/modules/vgtable/js/_options.js +16 -16
- package/app/modules/vgtable/js/_pagination.js +118 -118
- package/app/modules/vgtable/js/_responsive.js +79 -79
- package/app/modules/vgtable/js/_sticky-header.js +349 -280
- package/app/modules/vgtable/js/vgtable.js +98 -98
- package/app/modules/vgtable/scss/_sorting.scss +6 -6
- package/app/modules/vgtable/scss/_sticky-header.scss +31 -31
- package/app/modules/vgtable/scss/_variables.scss +6 -4
- package/app/modules/vgtable/scss/vgtable.scss +8 -4
- package/app/modules/vgtabs/js/vgtabs.js +91 -91
- package/app/modules/vgtabs/scss/_variables.scss +23 -23
- package/app/modules/vgtabs/scss/vgtabs.scss +5 -5
- package/app/modules/vgtoast/js/vgtoast.js +4 -4
- package/app/modules/vgtoast/scss/_variables.scss +13 -13
- package/app/modules/vgtoast/scss/vgtoast.scss +5 -5
- package/app/modules/vgtooltip/js/vgtooltip.js +127 -127
- package/app/modules/vgtooltip/scss/_variables.scss +10 -10
- package/app/modules/vgtooltip/scss/vgtooltip.scss +5 -5
- package/app/utils/js/components/audio-metadata.js +240 -240
- package/app/utils/js/components/backdrop.js +116 -116
- package/app/utils/js/components/file-icon.js +7 -7
- package/app/utils/js/components/file-preview.js +304 -304
- package/app/utils/js/components/responsive.js +165 -165
- package/app/utils/js/components/sanitize.js +150 -150
- package/app/utils/js/components/video-metadata.js +140 -140
- package/app/utils/scss/_backdrop.scss +5 -5
- package/app/utils/scss/_theme-default.scss +12 -12
- package/app/utils/scss/_theme.scss +7 -7
- package/app/utils/scss/default.scss +9 -9
- package/app/utils/scss/functions.scss +21 -21
- package/app/utils/scss/index.scss +4 -4
- package/app/utils/scss/mixin.scss +16 -16
- package/app/utils/scss/variables.scss +147 -147
- package/build/vgapp.css +1 -1
- package/build/vgapp.css.map +1 -1
- package/build/vgapp.js +1 -1
- package/build/vgapp.js.map +1 -1
- package/index.js +7 -7
- 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
|
-
|
|
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
|
-
//
|
|
701
|
-
this._element.classList.remove(CLASS_NAME_DROP_UP);
|
|
702
|
-
this.
|
|
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
|
-
|
|
768
|
-
|
|
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
|