vgapp 0.7.8 → 0.8.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +22 -0
  3. package/app/langs/en/buttons.json +10 -0
  4. package/app/langs/en/messages.json +32 -0
  5. package/app/langs/en/titles.json +6 -0
  6. package/app/langs/ru/buttons.json +10 -0
  7. package/app/langs/ru/messages.json +32 -0
  8. package/app/langs/ru/titles.json +6 -0
  9. package/app/modules/base-module.js +23 -2
  10. package/app/modules/module-fn.js +20 -9
  11. package/app/modules/vgalert/js/vgalert.js +362 -214
  12. package/app/modules/vgalert/readme.md +242 -0
  13. package/app/modules/vgcollapse/js/vgcollapse.js +216 -62
  14. package/app/modules/vgcollapse/readme.md +56 -0
  15. package/app/modules/vgcollapse/scss/_variables.scss +5 -0
  16. package/app/modules/vgcollapse/scss/vgcollapse.scss +41 -0
  17. package/app/modules/vgdropdown/js/vgdropdown.js +140 -38
  18. package/app/modules/vgdropdown/readme.md +225 -0
  19. package/app/modules/vgfiles/js/base.js +499 -0
  20. package/app/modules/vgfiles/js/droppable.js +159 -0
  21. package/app/modules/vgfiles/js/loader.js +389 -0
  22. package/app/modules/vgfiles/js/render.js +83 -0
  23. package/app/modules/vgfiles/js/sortable.js +155 -0
  24. package/app/modules/vgfiles/js/vgfiles.js +796 -280
  25. package/app/modules/vgfiles/readme.md +193 -0
  26. package/app/modules/vgfiles/scss/_animations.scss +18 -0
  27. package/app/modules/vgfiles/scss/_mixins.scss +73 -0
  28. package/app/modules/vgfiles/scss/_variables.scss +103 -26
  29. package/app/modules/vgfiles/scss/vgfiles.scss +573 -60
  30. package/app/modules/vgformsender/js/vgformsender.js +5 -1
  31. package/app/modules/vgformsender/readme.md +30 -1
  32. package/app/modules/vglawcookie/js/vglawcookie.js +96 -62
  33. package/app/modules/vglawcookie/readme.md +102 -0
  34. package/app/modules/vgsidebar/js/vgsidebar.js +6 -4
  35. package/app/utils/js/components/ajax.js +176 -104
  36. package/app/utils/js/components/animation.js +124 -39
  37. package/app/utils/js/components/backdrop.js +54 -31
  38. package/app/utils/js/components/lang.js +71 -64
  39. package/app/utils/js/components/params.js +34 -31
  40. package/app/utils/js/components/scrollbar.js +118 -67
  41. package/app/utils/js/components/templater.js +14 -4
  42. package/app/utils/js/dom/cookie.js +107 -64
  43. package/app/utils/js/dom/data.js +68 -20
  44. package/app/utils/js/dom/event.js +272 -239
  45. package/app/utils/js/dom/manipulator.js +135 -62
  46. package/app/utils/js/dom/selectors.js +134 -59
  47. package/app/utils/js/functions.js +183 -349
  48. package/build/vgapp.css +1 -1
  49. package/build/vgapp.css.map +1 -1
  50. package/index.scss +3 -0
  51. package/package.json +1 -1
  52. package/app/utils/js/components/overflow.js +0 -28
@@ -1,481 +1,315 @@
1
1
  /**
2
- * Набор скриптов для широкого применения
3
- * Включает утилиты для работы с DOM, типами данных, анимациями и массивами.
4
- * Все функции экспортируются для использования в других модулях.
2
+ * Утилиты: DOM, типы, массивы, анимации, объекты
3
+ * Экспортируются все функции.
5
4
  */
6
5
 
7
6
  /**
8
- * Экранирование селекторов для корректной работы идентификаторов,
9
- * содержащих специальные символы (например, `#my-id:with-colon`).
10
- * Использует `CSS.escape()` при наличии поддержки в браузере.
11
- *
12
- * @param {string} selector - CSS-селектор, который необходимо экранировать
13
- * @returns {string} Экранированный селектор, пригодный для `document.querySelector`
14
- * @example
15
- * parseSelector('#user:input') → '#user\\:input'
7
+ * Проверяет, является ли объект DOM-элементом.
8
+ * @param {*} obj
9
+ * @returns {boolean}
16
10
  */
