vgapp 0.7.9 → 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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/app/langs/en/buttons.json +10 -0
  3. package/app/langs/en/messages.json +32 -0
  4. package/app/langs/en/titles.json +6 -0
  5. package/app/langs/ru/buttons.json +10 -0
  6. package/app/langs/ru/messages.json +32 -0
  7. package/app/langs/ru/titles.json +6 -0
  8. package/app/modules/base-module.js +12 -1
  9. package/app/modules/module-fn.js +20 -9
  10. package/app/modules/vgalert/js/vgalert.js +12 -6
  11. package/app/modules/vgalert/readme.md +1 -1
  12. package/app/modules/vgcollapse/readme.md +1 -1
  13. package/app/modules/vgdropdown/js/vgdropdown.js +140 -38
  14. package/app/modules/vgdropdown/readme.md +225 -0
  15. package/app/modules/vgfiles/js/base.js +499 -0
  16. package/app/modules/vgfiles/js/droppable.js +159 -0
  17. package/app/modules/vgfiles/js/loader.js +389 -0
  18. package/app/modules/vgfiles/js/render.js +83 -0
  19. package/app/modules/vgfiles/js/sortable.js +155 -0
  20. package/app/modules/vgfiles/js/vgfiles.js +796 -280
  21. package/app/modules/vgfiles/readme.md +193 -0
  22. package/app/modules/vgfiles/scss/_animations.scss +18 -0
  23. package/app/modules/vgfiles/scss/_mixins.scss +73 -0
  24. package/app/modules/vgfiles/scss/_variables.scss +103 -26
  25. package/app/modules/vgfiles/scss/vgfiles.scss +573 -60
  26. package/app/modules/vgformsender/js/vgformsender.js +5 -1
  27. package/app/modules/vgformsender/readme.md +1 -1
  28. package/app/modules/vglawcookie/js/vglawcookie.js +96 -62
  29. package/app/modules/vglawcookie/readme.md +102 -0
  30. package/app/modules/vgsidebar/js/vgsidebar.js +6 -4
  31. package/app/utils/js/components/ajax.js +172 -122
  32. package/app/utils/js/components/animation.js +124 -39
  33. package/app/utils/js/components/backdrop.js +54 -31
  34. package/app/utils/js/components/lang.js +69 -88
  35. package/app/utils/js/components/params.js +34 -31
  36. package/app/utils/js/components/scrollbar.js +118 -67
  37. package/app/utils/js/components/templater.js +14 -4
  38. package/app/utils/js/dom/cookie.js +107 -64
  39. package/app/utils/js/dom/data.js +68 -20
  40. package/app/utils/js/dom/event.js +272 -239
  41. package/app/utils/js/dom/manipulator.js +135 -62
  42. package/app/utils/js/dom/selectors.js +134 -59
  43. package/app/utils/js/functions.js +183 -349
  44. package/build/vgapp.css +1 -1
  45. package/build/vgapp.css.map +1 -1
  46. package/package.json +1 -1
  47. package/app/utils/js/components/overflow.js +0 -28
@@ -1,4 +1,4 @@
1
- import {mergeDeepObject, noop, normalizeData} from "../functions";
1
+ import { mergeDeepObject, noop, normalizeData } from "../functions";
2
2
 
