vgapp 0.8.0 → 0.8.2

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.
@@ -0,0 +1,196 @@
1
+ # VGRollup — Модуль сворачивания/разворачивания контента
2
+
3
+ `VGRollup` — это JavaScript-модуль, позволяющий сворачивать и разворачивать контент на веб-странице. Поддерживает два режима: по высоте (для текста) и по количеству элементов. Автоматически создаёт кнопку управления с возможностью локализации.
4
+
5
+ ---
6
+
7
+ ## 🔧 Возможности
8
+
9
+ - **Два режима работы**:
10
+ - `text` — обрезка текста по заданной высоте или количеству строк (с поддержкой `line-clamp`).
11
+ - `elements` — отображение только первых N элементов (например, списка новостей).
12
+ - **Анимации и эффекты**:
13
+ - Поддержка плавного затухания (`fade`).
14
+ - CSS-переходы (`transition`).
15
+ - **Гибкая настройка кнопки**:
16
+ - Включение/отключение кнопки.
17
+ - Кастомизация текста: "Показать", "Свернуть", "ещё X".
18
+ - Автоматическое размещение после контента.
19
+ - **Локализация интерфейса**:
20
+ - Поддержка нескольких языков через `lang_buttons`.
21
+ - Тексты кнопок и подсказок подтягиваются в зависимости от языка (`ru`, `en`, и др.).
22
+ - **Data API**:
23
+ - Автоинициализация через `data-vg-rollup`.
24
+ - Управление через `data-vg-toggle="rollup"` и `data-vg-target`.
25
+ - **Программное управление**:
26
+ - Публичные методы: `.show()`, `.hide()`, `.toggle()`, `.isShow()`.
27
+ - Инициализация через `VGRollup.init()`.
28
+
29
+ ---
30
+
31
+ ## 📦 Установка и инициализация
32
+
33
+ ### Через JavaScript
34
+ ```js
35
+ import VGRollup from './app/modules/vgrollup/js/vgrollup';
36
+ VGRollup.init(document.querySelector('.rollup'), {
37
+ content: 'text',
38
+ height: 100,
39
+ button: {
40
+ enabled: true,
41
+ more: 'Показать',
42
+ less: 'Свернуть'
43
+ },
44
+ lang: 'ru'
45
+ });
46
+ ```
47
+
48
+ ### Через data-атрибуты
49
+ ```hmtl
50
+ <ul class="rollup-list"
51
+ data-vgrollup
52
+ data-content="elements"
53
+ data-elements="item"
54
+ data-cnt="3"
55
+ data-fade="false"
56
+ data-number="true"
57
+ data-more=" more "
58
+ data-button-more="Show"
59
+ data-button-less="Hide"
60
+ id="rollup-list-num">
61
+ <li class="item">Lorem ipsum dolor sit amet.</li>
62
+ <li class="item">Lorem ipsum dolor sit amet.</li>
63
+ <li class="item">Lorem ipsum dolor sit amet.</li>
64
+ <li class="item">Lorem ipsum dolor sit amet.</li>
65
+ <li class="item">Lorem ipsum dolor sit amet.</li>
66
+ <li class="item">Lorem ipsum dolor sit amet.</li>
67
+ <li class="item">Lorem ipsum dolor sit amet.</li>
68
+ <li class="item">Lorem ipsum dolor sit amet.</li>
69
+ <li class="item">Lorem ipsum dolor sit amet.</li>
70
+ <li class="item">Lorem ipsum dolor sit amet.</li>
71
+ </ul>
72
+ ```
73
+ ---
74
+
75
+ ## ⚙️ Параметры конфигурации
76
+
77
+ | Параметр | Тип | По умолчанию | Описание |
78
+ |--------|------|-------------|----------|
79
+ | `content` | `string` | `'text'` | Режим: `'text'` или `'elements'`. |
80
+ | `cnt` | `number` | `0` | Количество видимых элементов (в режиме `elements`). |
81
+ | `fade` | `boolean` | `true` | Добавлять эффект затухания к нижней части. |
82
+ | `transition` | `boolean` | `false` | Включить CSS-анимацию при переключении. |
83
+ | `number` | `boolean` | `false` | Показывать количество скрытых элементов (например, "ещё 5"). |
84
+ | `height` | `number` | `0` | Высота контейнера в px, до которой обрезать текст. |
85
+ | `ellipsis.line` | `number\|null` | `null` | Количество строк перед обрезкой (использует `line-clamp`). |
86
+ | `more` | `string` | `' еще '` | Текст для отображения количества скрытых элементов. |
87
+ | `button.enabled` | `boolean` | `true` | Отображать кнопку управления. |
88
+ | `button.more` | `string` | `'Показать'` | Текст кнопки для раскрытия. |
89
+ | `button.less` | `string` | `'Свернуть'` | Текст кнопки для сворачивания. |
90
+ | `lang` | `string` | `'ru'` | Язык интерфейса. Подтягивает локализацию из `lang_buttons`. |
91
+
92
+ ---
93
+
94
+ ## 🧩 Режимы работы
95
+
96
+ ### 1. Режим `text` (по высоте)
97
+
98
+ Обрезает текст по заданной высоте или количеству строк.
99
+
100
+ ```js
101
+ {
102
+ content: 'text',
103
+ height: 120
104
+ }
105
+ ```
106
+
107
+ Или с `line-clamp`:
108
+
109
+ ```js
110
+ {
111
+ content: 'text',
112
+ ellipsis: {
113
+ line: 3
114
+ }
115
+ ```
116
+
117
+ > ⚠️ `line-clamp` требует `display: -webkit-box` и `-webkit-line-clamp` в CSS.
118
+
119
+ ### 2. Режим `elements` (по количеству элементов)
120
+
121
+ Оставляет первые `cnt` элементов, остальные скрываются.
122
+ ```js
123
+ {
124
+ content: 'elements',
125
+ elements: 'news-item',
126
+ cnt: 5
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 🌍 Локализация
133
+
134
+ Тексты кнопок и подсказок локализуются через `lang_buttons(lang, 'rollup')`.
135
+
136
+ Поддерживаемые языки:
137
+ - `ru` — русский
138
+ - `en` — английский
139
+ - (и другие, если добавлены в `lang_buttons`)
140
+
141
+ ## 🖱️ Data API
142
+
143
+ - `data-vg-toggle="rollup"` — элемент, клик по которому переключает контент.
144
+ - `data-vg-target="#id"` — указывает на контейнер с контентом.
145
+ - `data-vg-rollup='{...}'` — параметры инициализации.
146
+
147
+ ---
148
+
149
+ ## 🧪 Методы
150
+
151
+ | Метод | Описание |
152
+ |------|---------|
153
+ | `VGRollup.toggle(target, button)` | Переключает состояние. |
154
+ | `instance.isShow()` | Возвращает `true`, если контент развёрнут. |
155
+ | `VGRollup.init(element, params, callback)` | Инициализирует модуль. |
156
+
157
+ ---
158
+
159
+ ## 🎨 CSS-классы
160
+
161
+ | Класс | Описание |
162
+ |------|---------|
163
+ | `.vg-rollup` | Базовый класс контейнера. |
164
+ | `.vg-rollup-content--hidden` | Скрытое состояние (ограничение высоты). |
165
+ | `.vg-rollup-content--fade` | Эффект затухания. |
166
+ | `.vg-rollup-content--ellipsis` | Режим `line-clamp`. |
167
+ | `.vg-rollup-content--transition` | Анимация переключения. |
168
+ | `.vg-rollup-display--none` | Состояние "развёрнуто" (управляется JS). |
169
+
170
+ ---
171
+ ## 📎 Зависимости
172
+
173
+ - `BaseModule` — базовый класс компонентов.
174
+ - `Classes`, `Manipulator` — утилиты для работы с классами и атрибутами.
175
+ - `Selectors` — безопасный поиск элементов.
176
+ - `EventHandler` — делегирование событий.
177
+ - `lang_buttons(lang, 'rollup')` — функция локализации.
178
+
179
+ ---
180
+
181
+ ## ✅ Требования
182
+
183
+ - Современный браузер (поддержка ES6, `classList`, `insertAdjacentHTML`).
184
+ - Для `line-clamp` требуется поддержка `-webkit-line-clamp`.
185
+
186
+ ---
187
+
188
+ ## 📄 Лицензия
189
+
190
+ MIT — свободное использование и модификация.
191
+
192
+ ---
193
+
194
+ 📌 *Разработано в рамках фронтенд-системы VG Modules.*
195
+ > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
196
+ > 📍 Поддерживается в проектах VEGAS
@@ -0,0 +1,220 @@
1
+ import VGSelect from "./vgselect";
2
+ import EventHandler from "../../../utils/js/dom/event";
3
+ import Selectors from "../../../utils/js/dom/selectors";
4
+ import {Manipulator} from "../../../utils/js/dom/manipulator";
5
+ import {transliterate} from "../../../utils/js/functions";
6
+
7
+ const NAME_KEY = 'vg.select';
8
+
9
+ const CLASS_NAME_SHOW = 'show';
10
+ const CLASS_NAME_CONTAINER = 'vg-select';
11
+ const CLASS_NAME_DROPDOWN = 'vg-select-dropdown';
12
+ const CLASS_NAME_LIST = 'vg-select-list';
13
+ const CLASS_NAME_OPTION = 'vg-select-list--option';
14
+ const CLASS_NAME_OPTGROUP = 'vg-select-list--optgroup';
15
+ const CLASS_NAME_TAGS = 'vg-select-tags';
16
+ const CLASS_NAME_TAG = 'vg-select-tag';
17
+ const CLASS_NAME_TAG_REMOVE = 'vg-select-tag-remove';
18
+
19
+ const EVENT_CLICK_DATA_API = `click.${NAME_KEY}.data.api`;
20
+ const EVENT_KEY_UP_DATA_API = `keyup.${NAME_KEY}.data.api`;
21
+ const EVENT_RESET_DATA_API = `reset.${NAME_KEY}.data.api`;
22
+ const EVENT_KEYDOWN_DATA_API = `keydown.${NAME_KEY}.data.api`;
23
+
24
+ const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
25
+ const EVENT_KEY_SEARCH = `${NAME_KEY}.search`;
26
+ const EVENT_KEY_SELECT = `${NAME_KEY}.select`;
27
+ const EVENT_KEY_DESELECT = `${NAME_KEY}.deselect`;
28
+
29
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="select"]';
30
+ const SELECTOR_OPTION_TOGGLE = '[data-vg-toggle="select-option"]';
31
+ const SELECTOR_SEARCH_TOGGLE = '[name="vg-select-search"]';
32
+ const SELECTOR_DROPDOWN = `.${CLASS_NAME_DROPDOWN}`;
33
+ const SELECTOR_MULTIPLE_INPUT = `.vg-select-multiple-input`;
34
+
35
+ const _handlersVGSelect = () => {
36
+ EventHandler.on(document, EVENT_CLICK_DATA_API, e => VGSelect.clearDrops(e));
37
+
38
+ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function(e) {
39
+ const target = e.target;
40
+ if (target.closest(`.${CLASS_NAME_TAG}`) || target.closest(`.${CLASS_NAME_TAG_REMOVE}`)) {
41
+ e.stopPropagation();
42
+ return;
43
+ }
44
+ const container = this.closest(`.${CLASS_NAME_CONTAINER}`);
45
+ const instance = VGSelect.getOrCreateInstance(container);
46
+ const open = Selectors.find(`.${CLASS_NAME_CONTAINER}.${CLASS_NAME_SHOW}`);
47
+ if (open && open !== container) VGSelect.getInstance(open)?.hide();
48
+ instance.toggle(this);
49
+ });
50
+
51
+ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_OPTION_TOGGLE, function(e) {
52
+ const option = e.target;
53
+ if (option.classList.contains('disabled')) return;
54
+ const container = option.closest(`.${CLASS_NAME_CONTAINER}`);
55
+ const select = container.previousElementSibling;
56
+ const isMultiple = select.multiple;
57
+ const value = option.dataset.value;
58
+ const realOpt = select.querySelector(`option[value="${CSS.escape(value)}"]`);
59
+ if (!realOpt) return;
60
+
61
+ const instance = VGSelect.getInstance(container);
62
+ const wasSelected = realOpt.selected;
63
+
64
+ if (isMultiple) {
65
+ realOpt.selected = !realOpt.selected;
66
+ option.classList.toggle('selected', realOpt.selected);
67
+ VGSelect.updateUI(select);
68
+ select.dispatchEvent(new Event('change', { bubbles: true }));
69
+ EventHandler.trigger(select, EVENT_KEY_CHANGE, {
70
+ data: { value, title: option.textContent, ...Manipulator.get(option) }
71
+ });
72
+
73
+ if (realOpt.selected) {
74
+ instance?._triggerEvent(EVENT_KEY_SELECT, { value, title: option.textContent, ...Manipulator.get(option) });
75
+ instance?._callCallback('onSelect', { value, title: option.textContent, ...Manipulator.get(option) });
76
+ } else {
77
+ instance?._triggerEvent(EVENT_KEY_DESELECT, { value, title: option.textContent, ...Manipulator.get(option) });
78
+ instance?._callCallback('onDeselect', { value, title: option.textContent, ...Manipulator.get(option) });
79
+ }
80
+ } else {
81
+ if (wasSelected) return;
82
+
83
+ container.querySelectorAll(`.${CLASS_NAME_OPTION}`).forEach(o => o.classList.remove('selected'));
84
+ option.classList.add('selected');
85
+ VGSelect.changeSelector(select, value, {
86
+ value,
87
+ title: option.textContent,
88
+ ...Manipulator.get(option)
89
+ });
90
+ instance?.hide();
91
+ }
92
+ });
93
+
94
+ EventHandler.on(document, EVENT_KEY_UP_DATA_API, SELECTOR_SEARCH_TOGGLE, function(e) {
95
+ const input = e.target;
96
+ const dropdown = input.closest(SELECTOR_DROPDOWN);
97
+ const list = dropdown?.querySelector(`.${CLASS_NAME_LIST}`);
98
+ if (!list) return;
99
+
100
+ const container = input.closest(`.${CLASS_NAME_CONTAINER}`);
101
+ const instance = VGSelect.getInstance(container);
102
+
103
+ const options = list.querySelectorAll(`.${CLASS_NAME_OPTION}`);
104
+ const groups = list.querySelectorAll(`.${CLASS_NAME_OPTGROUP}`);
105
+ const value = input.value.trim().toLowerCase();
106
+ const search = [value, transliterate(value), transliterate(value, true)];
107
+
108
+ options.forEach(el => { el.hidden = false; el.style.display = ''; });
109
+ groups.forEach(el => { el.hidden = false; el.style.display = ''; });
110
+
111
+ if (value) {
112
+ let visibleCount = 0;
113
+ groups.length ? groups.forEach(group => {
114
+ const items = group.querySelectorAll(`.${CLASS_NAME_OPTION}`);
115
+ const visible = Array.from(items).some(item => {
116
+ const t = item.textContent.toLowerCase();
117
+ return search.some(s => t.includes(s));
118
+ });
119
+ group.hidden = !visible;
120
+ group.style.display = visible ? '' : 'none';
121
+ items.forEach(item => {
122
+ const t = item.textContent.toLowerCase();
123
+ const match = search.some(s => t.includes(s));
124
+ item.hidden = !match;
125
+ item.style.display = match ? '' : 'none';
126
+ if (match) visibleCount++;
127
+ });
128
+ }) : options.forEach(option => {
129
+ const t = option.textContent.toLowerCase();
130
+ const match = search.some(s => t.includes(s));
131
+ option.hidden = !match;
132
+ option.style.display = match ? '' : 'none';
133
+ if (match) visibleCount++;
134
+ });
135
+
136
+ instance?._triggerEvent(EVENT_KEY_SEARCH, { query: value, results: visibleCount });
137
+ instance?._callCallback('onSearch', { query: value, results: visibleCount });
138
+ } else {
139
+ instance?._triggerEvent(EVENT_KEY_SEARCH, { query: '', results: options.length });
140
+ instance?._callCallback('onSearch', { query: '', results: options.length });
141
+ }
142
+ });
143
+
144
+ EventHandler.on(document, EVENT_RESET_DATA_API, 'form', function() {
145
+ Selectors.findAll('select[data-inited="true"]', this).forEach(select => VGSelect.build(select, true));
146
+ });
147
+
148
+ EventHandler.on(document, 'keydown', SELECTOR_DATA_TOGGLE, function(e) {
149
+ const inst = VGSelect.getInstance(this.closest(`.${CLASS_NAME_CONTAINER}`));
150
+ if (!inst) return;
151
+ switch (e.key) {
152
+ case 'Enter':
153
+ case ' ':
154
+ e.preventDefault();
155
+ inst.toggle();
156
+ break;
157
+ case 'Escape':
158
+ if (inst._isShown()) inst.hide();
159
+ break;
160
+ case 'ArrowDown':
161
+ e.preventDefault();
162
+ if (!inst._isShown()) inst.show();
163
+ break;
164
+ }
165
+ });
166
+
167
+ EventHandler.on(document, EVENT_CLICK_DATA_API, `.${CLASS_NAME_TAGS}`, function(e) {
168
+ const btn = e.target.closest(`.${CLASS_NAME_TAG_REMOVE}`);
169
+ if (!btn) return;
170
+ e.preventDefault();
171
+ e.stopPropagation();
172
+ const container = this.closest(`.${CLASS_NAME_CONTAINER}`);
173
+ const select = container.previousElementSibling;
174
+ const value = btn.dataset.value;
175
+ const opt = select.querySelector(`option[value="${CSS.escape(value)}"]`);
176
+ const item = container.querySelector(`.${CLASS_NAME_OPTION}[data-value="${CSS.escape(value)}"]`);
177
+ const instance = VGSelect.getInstance(container);
178
+
179
+ if (opt) {
180
+ opt.selected = false;
181
+ if (item) item.classList.remove('selected');
182
+ VGSelect.updateUI(select);
183
+ select.dispatchEvent(new Event('change', { bubbles: true }));
184
+ EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value } });
185
+
186
+ instance?._triggerEvent(EVENT_KEY_DESELECT, { value });
187
+ instance?._callCallback('onDeselect', { value });
188
+ }
189
+ });
190
+
191
+ EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MULTIPLE_INPUT, function(e) {
192
+ if (e.key === 'Backspace' && !e.target.value) {
193
+ const input = e.target;
194
+ const tagsContainer = input.parentElement;
195
+ const tags = tagsContainer.querySelectorAll(`.${CLASS_NAME_TAG}`);
196
+ if (tags.length > 0) {
197
+ const lastTag = tags[tags.length - 1];
198
+ const value = lastTag.querySelector('svg')?.dataset.value;
199
+ const select = input.closest(`.${CLASS_NAME_CONTAINER}`).previousElementSibling;
200
+ const option = select.querySelector(`option[value="${CSS.escape(value)}"]`);
201
+ const listItem = select.closest(`.${CLASS_NAME_CONTAINER}`).querySelector(`.${CLASS_NAME_OPTION}[data-value="${CSS.escape(value)}"]`);
202
+ const instance = VGSelect.getInstance(input.closest(`.${CLASS_NAME_CONTAINER}`));
203
+
204
+ if (option) {
205
+ option.selected = false;
206
+ if (listItem) listItem.classList.remove('selected');
207
+ VGSelect.updateUI(select);
208
+ select.dispatchEvent(new Event('change', { bubbles: true }));
209
+ EventHandler.trigger(select, EVENT_KEY_CHANGE, { data: { value } });
210
+
211
+ instance?._triggerEvent(EVENT_KEY_DESELECT, { value });
212
+ instance?._callCallback('onDeselect', { value });
213
+ }
214
+ }
215
+ }
216
+ });
217
+
218
+ }
219
+
220
+ export default _handlersVGSelect;