vgapp 1.5.5 → 1.5.7
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/modules/vgalert/js/vgalert.js +7 -4
- package/app/modules/vgfiles/js/base.js +25 -30
- package/app/modules/vgmodal/js/vgmodal.js +9 -3
- package/app/modules/vgnav/js/vgnav.js +62 -58
- package/app/modules/vgrangeslider/js/skins.js +8 -3
- package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -17
- package/app/modules/vgrollup/js/vgrollup.js +15 -7
- package/app/modules/vgselect/js/handlers.js +6 -53
- package/app/modules/vgselect/js/vgselect.js +107 -63
- package/app/modules/vgselect/scss/vgselect.scss +1 -1
- package/app/modules/vgsidebar/js/vgsidebar.js +21 -10
- package/app/modules/vgspy/js/vgspy.js +43 -22
- package/app/modules/vgtable/js/_options.js +16 -5
- package/app/modules/vgtable/js/_pagination.js +118 -24
- package/app/modules/vgtable/js/_responsive.js +79 -0
- package/app/modules/vgtable/js/vgtable.js +38 -11
- package/app/modules/vgtabs/js/vgtabs.js +91 -66
- package/app/modules/vgtoast/js/vgtoast.js +11 -5
- package/app/modules/vgtooltip/js/vgtooltip.js +19 -9
- package/app/utils/js/components/file-icon.js +7 -2
- package/app/utils/js/components/responsive.js +165 -0
- 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 -1
- package/package.json +1 -1
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: кастомный select с синхронизацией исходного поля и выпадающего списка.
|
|
3
|
+
* Возможности: поиск, группы, дерево, теги, AJAX-пагинация, события и освобождение ресурсов.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
2
6
|
import {
|
|
3
7
|
isDisabled,
|
|
4
8
|
isEmptyObj,
|
|
5
9
|
mergeDeepObject,
|
|
6
|
-
normalizeData,
|
|
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";
|
|
@@ -73,10 +78,10 @@ class VGSelect extends BaseModule {
|
|
|
73
78
|
autosearch: true,
|
|
74
79
|
// Dropdown placement behavior:
|
|
75
80
|
// - none: default CSS positioning (no JS)
|
|
76
|
-
// - auto: choose top/bottom based on available space in overflow ancestor/viewport
|
|
81
|
+
// - auto (default): choose top/bottom based on available space in overflow ancestor/viewport
|
|
77
82
|
// - top: force open upwards
|
|
78
83
|
// - bottom: force open downwards
|
|
79
|
-
position: '
|
|
84
|
+
position: 'auto',
|
|
80
85
|
search: {
|
|
81
86
|
enabled: false,
|
|
82
87
|
route: '',
|
|
@@ -104,7 +109,9 @@ class VGSelect extends BaseModule {
|
|
|
104
109
|
}, params));
|
|
105
110
|
|
|
106
111
|
this._observer = null;
|
|
107
|
-
this._observerTimeout = null;
|
|
112
|
+
this._observerTimeout = null;
|
|
113
|
+
this._searchTimeout = null;
|
|
114
|
+
this._disposed = false;
|
|
108
115
|
this._visibilityTransitionId = 0;
|
|
109
116
|
this._drop = Selectors.find(SELECTOR_DROPDOWN, this._element);
|
|
110
117
|
this._searchTerm = '';
|
|
@@ -152,8 +159,9 @@ class VGSelect extends BaseModule {
|
|
|
152
159
|
* @param {HTMLElement} drop - Контейнер выпадающего списка
|
|
153
160
|
* @returns {HTMLElement} - Обновлённый список
|
|
154
161
|
*/
|
|
155
|
-
static buildListOptions(selector, drop, params = {}) {
|
|
156
|
-
let list = drop.querySelector(`.${CLASS_NAME_LIST}`);
|
|
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}`)] : [];
|
|
157
165
|
if (!list) {
|
|
158
166
|
list = document.createElement('ul');
|
|
159
167
|
Classes.add(list, CLASS_NAME_LIST);
|
|
@@ -190,7 +198,8 @@ class VGSelect extends BaseModule {
|
|
|
190
198
|
});
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
|
|
201
|
+
controls.forEach(control => list.appendChild(control));
|
|
202
|
+
return list;
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
/**
|
|
@@ -459,17 +468,16 @@ class VGSelect extends BaseModule {
|
|
|
459
468
|
search.appendChild(searchInput);
|
|
460
469
|
this._drop.insertBefore(search, this._drop.firstChild);
|
|
461
470
|
|
|
462
|
-
|
|
463
|
-
searchInput.addEventListener('input', (e) => {
|
|
471
|
+
searchInput.addEventListener('input', (e) => {
|
|
464
472
|
const term = e.target.value.trim();
|
|
465
473
|
const params = this._params;
|
|
466
474
|
|
|
467
475
|
this._callCallback('onSearch', { term });
|
|
468
476
|
if (params.search.remote && params.search.route) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
477
|
+
clearTimeout(this._searchTimeout);
|
|
478
|
+
this._remoteSearchAbortController?.abort();
|
|
479
|
+
if (term.length < (params.search.minterm || 1)) return;
|
|
480
|
+
this._searchTimeout = setTimeout(() => {
|
|
473
481
|
this._fetchRemoteData(term);
|
|
474
482
|
}, params.search.delay || 300);
|
|
475
483
|
} else {
|
|
@@ -520,7 +528,7 @@ class VGSelect extends BaseModule {
|
|
|
520
528
|
if (searchInput) searchInput.focus();
|
|
521
529
|
|
|
522
530
|
return this._queueCallback(() => {
|
|
523
|
-
if (transitionId !== this._visibilityTransitionId || !this._isShown()) return;
|
|
531
|
+
if (this._disposed || transitionId !== this._visibilityTransitionId || !this._isShown()) return;
|
|
524
532
|
this._updateDropdownPlacement();
|
|
525
533
|
EventHandler.trigger(this._element, EVENT_KEY_SHOWN, { relatedTarget });
|
|
526
534
|
this._triggerEvent(EVENT_KEY_OPEN);
|
|
@@ -552,7 +560,7 @@ class VGSelect extends BaseModule {
|
|
|
552
560
|
this._element.querySelector(SELECTOR_DATA_TOGGLE).setAttribute('aria-expanded', 'false');
|
|
553
561
|
|
|
554
562
|
this._queueCallback(() => {
|
|
555
|
-
if (transitionId !== this._visibilityTransitionId) return;
|
|
563
|
+
if (this._disposed || transitionId !== this._visibilityTransitionId) return;
|
|
556
564
|
this._element.classList.remove(CLASS_NAME_SHOW);
|
|
557
565
|
EventHandler.trigger(this._element, EVENT_KEY_HIDDEN, relatedTarget);
|
|
558
566
|
this._triggerEvent(EVENT_KEY_CLOSE);
|
|
@@ -646,16 +654,21 @@ class VGSelect extends BaseModule {
|
|
|
646
654
|
/**
|
|
647
655
|
* Освобождает ресурсы (отключает observer, очищает таймеры)
|
|
648
656
|
*/
|
|
649
|
-
dispose() {
|
|
657
|
+
dispose() {
|
|
658
|
+
if (this._disposed) return;
|
|
650
659
|
this._visibilityTransitionId++;
|
|
651
660
|
if (this._observer) {
|
|
652
661
|
this._observer.disconnect();
|
|
653
662
|
this._observer = null;
|
|
654
663
|
}
|
|
655
664
|
clearTimeout(this._observerTimeout);
|
|
656
|
-
this._observerTimeout = null;
|
|
657
|
-
this.
|
|
658
|
-
|
|
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;
|
|
659
672
|
}
|
|
660
673
|
|
|
661
674
|
_setupDropdownPlacement() {
|
|
@@ -691,9 +704,9 @@ class VGSelect extends BaseModule {
|
|
|
691
704
|
|
|
692
705
|
_getPositionMode() {
|
|
693
706
|
const raw = this._params?.position;
|
|
694
|
-
const mode = raw == null ? '
|
|
707
|
+
const mode = raw == null ? 'auto' : String(raw).trim().toLowerCase();
|
|
695
708
|
if (mode === 'auto' || mode === 'top' || mode === 'bottom' || mode === 'none') return mode;
|
|
696
|
-
return '
|
|
709
|
+
return 'auto';
|
|
697
710
|
}
|
|
698
711
|
|
|
699
712
|
_getOverflowAncestor(startEl) {
|
|
@@ -764,23 +777,28 @@ class VGSelect extends BaseModule {
|
|
|
764
777
|
*/
|
|
765
778
|
static destroy(select) {
|
|
766
779
|
const container = select.nextElementSibling;
|
|
767
|
-
if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
|
|
768
|
-
|
|
769
|
-
|
|
780
|
+
if (container && container.classList.contains(CLASS_NAME_CONTAINER)) {
|
|
781
|
+
VGSelect.getInstance(container)?.dispose();
|
|
782
|
+
container.remove();
|
|
783
|
+
}
|
|
784
|
+
delete select.dataset.inited;
|
|
770
785
|
}
|
|
771
786
|
|
|
772
787
|
/**
|
|
773
788
|
* Обновляет отображаемое значение (текст, теги)
|
|
774
789
|
* @param {HTMLSelectElement} select - Исходный <select>
|
|
775
790
|
*/
|
|
776
|
-
static updateUI(select) {
|
|
791
|
+
static updateUI(select) {
|
|
777
792
|
const container = select.nextElementSibling;
|
|
778
793
|
if (!container || !container.classList.contains(CLASS_NAME_CONTAINER)) return;
|
|
779
794
|
|
|
780
795
|
const current = container.querySelector(SELECTOR_CURRENT);
|
|
781
796
|
const placeholder = select.dataset.placeholder || '';
|
|
782
797
|
const isMultiple = select.multiple;
|
|
783
|
-
const instance = VGSelect.getInstance(container);
|
|
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
|
+
});
|
|
784
802
|
|
|
785
803
|
if (isMultiple) {
|
|
786
804
|
const tags = current.querySelector(`.${CLASS_NAME_TAGS}`);
|
|
@@ -797,8 +815,12 @@ class VGSelect extends BaseModule {
|
|
|
797
815
|
instance?._callCallback('onClear');
|
|
798
816
|
}
|
|
799
817
|
|
|
800
|
-
if (selected.length === 0) {
|
|
801
|
-
input.placeholder = placeholder;
|
|
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);
|
|
802
824
|
} else {
|
|
803
825
|
input.placeholder = '';
|
|
804
826
|
selected.forEach(opt => {
|
|
@@ -983,15 +1005,16 @@ class VGSelect extends BaseModule {
|
|
|
983
1005
|
_initLoadMoreButton() {
|
|
984
1006
|
if (!this._params.search?.pagination || !this._params.search.remote) return;
|
|
985
1007
|
|
|
986
|
-
const list = this._element.querySelector(SELECTOR_LIST);
|
|
987
|
-
if (!list) return;
|
|
1008
|
+
const list = this._element.querySelector(SELECTOR_LIST);
|
|
1009
|
+
if (!list) return;
|
|
1010
|
+
if (list.querySelector(SELECTOR_LOAD_MORE_BTN)) return;
|
|
988
1011
|
|
|
989
1012
|
const btn = document.createElement('li');
|
|
990
1013
|
btn.className = CLASS_NAME_LOAD_MORE;
|
|
991
1014
|
btn.style.textAlign = 'center';
|
|
992
1015
|
btn.style.padding = '8px';
|
|
993
1016
|
btn.style.cursor = 'pointer';
|
|
994
|
-
btn.style.color = '#007bff';
|
|
1017
|
+
btn.style.color = 'var(--vg-primary-color, #007bff)';
|
|
995
1018
|
btn.style.fontSize = '14px';
|
|
996
1019
|
btn.style.fontWeight = '500';
|
|
997
1020
|
btn.textContent = this._params.search.loadMoreText;
|
|
@@ -1042,9 +1065,10 @@ class VGSelect extends BaseModule {
|
|
|
1042
1065
|
* Загружает следующую страницу данных по клику
|
|
1043
1066
|
* @private
|
|
1044
1067
|
*/
|
|
1045
|
-
async _loadNextPage() {
|
|
1068
|
+
async _loadNextPage() {
|
|
1046
1069
|
const { route, pageParam = 'page', termParam = 'q', perpage = 20 } = this._params.search;
|
|
1047
|
-
const nextPage = this._currentPage + 1;
|
|
1070
|
+
const nextPage = this._currentPage + 1;
|
|
1071
|
+
const requestId = this._remoteSearchRequestId;
|
|
1048
1072
|
|
|
1049
1073
|
const url = new URL(route, window.location.origin);
|
|
1050
1074
|
url.searchParams.set(termParam, this._searchTerm);
|
|
@@ -1055,9 +1079,11 @@ class VGSelect extends BaseModule {
|
|
|
1055
1079
|
this._showLoading(true);
|
|
1056
1080
|
this._hideLoadMoreButton(true);
|
|
1057
1081
|
|
|
1058
|
-
try {
|
|
1059
|
-
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
|
|
1060
|
-
|
|
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;
|
|
1061
1087
|
|
|
1062
1088
|
if (Array.isArray(data.results)) {
|
|
1063
1089
|
VGSelect.addOptions(this._element.previousElementSibling, data, { preserve: true });
|
|
@@ -1072,13 +1098,16 @@ class VGSelect extends BaseModule {
|
|
|
1072
1098
|
this._callCallback('onLoadNext', { page: this._currentPage, data });
|
|
1073
1099
|
this._triggerEvent(EVENT_KEY_LOAD_NEXT, { page: this._currentPage, term: this._searchTerm });
|
|
1074
1100
|
}
|
|
1075
|
-
} catch (err) {
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1076
1103
|
console.error('VGSelect: Failed to load next page', err);
|
|
1077
1104
|
this._triggerEvent(EVENT_KEY_ERROR, { error: 'Pagination fetch failed', term: this._searchTerm });
|
|
1078
1105
|
this._hideLoadMoreButton(false); // оставить кнопку при ошибке
|
|
1079
|
-
} finally {
|
|
1080
|
-
this.
|
|
1081
|
-
|
|
1106
|
+
} finally {
|
|
1107
|
+
if (!this._disposed && requestId === this._remoteSearchRequestId) {
|
|
1108
|
+
this._showLoading(false);
|
|
1109
|
+
this._loading = false;
|
|
1110
|
+
}
|
|
1082
1111
|
}
|
|
1083
1112
|
}
|
|
1084
1113
|
|
|
@@ -1100,7 +1129,8 @@ class VGSelect extends BaseModule {
|
|
|
1100
1129
|
// Обновляем текущее "желательное" состояние
|
|
1101
1130
|
this._searchTerm = term;
|
|
1102
1131
|
this._currentPage = 1;
|
|
1103
|
-
this._totalPages = null;
|
|
1132
|
+
this._totalPages = null;
|
|
1133
|
+
this._loading = false;
|
|
1104
1134
|
|
|
1105
1135
|
// Отменяем предыдущий запрос (если ещё летит)
|
|
1106
1136
|
if (this._remoteSearchAbortController) {
|
|
@@ -1120,10 +1150,14 @@ class VGSelect extends BaseModule {
|
|
|
1120
1150
|
headers: { 'Content-Type': 'application/json' },
|
|
1121
1151
|
signal
|
|
1122
1152
|
})
|
|
1123
|
-
.then(res =>
|
|
1153
|
+
.then(res => {
|
|
1154
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1155
|
+
return res.json();
|
|
1156
|
+
})
|
|
1124
1157
|
.then(data => {
|
|
1125
1158
|
// Если прилетел не самый свежий ответ — игнорируем
|
|
1126
|
-
if (requestId !== this._remoteSearchRequestId) return;
|
|
1159
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1160
|
+
if (!Array.isArray(data.results)) throw new Error('Invalid results');
|
|
1127
1161
|
|
|
1128
1162
|
// Если пользователь уже ввёл другой текст — тоже игнорируем
|
|
1129
1163
|
const liveTerm = searchInput ? searchInput.value.trim() : '';
|
|
@@ -1172,7 +1206,8 @@ class VGSelect extends BaseModule {
|
|
|
1172
1206
|
this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_ACTIVE);
|
|
1173
1207
|
}
|
|
1174
1208
|
})
|
|
1175
|
-
.catch(err => {
|
|
1209
|
+
.catch(err => {
|
|
1210
|
+
if (this._disposed || requestId !== this._remoteSearchRequestId) return;
|
|
1176
1211
|
// Abort — нормальная ситуация при быстром вводе
|
|
1177
1212
|
if (err && (err.name === 'AbortError')) return;
|
|
1178
1213
|
|
|
@@ -1182,7 +1217,7 @@ class VGSelect extends BaseModule {
|
|
|
1182
1217
|
})
|
|
1183
1218
|
.finally(() => {
|
|
1184
1219
|
// Лоадер убираем только для последнего актуального запроса
|
|
1185
|
-
if (requestId === this._remoteSearchRequestId) {
|
|
1220
|
+
if (!this._disposed && requestId === this._remoteSearchRequestId) {
|
|
1186
1221
|
this._showLoading(false);
|
|
1187
1222
|
}
|
|
1188
1223
|
});
|
|
@@ -1281,8 +1316,9 @@ class VGSelect extends BaseModule {
|
|
|
1281
1316
|
|
|
1282
1317
|
if (isRebuild) {
|
|
1283
1318
|
const drop = container.querySelector(`.${CLASS_NAME_DROPDOWN}`);
|
|
1284
|
-
VGSelect.buildListOptions(select, drop, instance?._params || {});
|
|
1285
|
-
instance?._syncSearch();
|
|
1319
|
+
VGSelect.buildListOptions(select, drop, instance?._params || {});
|
|
1320
|
+
instance?._syncSearch();
|
|
1321
|
+
this.updateUI(select);
|
|
1286
1322
|
instance?._triggerEvent(EVENT_KEY_REBUILD);
|
|
1287
1323
|
} else {
|
|
1288
1324
|
this.updateUI(select);
|
|
@@ -1294,21 +1330,29 @@ class VGSelect extends BaseModule {
|
|
|
1294
1330
|
* @param {string} term - Поисковый запрос
|
|
1295
1331
|
* @private
|
|
1296
1332
|
*/
|
|
1297
|
-
_filterLocalOptions(term) {
|
|
1298
|
-
const list = this._drop.querySelector(`.${CLASS_NAME_LIST}`);
|
|
1299
|
-
const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
el.
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
+
}
|
|
1312
1356
|
}
|
|
1313
1357
|
|
|
1314
1358
|
_handlersVGSelect();
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: боковые панели VGSidebar с декларативным и программным управлением.
|
|
3
|
+
* Возможности: четыре стороны экрана, backdrop, прокрутка, URL-хэш, AJAX, события и доступность.
|
|
4
|
+
*/
|
|
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";
|
|
4
8
|
import { dismissTrigger } from "../../module-fn";
|
|
@@ -211,7 +215,8 @@ class VGSidebar extends BaseModule {
|
|
|
211
215
|
EventHandler.on(window, EVENT_KEYS.POPSTATE_DATA_API, this._showPopstateHandler);
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
this._element.classList.add(CLASS_NAME_SHOW);
|
|
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 = () => {
|
|
@@ -237,7 +242,8 @@ class VGSidebar extends BaseModule {
|
|
|
237
242
|
const hideEvent = EventHandler.trigger(this._element, EVENT_KEYS.HIDE);
|
|
238
243
|
if (hideEvent.defaultPrevented) return;
|
|
239
244
|
|
|
240
|
-
this._element.classList.remove(CLASS_NAME_SHOW);
|
|
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);
|
|
@@ -288,13 +294,17 @@ class VGSidebar extends BaseModule {
|
|
|
288
294
|
* Очищает ресурсы модуля.
|
|
289
295
|
* @override
|
|
290
296
|
*/
|
|
291
|
-
dispose() {
|
|
292
|
-
|
|
293
|
-
EventHandler.off(
|
|
297
|
+
dispose() {
|
|
298
|
+
if (!this._element) return;
|
|
299
|
+
EventHandler.off(document, EVENT_KEYS.KEYDOWN_DISMISS, this._keydownHandler);
|
|
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
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: отслеживание активных секций и навигация по якорям.
|
|
3
|
+
* Возможности: Data API, нативная и виртуальная прокрутка, вложенные меню и обновление секций.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
6
|
+
import { getElement, isDisabled, isVisible, normalizeData } from "../../../utils/js/functions";
|
|
3
7
|
import EventHandler from "../../../utils/js/dom/event";
|
|
4
8
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
5
9
|
|
|
@@ -52,7 +56,7 @@ class VGSpy extends BaseModule {
|
|
|
52
56
|
* @property {number[]|string} threshold - пороги видимости (0.1, 0.5, 1)
|
|
53
57
|
*/
|
|
54
58
|
this._params = this._configAfterMerge(
|
|
55
|
-
|
|
59
|
+
Object.assign(
|
|
56
60
|
{
|
|
57
61
|
offset: null, // Устаревшее, для обратной совместимости
|
|
58
62
|
rootMargin: '0px 0px -25%',
|
|
@@ -60,7 +64,8 @@ class VGSpy extends BaseModule {
|
|
|
60
64
|
target: this._element,
|
|
61
65
|
threshold: [0.1, 0.5, 1],
|
|
62
66
|
},
|
|
63
|
-
params
|
|
67
|
+
params,
|
|
68
|
+
this._getDataOptions()
|
|
64
69
|
)
|
|
65
70
|
);
|
|
66
71
|
|
|
@@ -130,15 +135,27 @@ class VGSpy extends BaseModule {
|
|
|
130
135
|
* Ключ модуля (для хранения в data)
|
|
131
136
|
* @returns {string}
|
|
132
137
|
*/
|
|
133
|
-
static get NAME_KEY() {
|
|
134
|
-
return NAME_KEY;
|
|
135
|
-
}
|
|
138
|
+
static get NAME_KEY() {
|
|
139
|
+
return NAME_KEY;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_getDataOptions() {
|
|
143
|
+
const options = {};
|
|
144
|
+
for (const name of ['target', 'offset', 'rootMargin', 'smoothScroll', 'threshold']) {
|
|
145
|
+
const attribute = `data-${name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
|
|
146
|
+
if (this._element.hasAttribute(attribute)) {
|
|
147
|
+
options[name] = normalizeData(this._element.getAttribute(attribute));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return options;
|
|
151
|
+
}
|
|
136
152
|
|
|
137
153
|
/**
|
|
138
154
|
* Инициализирует или перезапускает модуль: находит ссылки и секции, создаёт observer
|
|
139
155
|
*/
|
|
140
|
-
refresh() {
|
|
141
|
-
this.
|
|
156
|
+
refresh() {
|
|
157
|
+
this._process(null);
|
|
158
|
+
this._initializeTargetsAndObservables();
|
|
142
159
|
this._updateRootElement();
|
|
143
160
|
this._maybeEnableSmoothScroll();
|
|
144
161
|
|
|
@@ -163,8 +180,8 @@ class VGSpy extends BaseModule {
|
|
|
163
180
|
|
|
164
181
|
// Smooth Scrollbar can be initialized after the spy; retry once on the next tick.
|
|
165
182
|
if (this._rootElement && !this._isScrollableSelf(this._rootElement) && window.Scrollbar) {
|
|
166
|
-
setTimeout(() => {
|
|
167
|
-
if (this._scrollbar) return;
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
if (!this._element || this._scrollbar) return;
|
|
168
185
|
this._updateSmoothScrollbar();
|
|
169
186
|
if (this._scrollbar) this._setupScrollbarTracking();
|
|
170
187
|
}, 0);
|
|
@@ -356,7 +373,9 @@ class VGSpy extends BaseModule {
|
|
|
356
373
|
return window.scrollY || document.documentElement.scrollTop || 0;
|
|
357
374
|
}
|
|
358
375
|
|
|
359
|
-
dispose() {
|
|
376
|
+
dispose() {
|
|
377
|
+
if (!this._element) return;
|
|
378
|
+
this._process(null);
|
|
360
379
|
if (this._observer) {
|
|
361
380
|
this._observer.disconnect();
|
|
362
381
|
}
|
|
@@ -415,8 +434,9 @@ class VGSpy extends BaseModule {
|
|
|
415
434
|
// If the target section is currently hidden (tabs/collapses/lazy layout),
|
|
416
435
|
// don't hijack the click; allow UI to reveal it, then retry.
|
|
417
436
|
if (!isVisible(section)) {
|
|
418
|
-
setTimeout(() => {
|
|
419
|
-
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
if (!this._element) return;
|
|
439
|
+
const revealed = Selectors.findID(id);
|
|
420
440
|
if (!revealed || !isVisible(revealed)) return;
|
|
421
441
|
|
|
422
442
|
this._targetLinks.set(revealed.id, link);
|
|
@@ -505,10 +525,10 @@ class VGSpy extends BaseModule {
|
|
|
505
525
|
const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
|
|
506
526
|
this._previousScrollData.parentScrollTop = parentScrollTop;
|
|
507
527
|
|
|
508
|
-
for (const entry of entries) {
|
|
509
|
-
if (!entry.isIntersecting) {
|
|
510
|
-
|
|
511
|
-
continue;
|
|
528
|
+
for (const entry of entries) {
|
|
529
|
+
if (!entry.isIntersecting) {
|
|
530
|
+
if (getTargetLink(entry) === this._activeTarget) this._process(null);
|
|
531
|
+
continue;
|
|
512
532
|
}
|
|
513
533
|
|
|
514
534
|
const entryTop = this._getSectionTop(entry.target);
|
|
@@ -516,7 +536,7 @@ class VGSpy extends BaseModule {
|
|
|
516
536
|
const shouldActivate =
|
|
517
537
|
(userScrollsDown && isEntryBelow) || (!userScrollsDown && !isEntryBelow);
|
|
518
538
|
|
|
519
|
-
if (shouldActivate) {
|
|
539
|
+
if (shouldActivate || !this._activeTarget) {
|
|
520
540
|
this._previousScrollData.visibleEntryTop = entryTop;
|
|
521
541
|
this._process(getTargetLink(entry));
|
|
522
542
|
}
|
|
@@ -595,7 +615,7 @@ class VGSpy extends BaseModule {
|
|
|
595
615
|
parent.classList.remove(CLASS_NAME_ACTIVE);
|
|
596
616
|
|
|
597
617
|
const activeLinks = Selectors.findAll(
|
|
598
|
-
`[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}`,
|
|
618
|
+
`[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}, ${SELECTOR_DROPDOWN_TOGGLE}.${CLASS_NAME_ACTIVE}`,
|
|
599
619
|
parent
|
|
600
620
|
);
|
|
601
621
|
for (const link of activeLinks) {
|
|
@@ -603,8 +623,9 @@ class VGSpy extends BaseModule {
|
|
|
603
623
|
}
|
|
604
624
|
}
|
|
605
625
|
|
|
606
|
-
_getTargetIdFromTrigger(trigger) {
|
|
607
|
-
if (!trigger || typeof trigger.getAttribute !== 'function') return null;
|
|
626
|
+
_getTargetIdFromTrigger(trigger) {
|
|
627
|
+
if (!trigger || typeof trigger.getAttribute !== 'function') return null;
|
|
628
|
+
if (isDisabled(trigger) || trigger.matches(SELECTOR_DROPDOWN_TOGGLE)) return null;
|
|
608
629
|
|
|
609
630
|
const dataTarget = (trigger.getAttribute('data-vg-target') || '').trim();
|
|
610
631
|
const href = (trigger.getAttribute('href') || '').trim();
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: константы и параметры по умолчанию базовой таблицы VGTable.
|
|
3
|
-
* Возможности: единая настройка Data API, i18n, wrapper, remote
|
|
4
|
-
*/
|
|
3
|
+
* Возможности: единая настройка Data API, i18n, wrapper, remote, фильтров, sticky, колонок, строк, выбора и адаптивной пагинации.
|
|
4
|
+
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Служебные константы модуля.
|
|
8
8
|
*/
|
|
9
9
|
// Имя компонента в публичном API. Варианты: фиксированное значение 'table'.
|
|
10
|
-
const NAME = 'table';
|
|
10
|
+
const NAME = 'table';
|
|
11
11
|
// Ключ экземпляра в Data API. Варианты: фиксированное значение 'vg.table'.
|
|
12
12
|
const NAME_KEY = `vg.${NAME}`;
|
|
13
13
|
// Селектор автоматической инициализации. Варианты: элементы с атрибутом data-vg-table.
|
|
@@ -21,7 +21,16 @@ const TABLE_CONTAINER_SELECTOR = '.vg-table-container';
|
|
|
21
21
|
// Маркер container, созданного компонентом. Варианты: пустой data-атрибут.
|
|
22
22
|
const GENERATED_TABLE_CONTAINER_ATTRIBUTE = 'data-vg-table-generated-container';
|
|
23
23
|
|
|
24
|
-
const DEFAULT_OPTIONS = {
|
|
24
|
+
const DEFAULT_OPTIONS = {
|
|
25
|
+
/** Профили представления: базовые параметры + все достигнутые границы xs–xxl. */
|
|
26
|
+
responsive: {
|
|
27
|
+
// Включается явно; Data API: data-responsive-enabled.
|
|
28
|
+
enabled: false,
|
|
29
|
+
// Локальные переопределения границ общего Responsive; пустой объект сохраняет глобальные значения.
|
|
30
|
+
breakpoints: {},
|
|
31
|
+
// Профили принимают pagination: maxButtons, align, position, size.enabled/label, quick.enabled.
|
|
32
|
+
xs: {}, sm: {}, md: {}, lg: {}, xl: {}, xxl: {},
|
|
33
|
+
},
|
|
25
34
|
// Активная локаль встроенного интерфейса. Data API: data-locale. Варианты: 'ru' | 'en' | региональный BCP 47 код.
|
|
26
35
|
locale: 'ru',
|
|
27
36
|
// Пользовательские словари, объединяемые со встроенными ru/en. Data API отсутствует; варианты: объект {locale: {group: {key: value}}}.
|
|
@@ -359,7 +368,9 @@ const DEFAULT_OPTIONS = {
|
|
|
359
368
|
// Задаёт число страниц, после которого разрешены многоточие и quick='auto'. Варианты: целое число >= 1.
|
|
360
369
|
threshold: 5,
|
|
361
370
|
// Задаёт максимальное число соседних страниц между первой и последней. Варианты: целое число >= 1.
|
|
362
|
-
visible: 5,
|
|
371
|
+
visible: 5,
|
|
372
|
+
// Лимит номеров и многоточий без prev/next; null сохраняет visible/threshold. Data API: data-pagination-max-buttons.
|
|
373
|
+
maxButtons: null,
|
|
363
374
|
|
|
364
375
|
/** Поле и VGDropdown для выбора количества строк. */
|
|
365
376
|
size: {
|