vgapp 0.8.1 → 0.8.3
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/CHANGELOG.md +10 -0
- package/app/langs/en/buttons.json +8 -0
- package/app/langs/en/messages.json +3 -0
- package/app/langs/en/titles.json +3 -0
- package/app/langs/ru/buttons.json +8 -0
- package/app/langs/ru/messages.json +3 -0
- package/app/langs/ru/titles.json +3 -0
- package/app/modules/vgloadmore/js/vgloadmore.js +159 -8
- package/app/modules/vgrollup/js/vgrollup.js +328 -160
- package/app/modules/vgrollup/readme.md +196 -0
- package/app/modules/vgselect/js/handlers.js +220 -0
- package/app/modules/vgselect/js/vgselect.js +783 -298
- package/app/modules/vgselect/readme.md +180 -0
- package/app/modules/vgselect/scss/_variables.scss +20 -0
- package/app/modules/vgselect/scss/vgselect.scss +42 -2
- package/app/modules/vgsidebar/js/vgsidebar.js +194 -84
- package/app/modules/vgsidebar/readme.md +157 -0
- package/app/modules/vgspy/js/vgspy.js +236 -132
- package/app/modules/vgspy/readme.md +105 -0
- package/app/modules/vgtabs/js/vgtabs.js +290 -182
- package/app/modules/vgtabs/readme.md +156 -0
- package/app/modules/vgtoast/js/vgtoast.js +260 -156
- package/app/modules/vgtoast/readme.md +145 -0
- package/app/utils/js/components/backdrop.js +4 -1
- package/build/vgapp.css +1 -1
- package/build/vgapp.css.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# VGSidebar – Модуль боковой панели (сайдбара)
|
|
2
|
+
|
|
3
|
+
`VGSidebar` – это модуль на чистом JavaScript, реализующий интерактивную боковую панель (сайдбар) с поддержкой анимаций, backdrop, AJAX-загрузки, управления скроллом и навигации через URL-хэш. Легко интегрируется в любые проекты без зависимостей.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ✅ Возможности
|
|
8
|
+
|
|
9
|
+
- Открытие/закрытие сайдбара по клику, хэшу URL или программно
|
|
10
|
+
- Поддержка затемнённого фона (backdrop)
|
|
11
|
+
- Блокировка скролла страницы при открытом сайдбаре
|
|
12
|
+
- Поддержка клавиши **Escape** для закрытия
|
|
13
|
+
- Анимации входа и выхода (через CSS-классы, например, с `animate.css`)
|
|
14
|
+
- Возможность загрузки контента через AJAX
|
|
15
|
+
- Поддержка открытия по `#id` в URL (хэш-роутинг)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🔧 Установка
|
|
20
|
+
|
|
21
|
+
HTML разметка сайдбара:
|
|
22
|
+
```html
|
|
23
|
+
<div class="vg-sidebar left" id="sidebar-left" data-animation='{"enable": true, "in": "animate__fadeInLeft", "out": "animate__fadeOutLeft"}'>
|
|
24
|
+
<div class="vg-sidebar-header">
|
|
25
|
+
<div class="vg-sidebar-header--title">...</div>
|
|
26
|
+
<button type="button" class="vg-btn-close" data-vg-dismiss="sidebar" data-vg-target="#sidebar-left" aria-label="Close"></button>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="vg-sidebar-body">...</div>
|
|
29
|
+
<div class="vg-sidebar-footer">...</div>
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ⚙️ Параметры (настройки)
|
|
35
|
+
|
|
36
|
+
Параметры можно задать:
|
|
37
|
+
- Через `data-*` атрибуты
|
|
38
|
+
- Через JavaScript при инициализации
|
|
39
|
+
- Через объединение обоих способов
|
|
40
|
+
|
|
41
|
+
| Параметр | Тип | По умолчанию | Описание |
|
|
42
|
+
|--------------------|----------|--------------------|---------|
|
|
43
|
+
| `backdrop` | boolean | `true` | Показывать затемнённый фон |
|
|
44
|
+
| `overflow` | boolean | `true` | Блокировать скролл страницы |
|
|
45
|
+
| `keyboard` | boolean | `true` | Закрывать по нажатию `Esc` |
|
|
46
|
+
| `hash` | boolean | `false` | Поддержка открытия по `#id` в URL |
|
|
47
|
+
| `animation.enable` | boolean | `false` | Включить анимации |
|
|
48
|
+
| `animation.in` | string | `animate__rollIn` | CSS-класс анимации входа |
|
|
49
|
+
| `animation.out` | string | `animate__rollOut` | CSS-класс анимации выхода |
|
|
50
|
+
| `animation.delay` | number | `800` | Задержка перед закрытием (мс) |
|
|
51
|
+
| `ajax.route` | string | `''` | URL для AJAX-загрузки |
|
|
52
|
+
| `ajax.target` | string | `''` | Селектор внутри сайдбара для вставки |
|
|
53
|
+
| `ajax.method` | string | `'get'` | HTTP-метод (`get`, `post`) |
|
|
54
|
+
| `ajax.loader` | boolean | `false` | Показывать лоадер |
|
|
55
|
+
| `ajax.once` | boolean | `false` | Загружать контент только один раз |
|
|
56
|
+
| `ajax.output` | boolean | `true` | Вставлять ответ в DOM |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🖱️ Использование
|
|
61
|
+
|
|
62
|
+
### 1. Через Data API (рекомендуется)
|
|
63
|
+
```html
|
|
64
|
+
<a href="#sidebar-left" data-vg-toggle="sidebar">Открыть панель слева</a>
|
|
65
|
+
или
|
|
66
|
+
<button class="btn btn-primary" data-vg-target="#sidebar-right" data-vg-toggle="sidebar">Открыть панель справа</button>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> ⚠️ Обязательно задавайте `id`, если используете `data-vg-target` или хэш.
|
|
70
|
+
|
|
71
|
+
### 2. Через JavaScript
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import VGSidebar from './modules/vgsidebar/js/vgsidebar.js';
|
|
75
|
+
const sidebar = new VGSidebar(document.getElementById('sidebar-left'), {
|
|
76
|
+
backdrop: true,
|
|
77
|
+
overflow: true,
|
|
78
|
+
keyboard: true,
|
|
79
|
+
hash: true,
|
|
80
|
+
animation: {
|
|
81
|
+
enable: true,
|
|
82
|
+
in: 'animate__fadeInLeft',
|
|
83
|
+
out: 'animate__fadeOutLeft',
|
|
84
|
+
delay: 500
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
sidebar.show();
|
|
89
|
+
sidebar.hide();
|
|
90
|
+
sidebar.toggle();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Открытие по хэшу URL
|
|
94
|
+
|
|
95
|
+
Включите параметр `hash: true`, и при переходе по ссылке вида:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
https://example.com/page#sidebar-left
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Сайдбар с `id="sidebar-left"` автоматически откроется.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 4. AJAX-загрузка контента
|
|
106
|
+
```html
|
|
107
|
+
<div class="vg-sidebar right" id="sidebar-right" data-params='{"ajax": {"route": "/core/server.php?sidebar=right", "target": "#sidebar-ajax-content", "loader": true}}'>
|
|
108
|
+
...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
При открытии сайдбара контент будет загружен с `/api/sidebar-content` и вставлен в `.vg-sidebar-content`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🎉 События
|
|
116
|
+
|
|
117
|
+
Модуль генерирует пользовательские события:
|
|
118
|
+
|
|
119
|
+
| Событие | Описание |
|
|
120
|
+
|---------------------------|---------|
|
|
121
|
+
| `vg.sidebar.show` | Перед открытием |
|
|
122
|
+
| `vg.sidebar.shown` | После открытия |
|
|
123
|
+
| `vg.sidebar.hide` | Перед закрытием |
|
|
124
|
+
| `vg.sidebar.hidden` | После закрытия |
|
|
125
|
+
| `vg.sidebar.loaded` | После AJAX-загрузки |
|
|
126
|
+
| `vg.sidebar.hidePrevented`| Если закрытие отменено (например, `Esc`, но `keyboard: false`) |
|
|
127
|
+
|
|
128
|
+
## 🧹 Очистка
|
|
129
|
+
|
|
130
|
+
При необходимости удалите экземпляр:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
sidebar.dispose();
|
|
134
|
+
````
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🧩 Зависимости
|
|
139
|
+
|
|
140
|
+
- `BaseModule` – базовый класс модулей
|
|
141
|
+
- `Backdrop` – управление затемнением
|
|
142
|
+
- `ScrollBarHelper` – блокировка скролла
|
|
143
|
+
- `EventHandler` – гибкая система событий
|
|
144
|
+
- `Selectors` – безопасный поиск элементов
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## 📝 Лицензия
|
|
150
|
+
|
|
151
|
+
MIT. Свободно использовать и модифицировать.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
📌 *Разработано в рамках фронтенд-системы VG Modules.*
|
|
156
|
+
> 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
|
|
157
|
+
> 📍 Поддерживается в проектах VEGAS
|
|
@@ -1,228 +1,332 @@
|
|
|
1
1
|
import BaseModule from "../../base-module";
|
|
2
|
-
import {mergeDeepObject, getElement, isDisabled, isVisible} from "../../../utils/js/functions";
|
|
2
|
+
import { mergeDeepObject, getElement, isDisabled, isVisible } from "../../../utils/js/functions";
|
|
3
3
|
import EventHandler from "../../../utils/js/dom/event";
|
|
4
4
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Константы модуля VGSpy
|
|
8
8
|
*/
|
|
9
9
|
const NAME = 'spy';
|
|
10
10
|
const NAME_KEY = 'vg.spy';
|
|
11
|
-
const EVENT_KEY = `.${NAME_KEY}
|
|
12
|
-
const DATA_API_KEY = '.data-api'
|
|
13
|
-
|
|
14
|
-
const EVENT_ACTIVATE = `activate${EVENT_KEY}
|
|
15
|
-
const EVENT_CLICK = `click${EVENT_KEY}
|
|
16
|
-
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}
|
|
17
|
-
|
|
18
|
-
const CLASS_NAME_DROPDOWN_ITEM = 'vg-dropdown-item'
|
|
19
|
-
const CLASS_NAME_ACTIVE = 'active'
|
|
20
|
-
|
|
21
|
-
const SELECTOR_DATA_SPY = '[data-vg-toggle="spy"]'
|
|
22
|
-
const SELECTOR_TARGET_LINKS = '[href]'
|
|
23
|
-
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
|
|
24
|
-
const SELECTOR_NAV_LINKS = '.nav-link'
|
|
25
|
-
const SELECTOR_NAV_ITEMS = '.nav-item'
|
|
26
|
-
const SELECTOR_LIST_ITEMS = '.list-group-item'
|
|
27
|
-
const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}
|
|
28
|
-
const SELECTOR_DROPDOWN = '.vg-dropdown'
|
|
29
|
-
const SELECTOR_DROPDOWN_TOGGLE = '[data-vg-toggle="dropdown"]'
|
|
30
|
-
|
|
11
|
+
const EVENT_KEY = `.${NAME_KEY}`;
|
|
12
|
+
const DATA_API_KEY = '.data-api';
|
|
13
|
+
|
|
14
|
+
const EVENT_ACTIVATE = `activate${EVENT_KEY}`;
|
|
15
|
+
const EVENT_CLICK = `click${EVENT_KEY}`;
|
|
16
|
+
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
|
|
17
|
+
|
|
18
|
+
const CLASS_NAME_DROPDOWN_ITEM = 'vg-dropdown-item';
|
|
19
|
+
const CLASS_NAME_ACTIVE = 'active';
|
|
20
|
+
|
|
21
|
+
const SELECTOR_DATA_SPY = '[data-vg-toggle="spy"]';
|
|
22
|
+
const SELECTOR_TARGET_LINKS = '[href]';
|
|
23
|
+
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';
|
|
24
|
+
const SELECTOR_NAV_LINKS = '.nav-link';
|
|
25
|
+
const SELECTOR_NAV_ITEMS = '.nav-item';
|
|
26
|
+
const SELECTOR_LIST_ITEMS = '.list-group-item';
|
|
27
|
+
const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;
|
|
28
|
+
const SELECTOR_DROPDOWN = '.vg-dropdown';
|
|
29
|
+
const SELECTOR_DROPDOWN_TOGGLE = '[data-vg-toggle="dropdown"]';
|
|
31
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Модуль "Spy" — отслеживает прокрутку и активные секции на странице.
|
|
33
|
+
* Автоматически подсвечивает навигационные ссылки в зависимости от текущего положения скролла.
|
|
34
|
+
* Поддерживает плавную прокрутку и работу внутри контейнеров с overflow.
|
|
35
|
+
*/
|
|
32
36
|
class VGSpy extends BaseModule {
|
|
37
|
+
/**
|
|
38
|
+
* Создаёт экземпляр VGSpy
|
|
39
|
+
* @param {HTMLElement} element — корневой элемент навигации (например, .nav)
|
|
40
|
+
* @param {Object} params — параметры конфигурации
|
|
41
|
+
*/
|
|
33
42
|
constructor(element, params) {
|
|
34
43
|
super(element, params);
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Объединённые параметры с настройками по умолчанию
|
|
47
|
+
* @type {Object}
|
|
48
|
+
* @property {number|null} offset - смещение (устаревшее, для совместимости)
|
|
49
|
+
* @property {string} rootMargin - отступ для IntersectionObserver
|
|
50
|
+
* @property {boolean} smoothScroll - включить плавную прокрутку по якорям
|
|
51
|
+
* @property {HTMLElement|string} target - целевой контейнер прокрутки
|
|
52
|
+
* @property {number[]|string} threshold - пороги видимости (0.1, 0.5, 1)
|
|
53
|
+
*/
|
|
54
|
+
this._params = this._configAfterMerge(
|
|
55
|
+
mergeDeepObject(
|
|
56
|
+
{
|
|
57
|
+
offset: null, // Устаревшее, для обратной совместимости
|
|
58
|
+
rootMargin: '0px 0px -25%',
|
|
59
|
+
smoothScroll: true,
|
|
60
|
+
target: this._element,
|
|
61
|
+
threshold: [0.1, 0.5, 1],
|
|
62
|
+
},
|
|
63
|
+
params
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Карта: хеш-ссылка → HTML-элемент ссылки
|
|
69
|
+
* @type {Map<string, HTMLElement>}
|
|
70
|
+
*/
|
|
71
|
+
this._targetLinks = new Map();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Карта: хеш-ссылка → HTML-элемент наблюдаемой секции
|
|
75
|
+
* @type {Map<string, HTMLElement>}
|
|
76
|
+
*/
|
|
77
|
+
this._observableSections = new Map();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Корневой элемент для IntersectionObserver (если скролл не окно)
|
|
81
|
+
* @type {HTMLElement|null}
|
|
82
|
+
*/
|
|
83
|
+
this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Текущая активная секция
|
|
87
|
+
* @type {HTMLElement|null}
|
|
88
|
+
*/
|
|
89
|
+
this._activeTarget = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Экземпляр IntersectionObserver
|
|
93
|
+
* @type {IntersectionObserver|null}
|
|
94
|
+
*/
|
|
95
|
+
this._observer = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Данные о предыдущей прокрутке для определения направления
|
|
99
|
+
* @type {{visibleEntryTop: number, parentScrollTop: number}}
|
|
100
|
+
*/
|
|
49
101
|
this._previousScrollData = {
|
|
50
102
|
visibleEntryTop: 0,
|
|
51
|
-
parentScrollTop: 0
|
|
52
|
-
}
|
|
53
|
-
this._params = this._configAfterMerge(this._params);
|
|
103
|
+
parentScrollTop: 0,
|
|
104
|
+
};
|
|
54
105
|
|
|
55
106
|
this.refresh();
|
|
56
107
|
}
|
|
57
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Имя модуля
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
58
113
|
static get NAME() {
|
|
59
114
|
return NAME;
|
|
60
115
|
}
|
|
61
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Ключ модуля (для хранения в data)
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
62
121
|
static get NAME_KEY() {
|
|
63
|
-
return NAME_KEY
|
|
122
|
+
return NAME_KEY;
|
|
64
123
|
}
|
|
65
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Инициализирует или перезапускает модуль: находит ссылки и секции, создаёт observer
|
|
127
|
+
*/
|
|
66
128
|
refresh() {
|
|
67
|
-
this._initializeTargetsAndObservables()
|
|
68
|
-
this._maybeEnableSmoothScroll()
|
|
129
|
+
this._initializeTargetsAndObservables();
|
|
130
|
+
this._maybeEnableSmoothScroll();
|
|
69
131
|
|
|
70
132
|
if (this._observer) {
|
|
71
|
-
this._observer.disconnect()
|
|
133
|
+
this._observer.disconnect();
|
|
72
134
|
} else {
|
|
73
|
-
this._observer = this._getNewObserver()
|
|
135
|
+
this._observer = this._getNewObserver();
|
|
74
136
|
}
|
|
75
137
|
|
|
138
|
+
// Подписываемся на наблюдение за секциями
|
|
76
139
|
for (const section of this._observableSections.values()) {
|
|
77
|
-
this._observer.observe(section)
|
|
140
|
+
this._observer.observe(section);
|
|
78
141
|
}
|
|
79
142
|
}
|
|
80
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Очищает ресурсы (отключает observer)
|
|
146
|
+
*/
|
|
81
147
|
dispose() {
|
|
82
|
-
this._observer
|
|
83
|
-
|
|
148
|
+
if (this._observer) {
|
|
149
|
+
this._observer.disconnect();
|
|
150
|
+
}
|
|
151
|
+
super.dispose();
|
|
84
152
|
}
|
|
85
153
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Обрабатывает и нормализует параметры после слияния
|
|
156
|
+
* @param {Object} config
|
|
157
|
+
* @returns {Object}
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
_configAfterMerge(config) {
|
|
161
|
+
config.target = getElement(config.target) || document.body;
|
|
162
|
+
|
|
163
|
+
// Поддержка устаревшего параметра `offset`
|
|
164
|
+
if (config.offset != null) {
|
|
165
|
+
config.rootMargin = `${config.offset}px 0px -30%`;
|
|
166
|
+
}
|
|
89
167
|
|
|
90
|
-
|
|
91
|
-
|
|
168
|
+
// Преобразуем строку порогов в массив чисел
|
|
169
|
+
if (typeof config.threshold === 'string') {
|
|
170
|
+
config.threshold = config.threshold
|
|
171
|
+
.split(',')
|
|
172
|
+
.map((value) => Number.parseFloat(value.trim()));
|
|
92
173
|
}
|
|
93
174
|
|
|
94
|
-
return
|
|
175
|
+
return config;
|
|
95
176
|
}
|
|
96
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Подключает плавную прокрутку по якорным ссылкам
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
97
182
|
_maybeEnableSmoothScroll() {
|
|
98
|
-
if (!this._params.smoothScroll)
|
|
99
|
-
|
|
100
|
-
|
|
183
|
+
if (!this._params.smoothScroll) return;
|
|
184
|
+
|
|
185
|
+
EventHandler.off(this._params.target, EVENT_CLICK);
|
|
186
|
+
EventHandler.on(this._params.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, (event) => {
|
|
187
|
+
const hash = event.target.hash;
|
|
188
|
+
if (!hash) return;
|
|
189
|
+
|
|
190
|
+
const section = this._observableSections.get(hash);
|
|
191
|
+
if (!section) return;
|
|
192
|
+
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
|
|
195
|
+
const root = this._rootElement || window;
|
|
196
|
+
const scrollTop = section.offsetTop - this._element.offsetTop;
|
|
101
197
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (observableSection) {
|
|
107
|
-
event.preventDefault()
|
|
108
|
-
const root = this._rootElement || window
|
|
109
|
-
const height = observableSection.offsetTop - this._element.offsetTop
|
|
110
|
-
if (root.scrollTo) {
|
|
111
|
-
root.scrollTo({ top: height, behavior: 'smooth' })
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
root.scrollTop = height
|
|
198
|
+
if (root.scrollTo) {
|
|
199
|
+
root.scrollTo({ top: scrollTop, behavior: 'smooth' });
|
|
200
|
+
} else {
|
|
201
|
+
root.scrollTop = scrollTop;
|
|
115
202
|
}
|
|
116
|
-
})
|
|
203
|
+
});
|
|
117
204
|
}
|
|
118
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Создаёт новый экземпляр IntersectionObserver
|
|
208
|
+
* @returns {IntersectionObserver}
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
119
211
|
_getNewObserver() {
|
|
120
212
|
const options = {
|
|
121
213
|
root: this._rootElement,
|
|
214
|
+
rootMargin: this._params.rootMargin,
|
|
122
215
|
threshold: this._params.threshold,
|
|
123
|
-
|
|
124
|
-
}
|
|
216
|
+
};
|
|
125
217
|
|
|
126
|
-
return new IntersectionObserver(entries => this._observerCallback(entries), options)
|
|
218
|
+
return new IntersectionObserver((entries) => this._observerCallback(entries), options);
|
|
127
219
|
}
|
|
128
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Обработчик пересечений (IntersectionObserver)
|
|
223
|
+
* @param {IntersectionObserverEntry[]} entries
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
129
226
|
_observerCallback(entries) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
this._process(targetElement(entry));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
|
|
138
|
-
const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
|
|
139
|
-
this._previousScrollData.parentScrollTop = parentScrollTop
|
|
227
|
+
const getTargetLink = (entry) => this._targetLinks.get(`#${entry.target.id}`);
|
|
228
|
+
const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;
|
|
229
|
+
const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
|
|
230
|
+
this._previousScrollData.parentScrollTop = parentScrollTop;
|
|
140
231
|
|
|
141
232
|
for (const entry of entries) {
|
|
142
233
|
if (!entry.isIntersecting) {
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
continue
|
|
234
|
+
this._clearActiveClass(getTargetLink(entry));
|
|
235
|
+
continue;
|
|
147
236
|
}
|
|
148
237
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!parentScrollTop) {
|
|
153
|
-
return
|
|
154
|
-
}
|
|
238
|
+
const isEntryBelow = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;
|
|
239
|
+
const shouldActivate =
|
|
240
|
+
(userScrollsDown && isEntryBelow) || (!userScrollsDown && !isEntryBelow);
|
|
155
241
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!userScrollsDown && !entryIsLowerThanPrevious) {
|
|
160
|
-
activate(entry)
|
|
242
|
+
if (shouldActivate) {
|
|
243
|
+
this._previousScrollData.visibleEntryTop = entry.target.offsetTop;
|
|
244
|
+
this._process(getTargetLink(entry));
|
|
161
245
|
}
|
|
162
246
|
}
|
|
163
247
|
}
|
|
164
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Находит все ссылки и соответствующие им секции
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
165
253
|
_initializeTargetsAndObservables() {
|
|
166
|
-
this._targetLinks
|
|
167
|
-
this._observableSections
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (isVisible(observableSection)) {
|
|
179
|
-
this._targetLinks.set(decodeURI(anchor.hash), anchor)
|
|
180
|
-
this._observableSections.set(anchor.hash, observableSection)
|
|
254
|
+
this._targetLinks.clear();
|
|
255
|
+
this._observableSections.clear();
|
|
256
|
+
|
|
257
|
+
const links = Selectors.findAll(SELECTOR_TARGET_LINKS, this._params.target);
|
|
258
|
+
for (const link of links) {
|
|
259
|
+
const hash = link.hash;
|
|
260
|
+
if (!hash || isDisabled(link)) continue;
|
|
261
|
+
|
|
262
|
+
const section = Selectors.find(decodeURI(hash));
|
|
263
|
+
if (isVisible(section)) {
|
|
264
|
+
this._targetLinks.set(decodeURI(hash), link);
|
|
265
|
+
this._observableSections.set(hash, section);
|
|
181
266
|
}
|
|
182
267
|
}
|
|
183
268
|
}
|
|
184
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Активирует элемент и запускает событие
|
|
272
|
+
* @param {HTMLElement|null} target — элемент ссылки, который нужно активировать
|
|
273
|
+
* @private
|
|
274
|
+
*/
|
|
185
275
|
_process(target) {
|
|
186
|
-
if (this._activeTarget === target)
|
|
187
|
-
return
|
|
188
|
-
}
|
|
276
|
+
if (this._activeTarget === target) return;
|
|
189
277
|
|
|
190
|
-
this._clearActiveClass(this._params.target)
|
|
191
|
-
this._activeTarget = target
|
|
192
|
-
target.classList.add(CLASS_NAME_ACTIVE)
|
|
193
|
-
this._activateParents(target)
|
|
278
|
+
this._clearActiveClass(this._params.target);
|
|
279
|
+
this._activeTarget = target;
|
|
194
280
|
|
|
195
|
-
|
|
281
|
+
if (target) {
|
|
282
|
+
target.classList.add(CLASS_NAME_ACTIVE);
|
|
283
|
+
this._activateParents(target);
|
|
284
|
+
EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target });
|
|
285
|
+
}
|
|
196
286
|
}
|
|
197
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Активирует родительские элементы (навигация, dropdown)
|
|
290
|
+
* @param {HTMLElement} target — активная ссылка
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
198
293
|
_activateParents(target) {
|
|
199
294
|
if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
|
|
200
|
-
Selectors.find(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))
|
|
201
|
-
|
|
202
|
-
return
|
|
295
|
+
const dropdownToggle = Selectors.find(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN));
|
|
296
|
+
if (dropdownToggle) dropdownToggle.classList.add(CLASS_NAME_ACTIVE);
|
|
297
|
+
return;
|
|
203
298
|
}
|
|
204
299
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
300
|
+
// Активируем предыдущие элементы в nav/list-group
|
|
301
|
+
for (const parentGroup of Selectors.parents(target, SELECTOR_NAV_LIST_GROUP)) {
|
|
302
|
+
for (const sibling of Selectors.prev(parentGroup, SELECTOR_LINK_ITEMS)) {
|
|
303
|
+
sibling.classList.add(CLASS_NAME_ACTIVE);
|
|
208
304
|
}
|
|
209
305
|
}
|
|
210
306
|
}
|
|
211
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Убирает активный класс со всех элементов
|
|
310
|
+
* @param {HTMLElement} parent — контейнер для очистки
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
212
313
|
_clearActiveClass(parent) {
|
|
213
|
-
parent.classList.remove(CLASS_NAME_ACTIVE)
|
|
314
|
+
parent.classList.remove(CLASS_NAME_ACTIVE);
|
|
214
315
|
|
|
215
|
-
const
|
|
216
|
-
for (const
|
|
217
|
-
|
|
316
|
+
const activeLinks = Selectors.findAll(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);
|
|
317
|
+
for (const link of activeLinks) {
|
|
318
|
+
link.classList.remove(CLASS_NAME_ACTIVE);
|
|
218
319
|
}
|
|
219
320
|
}
|
|
220
321
|
}
|
|
221
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Инициализация через data-атрибуты при загрузке DOM
|
|
325
|
+
*/
|
|
222
326
|
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
|
|
223
327
|
for (const spy of Selectors.findAll(SELECTOR_DATA_SPY)) {
|
|
224
|
-
VGSpy.getOrCreateInstance(spy)
|
|
328
|
+
VGSpy.getOrCreateInstance(spy);
|
|
225
329
|
}
|
|
226
|
-
})
|
|
330
|
+
});
|
|
227
331
|
|
|
228
332
|
export default VGSpy;
|