vgapp 1.5.6 → 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/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/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/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/package.json +1 -1
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Возможности: modal, overlay и dropdown-рендер, компактный размер, Data API, AJAX и Promise-сценарии.
|
|
4
4
|
*/
|
|
5
5
|
import BaseModule from "../../base-module";
|
|
6
|
-
import VGModal from "../../vgmodal";
|
|
6
|
+
import VGModal from "../../vgmodal";
|
|
7
|
+
import VGSidebar from "../../vgsidebar";
|
|
7
8
|
import VGDropdown from "../../vgdropdown";
|
|
8
9
|
|
|
9
10
|
import { execute, isElement, isVisible, makeRandomString, mergeDeepObject, reflow } from "../../../utils/js/functions";
|
|
@@ -450,8 +451,10 @@ class VGAlert {
|
|
|
450
451
|
};
|
|
451
452
|
}
|
|
452
453
|
|
|
453
|
-
const
|
|
454
|
-
|
|
454
|
+
const parent = containerWrap.classList.contains('vg-sidebar')
|
|
455
|
+
? VGSidebar.getOrCreateInstance(containerWrap)
|
|
456
|
+
: VGModal.getOrCreateInstance(containerWrap);
|
|
457
|
+
const container = Selectors.find('.vg-modal-content', containerWrap) || containerWrap;
|
|
455
458
|
|
|
456
459
|
const overlay = document.createElement('div');
|
|
457
460
|
|
|
@@ -463,7 +466,7 @@ class VGAlert {
|
|
|
463
466
|
|
|
464
467
|
return {
|
|
465
468
|
element: overlay,
|
|
466
|
-
render:
|
|
469
|
+
render: parent,
|
|
467
470
|
};
|
|
468
471
|
}
|
|
469
472
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Описание: базовая логика выбора, валидации и отображения файлов VGFiles.
|
|
3
|
-
* Возможности: управляет
|
|
3
|
+
* Возможности: управляет файлами, валидацией, списками, миниатюрами и иконками, действиями, статистикой и скрытыми полями.
|
|
4
4
|
*/
|
|
5
5
|
import BaseModule from "../../base-module";
|
|
6
6
|
import {mergeDeepObject} from "../../../utils/js/functions";
|
|
@@ -8,7 +8,8 @@ import Html from "../../../utils/js/components/templater";
|
|
|
8
8
|
import {lang_messages} from "../../../utils/js/components/lang";
|
|
9
9
|
import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
|
|
10
10
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
11
|
-
import {getSVG} from "../../module-fn";
|
|
11
|
+
import {getSVG} from "../../module-fn";
|
|
12
|
+
import {resolveFileIconName} from "../../../utils/js/components/file-icon";
|
|
12
13
|
import VGFilePreview from "../../vgfilepreview";
|
|
13
14
|
import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
|
|
14
15
|
|
|
@@ -813,34 +814,28 @@ class VGFilesBase extends BaseModule {
|
|
|
813
814
|
}
|
|
814
815
|
}
|
|
815
816
|
|
|
816
|
-
_renderUIImage(file) {
|
|
817
|
-
const $container = this._tpl.div({ class: 'file-image' });
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return $container;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const icon = this._getIconByFileType(file);
|
|
842
|
-
$container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
|
|
843
|
-
return $container;
|
|
817
|
+
_renderUIImage(file) {
|
|
818
|
+
const $container = this._tpl.div({ class: 'file-image' });
|
|
819
|
+
const renderIcon = () => {
|
|
820
|
+
const icon = this._getIconByFileType(file);
|
|
821
|
+
$container.replaceChildren(this._tpl.i({}, icon, { isHTML: true }));
|
|
822
|
+
};
|
|
823
|
+
const customData = this._getFileCustomData(file);
|
|
824
|
+
let imageSrc = String(file?.image || '').trim() || String(customData.audioCover || '').trim();
|
|
825
|
+
|
|
826
|
+
if (!imageSrc && resolveFileIconName({ type: file?.type, name: file?.name || file?.src }) === 'file-image') {
|
|
827
|
+
imageSrc = String(file?.src || '').trim() || this._getFileObjectUrl(file);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (imageSrc) {
|
|
831
|
+
const image = this._tpl.img(imageSrc, this._resolveDisplayName(file), { class: 'file-preview' });
|
|
832
|
+
image.addEventListener('error', renderIcon, { once: true });
|
|
833
|
+
$container.appendChild(image);
|
|
834
|
+
} else {
|
|
835
|
+
renderIcon();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return $container;
|
|
844
839
|
}
|
|
845
840
|
|
|
846
841
|
_resolveDisplayName(file) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: модальные окна VGModal с декларативным и программным управлением.
|
|
3
|
+
* Возможности: AJAX, backdrop, геометрия окна, сворачивание и синхронизация доступности.
|
|
4
|
+
*/
|
|
5
|
+
import BaseModule from "../../base-module";
|
|
2
6
|
import ScrollBarHelper from "../../../utils/js/components/scrollbar";
|
|
3
7
|
import Backdrop from "../../../utils/js/components/backdrop";
|
|
4
8
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
@@ -346,7 +350,8 @@ class VGModal extends BaseModule {
|
|
|
346
350
|
this._saveInteractionState();
|
|
347
351
|
this._disableInteractionHandlers();
|
|
348
352
|
this._minimized.reset();
|
|
349
|
-
this._element.style.display = 'none';
|
|
353
|
+
this._element.style.display = 'none';
|
|
354
|
+
this._element.setAttribute('aria-hidden', 'true');
|
|
350
355
|
this._element.removeAttribute('aria-modal');
|
|
351
356
|
this._element.removeAttribute('role');
|
|
352
357
|
this._isTransitioning = false;
|
|
@@ -395,7 +400,8 @@ class VGModal extends BaseModule {
|
|
|
395
400
|
document.body.append(this._element);
|
|
396
401
|
}
|
|
397
402
|
|
|
398
|
-
this._element.style.display = 'block';
|
|
403
|
+
this._element.style.display = 'block';
|
|
404
|
+
this._element.removeAttribute('aria-hidden');
|
|
399
405
|
this._element.setAttribute('aria-modal', true);
|
|
400
406
|
this._element.setAttribute('role', 'dialog');
|
|
401
407
|
this._element.scrollTop = 0;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Описание: делегированные обработчики интерфейса VGSelect.
|
|
3
|
+
* Возможности: открытие списка, выбор опций, теги, клавиатура и сброс формы.
|
|
4
|
+
*/
|
|
5
|
+
import VGSelect from "./vgselect";
|
|
2
6
|
import EventHandler from "../../../utils/js/dom/event";
|
|
3
7
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
4
8
|
import {Manipulator} from "../../../utils/js/dom/manipulator";
|
|
5
|
-
import {transliterate} from "../../../utils/js/functions";
|
|
6
9
|
|
|
7
10
|
const NAME_KEY = 'vg.select';
|
|
8
11
|
|
|
@@ -100,56 +103,6 @@ const _handlersVGSelect = () => {
|
|
|
100
103
|
}
|
|
101
104
|
});
|
|
102
105
|
|
|
103
|
-
EventHandler.on(document, EVENT_KEY_UP_DATA_API, SELECTOR_SEARCH_TOGGLE, function(e) {
|
|
104
|
-
const input = e.target;
|
|
105
|
-
const dropdown = input.closest(SELECTOR_DROPDOWN);
|
|
106
|
-
const list = dropdown?.querySelector(`.${CLASS_NAME_LIST}`);
|
|
107
|
-
if (!list) return;
|
|
108
|
-
|
|
109
|
-
const container = input.closest(`.${CLASS_NAME_CONTAINER}`);
|
|
110
|
-
const instance = VGSelect.getInstance(container);
|
|
111
|
-
|
|
112
|
-
const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
|
|
113
|
-
const groups = list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`);
|
|
114
|
-
const value = input.value.trim().toLowerCase();
|
|
115
|
-
const search = [value, transliterate(value), transliterate(value, true)];
|
|
116
|
-
|
|
117
|
-
options.forEach(el => { el.hidden = false; el.style.display = ''; });
|
|
118
|
-
groups.forEach(el => { el.hidden = false; el.style.display = ''; });
|
|
119
|
-
|
|
120
|
-
if (value) {
|
|
121
|
-
let visibleCount = 0;
|
|
122
|
-
groups.length ? groups.forEach(group => {
|
|
123
|
-
const items = group.querySelectorAll(`.${CLASS_NAME_OPTION}`);
|
|
124
|
-
const visible = Array.from(items).some(item => {
|
|
125
|
-
const t = item.textContent.toLowerCase();
|
|
126
|
-
return search.some(s => t.includes(s));
|
|
127
|
-
});
|
|
128
|
-
group.hidden = !visible;
|
|
129
|
-
group.style.display = visible ? '' : 'none';
|
|
130
|
-
items.forEach(item => {
|
|
131
|
-
const t = item.textContent.toLowerCase();
|
|
132
|
-
const match = search.some(s => t.includes(s));
|
|
133
|
-
item.hidden = !match;
|
|
134
|
-
item.style.display = match ? '' : 'none';
|
|
135
|
-
if (match) visibleCount++;
|
|
136
|
-
});
|
|
137
|
-
}) : options.forEach(option => {
|
|
138
|
-
const t = option.textContent.toLowerCase();
|
|
139
|
-
const match = search.some(s => t.includes(s));
|
|
140
|
-
option.hidden = !match;
|
|
141
|
-
option.style.display = match ? '' : 'none';
|
|
142
|
-
if (match) visibleCount++;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
instance?._triggerEvent(EVENT_KEY_SEARCH, { query: value, results: visibleCount });
|
|
146
|
-
instance?._callCallback('onSearch', { query: value, results: visibleCount });
|
|
147
|
-
} else {
|
|
148
|
-
instance?._triggerEvent(EVENT_KEY_SEARCH, { query: '', results: options.length });
|
|
149
|
-
instance?._callCallback('onSearch', { query: '', results: options.length });
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
106
|
EventHandler.on(document, EVENT_RESET_DATA_API, 'form', function() {
|
|
154
107
|
Selectors.findAll('select[data-inited="true"]', this).forEach(select => VGSelect.build(select, true));
|
|
155
108
|
});
|
|
@@ -236,4 +189,4 @@ const _handlersVGSelect = () => {
|
|
|
236
189
|
});
|
|
237
190
|
}
|
|
238
191
|
|
|
239
|
-
export default _handlersVGSelect;
|
|
192
|
+
export default _handlersVGSelect;
|
|
@@ -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
|
|