vgapp 1.5.6 → 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 +6 -3
- 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 +189 -194
- 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 +6 -0
- 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 +5 -52
- package/app/modules/vgselect/js/vgselect.js +128 -40
- 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 +16 -5
- 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 +28 -7
- 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 +82 -57
- 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 +11 -5
- 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 +121 -111
- 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 +5 -0
- 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,9 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
|
|
3
|
+
* Возможности: поиск, группы, дерево, теги, ограничение высоты, AJAX-пагинация, события и освобождение ресурсов.
|
|
4
|
+
*/
|
|
1
5
|
import BaseModule from "../../base-module";
|
|
2
6
|
import {
|
|
3
7
|
isDisabled,
|
|
4
8
|
isEmptyObj,
|
|
5
9
|
mergeDeepObject,
|
|
6
10
|
normalizeData,
|
|
11
|
+
transliterate,
|
|
7
12
|
} from "../../../utils/js/functions";
|
|
8
13
|
import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
|
|
9
14
|
import EventHandler from "../../../utils/js/dom/event";
|
|
@@ -30,7 +35,9 @@ const CLASS_NAME_TAG = 'vg-select-tag';
|
|
|
30
35
|
const CLASS_NAME_TAG_REMOVE = 'vg-select-tag-remove';
|
|
31
36
|
const CLASS_NAME_LOAD_MORE = 'vg-select-load-more';
|
|
32
37
|
const CLASS_NAME_LOADING = 'vg-select-loading';
|
|
33
|
-
const CLASS_NAME_DROP_UP = 'drop-up';
|
|
38
|
+
const CLASS_NAME_DROP_UP = 'drop-up';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_LIST_MAX_HEIGHT = 300;
|
|
34
41
|
|
|
35
42
|
const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
|
|
36
43
|
const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
|
|
@@ -73,11 +80,12 @@ class VGSelect extends BaseModule {
|
|
|
73
80
|
autosearch: true,
|
|
74
81
|
// Dropdown placement behavior:
|
|
75
82
|
// - none: default CSS positioning (no JS)
|
|
76
|
-
// - auto: 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
|
|
77
84
|
// - top: force open upwards
|
|
78
85
|
// - bottom: force open downwards
|
|
79
|
-
position: '
|
|
80
|
-
|
|
86
|
+
position: 'auto',
|
|
87
|
+
height: true,
|
|
88
|
+
search: {
|
|
81
89
|
enabled: false,
|
|
82
90
|
route: '',
|
|
83
91
|
remote: false,
|
|
@@ -105,8 +113,11 @@ class VGSelect extends BaseModule {
|
|
|
105
113
|
|
|
106
114
|
this._observer = null;
|
|
107
115
|
this._observerTimeout = null;
|
|
116
|
+
this._searchTimeout = null;
|
|
117
|
+
this._disposed = false;
|
|
108
118
|
this._visibilityTransitionId = 0;
|
|
109
|
-
this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
|
|
119
|
+
this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
|
|
120
|
+
this._applyListMaxHeight();
|
|
110
121
|
this._searchTerm = '';
|
|
111
122
|
this._currentPage = 1;
|
|
112
123
|
this._totalPages = null;
|
|
@@ -154,6 +165,7 @@ class VGSelect extends BaseModule {
|
|
|
154
165
|
*/
|
|
155
166
|
static buildListOptions(selector, drop, params = {}) {
|
|
156
167
|
let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
|
|
168
|
+
const controls = list ? [...list.querySelectorAll(`.${CLASS_NAME_LOAD_MORE}, .${CLASS_NAME_LOADING}`)] : [];
|
|
157
169
|
if (!list) {
|
|
158
170
|
list = document.createElement('ul');
|
|
159
171
|
Classes.add(list, CLASS_NAME_LIST);
|
|
@@ -190,6 +202,7 @@ class VGSelect extends BaseModule {
|
|
|
190
202
|
});
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
controls.forEach(control => list.appendChild(control));
|
|
193
206
|
return list;
|
|
194
207
|
}
|
|
195
208
|
|
|
@@ -228,9 +241,6 @@ class VGSelect extends BaseModule {
|
|
|
228
241
|
li.style.paddingLeft = `${16 + (level * 16)}px`;
|
|
229
242
|
}
|
|
230
243
|
|
|
231
|
-
// Раньше подсветка зависела от "явно выбранных" option (атрибут selected),
|
|
232
|
-
// из-за этого UI мог показывать placeholder, но DOM считал, что выбрана 1-я опция.
|
|
233
|
-
// Подсвечиваем по фактическому состоянию option.selected.
|
|
234
244
|
if (option.selected) {
|
|
235
245
|
li.classList.add('selected');
|
|
236
246
|
}
|
|
@@ -459,17 +469,16 @@ class VGSelect extends BaseModule {
|
|
|
459
469
|
search.appendChild(searchInput);
|
|
460
470
|
this._drop.insertBefore(search, this._drop.firstChild);
|
|
461
471
|
|
|
462
|
-
let searchTimeout;
|
|
463
472
|
searchInput.addEventListener('input', (e) => {
|
|
464
473
|
const term = e.target.value.trim();
|
|
465
474
|
const params = this._params;
|
|
466
475
|
|
|
467
476
|
this._callCallback('onSearch', { term });
|
|
468
477
|
if (params.search.remote && params.search.route) {
|
|
478
|
+
clearTimeout(this._searchTimeout);
|
|
479
|
+
this._remoteSearchAbortController?.abort();
|
|
469
480
|
if (term.length < (params.search.minterm || 1)) return;
|
|
470
|
-
|
|
471
|
-
clearTimeout(searchTimeout);
|
|
472
|
-
searchTimeout = setTimeout(() => {
|
|
481
|
+
this._searchTimeout = setTimeout(() => {
|
|
473
482
|
this._fetchRemoteData(term);
|
|
474
483
|
}, params.search.delay || 300);
|
|
475
484
|
} else {
|
|
@@ -520,7 +529,7 @@ class VGSelect extends BaseModule {
|
|
|
520
529
|
if (searchInput) searchInput.focus();
|
|
521
530
|
|
|
522
531
|
return this._queueCallback(() => {
|
|
523
|
-
if (transitionId !== this._visibilityTransitionId || !this._isShown()) return;
|
|
532
|
+
if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
|
|
524
533
|
this._updateDropdownPlacement();
|
|
525
534
|
EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
|
|
526
535
|
this._triggerEvent(EVENT_KEY_OPEN);
|
|
@@ -552,7 +561,7 @@ class VGSelect extends BaseModule {
|
|
|
552
561
|
this._element.querySelector(SELECTOR_DATA_TOGGLE).setAttribute('aria-expanded', 'false');
|
|
553
562
|
|
|
554
563
|
this._queueCallback(() => {
|
|
555
|
-
if (transitionId !== this._visibilityTransitionId) return;
|
|
564
|
+
if (this._disposed || transitionId !== this._visibilityTransitionId) return;
|
|
556
565
|
this._element.classList.remove(CLASS_NAME_SHOW);
|
|
557
566
|
EventHandler.trigger(this._element, EVENT_KEY_HIDDEN, relatedTarget);
|
|
558
567
|
this._triggerEvent(EVENT_KEY_CLOSE);
|
|
@@ -647,6 +656,7 @@ class VGSelect extends BaseModule {
|
|
|
647
656
|
* Освобождает ресурсы (отключает observer, очищает таймеры)
|
|
648
657
|
*/
|
|
649
658
|
dispose() {
|
|
659
|
+
if (this._disposed) return;
|
|
650
660
|
this._visibilityTransitionId++;
|
|
651
661
|
if (this._observer) {
|
|
652
662
|
this._observer.disconnect();
|
|
@@ -654,8 +664,12 @@ class VGSelect extends BaseModule {
|
|
|
654
664
|
}
|
|
655
665
|
clearTimeout(this._observerTimeout);
|
|
656
666
|
this._observerTimeout = null;
|
|
667
|
+
clearTimeout(this._searchTimeout);
|
|
668
|
+
this._remoteSearchAbortController?.abort();
|
|
669
|
+
if (this._isShown() && 'ontouchstart' in document.documentElement) document.body.style.pointerEvents = '';
|
|
657
670
|
this._teardownDropdownPlacement();
|
|
658
671
|
super.dispose();
|
|
672
|
+
this._disposed = true;
|
|
659
673
|
}
|
|
660
674
|
|
|
661
675
|
_setupDropdownPlacement() {
|
|
@@ -675,7 +689,7 @@ class VGSelect extends BaseModule {
|
|
|
675
689
|
this._updateDropdownPlacement();
|
|
676
690
|
}
|
|
677
691
|
|
|
678
|
-
_teardownDropdownPlacement() {
|
|
692
|
+
_teardownDropdownPlacement() {
|
|
679
693
|
window.removeEventListener('resize', this._boundUpdateDropdownPlacement);
|
|
680
694
|
window.removeEventListener('scroll', this._boundUpdateDropdownPlacement);
|
|
681
695
|
|
|
@@ -684,16 +698,63 @@ class VGSelect extends BaseModule {
|
|
|
684
698
|
}
|
|
685
699
|
this._positioningScrollParent = null;
|
|
686
700
|
|
|
687
|
-
//
|
|
688
|
-
this._element.classList.remove(CLASS_NAME_DROP_UP);
|
|
689
|
-
this.
|
|
690
|
-
}
|
|
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
|
+
}
|
|
691
752
|
|
|
692
753
|
_getPositionMode() {
|
|
693
754
|
const raw = this._params?.position;
|
|
694
|
-
const mode = raw == null ? '
|
|
755
|
+
const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
|
|
695
756
|
if (mode === 'auto' || mode === 'top' || mode === 'bottom' || mode === 'none') return mode;
|
|
696
|
-
return '
|
|
757
|
+
return 'auto';
|
|
697
758
|
}
|
|
698
759
|
|
|
699
760
|
_getOverflowAncestor(startEl) {
|
|
@@ -751,12 +812,8 @@ class VGSelect extends BaseModule {
|
|
|
751
812
|
|
|
752
813
|
const available = openUp ? spaceAbove : spaceBelow;
|
|
753
814
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (Number.isFinite(listMax) && listMax > 0) {
|
|
757
|
-
this._element.style.setProperty('--vg-select-list-max-height', `${listMax}px`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
815
|
+
this._applyListMaxHeight(available);
|
|
816
|
+
}
|
|
760
817
|
|
|
761
818
|
/**
|
|
762
819
|
* Удаляет кастомный UI
|
|
@@ -765,8 +822,10 @@ class VGSelect extends BaseModule {
|
|
|
765
822
|
static destroy(select) {
|
|
766
823
|
const container = select.nextElementSibling;
|
|
767
824
|
if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
|
|
825
|
+
VGSelect.getInstance(container)?.dispose();
|
|
768
826
|
container.remove();
|
|
769
827
|
}
|
|
828
|
+
delete select.dataset.inited;
|
|
770
829
|
}
|
|
771
830
|
|
|
772
831
|
/**
|
|
@@ -781,6 +840,9 @@ class VGSelect extends BaseModule {
|
|
|
781
840
|
const placeholder = select.dataset.placeholder || '';
|
|
782
841
|
const isMultiple = select.multiple;
|
|
783
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
|
+
});
|
|
784
846
|
|
|
785
847
|
if (isMultiple) {
|
|
786
848
|
const tags = current.querySelector(`.${CLASS_NAME_TAGS}`);
|
|
@@ -799,6 +861,10 @@ class VGSelect extends BaseModule {
|
|
|
799
861
|
|
|
800
862
|
if (selected.length === 0) {
|
|
801
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);
|
|
802
868
|
} else {
|
|
803
869
|
input.placeholder = '';
|
|
804
870
|
selected.forEach(opt => {
|
|
@@ -985,13 +1051,14 @@ class VGSelect extends BaseModule {
|
|
|
985
1051
|
|
|
986
1052
|
const list = this._element.querySelector(SELECTOR_LIST);
|
|
987
1053
|
if (!list) return;
|
|
1054
|
+
if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
|
|
988
1055
|
|
|
989
1056
|
const btn = document.createElement('li');
|
|
990
1057
|
btn.className = CLASS_NAME_LOAD_MORE;
|
|
991
1058
|
btn.style.textAlign = 'center';
|
|
992
1059
|
btn.style.padding = '8px';
|
|
993
1060
|
btn.style.cursor = 'pointer';
|
|
994
|
-
btn.style.color = '#007bff';
|
|
1061
|
+
btn.style.color = 'var(--vg-primary-color, #007bff)';
|
|
995
1062
|
btn.style.fontSize = '14px';
|
|
996
1063
|
btn.style.fontWeight = '500';
|
|
997
1064
|
btn.textContent = this._params.search.loadMoreText;
|
|
@@ -1045,6 +1112,7 @@ class VGSelect extends BaseModule {
|
|
|
1045
1112
|
async _loadNextPage() {
|
|
1046
1113
|
const { route, pageParam = 'page', termParam = 'q', perpage = 20 } = this._params.search;
|
|
1047
1114
|
const nextPage = this._currentPage + 1;
|
|
1115
|
+
const requestId = this._remoteSearchRequestId;
|
|
1048
1116
|
|
|
1049
1117
|
const url = new URL(route, window.location.origin);
|
|
1050
1118
|
url.searchParams.set(termParam, this._searchTerm);
|
|
@@ -1057,7 +1125,9 @@ class VGSelect extends BaseModule {
|
|
|
1057
1125
|
|
|
1058
1126
|
try {
|
|
1059
1127
|
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
|
|
1128
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1060
1129
|
const data = await res.json();
|
|
1130
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1061
1131
|
|
|
1062
1132
|
if (Array.isArray(data.results)) {
|
|
1063
1133
|
VGSelect.addOptions(this._element.previousElementSibling, data, { preserve: true });
|
|
@@ -1073,12 +1143,15 @@ class VGSelect extends BaseModule {
|
|
|
1073
1143
|
this._triggerEvent(EVENT_KEY_LOAD_NEXT, { page: this._currentPage, term: this._searchTerm });
|
|
1074
1144
|
}
|
|
1075
1145
|
} catch (err) {
|
|
1146
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1076
1147
|
console.error('VGSelect: Failed to load next page', err);
|
|
1077
1148
|
this._triggerEvent(EVENT_KEY_ERROR, { error: 'Pagination fetch failed', term: this._searchTerm });
|
|
1078
1149
|
this._hideLoadMoreButton(false); // оставить кнопку при ошибке
|
|
1079
1150
|
} finally {
|
|
1080
|
-
this.
|
|
1081
|
-
|
|
1151
|
+
if (!this._disposed && requestId === this._remoteSearchRequestId) {
|
|
1152
|
+
this._showLoading(false);
|
|
1153
|
+
this._loading = false;
|
|
1154
|
+
}
|
|
1082
1155
|
}
|
|
1083
1156
|
}
|
|
1084
1157
|
|
|
@@ -1101,6 +1174,7 @@ class VGSelect extends BaseModule {
|
|
|
1101
1174
|
this._searchTerm = term;
|
|
1102
1175
|
this._currentPage = 1;
|
|
1103
1176
|
this._totalPages = null;
|
|
1177
|
+
this._loading = false;
|
|
1104
1178
|
|
|
1105
1179
|
// Отменяем предыдущий запрос (если ещё летит)
|
|
1106
1180
|
if (this._remoteSearchAbortController) {
|
|
@@ -1120,10 +1194,14 @@ class VGSelect extends BaseModule {
|
|
|
1120
1194
|
headers: { 'Content-Type': 'application/json' },
|
|
1121
1195
|
signal
|
|
1122
1196
|
})
|
|
1123
|
-
.then(res =>
|
|
1197
|
+
.then(res => {
|
|
1198
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1199
|
+
return res.json();
|
|
1200
|
+
})
|
|
1124
1201
|
.then(data => {
|
|
1125
1202
|
// Если прилетел не самый свежий ответ — игнорируем
|
|
1126
|
-
if (requestId !== this._remoteSearchRequestId) return;
|
|
1203
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1204
|
+
if (!Array.isArray(data.results)) throw new Error('Invalid results');
|
|
1127
1205
|
|
|
1128
1206
|
// Если пользователь уже ввёл другой текст — тоже игнорируем
|
|
1129
1207
|
const liveTerm = searchInput ? searchInput.value.trim() : '';
|
|
@@ -1173,6 +1251,7 @@ class VGSelect extends BaseModule {
|
|
|
1173
1251
|
}
|
|
1174
1252
|
})
|
|
1175
1253
|
.catch(err => {
|
|
1254
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1176
1255
|
// Abort — нормальная ситуация при быстром вводе
|
|
1177
1256
|
if (err && (err.name === 'AbortError')) return;
|
|
1178
1257
|
|
|
@@ -1182,7 +1261,7 @@ class VGSelect extends BaseModule {
|
|
|
1182
1261
|
})
|
|
1183
1262
|
.finally(() => {
|
|
1184
1263
|
// Лоадер убираем только для последнего актуального запроса
|
|
1185
|
-
if (requestId === this._remoteSearchRequestId) {
|
|
1264
|
+
if (!this._disposed && requestId === this._remoteSearchRequestId) {
|
|
1186
1265
|
this._showLoading(false);
|
|
1187
1266
|
}
|
|
1188
1267
|
});
|
|
@@ -1283,6 +1362,7 @@ class VGSelect extends BaseModule {
|
|
|
1283
1362
|
const drop = container.querySelector(`.${CLASS_NAME_DROPDOWN}`);
|
|
1284
1363
|
VGSelect.buildListOptions(select, drop, instance?._params || {});
|
|
1285
1364
|
instance?._syncSearch();
|
|
1365
|
+
this.updateUI(select);
|
|
1286
1366
|
instance?._triggerEvent(EVENT_KEY_REBUILD);
|
|
1287
1367
|
} else {
|
|
1288
1368
|
this.updateUI(select);
|
|
@@ -1297,17 +1377,25 @@ class VGSelect extends BaseModule {
|
|
|
1297
1377
|
_filterLocalOptions(term) {
|
|
1298
1378
|
const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
|
|
1299
1379
|
const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
|
|
1300
|
-
|
|
1301
|
-
if (!term) {
|
|
1302
|
-
options.forEach(el => el.hidden = false);
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
1380
|
term = term.toLowerCase();
|
|
1381
|
+
const search = [term, transliterate(term), transliterate(term, true)];
|
|
1382
|
+
let results = 0;
|
|
1307
1383
|
options.forEach(el => {
|
|
1308
1384
|
const text = el.textContent.toLowerCase();
|
|
1309
|
-
|
|
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';
|
|
1310
1396
|
});
|
|
1397
|
+
this._triggerEvent(`${NAME_KEY}.search`, {query: term, results});
|
|
1398
|
+
this._callCallback('onSearch', {query: term, results});
|
|
1311
1399
|
}
|
|
1312
1400
|
}
|
|
1313
1401
|
|
|
@@ -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
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
@use "../../../utils/scss" as vg;
|
|
2
|
-
|
|
3
|
-
$select-current-map: (
|
|
4
|
-
background-color: var(--vg-control-bg, #ffffff),
|
|
5
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
6
|
-
border-width: vg.$border-width,
|
|
7
|
-
border-style: vg.$border-style,
|
|
8
|
-
border-color: var(--vg-control-border-color, #dddddd),
|
|
1
|
+
@use "../../../utils/scss" as vg;
|
|
2
|
+
|
|
3
|
+
$select-current-map: (
|
|
4
|
+
background-color: var(--vg-control-bg, #ffffff),
|
|
5
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
6
|
+
border-width: vg.$border-width,
|
|
7
|
+
border-style: vg.$border-style,
|
|
8
|
+
border-color: var(--vg-control-border-color, #dddddd),
|
|
9
9
|
border-radius: 5px,
|
|
10
10
|
padding-left: 16px,
|
|
11
11
|
padding-right: 40px,
|
|
@@ -16,69 +16,69 @@ $select-current-map: (
|
|
|
16
16
|
z-index: 10,
|
|
17
17
|
) !default;
|
|
18
18
|
|
|
19
|
-
$select-placeholder-map: (
|
|
20
|
-
placehoder-color: var(--vg-text-placeholder-color, rgba(0, 0, 0, .32)),
|
|
21
|
-
placehoder-font-size: 16px,
|
|
22
|
-
) !default;
|
|
19
|
+
$select-placeholder-map: (
|
|
20
|
+
placehoder-color: var(--vg-text-placeholder-color, rgba(0, 0, 0, .32)),
|
|
21
|
+
placehoder-font-size: 16px,
|
|
22
|
+
) !default;
|
|
23
23
|
|
|
24
24
|
$select-current-svg: '<svg aria-hidden="true" focusable="false" data-prefix="fa-light" data-icon="chevron-down" class="svg-inline--fa fa-chevron-down fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M4 181C7 178 12 176 16 176C20 176 24 177 27 180L224 362L421 180C428 174 438 175 444 181C450 188 449 198 443 204L235 396C229 401 219 401 213 396L5 204C-1 198 -2 188 4 181Z" fill="gray"/></svg>' !default;
|
|
25
25
|
|
|
26
|
-
$select-dropdown-map: (
|
|
27
|
-
background-color: var(--vg-control-bg, #ffffff),
|
|
28
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
29
|
-
border-width: vg.$border-width,
|
|
30
|
-
border-style: vg.$border-style,
|
|
31
|
-
border-color: var(--vg-control-border-color, #dddddd),
|
|
32
|
-
border-radius: vg.$border-radius,
|
|
33
|
-
box-shadow: 0 7px 12px 1px var(--vg-shadow-soft-color, rgba(68, 68, 68, .11)),
|
|
26
|
+
$select-dropdown-map: (
|
|
27
|
+
background-color: var(--vg-control-bg, #ffffff),
|
|
28
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
29
|
+
border-width: vg.$border-width,
|
|
30
|
+
border-style: vg.$border-style,
|
|
31
|
+
border-color: var(--vg-control-border-color, #dddddd),
|
|
32
|
+
border-radius: vg.$border-radius,
|
|
33
|
+
box-shadow: 0 7px 12px 1px var(--vg-shadow-soft-color, rgba(68, 68, 68, .11)),
|
|
34
34
|
z-index: 15,
|
|
35
35
|
) !default;
|
|
36
36
|
|
|
37
|
-
$select-list-map: (
|
|
38
|
-
background-color: transparent,
|
|
39
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
37
|
+
$select-list-map: (
|
|
38
|
+
background-color: transparent,
|
|
39
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
40
40
|
padding-left: 8px,
|
|
41
41
|
padding-right: 8px,
|
|
42
42
|
padding-top: 8px,
|
|
43
43
|
padding-bottom: 8px,
|
|
44
|
-
border-bottom-width: vg.$border-width,
|
|
45
|
-
border-bottom-style: vg.$border-style,
|
|
46
|
-
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
44
|
+
border-bottom-width: vg.$border-width,
|
|
45
|
+
border-bottom-style: vg.$border-style,
|
|
46
|
+
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
47
47
|
) !default;
|
|
48
48
|
|
|
49
|
-
$select-optgroup-map: (
|
|
50
|
-
background-color: transparent,
|
|
51
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
49
|
+
$select-optgroup-map: (
|
|
50
|
+
background-color: transparent,
|
|
51
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
52
52
|
padding-left: 8px,
|
|
53
53
|
padding-right: 8px,
|
|
54
54
|
padding-top: 8px,
|
|
55
55
|
padding-bottom: 8px,
|
|
56
|
-
border-bottom-width: vg.$border-width,
|
|
57
|
-
border-bottom-style: vg.$border-style,
|
|
58
|
-
border-bottom-color: var(--vg-border-strong-color, #838383),
|
|
56
|
+
border-bottom-width: vg.$border-width,
|
|
57
|
+
border-bottom-style: vg.$border-style,
|
|
58
|
+
border-bottom-color: var(--vg-border-strong-color, #838383),
|
|
59
59
|
font-weight: 500,
|
|
60
60
|
) !default;
|
|
61
61
|
|
|
62
|
-
$select-list-max-height:
|
|
62
|
+
$select-list-max-height: 300px !default;
|
|
63
63
|
$select-list-scrollbar-width: 6px !default;
|
|
64
|
-
$select-list-scrollbar-bg: var(--vg-surface-muted-bg, #f5f5f5) !default;
|
|
65
|
-
$select-list-scrollbar-thumb: var(--vg-text-muted-color, #999999) !default;
|
|
64
|
+
$select-list-scrollbar-bg: var(--vg-surface-muted-bg, #f5f5f5) !default;
|
|
65
|
+
$select-list-scrollbar-thumb: var(--vg-text-muted-color, #999999) !default;
|
|
66
66
|
|
|
67
|
-
$select-list-hover-map: (
|
|
68
|
-
background-color: var(--vg-surface-hover-bg, #f7f7f7),
|
|
69
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
70
|
-
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
67
|
+
$select-list-hover-map: (
|
|
68
|
+
background-color: var(--vg-surface-hover-bg, #f7f7f7),
|
|
69
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
70
|
+
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
71
71
|
) !default;
|
|
72
72
|
|
|
73
|
-
$select-optgroup-hover-map: (
|
|
74
|
-
background-color: var(--vg-surface-hover-bg, #f7f7f7),
|
|
75
|
-
color: var(--vg-text-color, #{vg.$color}),
|
|
76
|
-
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
73
|
+
$select-optgroup-hover-map: (
|
|
74
|
+
background-color: var(--vg-surface-hover-bg, #f7f7f7),
|
|
75
|
+
color: var(--vg-text-color, #{vg.$color}),
|
|
76
|
+
border-bottom-color: var(--vg-border-disabled-color, #f5f5f5),
|
|
77
77
|
) !default;
|
|
78
78
|
|
|
79
|
-
$select-search-map: (
|
|
80
|
-
background-color: var(--vg-control-muted-bg, #f2f2f2),
|
|
81
|
-
color: var(--vg-text-color, #{vg.$color})
|
|
79
|
+
$select-search-map: (
|
|
80
|
+
background-color: var(--vg-control-muted-bg, #f2f2f2),
|
|
81
|
+
color: var(--vg-text-color, #{vg.$color})
|
|
82
82
|
) !default;
|
|
83
83
|
|
|
84
84
|
$select-tags-map: (
|
|
@@ -88,15 +88,15 @@ $select-tags-map: (
|
|
|
88
88
|
padding: 2px 0
|
|
89
89
|
) !default;
|
|
90
90
|
|
|
91
|
-
$select-tag-map: (
|
|
92
|
-
gap: 4px,
|
|
93
|
-
background: var(--vg-primary-color, #0d6efd),
|
|
94
|
-
color: var(--vg-primary-contrast-color, #ffffff),
|
|
91
|
+
$select-tag-map: (
|
|
92
|
+
gap: 4px,
|
|
93
|
+
background: var(--vg-primary-color, #0d6efd),
|
|
94
|
+
color: var(--vg-primary-contrast-color, #ffffff),
|
|
95
95
|
padding: 2px 6px,
|
|
96
96
|
border-radius: 4px,
|
|
97
97
|
font-size: 14px,
|
|
98
98
|
remove-width: 12px,
|
|
99
99
|
remove-height: 12px,
|
|
100
|
-
remove-stroke: var(--vg-primary-contrast-color, #ffffff),
|
|
100
|
+
remove-stroke: var(--vg-primary-contrast-color, #ffffff),
|
|
101
101
|
remove-stroke-width: 2
|
|
102
102
|
) !default;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Описание: боковые панели VGSidebar с декларативным и программным управлением.
|
|
3
|
+
* Возможности: четыре стороны экрана, backdrop, прокрутка, URL-хэш, AJAX, события и доступность.
|
|
4
|
+
*/
|
|
1
5
|
import BaseModule from "../../base-module";
|
|
2
6
|
import { isDisabled, isVisible, mergeDeepObject } from "../../../utils/js/functions";
|
|
3
7
|
import EventHandler from "../../../utils/js/dom/event";
|
|
@@ -212,6 +216,7 @@ class VGSidebar extends BaseModule {
|
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
this._element.classList.add(CLASS_NAME_SHOW);
|
|
219
|
+
this._element.removeAttribute('aria-hidden');
|
|
215
220
|
document.body.classList.add(CLASS_NAME_OPEN);
|
|
216
221
|
|
|
217
222
|
const completeCallback = () => {
|
|
@@ -238,6 +243,7 @@ class VGSidebar extends BaseModule {
|
|
|
238
243
|
if (hideEvent.defaultPrevented) return;
|
|
239
244
|
|
|
240
245
|
this._element.classList.remove(CLASS_NAME_SHOW);
|
|
246
|
+
this._element.setAttribute('aria-hidden', 'true');
|
|
241
247
|
const remainingOpenSidebars = VGSidebar.getOpenSidebars(this._element);
|
|
242
248
|
if (!remainingOpenSidebars.length) {
|
|
243
249
|
document.body.classList.remove(CLASS_NAME_OPEN);
|
|
@@ -289,12 +295,16 @@ class VGSidebar extends BaseModule {
|
|
|
289
295
|
* @override
|
|
290
296
|
*/
|
|
291
297
|
dispose() {
|
|
292
|
-
|
|
298
|
+
if (!this._element) return;
|
|
299
|
+
EventHandler.off(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
|
|
293
300
|
EventHandler.off(this._element, EVENT_KEYS.HIDE);
|
|
294
301
|
if (this._showPopstateHandler) {
|
|
295
302
|
EventHandler.off(window, EVENT_KEYS.POPSTATE_DATA_API, this._showPopstateHandler);
|
|
296
303
|
}
|
|
297
|
-
this.
|
|
304
|
+
if (!VGSidebar.getOpenSidebars(this._element).length && !Backdrop.isActive()) {
|
|
305
|
+
this._scrollBar.reset();
|
|
306
|
+
}
|
|
307
|
+
super.dispose();
|
|
298
308
|
}
|
|
299
309
|
|
|
300
310
|
/**
|
|
@@ -311,15 +321,16 @@ class VGSidebar extends BaseModule {
|
|
|
311
321
|
* @private
|
|
312
322
|
*/
|
|
313
323
|
_addEventListeners() {
|
|
314
|
-
|
|
315
|
-
if (event.key !== 'Escape') return;
|
|
324
|
+
this._keydownHandler = (event) => {
|
|
325
|
+
if (event.key !== 'Escape' || !this._isShown()) return;
|
|
316
326
|
|
|
317
327
|
if (this._params.keyboard) {
|
|
318
328
|
this.hide();
|
|
319
329
|
} else {
|
|
320
330
|
EventHandler.trigger(this._element, EVENT_KEYS.HIDE_PREVENTED);
|
|
321
331
|
}
|
|
322
|
-
}
|
|
332
|
+
};
|
|
333
|
+
EventHandler.on(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
336
|
|