17
- const parseSelector = (selector) => {
18
- if (selector && window.CSS && window.CSS.escape) {
19
- // document.querySelector needs escaping to handle IDs (html5+) containing for instance /
20
- selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`);
21
- }
22
-
23
- return selector;
24
- };
11
+ const isElement = (obj) => obj?.nodeType === Node.ELEMENT_NODE;
25
12
 
26
13
  /**
27
- * Проверяет, является ли объект пустым.
28
- * Объект считается пустым, если не содержит собственных перечисляемых свойств.
29
- *
30
- * @param {Object} obj - Объект для проверки
31
- * @returns {boolean} `true`, если объект пуст или не является объектом, иначе `false`
32
- * @example
33
- * isEmptyObj({}) → true
34
- * isEmptyObj({ a: 1 }) → false
14
+ * Проверяет, является ли значение объектом (не null, включая массивы).
15
+ * @param {*} obj
16
+ * @returns {boolean}
35
17
  */
36
- function isEmptyObj(obj) {
37
- for (let prop in obj) {
38
- if (Object.prototype.hasOwnProperty.call(obj, prop)) {
39
- return false;
40
- }
41
- }
42
-
43
- return true;
44
- }
18
+ const isObject = (obj) => obj && typeof obj === 'object';
45
19
 
46
20
  /**
47
- * Получает DOM-элемент по селектору или возвращает сам элемент, если передан.
48
- * Поддерживает jQuery-объекты (через `.jquery` и `[0]`).
49
- *
50
- * @param {string|Element|jQuery} object - Селектор строки, DOM-элемент или jQuery-объект
51
- * @returns {Element|null} Найденный DOM-элемент или `null`, если не найден
52
- * @example
53
- * getElement('#myId') → <div id="myId">...</div>
54
- * getElement(element) → element
21
+ * Проверяет, пуст ли объект (нет собственных перечисляемых ключей).
22
+ * @param {Object} obj
23
+ * @returns {boolean}
55
24
  */
56
- const getElement = (object) => {
57
- // it's a jQuery object or a node element
58
- if (isElement(object)) {
59
- return object.jquery ? object[0] : object;
60
- }
61
-
62
- if (typeof object === 'string' && object.length > 0) {
63
- return document.querySelector(parseSelector(object));
25
+ const isEmptyObj = (obj) => {
26
+ if (!isObject(obj)) return true;
27
+ for (const key in obj) {
28
+ if (Object.prototype.hasOwnProperty.call(obj, key)) return false;
64
29
  }
65
-
66
- return null;
30
+ return true;
67
31
  };
68
32
 
69
33
  /**
70
- * Проверяет, является ли переданный объект DOM-элементом.
71
- *
72
- * @param {*} object - Объект для проверки
73
- * @returns {boolean} `true`, если объект является DOM-элементом, иначе `false`
74
- * @example
75
- * isElement(document.body) → true
76
- * isElement({}) → false
34
+ * Возвращает DOM-элемент по селектору, элементу или jQuery-объекту.
35
+ * @param {string|Element|jQuery} selector
36
+ * @returns {Element|null}
77
37
  */
78
- const isElement = (object) => {
79
- if (!isObject(object)) {
80
- return false;
38
+ const getElement = (selector) => {
39
+ if (isElement(selector)) return selector;
40
+ if (selector?.jquery) return selector[0] || null;
41
+ if (typeof selector === 'string' && selector.trim() !== '') {
42
+ try {
43
+ return document.querySelector(selector);
44
+ } catch (e) {
45
+ console.warn('Invalid selector:', selector);
46
+ return null;
47
+ }
81
48
  }
82
-
83
- return typeof object.nodeType !== 'undefined';
49
+ return null;
84
50
  };
85
51
 
86
52
  /**
87
- * Проверяет, отключён ли DOM-элемент.
88
- * Учитывает класс `.disabled`, атрибут `disabled`, а также свойство `disabled`.
89
- *
90
- * @param {Element} element - DOM-элемент для проверки
91
- * @returns {boolean} `true`, если элемент отключён, иначе `false`
92
- * @example
93
- * isDisabled(document.querySelector('[disabled]')) → true
53
+ * Проверяет, отключён ли элемент (по классу .disabled, атрибуту или свойству).
54
+ * @param {Element} el
55
+ * @returns {boolean}
94
56
  */
95
- const isDisabled = (element) => {
96
- if (!element || element.nodeType !== Node.ELEMENT_NODE) {
97
- return true;
98
- }
99
-
100
- if (element.classList.contains('disabled')) {
101
- return true;
102
- }
103
-
104
- if (typeof element.disabled !== 'undefined') {
105
- return element.disabled;
106
- }
107
-
108
- return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';
57
+ const isDisabled = (el) => {
58
+ if (!isElement(el)) return true;
59
+ if (el.classList.contains('disabled')) return true;
60
+ if (typeof el.disabled !== 'undefined') return !!el.disabled;
61
+ return el.hasAttribute('disabled') && el.getAttribute('disabled') !== 'false';
109
62
  };
110
63
 
111
64
  /**
112
- * Проверяет, видим ли DOM-элемент.
113
- * Учитывает размеры (getClientRects), `visibility: hidden`, `details:not([open])` и `summary`.
114
- *
115
- * @param {Element} element - DOM-элемент для проверки видимости
116
- * @returns {boolean} `true`, если элемент видим, иначе `false`
117
- * @example
118
- * isVisible(button) → true
65
+ * Проверяет видимость элемента (размер, visibility, details).
66
+ * @param {Element} el
67
+ * @returns {boolean}
119
68
  */
120
- function isVisible(element) {
121
- if (!isElement(element) || element.getClientRects().length === 0) {
122
- return false;
123
- }
124
-
125
- const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';
126
- const closedDetails = element.closest('details:not([open])');
127
-
128
- if (!closedDetails) {
129
- return elementIsVisible;
130
- }
69
+ const isVisible = (el) => {
70
+ if (!isElement(el) || el.getClientRects().length === 0) return false;
131
71
 
132
- if (closedDetails !== element) {
133
- const summary = element.closest('summary');
134
- if (summary && summary.parentNode !== closedDetails) {
135
- return false;
136
- }
72
+ const style = getComputedStyle(el);
73
+ if (style.visibility === 'hidden') return false;
137
74
 
138
- if (summary === null) {
139
- return false;
140
- }
141
- }
75
+ const parentDetails = el.closest('details:not([open])');
76
+ if (!parentDetails) return true;
142
77
 
143
- return elementIsVisible;
144
- }
78
+ const summary = el.closest('summary');
79
+ if (summary && summary.parentNode === parentDetails) return true;
145
80
 
146
- /**
147
- * Проверяет, является ли значение объектом (включая массивы и null).
148
- *
149
- * @param {*} obj - Значение для проверки
150
- * @returns {boolean} `true`, если значение — объект, иначе `false`
151
- * @example
152
- * isObject({}) → true
153
- * isObject([]) → true
154
- * isObject(null) → false
155
- */
156
- function isObject(obj) {
157
- return obj && typeof obj === 'object';
158
- }
81
+ return false;
82
+ };
159
83
 
160
84
  /**
161
- * Нормализует строку в соответствующий тип данных.
162
- * Преобразует строки в `true`, `false`, `null`, числа, JSON и т.д.
163
- *
164
- * @param {string|*} value - Значение для нормализации
165
- * @returns {*} Нормализованное значение: boolean, number, null, object, string и т.д.
166
- * @example
167
- * normalizeData('true') → true
168
- * normalizeData('{"a":1}') → { a: 1 }
85
+ * Преобразует строку в соответствующий тип: boolean, number, null, объект.
86
+ * @param {*} value
87
+ * @returns {*}
169
88
  */
170
- function normalizeData(value) {
171
- if (value === 'true') {
172
- return true;
173
- }
174
-
175
- if (value === 'false') {
176
- return false;
177
- }
178
-
179
- if (value === Number(value).toString()) {
180
- return Number(value);
181
- }
182
-
183
- if (value === '' || value === 'null') {
184
- return null;
185
- }
186
-
187
- if (typeof value !== 'string') {
188
- return value;
189
- }
89
+ const normalizeData = (value) => {
90
+ if (value === 'true') return true;
91
+ if (value === 'false') return false;
92
+ if (value === 'null') return null;
93
+ if (value === '') return null;
94
+ if (typeof value === 'number' || value === Number(value)) return Number(value);
95
+ if (typeof value !== 'string') return value;
190
96
 
191
97
  try {
192
98
  return JSON.parse(decodeURIComponent(value));
193
99
  } catch {
194
100
  return value;
195
101
  }
196
- }
102
+ };
197
103
 
198
104
  /**
199
- * Удаляет указанные элементы из массива.
200
- *
201
- * @param {Array} arr - Исходный массив
202
- * @param {Array} el - Массив элементов, которые нужно удалить
203
- * @returns {Array} Новый массив без указанных элементов
204
- * @example
205
- * removeElementArray([1, 2, 3], [2]) → [1, 3]
105
+ * Удаляет элементы из массива.
106
+ * @param {Array} arr
107
+ * @param {Array} toRemove
108
+ * @returns {Array}
206
109
  */
207
- function removeElementArray(arr, el) {
208
- return arr.filter((item) => !el.includes(item));
209
- }
110
+ const removeElementArray = (arr, toRemove) => arr.filter((item) => !toRemove.includes(item));
210
111
 
211
112
  /**
212
- * Глубокое объединение нескольких объектов.
213
- * Поддерживает вложенные объекты и массивы (конкатенация).
214
- *
215
- * @param {...Object} objects - Объекты для объединения
216
- * @returns {Object} Новый объект с объединёнными данными
217
- * @example
218
- * mergeDeepObject({ a: [1] }, { a: [2] }) → { a: [1, 2] }
219
- * mergeDeepObject({ a: { b: 1 } }, { a: { c: 2 } }) → { a: { b: 1, c: 2 } }
113
+ * Глубокое слияние объектов с конкатенацией массивов.
114
+ * @param {...Object} sources
115
+ * @returns {Object}
220
116
  */
221
- function mergeDeepObject(...objects) {
222
- const isObject = (obj) => obj && typeof obj === 'object';
117
+ const mergeDeepObject = (...sources) => {
118
+ const isObj = (o) => o && typeof o === 'object';
223
119
 
224
- if (!isObject) return;
120
+ return sources.reduce((acc, obj) => {
121
+ if (!isObj(obj)) return acc;
225
122
 
226
- return objects.reduce((prev, obj) => {
227
123
  Object.keys(obj).forEach((key) => {
228
- const pVal = prev[key];
229
- const oVal = obj[key];
124
+ const src = acc[key];
125
+ const val = obj[key];
230
126
 
231
- if (Array.isArray(pVal) && Array.isArray(oVal)) {
232
- prev[key] = pVal.concat(...oVal);
233
- } else if (isObject(pVal) && isObject(oVal)) {
234
- prev[key] = mergeDeepObject(pVal, oVal);
127
+ if (Array.isArray(src) && Array.isArray(val)) {
128
+ acc[key] = src.concat(val);
129
+ } else if (isObj(src) && isObj(val)) {
130
+ acc[key] = mergeDeepObject(src, val);
235
131
  } else {
236
- prev[key] = oVal;
132
+ acc[key] = val;
237
133
  }
238
134
  });
239
135
 
240
- return prev;
136
+ return acc;
241
137
  }, {});
242
- }
138
+ };
243
139
 
244
140
  /**
245
- * Выполняет функцию, если передан колбэк; иначе возвращает значение по умолчанию.
246
- *
247
- * @param {Function|*} possibleCallback - Функция или любое значение
248
- * @param {Array} [args=[]] - Аргументы для вызова функции
249
- * @param {*} [defaultValue=possibleCallback] - Значение по умолчанию, если колбэк не функция
250
- * @returns {*} Результат выполнения колбэка или `defaultValue`
251
- * @example
252
- * execute(() => 'hi') → 'hi'
253
- * execute('notFunc', [], 'fallback') → 'fallback'
141
+ * Выполняет функцию с аргументами или возвращает значение по умолчанию.
142
+ * @param {Function|*} fn
143
+ * @param {Array} args
144
+ * @param {*} defaultValue
145
+ * @returns {*}
254
146
  */
255
- function execute(possibleCallback, args = [], defaultValue = possibleCallback) {
256
- return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
257
- }
147
+ const execute = (fn, args = [], defaultValue = fn) => {
148
+ return typeof fn === 'function' ? fn(...args) : defaultValue;
149
+ };
258
150
 
259
151
  /**
260
- * Событие окончания CSS-перехода.
152
+ * Событие завершения CSS-перехода.
261
153
  * @type {string}
262
154
  */
263
155
  const TRANSITION_END = 'transitionend';
264
156
 
265
157
  /**
266
- * Множитель для перевода секунд в миллисекунды.
158
+ * Множитель для мс.
267
159
  * @type {number}
268
160
  */
269
161
  const MILLISECONDS_MULTIPLIER = 1000;
270
162
 
271
163
  /**
272
- * Выполняет колбэк после завершения CSS-перехода.
273
- * Использует `transitionend`, с fallback на `setTimeout`.
274
- *
275
- * @param {Function} callback - Функция, вызываемая после перехода
276
- * @param {Element} transitionElement - Элемент, за переходом которого нужно следить
277
- * @param {boolean} [waitForTransition=true] - Ожидать ли переход
278
- * @param {number} [timeOutMs] - Фиксированная задержка (вместо авто-определения)
279
- * @example
280
- * executeAfterTransition(() => console.log('done'), button)
164
+ * Выполняет колбэк после CSS-перехода с fallback на setTimeout.
165
+ * @param {Function} callback
166
+ * @param {Element} element
167
+ * @param {boolean} waitForTransition
168
+ * @param {number} [timeoutMs]
281
169
  */
282
- function executeAfterTransition(callback, transitionElement, waitForTransition = true, timeOutMs) {
170
+ const executeAfterTransition = (callback, element, waitForTransition = true, timeoutMs) => {
283
171
  if (!waitForTransition) {
284
172
  execute(callback);
285
173
  return;
286
174
  }
287
175
 
288
- const durationPadding = 5;
289
- const emulatedDuration = timeOutMs ? timeOutMs : getTransitionDurationFromElement(transitionElement) + durationPadding;
290
-
176
+ const duration = timeoutMs ?? getTransitionDurationFromElement(element) + 5;
291
177
  let called = false;
292
178
 
293
- const handler = ({ target }) => {
294
- if (target !== transitionElement) {
295
- return;
179
+ const handler = (e) => {
180
+ if (e.target === element) {
181
+ called = true;
182
+ element.removeEventListener(TRANSITION_END, handler);
183
+ execute(callback);
296
184
  }
297
-
298
- called = true;
299
- transitionElement.removeEventListener(TRANSITION_END, handler);
300
- execute(callback);
301
185
  };
302
186
 
303
- transitionElement.addEventListener(TRANSITION_END, handler);
187
+ element.addEventListener(TRANSITION_END, handler);
304
188
  setTimeout(() => {
305
- if (!called) {
306
- triggerTransitionEnd(transitionElement);
307
- }
308
- }, emulatedDuration);
309
- }
189
+ if (!called) triggerTransitionEnd(element);
190
+ }, duration);
191
+ };
310
192
 
311
193
  /**
312
- * Получает длительность CSS-перехода элемента в миллисекундах.
313
- * Учитывает `transition-duration` и `transition-delay`.
314
- *
315
- * @param {Element} element - DOM-элемент
316
- * @returns {number} Длительность перехода в миллисекундах
317
- * @example
318
- * getTransitionDurationFromElement(div) → 300
194
+ * Получает длительность CSS-перехода элемента в мс.
195
+ * @param {Element} element
196
+ * @returns {number}
319
197
  */
320
198
  const getTransitionDurationFromElement = (element) => {
321
- if (!element) {
322
- return 0;
323
- }
199
+ if (!element) return 0;
200
+ const { transitionDuration, transitionDelay } = getComputedStyle(element);
324
201
 
325
- // Get transition-duration of the element
326
- let { transitionDuration, transitionDelay } = window.getComputedStyle(element);
202
+ const durations = transitionDuration.split(', ');
203
+ const delays = transitionDelay.split(', ');
327
204
 
328
- const floatTransitionDuration = Number.parseFloat(transitionDuration);
329
- const floatTransitionDelay = Number.parseFloat(transitionDelay);
205
+ const duration = Number.parseFloat(durations[0]) || 0;
206
+ const delay = Number.parseFloat(delays[0]) || 0;
330
207
 
331
- // Return 0 if element or transition duration is not found
332
- if (!floatTransitionDuration && !floatTransitionDelay) {
333
- return 0;
334
- }
335
-
336
- // If multiple durations are defined, take the first
337
- transitionDuration = transitionDuration.split(',')[0];
338
- transitionDelay = transitionDelay.split(',')[0];
339
-
340
- return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;
208
+ return (duration + delay) * MILLISECONDS_MULTIPLIER;
341
209
  };
342
210
 
343
211
  /**
344
- * Принудительно генерирует событие окончания перехода на элементе.
345
- *
346
- * @param {Element} element - Элемент, на котором нужно вызвать событие
347
- * @example
348
- * triggerTransitionEnd(button)
212
+ * Принудительно вызывает событие перехода.
213
+ * @param {Element} element
349
214
  */
350
215
  const triggerTransitionEnd = (element) => {
351
216
  element.dispatchEvent(new Event(TRANSITION_END));
352
217
  };
353
218
 
354
219
  /**
355
- * Принудительный рефлоу (пересчёт макета) для перезапуска CSS-анимаций.
356
- *
357
- * @param {HTMLElement} element - DOM-элемент
358
- * @returns {void}
359
- * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
360
- * @example
361
- * reflow(myElement);
220
+ * Принудительный рефлоу (для перезапуска анимаций).
221
+ * @param {HTMLElement} element
362
222
  */
363
223
  const reflow = (element) => {
364
- element.offsetHeight; // eslint-disable-line no-unused-expressions
224
+ /* eslint-disable no-unused-expressions */
225
+ element.offsetHeight;
226
+ /* eslint-enable no-unused-expressions */
365
227
  };
366
228
 
367
229
  /**
368
- * Пустая функция, используемая как заглушка.
369
- *
370
- * @returns {void}
371
- * @example
372
- * someFunc || noop
230
+ * Пустая функция-заглушка.
373
231
  */
374
232
  const noop = () => {};
375
233
 
376
234
  /**
377
- * Генерирует случайную строку заданной длины.
378
- *
379
- * @param {number} [length=7] - Длина строки
380
- * @returns {string} Случайная строка из латинских букв и цифр
381
- * @example
382
- * makeRandomString(5) → 'aB3xY'
235
+ * Генерирует случайную строку.
236
+ * @param {number} length
237
+ * @returns {string}
383
238
  */
384
- function makeRandomString(length = 7) {
239
+ const makeRandomString = (length = 7) => {
240
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
385
241
  let result = '';
386
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
387
- const charactersLength = characters.length;
388
- let counter = 0;
389
- while (counter < length) {
390
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
391
- counter += 1;
242
+ for (let i = 0; i < length; i++) {
243
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
392
244
  }
393
245
  return result;
394
- }
246
+ };
395
247
 
396
248
  /**
397
- * Транслитерация символов между кириллицей и латиницей.
398
- *
399
- * @param {string} text - Текст для транслитерации
400
- * @param {boolean} [enToRu=true] - Направление: `true` — en→ru, `false` — ru→en
401
- * @returns {string} Транслитерированный текст
402
- * @example
403
- * transliterate('privet', true) → 'привет'
404
- * transliterate('привет', false) → 'privet'
249
+ * Транслитерация: en ru.
250
+ * @param {string} text
251
+ * @param {boolean} [enToRu=true] - true: en→ru, false: ru→en
252
+ * @returns {string}
405
253
  */
406
- function transliterate(text, enToRu) {
407
- let ru = 'й ц у к е н г ш щ з х ъ ф ы в а п р о л д ж э я ч с м и т ь б ю'.split(/ +/g);
408
- let en = 'q w e r t y u i o p [ ] a s d f g h j k l ; \' z x c v b n m , .'.split(/ +/g);
409
- let x;
410
-
411
- for (x = 0; x < ru.length; x++) {
412
- text = text.split(enToRu ? en[x] : ru[x]).join(enToRu ? ru[x] : en[x]);
413
- text = text
414
- .split(enToRu ? en[x].toUpperCase() : ru[x].toUpperCase())
415
- .join(enToRu ? ru[x].toUpperCase() : en[x].toUpperCase());
416
- }
254
+ const transliterate = (text, enToRu = true) => {
255
+ const from = 'q w e r t y u i o p [ ] a s d f g h j k l ; \' z x c v b n m , .'.split(' ');
256
+ const to = 'й ц у к е н г ш щ з х ъ ф ы в а п р о л д ж э я ч с м и т ь б ю'.split(' ');
417
257
 
418
- return text;
419
- }
258
+ const src = enToRu ? from : to;
259
+ const dst = enToRu ? to : from;
420
260
 
421
- /**
422
- * Возвращает следующий или предыдущий элемент в списке.
423
- * Поддерживает циклическое переключение.
424
- *
425
- * @param {Array<Element>} list - Массив DOM-элементов
426
- * @param {Element} activeElement - Текущий активный элемент
427
- * @param {boolean} shouldGetNext - `true` — следующий, `false` — предыдущий
428
- * @param {boolean} isCycleAllowed - Разрешить цикл (с конца к началу и наоборот)
429
- * @returns {Element} Следующий/предыдущий элемент
430
- * @example
431
- * getNextActiveElement(items, current, true, true) → следующий элемент (с циклом)
432
- */
433
- const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
434
- const listLength = list.length;
435
- let index = list.indexOf(activeElement);
436
-
437
- // if the element does not exist in the list return an element
438
- // depending on the direction and if cycle is allowed
439
- if (index === -1) {
440
- return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];
441
- }
261
+ let result = text;
442
262
 
443
- index += shouldGetNext ? 1 : -1;
263
+ for (let i = 0; i < src.length; i++) {
264
+ const srcLower = src[i];
265
+ const dstLower = dst[i];
266
+ const srcUpper = src[i].toUpperCase();
267
+ const dstUpper = dst[i].toUpperCase();
444
268
 
445
- if (isCycleAllowed) {
446
- index = (index + listLength) % listLength;
269
+ result = result.split(srcLower).join(dstLower).split(srcUpper).join(dstUpper);
447
270
  }
448
271
 
449
- return list[Math.max(0, Math.min(index, listLength - 1))];
272
+ return result;
450
273
  };
451
274
 
452
275
  /**
453
- * Рекурсивно возвращает самый глубокий последний дочерний элемент.
454
- *
455
- * @param {Element} element - Родительский элемент
456
- * @returns {Element} Самый вложенный `lastElementChild`
457
- * @example
458
- * getDeepestLastChild(div) → <span>...</span>
276
+ * Получает следующий/предыдущий элемент с циклом.
277
+ * @param {Array<Element>} list
278
+ * @param {Element} active
279
+ * @param {boolean} getNext
280
+ * @param {boolean} cycle
281
+ * @returns {Element}
459
282
  */
460
- function getDeepestLastChild(element) {
461
- let current = element;
283
+ const getNextActiveElement = (list, active, getNext, cycle) => {
284
+ const len = list.length;
285
+ let idx = list.indexOf(active);
462
286
 
463
- while (current.lastElementChild) {
464
- current = current.lastElementChild;
465
- }
287
+ if (idx === -1) return getNext || !cycle ? list[0] : list[len - 1];
288
+
289
+ idx += getNext ? 1 : -1;
290
+ if (cycle) idx = ((idx % len) + len) % len;
291
+
292
+ return list[Math.max(0, Math.min(idx, len - 1))];
293
+ };
466
294
 
467
- return current;
468
- }
295
+ /**
296
+ * Возвращает самый глубокий последний дочерний элемент.
297
+ * @param {Element} el
298
+ * @returns {Element}
299
+ */
300
+ const getDeepestLastChild = (el) => {
301
+ let node = el;
302
+ while (node.lastElementChild) node = node.lastElementChild;
303
+ return node;
304
+ };
469
305
 
470
306
  /**
471
- * Проверяет, используется ли направление текста справа налево (RTL).
472
- *
473
- * @returns {boolean} `true`, если `dir="rtl"`, иначе `false`
474
- * @example
475
- * isRTL() → true (если <html dir="rtl">)
307
+ * Проверяет, включён ли RTL.
308
+ * @returns {boolean}
476
309
  */
477
310
  const isRTL = () => document.documentElement.dir === 'rtl';
478
311
 
312
+ // экспорт
479
313
  export {
480
314
  getDeepestLastChild,
481
315
  isElement,