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.
@@ -1,7 +1,32 @@
1
1
  import BaseModule from "../../base-module";
2
- import {execute, isDisabled, mergeDeepObject} from "../../../utils/js/functions";
2
+ import { execute, isDisabled, mergeDeepObject } from "../../../utils/js/functions";
3
3
  import EventHandler from "../../../utils/js/dom/event";
4
4
  import Selectors from "../../../utils/js/dom/selectors";
5
+ import {lang_buttons} from "../../../utils/js/components/lang";
6
+ import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
7
+
8
+ /**
9
+ * @class VGRollup
10
+ * @extends BaseModule
11
+ * @description
12
+ * Модуль "Rollup" — реализует функционал сворачивания/разворачивания контента.
13
+ * Поддерживает два режима: текст (ограничение по высоте) и элементы (ограничение по количеству).
14
+ * Автоматически создаёт кнопку управления, если включена.
15
+ *
16
+ * @example
17
+ * // Инициализация через JS
18
+ * VGRollup.init(document.querySelector('.rollup'), {
19
+ * height: 100,
20
+ * button: {
21
+ * enabled: true,
22
+ * more: "Показать",
23
+ * less: "Свернуть"
24
+ * }
25
+ * });
26
+ *
27
+ * // Инициализация через data-атрибут
28
+ * // <div class="rollup" data-vg-rollup='{"height": 80, "button": {"more": "Еще"}}'>...</div>
29
+ */
5
30
 