3
3
  class Ajax {
4
4
  /**
@@ -10,25 +10,24 @@ class Ajax {
10
10
  * @param {string} options._token - Токен (авто-чтение из meta)
11
11
  */
12
12
  constructor(options = {}) {
13
- this.baseUrl = options.baseUrl || '';
13
+ this.baseUrl = options.baseUrl || "";
14
14
  this.defaultHeaders = {
15
- 'X-Requested-With': 'XMLHttpRequest',
16
- ...options.headers
15
+ "X-Requested-With": "XMLHttpRequest",
16
+ ...options.headers,
17
17
  };
18
18
  this.withCredentials = options.withCredentials || false;
19
19
  this.csrfToken = options._token || this._getCsrfToken();
20
20
  }
21
21
 
22
22
  /**
23
- * Получение csrf токена из тега meta
23
+ * Получение CSRF-токена из тега meta
24
24
  * @returns {string}
25
- * @private
26
25
  */
27
26
  _getCsrfToken() {
28
27
  const meta = document.querySelector('meta[name="csrf-token"]');
29
- if (meta) return meta.getAttribute('content');
28
+ if (meta) return meta.getAttribute("content");
30
29
  console.warn('CSRF-токен не найден в <meta name="csrf-token">');
31
- return '';
30
+ return "";
32
31
  }
33
32
 
34
33
  /**
@@ -36,201 +35,252 @@ class Ajax {
36
35
  * @param {string} url
37
36
  * @param {Object} options
38
37
  * @param {'GET'|'POST'|'PUT'|'DELETE'|'PATCH'} options.method
39
- * @param {Object|FormData} options.body - Данные (обычный объект или FormData)
40
- * @param {Object} options.headers - Дополнительные заголовки
41
- * @param {Function} options.onProgress - Колбэк прогресса (только для POST/PUT)
38
+ * @param {Object|FormData} options.body
39
+ * @param {Object} options.headers
40
+ * @param {AbortSignal} [options.signal] - Для отмены запроса
41
+ * @param {Function} [options.onProgress] - Только для POST/PUT с FormData
42
42
  * @param {Function} options.onSuccess
43
43
  * @param {Function} options.onError
44
- * @param {Function} options.onUploadStart
45
- * @param {Function} options.onUploadEnd
44
+ * @param {Function} [options.onUploadStart]
45
+ * @param {Function} [options.onUploadEnd]
46
46
  */
47
47
  request(url, {
48
- method = 'GET',
48
+ method = "GET",
49
49
  body = null,
50
50
  headers = {},
51
+ signal = null,
51
52
  onProgress = null,
52
- onSuccess = (data) => noop(),
53
- onError = (error) => noop(),
54
- onUploadStart = () => {},
55
- onUploadEnd = () => {}
53
+ onSuccess = noop,
54
+ onError = noop,
55
+ onUploadStart = noop,
56
+ onUploadEnd = noop,
56
57
  } = {}) {
57
58
  const fullUrl = this.baseUrl + url;
58
59
  const isFormData = body instanceof FormData;
59
60
  const requestHeaders = { ...this.defaultHeaders, ...headers };
60
- const token = {};
61
+ const isGet = method.toUpperCase() === "GET";
61
62
 
62
- if (!isFormData && this.csrfToken) {
63
- token.body = JSON.stringify({
64
- _token: this.csrfToken
65
- })
66
- }
63
+ // Удаление тела для GET
64
+ if (isGet) body = null;
67
65
 
68
- // Для JSON устанавливаем заголовок, для FormData НЕЛЬЗЯ
69
- if (!isFormData && !('Content-Type' in headers)) {
70
- requestHeaders['Content-Type'] = 'application/json';
66
+ // Установка CSRF токена: в body, если не FormData
67
+ if (!isGet && !isFormData && this.csrfToken) {
68
+ if (!body) body = {};
69
+ if (typeof body === "object" && !Array.isArray(body)) {
70
+ body._token = this.csrfToken;
71
+ }
71
72
  }
72
73
 
73
- // Если это GET-запрос — тело игнорируется
74
- if (method.toUpperCase() === 'GET') {
75
- return this._makeFetch(fullUrl, {
76
- method,
77
- headers: requestHeaders,
78
- withCredentials: this.withCredentials
79
- }, onSuccess, onError);
74
+ // Content-Type только для JSON
75
+ if (!isFormData && !("Content-Type" in headers)) {
76
+ requestHeaders["Content-Type"] = "application/json";
80
77
  }
81
78
 
82
- // Для FormData — используем XMLHttpRequest, чтобы отслеживать прогресс
79
+ // Если нужно отслеживать прогресс или FormData — используем XHR
83
80
  if (isFormData || onProgress) {
84
81
  return this._makeXHR({
85
82
  method,
86
83
  url: fullUrl,
87
84
  body,
88
85
  headers: requestHeaders,
86
+ signal,
89
87
  onProgress,
90
88
  onSuccess,
91
89
  onError,
92
90
  onUploadStart,
93
- onUploadEnd
91
+ onUploadEnd,
92
+ });
93
+ } else {
94
+ return this._makeFetch({
95
+ url: fullUrl,
96
+ method,
97
+ body: isGet ? undefined : this._serializeBody(body),
98
+ headers: requestHeaders,
99
+ signal,
100
+ withCredentials: this.withCredentials,
101
+ onSuccess,
102
+ onError,
94
103
  });
95
104
  }
96
-
97
- // Остальные случаи — fetch
98
- return this._makeFetch(fullUrl, mergeDeepObject({
99
- method,
100
- headers: requestHeaders,
101
- withCredentials: this.withCredentials
102
- }, token), onSuccess, onError);
103
105
  }
104
106
 
105
107
  /**
106
- * Использование fetch (для JSON)
108
+ * fetch-реализация (для JSON)
107
109
  */
108
- _makeFetch(url, config, onSuccess, onError) {
109
- return fetch(url, config)
110
- .then(response => {
110
+ _makeFetch({
111
+ url,
112
+ method,
113
+ body,
114
+ headers,
115
+ signal,
116
+ withCredentials,
117
+ onSuccess,
118
+ onError,
119
+ }) {
120
+ const config = {
121
+ method,
122
+ headers,
123
+ signal,
124
+ withCredentials,
125
+ ...(body !== undefined && { body }),
126
+ };
127
+
128
+ fetch(url, config)
129
+ .then((response) => {
130
+ const contentType = response.headers.get("content-type");
131
+ const isJson = contentType && contentType.includes("application/json");
132
+
111
133
  if (!response.ok) {
112
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
134
+ return Promise.reject(
135
+ normalizeData({
136
+ code: response.status,
137
+ response: isJson
138
+ ? response.json().catch(() => response.text())
139
+ : response.text(),
140
+ })
141
+ );
113
142
  }
114
- const contentType = response.headers.get('content-type');
115
- if (contentType && contentType.includes('application/json')) {
116
- return {
117
- code: response.status,
118
- response: response.json()
119
- };
143
+
144
+ let data = { code: response.status };
145
+ if (isJson) {
146
+ data.response = response.json();
147
+ } else {
148
+ data.response = response.text();
120
149
  }
121
150
 
122
- return {
123
- code: response.status,
124
- response: response.text()
125
- };
151
+ return data;
126
152
  })
127
- .then(data => {
128
- if ('response' in data) {
129
- if (data.response instanceof Promise) {
130
- data.response.then(text => {
131
- onSuccess({
132
- code: data.code,
133
- response: text
134
- })
135
- })
136
- } else {
137
- onSuccess(data)
138
- }
153
+ .then((data) => {
154
+ if (data && data.response instanceof Promise) {
155
+ data.response.then(
156
+ (resolved) => onSuccess({ ...data, response: resolved }),
157
+ () => {}
158
+ );
139
159
  } else {
140
- onSuccess(data)
160
+ console.log(data)
161
+ onSuccess(data);
141
162
  }
142
163
  })
143
- .catch(error => onError(error));
164
+ .catch((error) => {
165
+ if (error.name === "AbortError") return; // отмена — не ошибка
166
+
167
+ if (error && error.response instanceof Promise) {
168
+ error.response.then((errData) => {
169
+ onError({ ...error, response: errData });
170
+ }, () => {
171
+ onError({ ...error, response: "Request failed" });
172
+ });
173
+ } else {
174
+ onError(error);
175
+ }
176
+ });
144
177
  }
145
178
 
146
179
  /**
147
- * Использование XHR (для FormData и прогресса)
180
+ * XHR-реализация (с прогрессом и AbortController)
148
181
  */
149
182
  _makeXHR({
150
183
  method,
151
184
  url,
152
185
  body,
153
186
  headers,
187
+ signal,
154
188
  onProgress,
155
189
  onSuccess,
156
190
  onError,
157
191
  onUploadStart,
158
- onUploadEnd
192
+ onUploadEnd,
159
193
  }) {
160
- return new Promise((resolve, reject) => {
161
- const xhr = new XMLHttpRequest();
194
+ const xhr = new XMLHttpRequest();
162
195
 
163
- xhr.open(method, url, true);
164
- xhr.withCredentials = this.withCredentials;
196
+ xhr.open(method, url, true);
197
+ xhr.withCredentials = this.withCredentials;
165
198
 
166
- // Устанавливаем только пользовательские заголовки (кроме Content-Type для FormData)
167
- Object.keys(headers).forEach(key => {
168
- if (key.toLowerCase() !== 'content-type' || !(body instanceof FormData)) {
169
- xhr.setRequestHeader(key, headers[key]);
199
+ // Установка заголовков
200
+ Object.keys(headers).forEach((key) => {
201
+ if (key.toLowerCase() !== "content-type" || !(body instanceof FormData)) {
202
+ xhr.setRequestHeader(key, headers[key]);
203
+ }
204
+ });
205
+
206
+ // Прогресс
207
+ if (onProgress) {
208
+ xhr.upload.addEventListener("progress", (e) => {
209
+ if (e.lengthComputable) {
210
+ onProgress(Math.round((e.loaded / e.total) * 100), e);
170
211
  }
171
212
  });
213
+ }
172
214
 
173
- // Отслеживание прогресса загрузки
174
- if (onProgress) {
175
- xhr.upload.addEventListener('progress', (e) => {
176
- if (e.lengthComputable) {
177
- const percent = (e.loaded / e.total) * 100;
178
- onProgress(percent, e);
179
- }
215
+ // События
216
+ xhr.onload = () => {
217
+ if (xhr.status >= 200 && xhr.status < 300) {
218
+ const data = {
219
+ code: xhr.status,
220
+ response: normalizeData(xhr.responseText),
221
+ };
222
+ onSuccess(data);
223
+ } else {
224
+ const error = normalizeData({
225
+ code: xhr.status,
226
+ response: xhr.responseText || `HTTP ${xhr.status}`,
180
227
  });
228
+ onError(error);
181
229
  }
230
+ onUploadEnd();
231
+ };
182
232
 
183
- xhr.onload = () => {
184
- if (xhr.status >= 200 && xhr.status < 300) {
185
- let data = {
186
- code: xhr.status,
187
- response: normalizeData(xhr.responseText)
188
- };
189
- onSuccess(data);
190
- resolve(data);
191
- } else {
192
- const error = new Error(`Ошибка ${xhr.status}: ${xhr.statusText}`);
193
- let data = {
194
- code: xhr.status,
195
- response: error
196
- }
197
- onError(data);
198
- reject(data);
199
- }
200
- onUploadEnd();
201
- };
233
+ xhr.onerror = () => {
234
+ onError(normalizeData({ code: 0, response: "Network Error" }));
235
+ onUploadEnd();
236
+ };
202
237
 
203
- xhr.onerror = () => {
204
- const error = new Error('Network Error');
205
- onError(error);
206
- reject(error);
207
- onUploadEnd();
208
- };
238
+ xhr.ontimeout = () => {
239
+ onError(normalizeData({ code: 0, response: "Request Timeout" }));
240
+ onUploadEnd();
241
+ };
209
242
 
210
- onUploadStart();
211
- xhr.send(body);
212
- });
243
+ // Привязка AbortController
244
+ if (signal) {
245
+ signal.addEventListener("abort", () => {
246
+ xhr.abort();
247
+ });
248
+ }
249
+
250
+ onUploadStart();
251
+ xhr.send(body);
252
+
253
+ return xhr; // для отмены снаружи
213
254
  }
214
255
 
215
256
  // === Сокращённые методы ===
257
+
216
258
  get(url, options = {}) {
217
- return this.request(url, { method: 'GET', ...options });
259
+ return this.request(url, { method: "GET", ...options });
218
260
  }
219
261
 
220
262
  post(url, body, options = {}) {
221
- return this.request(url, { method: 'POST', body, ...options });
263
+ return this.request(url, { method: "POST", body, ...options });
222
264
  }
223
265
 
224
266
  put(url, body, options = {}) {
225
- return this.request(url, { method: 'PUT', body, ...options });
267
+ return this.request(url, { method: "PUT", body, ...options });
226
268
  }
227
269
 
228
270
  delete(url, options = {}) {
229
- return this.request(url, { method: 'DELETE', ...options });
271
+ return this.request(url, { method: "DELETE", ...options });
230
272
  }
231
273
 
232
274
  patch(url, body, options = {}) {
233
- return this.request(url, { method: 'PATCH', body, ...options });
275
+ return this.request(url, { method: "PATCH", body, ...options });
276
+ }
277
+
278
+ /**
279
+ * Сериализация тела (если не FormData)
280
+ */
281
+ _serializeBody(body) {
282
+ if (!body || body instanceof FormData) return undefined;
283
+ return JSON.stringify(body);
234
284
  }
235
285
  }
236
286
 
@@ -1,61 +1,146 @@
1
- import {isElement, mergeDeepObject} from "../functions";
1
+ import { isElement, mergeDeepObject } from "../functions";
2
2
  import EventHandler from "../dom/event";
3
3
 
4
4
  /**
5
- * Классы для анимаций смотрим здесь
6
- * https://animate.style/
5
+ * Анимация на основе Animate.css
6
+ * Поддерживает модули с событиях: show, shown, hide, hidden
7
7
  *
8
- * Работает с модулями у которых есть события show, hide, hidden
8
+ * @see https://animate.style/
9
9
  */
10
10
  class Animation {
11
- constructor(element, key, params = {}) {
12
- this._params = mergeDeepObject({
11
+ static get DEFAULTS() {
12
+ return {
13
13
  enable: false,
14
- in: 'animate__backInUp',
15
- out: 'animate__backOutUp',
16
- delay: 0,
17
- duration: 800,
18
- }, params);
14
+ in: 'animate__fadeIn', // Анимация при появлении
15
+ out: 'animate__fadeOut', // Анимация при скрытии
16
+ duration: 500, // Длительность анимации (мс)
17
+ };
18
+ }
19
+
20
+ constructor(element, key, userParams = {}) {
21
+ this._element = element;
22
+ this._nameKey = key;
23
+
24
+ // Объединение параметров
25
+ this._params = mergeDeepObject(Animation.DEFAULTS, userParams);
19
26
 
20
- this.classes = {
27
+ // Ранний выход, если анимация отключена или элемент не валиден
28
+ if (!this._params.enable || !isElement(element)) {
29
+ return;
30
+ }
31
+
32
+ this._classes = {
21
33
  animated: 'animate__animated',
22
- duration: 'animate__duration-' + this._params.duration
34
+ duration: 'animate__fast' // Используем классы animate.css
35
+ };
36
+
37
+ this._init();
38
+ }
39
+
40
+ /**
41
+ * Инициализация анимации
42
+ * @private
43
+ */
44
+ _init() {
45
+ const { classList } = this._element;
46
+
47
+ // Добавляем общие классы
48
+ classList.add(this._classes.animated, this._classes.duration);
49
+
50
+ // Удаляем стандартный класс 'fade', если используется animate.css
51
+ if (classList.contains('fade')) {
52
+ classList.remove('fade');
23
53
  }
24
54
 
25
- if (!this._params.enable) return;
26
- if (!isElement(element)) return;
55
+ // Назначаем обработчики событий
56
+ this._setupEventListeners();
57
+ }
27
58
 
28
- this._element = element;
29
- this._name_key = key;
59
+ /**
60
+ * Назначение обработчиков событий
61
+ * @private
62
+ */
63
+ _setupEventListeners() {
64
+ EventHandler.on(this._element, `${this._nameKey}.show`, this._handleShow.bind(this), null);
65
+ EventHandler.on(this._element, `${this._nameKey}.shown`, this._handleShown.bind(this), null);
66
+ EventHandler.on(this._element, `${this._nameKey}.hide`, this._handleHide.bind(this), null);
67
+ EventHandler.on(this._element, `${this._nameKey}.hidden`, this._handleHidden.bind(this), null);
68
+ }
69
+
70
+ /**
71
+ * Обработка события "show" — запуск анимации входа
72
+ * @private
73
+ */
74
+ _handleShow() {
75
+ const { classList } = this._element;
76
+ const { in: inClass, out: outClass } = this._params;
77
+
78
+ // Убираем выходную анимацию, если была
79
+ if (classList.contains(outClass)) {
80
+ classList.remove(outClass);
81
+ }
82
+
83
+ // Добавляем входную анимацию
84
+ classList.add(inClass);
85
+ }
86
+
87
+ /**
88
+ * Обработка события "shown" — завершение анимации входа
89
+ * @private
90
+ */
91
+ _handleShown() {
92
+ this._element.classList.add(this._classes.animated);
93
+ }
94
+
95
+ /**
96
+ * Обработка события "hide" — запуск анимации выхода
97
+ * @private
98
+ */
99
+ _handleHide() {
100
+ const { classList } = this._element;
101
+ const { in: inClass, out: outClass } = this._params;
102
+
103
+ // Убираем входную анимацию
104
+ if (classList.contains(inClass)) {
105
+ classList.remove(inClass);
106
+ }
107
+
108
+ // Добавляем выходную анимацию
109
+ classList.add(outClass);
110
+ }
30
111
 
31
- this._element.classList.add(this.classes.duration);
112
+ /**
113
+ * Обработка события "hidden" — очистка анимационных классов
114
+ * @private
115
+ */
116
+ _handleHidden() {
117
+ const { classList } = this._element;
118
+ const { in: inClass, out: outClass } = this._params;
32
119
 
33
- if (this._element.classList.contains('fade')) this._element.classList.remove('fade');
120
+ // Удаляем все анимационные классы animate.css
121
+ [...classList]
122
+ .filter(cls => cls.startsWith('animate__'))
123
+ .forEach(cls => classList.remove(cls));
34
124
 
35
- this._triggers();
125
+ // Восстанавливаем базовые классы для будущих анимаций
126
+ classList.add(this._classes.animated, this._classes.duration);
36
127
  }
37
128
 
38
- _triggers() {
39
- EventHandler.on(this._element, this._name_key + '.show', () => {
40
- this._element.classList.remove(this._params.out);
41
- this._element.classList.add(this._params.in);
42
- });
43
- EventHandler.on(this._element, this._name_key + '.shown', () => {
44
- this._element.classList.add(this.classes.animated);
45
- });
129
+ /**
130
+ * Уничтожение экземпляра (очистка событий)
131
+ */
132
+ dispose() {
133
+ EventHandler.off(this._element, `${this._nameKey}.show`, null, null);
134
+ EventHandler.off(this._element, `${this._nameKey}.shown`, null, null);
135
+ EventHandler.off(this._element, `${this._nameKey}.hide`, null, null);
136
+ EventHandler.off(this._element, `${this._nameKey}.hidden`, null, null);
46
137
 
47
- EventHandler.on(this._element, this._name_key + '.hide', () => {
48
- this._element.classList.remove(this._params.in);
49
- this._element.classList.add(this._params.out);
50
- });
138
+ // Очищаем анимационные классы
139
+ [...this._element.classList]
140
+ .filter(cls => cls.startsWith('animate__'))
141
+ .forEach(cls => this._element.classList.remove(cls));
51
142
 
52
- EventHandler.on(this._element, this._name_key + '.hidden', () => {
53
- [... this._element.classList].forEach((cl) => {
54
- if (cl.indexOf('animate__') !== -1) {
55
- this._element.classList.remove(cl);
56
- }
57
- })
58
- });
143
+ this._element = null;
59
144
  }
60
145
  }
61
146