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,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,179 +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
+ );
142
+ }
143
+
144
+ let data = { code: response.status };
145
+ if (isJson) {
146
+ data.response = response.json();
147
+ } else {
148
+ data.response = response.text();
113
149
  }
114
- const contentType = response.headers.get('content-type');
115
- if (contentType && contentType.includes('application/json')) {
116
- return response.json();
150
+
151
+ return data;
152
+ })
153
+ .then((data) => {
154
+ if (data && data.response instanceof Promise) {
155
+ data.response.then(
156
+ (resolved) => onSuccess({ ...data, response: resolved }),
157
+ () => {}
158
+ );
159
+ } else {
160
+ console.log(data)
161
+ onSuccess(data);
117
162
  }
118
- return response.text();
119
163
  })
120
- .then(data => onSuccess(data))
121
- .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
+ });
122
177
  }
123
178
 
124
179
  /**
125
- * Использование XHR (для FormData и прогресса)
180
+ * XHR-реализация (с прогрессом и AbortController)
126
181
  */
127
182
  _makeXHR({
128
183
  method,
129
184
  url,
130
185
  body,
131
186
  headers,
187
+ signal,
132
188
  onProgress,
133
189
  onSuccess,
134
190
  onError,
135
191
  onUploadStart,
136
- onUploadEnd
192
+ onUploadEnd,
137
193
  }) {
138
- return new Promise((resolve, reject) => {
139
- const xhr = new XMLHttpRequest();
194
+ const xhr = new XMLHttpRequest();
140
195
 
141
- xhr.open(method, url, true);
142
- xhr.withCredentials = this.withCredentials;
196
+ xhr.open(method, url, true);
197
+ xhr.withCredentials = this.withCredentials;
143
198
 
144
- // Устанавливаем только пользовательские заголовки (кроме Content-Type для FormData)
145
- Object.keys(headers).forEach(key => {
146
- if (key.toLowerCase() !== 'content-type' || !(body instanceof FormData)) {
147
- 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);
148
211
  }
149
212
  });
213
+ }
150
214
 
151
- // Отслеживание прогресса загрузки
152
- if (onProgress) {
153
- xhr.upload.addEventListener('progress', (e) => {
154
- if (e.lengthComputable) {
155
- const percent = (e.loaded / e.total) * 100;
156
- onProgress(percent, e);
157
- }
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}`,
158
227
  });
228
+ onError(error);
159
229
  }
230
+ onUploadEnd();
231
+ };
160
232
 
161
- xhr.onload = () => {
162
- if (xhr.status >= 200 && xhr.status < 300) {
163
- let data = {
164
- code: xhr.status,
165
- response: normalizeData(xhr.responseText)
166
- };
167
- onSuccess(data);
168
- resolve(data);
169
- } else {
170
- const error = new Error(`Ошибка ${xhr.status}: ${xhr.statusText}`);
171
- let data = {
172
- code: xhr.status,
173
- response: error
174
- }
175
- onError(data);
176
- reject(data);
177
- }
178
- onUploadEnd();
179
- };
233
+ xhr.onerror = () => {
234
+ onError(normalizeData({ code: 0, response: "Network Error" }));
235
+ onUploadEnd();
236
+ };
180
237
 
181
- xhr.onerror = () => {
182
- const error = new Error('Network Error');
183
- onError(error);
184
- reject(error);
185
- onUploadEnd();
186
- };
238
+ xhr.ontimeout = () => {
239
+ onError(normalizeData({ code: 0, response: "Request Timeout" }));
240
+ onUploadEnd();
241
+ };
187
242
 
188
- onUploadStart();
189
- xhr.send(body);
190
- });
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; // для отмены снаружи
191
254
  }
192
255
 
193
256
  // === Сокращённые методы ===
257
+
194
258
  get(url, options = {}) {
195
- return this.request(url, { method: 'GET', ...options });
259
+ return this.request(url, { method: "GET", ...options });
196
260
  }
197
261
 
198
262
  post(url, body, options = {}) {
199
- return this.request(url, { method: 'POST', body, ...options });
263
+ return this.request(url, { method: "POST", body, ...options });
200
264
  }
201
265
 
202
266
  put(url, body, options = {}) {
203
- return this.request(url, { method: 'PUT', body, ...options });
267
+ return this.request(url, { method: "PUT", body, ...options });
204
268
  }
205
269
 
206
270
  delete(url, options = {}) {
207
- return this.request(url, { method: 'DELETE', ...options });
271
+ return this.request(url, { method: "DELETE", ...options });
208
272
  }
209
273
 
210
274
  patch(url, body, options = {}) {
211
- 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);
212
284
  }
213
285
  }
214
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
 
@@ -1,7 +1,8 @@
1
- import {execute} from "../functions";
2
- import Selectors from "../dom/selectors";
1
+ import { execute } from "../functions";
3
2
  import EventHandler from "../dom/event";
4
- import Overflow from "./overflow";
3
+ import Html from "../components/templater";
4
+ import { Classes } from "../dom/manipulator";
5
+ import ScrollBarHelper from "./scrollbar";
5
6
 
6
7
  const NAME = 'backdrop';
7
8
  const CLASS_NAME = 'vg-backdrop';
@@ -9,50 +10,72 @@ const CLASS_NAME_FADE = 'fade';
9
10
  const CLASS_NAME_SHOW = 'show';
10
11
  const EVENT_MOUSEDOWN = `mousedown.vg.${NAME}`;
11
12
 
12
- let backdrop_delay = 500;
13
+ const backdropDelay = 150; // Уменьшено для более плавного UX
13
14
 
14
15
  class Backdrop {
16
+ static _rootEl = document.body;
17
+ static _scrollbar = new ScrollBarHelper();
18
+ static _backdrop = null;
19
+
20
+ /**
21
+ * Показывает бэкдроп
22
+ * @param {Function} callback - вызывается после отображения
23
+ */
15
24
  static show(callback) {
16
- Backdrop._append()
25
+ if (!this._backdrop) {
26
+ this._append();
27
+ }
28
+
17
29
  execute(callback);
18
30
  }
19
31
 
32
+ /**
33
+ * Скрывает бэкдроп
34
+ * @param {Function} callback - вызывается после скрытия
35
+ */
20
36
  static hide(callback) {
21
- Backdrop._destroy();
22
- execute(callback);
37
+ if (!this._backdrop) return;
38
+
39
+ this._destroy().then(execute.bind(null, callback));
23
40
  }
24
41
 
42
+ /**
43
+ * Создаёт и добавляет элемент бэкдропа
44
+ * @private
45
+ */
25
46
  static _append() {
26
- if (Selectors.find('.' + CLASS_NAME)) {
27
- return false;
28
- }
47
+ const html = Html('dom');
48
+ this._backdrop = html.div({ class: CLASS_NAME });
29
49
 
30
- let backdrop = document.createElement('div');
31
- backdrop.classList.add(CLASS_NAME);
32
-
33
- document.body.append(backdrop);
34
- backdrop.classList.add(CLASS_NAME_SHOW)
35
-
36
- setTimeout(() => {
37
- backdrop.classList.add(CLASS_NAME_FADE)
38
- }, 50);
50
+ this._rootEl.appendChild(this._backdrop);
51
+ requestAnimationFrame(() => {
52
+ Classes.add(this._backdrop, CLASS_NAME_SHOW);
53
+ setTimeout(() => {
54
+ Classes.add(this._backdrop, CLASS_NAME_FADE);
55
+ }, backdropDelay);
56
+ });
39
57
 
40
- EventHandler.on(backdrop, EVENT_MOUSEDOWN, () => {
41
- Backdrop.hide()
42
- Overflow.destroy();
58
+ EventHandler.on(this._backdrop, EVENT_MOUSEDOWN, () => {
59
+ this.hide();
43
60
  });
44
61
  }
45
62
 
63
+ /**
64
+ * Удаляет бэкдроп с анимацией
65
+ * @returns {Promise}
66
+ * @private
67
+ */
46
68
  static _destroy() {
47
- let element = Selectors.find('.' + CLASS_NAME);
48
- if (!element) return;
49
-
50
- element.classList.remove(CLASS_NAME_FADE);
51
-
52
- setTimeout(() => {
53
- element.classList.remove(CLASS_NAME_SHOW);
54
- element.remove();
55
- }, backdrop_delay);
69
+ return new Promise((resolve) => {
70
+ Classes.remove(this._backdrop, CLASS_NAME_FADE);
71
+ setTimeout(() => {
72
+ Classes.remove(this._backdrop, CLASS_NAME_SHOW);
73
+ this._backdrop.remove();
74
+ this._backdrop = null;
75
+ this._scrollbar.reset();
76
+ resolve();
77
+ }, backdropDelay);
78
+ });
56
79
  }
57
80
  }
58
81