6
31
  /**
7
32
  * Constants
@@ -10,18 +35,46 @@ const NAME = 'rollup';
10
35
  const NAME_KEY = 'vg.rollup';
11
36
  const CLASS_NAME_SHOW = 'show';
12
37
  const CLASS_NAME_HIDE = 'vg-rollup-display--none';
13
- const SELECTOR_DATA_TOGGLE= '[data-vg-toggle="rollup"]'
14
-
15
- const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
16
- const EVENT_KEY_SHOW = `${NAME_KEY}.show`;
38
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="rollup"]';
17
39
 
40
+ const EVENT_KEY_HIDE = `${NAME_KEY}.hide`;
41
+ const EVENT_KEY_SHOW = `${NAME_KEY}.show`;
18
42
  const EVENT_KEY_CLICK_DATA_API = `click.${NAME_KEY}.data.api`;
19
43
 
20
- class VGRollup extends BaseModule {
44
+ class VGRollup extends BaseModule {
45
+
46
+ /**
47
+ * @constructor
48
+ * @param {HTMLElement} element - Основной контейнер контента.
49
+ * @param {Object} params - Параметры конфигурации.
50
+ * @param {Object} [params.lang = 'ru'] - Локализация
51
+ * @param {string} [params.content='text'] - Режим: `'text'` (ограничение по высоте) или `'elements'` (по количеству).
52
+ * @param {number} [params.cnt=0] - Количество видимых элементов в режиме `'elements'`.
53
+ * @param {boolean} [params.fade=true] - Добавлять эффект затухания.
54
+ * @param {boolean} [params.transition=false] - Включить CSS-анимацию при переключении.
55
+ * @param {boolean} [params.number=false] - Показывать количество скрытых элементов.
56
+ * @param {number} [params.height=0] - Высота в px, до которой сворачивается текст.
57
+ * @param {Object} [params.ellipsis] - Настройки для многоточия.
58
+ * @param {number|null} [params.ellipsis.line=null] - Количество строк перед обрезкой (только для `display: -webkit-box`).
59
+ * @param {string} [params.more=' еще '] - Текст для отображения количества скрытых элементов.
60
+ * @param {Object} [params.button] - Настройки кнопки.
61
+ * @param {boolean} [params.button.enabled=true] - Показывать кнопку управления.
62
+ * @param {string} [params.button.more="Показать"] - Текст кнопки для раскрытия.
63
+ * @param {string} [params.button.less="Свернуть"] - Текст кнопки для сворачивания.
64
+ *
65
+ * @example
66
+ * new VGRollup(document.querySelector('.rollup'), {
67
+ * content: 'elements',
68
+ * elements: 'item',
69
+ * cnt: 3,
70
+ * button: { more: 'Показать ещё', less: 'Свернуть' }
71
+ * });
72
+ */
21
73
  constructor(element, params = {}) {
22
74
  super(element, params);
23
75
 
24
76
  this._params = this._getParams(element, mergeDeepObject({
77
+ lang: document.documentElement.lang || 'ru',
25
78
  content: 'text',
26
79
  cnt: 0,
27
80
  fade: true,
@@ -33,12 +86,22 @@ class VGRollup extends BaseModule {
33
86
  },
34
87
  more: ' еще ',
35
88
  button: {
36
- enable: true,
89
+ enabled: true,
37
90
  more: "Показать",
38
91
  less: "Свернуть"
39
92
  }
40
93
  }, params));
41
94
 
95
+ /**
96
+ * CSS-классы, используемые модулем.
97
+ * @type {Object}
98
+ * @property {string} container - Базовый класс контейнера.
99
+ * @property {string} hidden - Класс для скрытого состояния.
100
+ * @property {string} fade - Класс для эффекта затухания.
101
+ * @property {string} ellipsis - Класс для многоточия.
102
+ * @property {string} button - Класс контейнера кнопки.
103
+ * @property {string} transition - Класс для анимации.
104
+ */
42
105
  this.classes = {
43
106
  container: 'vg-rollup',
44
107
  hidden: "vg-rollup-content--hidden",
@@ -48,201 +111,306 @@ class VGRollup extends BaseModule {
48
111
  transition: "vg-rollup-content--transition"
49
112
  };
50
113
 
51
- this.total = 0;
52
- this.count = 0;
114
+ /**
115
+ * Общее количество элементов (в режиме `elements`).
116
+ * @type {number}
117
+ */
118
+ this.total = 0;
119
+
120
+ /**
121
+ * Количество видимых элементов (в режиме `elements`).
122
+ * @type {number}
123
+ */
124
+ this.count = 0;
125
+
126
+ /**
127
+ * Смещение для подгрузки (если используется).
128
+ * @type {number}
129
+ */
130
+ this.offset = 0;
131
+
132
+ /**
133
+ * Флаг активности режима смещения.
134
+ * @type {boolean}
135
+ */
136
+ this.isOffset = false;
137
+
138
+ // Локализация текстов кнопок
139
+ this._params.button.more = lang_buttons(this._params.lang, NAME)['show'];
140
+ this._params.button.less = lang_buttons(this._params.lang, NAME)['less'];
141
+ this._params.more = lang_buttons(this._params.lang, NAME)['more'];
53
142
 
54
143
  this.build();
55
144
  }
56
145
 
57
- static get NAME() {
58
- return NAME;
59
- }
146
+ /**
147
+ * Имя модуля
148
+ * @type {string}
149
+ */
150
+ static get NAME() { return NAME; }
60
151
 
61
- static get NAME_KEY() {
62
- return NAME_KEY
63
- }
152
+ /**
153
+ * Ключ модуля (пространство имён событий)
154
+ * @type {string}
155
+ */
156
+ static get NAME_KEY() { return NAME_KEY; }
64
157
 
158
+ /**
159
+ * Переключает состояние контента (свёрнут/развёрнут).
160
+ * @param {HTMLElement} target - Целевой контейнер контента.
161
+ * @param {HTMLElement} relatedTarget - Кнопка, вызвавшая переключение.
162
+ * @static
163
+ * @example
164
+ * VGRollup.toggle(document.querySelector('.rollup'), buttonEl);
165
+ */
65
166
  static toggle(target, relatedTarget) {
66
167
  const instance = VGRollup.getOrCreateInstance(target);
67
- let isShown = instance.isShow();
168
+ const isShown = instance.isShow();
68
169
 
69
170
  if (!isShown) {
70
- instance._element.classList.add(CLASS_NAME_SHOW);
71
- relatedTarget.innerHTML = instance._params.button.less;
72
- relatedTarget.setAttribute("aria-expanded", true);
73
-
74
- if (instance.offset > 0) {
75
- if (instance.isOffset) {
76
- relatedTarget.innerHTML = instance._params.button.more;
77
- relatedTarget.setAttribute("aria-expanded", true);
78
- } else {
79
- relatedTarget.innerHTML = instance._params.button.less;
80
- relatedTarget.setAttribute("aria-expanded", false);
81
- }
82
- }
83
-
84
- instance.switch(instance._element, false);
85
- EventHandler.trigger(instance._element, EVENT_KEY_SHOW, { relatedTarget });
171
+ instance._show(relatedTarget);
86
172
  } else {
87
- let textShowNum = '',
88
- isShowNum = instance._params.number;
89
-
173
+ instance._hide(relatedTarget);
174
+ }
175
+ }
90
176
 
91
- if (isShowNum) {
92
- let sum = (instance.total) - (instance.count);
177
+ /**
178
+ * Открывает (разворачивает) контент.
179
+ * @param {HTMLElement} relatedTarget - Элемент, вызвавший событие (кнопка).
180
+ * @private
181
+ */
182
+ _show(relatedTarget) {
183
+ Classes.add(this._element, CLASS_NAME_SHOW);
184
+ relatedTarget.innerHTML = this._params.button.less;
185
+ Manipulator.set(relatedTarget, 'aria-expanded', 'true');
186
+
187
+ if (this.offset > 0) {
188
+ relatedTarget.innerHTML = this.isOffset ? this._params.button.more : this._params.button.less;
189
+ Manipulator.set(relatedTarget, 'aria-expanded', this.isOffset ? "true" : "false");
190
+ }
93
191
 
94
- if (sum > 0) {
95
- textShowNum = instance._params.more + sum;
96
- }
97
- }
192
+ this.switch(this._element, false);
193
+ EventHandler.trigger(this._element, EVENT_KEY_SHOW, { relatedTarget });
194
+ }
98
195
 
99
- if (instance.isOffset) {
100
- relatedTarget.setAttribute("aria-expanded", true);
101
- } else {
102
- relatedTarget.setAttribute("aria-expanded", false);
196
+ /**
197
+ * Закрывает (сворачивает) контент.
198
+ * @param {HTMLElement} relatedTarget - Элемент, вызвавший событие (кнопка).
199
+ * @private
200
+ */
201
+ _hide(relatedTarget) {
202
+ let buttonText = this._params.button.more;
203
+ const isShowNum = this._params.number;
204
+
205
+ if (isShowNum) {
206
+ const sum = this.total - this.count;
207
+ if (sum > 0) {
208
+ buttonText += this._params.more + sum;
103
209
  }
210
+ }
104
211
 
105
- instance._element.classList.remove(CLASS_NAME_SHOW);
106
- relatedTarget.innerHTML = instance._params.button.more + textShowNum;
107
- instance.switch(instance._element, true);
212
+ Classes.remove(this._element, CLASS_NAME_SHOW);
213
+ Manipulator.set(relatedTarget, 'aria-expanded', 'false');
214
+ relatedTarget.textContent = buttonText;
108
215
 
109
- EventHandler.trigger(instance._element, EVENT_KEY_HIDE, { relatedTarget });
110
- }
216
+ this.switch(this._element, true);
217
+ EventHandler.trigger(this._element, EVENT_KEY_HIDE, { relatedTarget });
111
218
  }
112
219
 
220
+ /**
221
+ * Инициализирует отображение контента и создаёт кнопку (если нужно).
222
+ * @param {HTMLElement|null} el - Элемент, который нужно инициализировать.
223
+ * @param {boolean} isButtonAppend - Разрешено ли добавление кнопки.
224
+ * @example
225
+ * instance.build(); // перестроить текущий элемент
226
+ */
113
227
  build(el = null, isButtonAppend = true) {
114
- let _this = this,
115
- element = el || _this._element,
116
- self_height = element.clientHeight, set_height = _this._params.height || (self_height / 2);
117
-
118
- element.classList.add(_this.classes.container)
119
-
120
- let isFade = _this._params.fade,
121
- isTransition = _this._params.transition,
122
- isEllipsis = _this._params.ellipsis.line !== null,
123
- isButton = _this._params.button.enable,
124
- isShowNum = _this._params.number;
125
-
126
- if (!isButtonAppend) _this.switch(element);
127
-
128
- if (self_height > set_height && _this._params.content === 'text') {
129
- element.classList.add(_this.classes.hidden);
130
- element.style.height = set_height + "px";
131
-
132
- ellipsis();
133
- transition();
134
- fade();
135
- button();
136
- } else if (_this._params.content === 'elements') {
137
- let elementClass = _this._params.elements || 'item',
138
- items = element.querySelectorAll('.' + elementClass),
139
- cnt = _this._params.cnt || 5,
140
- i = 1;
141
-
142
- _this.total = items.length;
143
- _this.count = cnt;
144
-
145
- for (const item of items) {
146
- if (i > cnt) {
147
- item.classList.add(CLASS_NAME_HIDE)
148
- }
149
-
150
- i++;
151
- }
228
+ const element = el || this._element;
229
+ const selfHeight = element.clientHeight;
230
+ const setHeight = this._params.height || (selfHeight / 2);
231
+
232
+ const {
233
+ fade,
234
+ transition,
235
+ button,
236
+ number: showNum,
237
+ content,
238
+ elements: elementClass,
239
+ cnt,
240
+ ellipsis: ellipsisCfg
241
+ } = this._params;
242
+
243
+ const isEllipsis = ellipsisCfg.line !== null;
244
+ const isButton = button.enabled && isButtonAppend;
245
+
246
+ Classes.add(element, this.classes.container);
247
+
248
+ if (!isButtonAppend) {
249
+ this.switch(element);
250
+ return;
251
+ }
152
252
 
153
- if (isButton === true) isButton = (i - 1) > cnt;
253
+ if (content === 'text' && selfHeight > setHeight) {
254
+ this._setupTextContent(element, setHeight, fade, transition, isEllipsis, ellipsisCfg.line, isButton, showNum);
255
+ } else if (content === 'elements') {
256
+ this._setupElementsContent(element, elementClass, cnt, fade, transition, isEllipsis, isButton, showNum);
257
+ }
258
+ }
154
259
 
155
- ellipsis();
156
- transition();
157
- fade();
158
- button();
260
+ /**
261
+ * Настраивает контент типа 'text' (ограничение по высоте).
262
+ * @param {HTMLElement} element - Контейнер текста.
263
+ * @param {number} height - Высота, до которой обрезать.
264
+ * @param {boolean} fade - Использовать затухание.
265
+ * @param {boolean} transition - Использовать анимацию.
266
+ * @param {boolean} isEllipsis - Использовать многоточие.
267
+ * @param {number|null} line - Количество строк.
268
+ * @param {boolean} isButton - Показывать кнопку.
269
+ * @param {boolean} showNum - Показывать счётчик.
270
+ * @private
271
+ */
272
+ _setupTextContent(element, height, fade, transition, isEllipsis, line, isButton, showNum) {
273
+ Classes.add(element, this.classes.hidden);
274
+ element.style.height = height + "px";
275
+
276
+ if (isEllipsis && line) {
277
+ Classes.add(element, this.classes.ellipsis);
278
+ element.style.lineClamp = Number(line);
279
+ } else if (isEllipsis) {
280
+ console.error("Переменная [data-line] или параметр[line] не должны быть пустыми");
159
281
  }
160
282
 
161
- function ellipsis() {
162
- if (isEllipsis) {
163
- let line = _this._params.ellipsis.line;
164
- isFade = false;
283
+ if (transition) Classes.add(element, this.classes.transition);
284
+ if (fade) Classes.add(element, this.classes.fade);
165
285
 
166
- if (line) {
167
- element.classList.add(_this.classes.ellipsis);
168
- element.style.webkitLineClamp = line;
169
- } else {
170
- console.error("Переменная [data-line] или параметр[line] не должны быть пустыми");
171
- }
172
- }
173
- }
286
+ if (isButton) this._createButton(element, '', showNum);
287
+ }
174
288
 
175
- // TODO no work
176
- function transition() {
177
- if (isTransition) {
178
- element.classList.add(_this.classes.transition);
289
+ /**
290
+ * Настраивает контент типа 'elements' (ограничение по количеству).
291
+ * @param {HTMLElement} element - Контейнер элементов.
292
+ * @param {string} elementClass - Класс видимых элементов.
293
+ * @param {number} cnt - Количество видимых элементов.
294
+ * @param {boolean} fade - Использовать затухание.
295
+ * @param {boolean} transition - Использовать анимацию.
296
+ * @param {boolean} isEllipsis - Использовать многоточие.
297
+ * @param {boolean} isButton - Показывать кнопку.
298
+ * @param {boolean} showNum - Показывать счётчик.
299
+ * @private
300
+ */
301
+ _setupElementsContent(element, elementClass, cnt, fade, transition, isEllipsis, isButton, showNum) {
302
+ const items = Selectors.findAll('.' + elementClass, element);
303
+ this.total = items.length;
304
+ this.count = cnt;
305
+
306
+ items.forEach((item, index) => {
307
+ if (index >= cnt) {
308
+ Classes.add(item, CLASS_NAME_HIDE);
179
309
  }
180
- }
310
+ });
181
311
 
182
- function fade() {
183
- if (isFade) {
184
- element.classList.add(_this.classes.fade);
185
- }
186
- }
312
+ const shouldShowButton = isButton && items.length > cnt;
187
313
 
188
- function button() {
189
- if (isButtonAppend) {
190
- element.setAttribute("id", element.id);
314
+ if (isEllipsis) Classes.add(element, this.classes.ellipsis);
315
+ if (transition) Classes.add(element, this.classes.transition);
316
+ if (fade) Classes.add(element, this.classes.fade);
191
317
 
192
- if (isButton) {
193
- let textShowNum = '';
318
+ if (shouldShowButton) {
319
+ const sum = this.total - this.count;
320
+ const textShowNum = showNum && sum > 0 ? this._params.more + sum : '';
321
+ this._createButton(element, textShowNum, false);
322
+ }
323
+ }
194
324
 
195
- if (isShowNum) {
196
- let sum = (_this.total) - (_this.count);
325
+ /**
326
+ * Создаёт кнопку управления разворачиванием.
327
+ * @param {HTMLElement} element - Целевой контейнер.
328
+ * @param {string} textNum - Дополнительный текст (например, количество).
329
+ * @param {boolean} showNum - Флаг отображения числа (не используется).
330
+ * @private
331
+ */
332
+ _createButton(element, textNum = '', showNum = false) {
333
+ if (!element.id) {
334
+ element.id = `vg-rollup-${Math.random().toString(36).substr(2, 9)}`;
335
+ }
197
336
 
198
- if (sum > 0) {
199
- textShowNum = _this._params.more + sum;
200
- }
201
- }
337
+ const btnTextMore = this._params.button.more;
338
+ const btnHTML = `<div class="${this.classes.button}">
339
+ <a href="#" aria-expanded="false" data-vg-toggle="rollup" data-vg-target="#${element.id}">
340
+ ${btnTextMore}${textNum}
341
+ </a>
342
+ </div>`;
202
343
 
203
- let btnTextMore = _this._params.button.more;
204
- element.insertAdjacentHTML("afterend", "<div class=\"" + _this.classes.button + "\"><a href=\"#\" aria-expanded=\"false\" data-vg-toggle=\"rollup\" data-vg-target=\"#" + element.id + "\">" + btnTextMore + textShowNum + "</a></div>");
205
- }
206
- }
207
- }
344
+ element.insertAdjacentHTML("afterend", btnHTML);
208
345
  }
209
346
 
347
+ /**
348
+ * Переключает состояние скрытия/показа контента.
349
+ * @param {HTMLElement} el - Элемент контента.
350
+ * @param {boolean} switcher - Если `true` — свернуть, иначе — полностью открыть.
351
+ * @example
352
+ * instance.switch(element, true); // свернуть
353
+ * instance.switch(element, false); // развернуть
354
+ */
210
355
  switch(el, switcher = false) {
211
356
  if (switcher && !this.isOffset) {
212
- this.build(el, false);
357
+ const { content } = this._params;
358
+ const selfHeight = el.clientHeight;
359
+ const setHeight = this._params.height || selfHeight / 2;
213
360
 
214
- if (this._params.offset > 0) {
215
- this.offset = this._params.offset;
216
- if (this.offset > 0) this.isOffset = true;
217
- }
218
- } else {
219
- el.classList.remove(this.classes.hidden);
220
- el.classList.remove(this.classes.ellipsis);
221
- el.classList.remove(this.classes.fade);
361
+ if (content === 'text' && selfHeight > setHeight) {
362
+ Classes.add(el, this.classes.hidden);
363
+ el.style.height = setHeight + "px";
222
364
 
223
- el.removeAttribute("style");
365
+ if (this._params.ellipsis.line) {
366
+ Classes.add(el, this.classes.ellipsis);
367
+ el.style.lineClamp = this._params.ellipsis.line;
368
+ }
224
369
 
225
- if (this._params.content === 'elements') {
226
- let className = this._params.elements;
370
+ if (this._params.fade) Classes.add(el, this.classes.fade);
371
+ if (this._params.transition) Classes.add(el, this.classes.transition);
372
+ } else if (content === 'elements') {
373
+ const items = Selectors.findAll('.' + this._params.elements, el);
374
+ items.forEach((item, index) => {
375
+ if (index >= this.count) {
376
+ Classes.add(item, CLASS_NAME_HIDE);
377
+ }
378
+ });
379
+ }
227
380
 
228
- let items = Selectors.findAll('.' + className, el);
381
+ Classes.add(el, this.classes.container);
382
+ } else {
383
+ const { hidden, ellipsis, fade } = this.classes;
384
+ Classes.remove(el, [hidden, ellipsis, fade]);
385
+ Manipulator.remove(el, 'style');
229
386
 
230
- if (items.length) {
231
- items.forEach((item) => item.classList.remove(CLASS_NAME_HIDE))
232
- }
387
+ if (this._params.content === 'elements') {
388
+ const items = Selectors.findAll('.' + this._params.elements, el);
389
+ items.forEach(item => Classes.remove(item, CLASS_NAME_HIDE));
233
390
  }
234
391
  }
235
392
  }
236
393
 
394
+ /**
395
+ * Проверяет, развёрнут ли контент.
396
+ * @returns {boolean} `true`, если контент развёрнут.
397
+ * @example
398
+ * if (instance.isShow()) { ... }
399
+ */
237
400
  isShow() {
238
- return this._element.classList.contains(CLASS_NAME_SHOW);
401
+ return Classes.has(this._element, CLASS_NAME_SHOW);
239
402
  }
240
403
 
241
404
  /**
242
- * Инициализация
243
- * @param element
244
- * @param params
245
- * @param callback
405
+ * Инициализирует экземпляр VGRollup для элемента.
406
+ * @param {HTMLElement} element - Целевой элемент.
407
+ * @param {Object} params - Параметры конфигурации.
408
+ * @param {Function} [callback] - Колбэк, вызываемый после инициализации.
409
+ * @static
410
+ * @example
411
+ * VGRollup.init(document.querySelector('.rollup'), { height: 100 }, (instance) => {
412
+ * console.log('Rollup инициализирован:', instance);
413
+ * });
246
414
  */
247
415
  static init(element, params = {}, callback) {
248
416
  const instance = VGRollup.getOrCreateInstance(element, params);
@@ -251,19 +419,19 @@ class VGRollup extends BaseModule {
251
419
  }
252
420
 
253
421
  /**
254
- * Data API implementation
422
+ * Подключает обработчик кликов по data-атрибуту для автоматической инициализации.
423
+ * @listens click
424
+ * @event
255
425
  */
256
426
  EventHandler.on(document, EVENT_KEY_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
257
- const target = Selectors.getElementFromSelector(this);
258
- if (!target) return;
259
-
260
427
  if (['A', 'AREA'].includes(this.tagName)) {
261
- event.preventDefault()
428
+ event.preventDefault();
262
429
  }
263
430
 
264
- if (isDisabled(this)) {
265
- return
266
- }
431
+ if (isDisabled(this)) return;
432
+
433
+ const target = Selectors.getElementFromSelector(this);
434
+ if (!target) return;
267
435
 
268
436
  VGRollup.toggle(target, this);
269
437
  });