vgapp 0.7.7 → 0.7.9

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 (33) hide show
  1. package/CHANGELOG.md +20 -4
  2. package/LICENSE +22 -0
  3. package/app/modules/base-module.js +62 -17
  4. package/app/modules/module-fn.js +10 -100
  5. package/app/modules/vgalert/js/vgalert.js +356 -214
  6. package/app/modules/vgalert/readme.md +242 -0
  7. package/app/modules/vgcollapse/js/vgcollapse.js +216 -62
  8. package/app/modules/vgcollapse/readme.md +56 -0
  9. package/app/modules/vgcollapse/scss/_variables.scss +5 -0
  10. package/app/modules/vgcollapse/scss/vgcollapse.scss +41 -0
  11. package/app/modules/vgdropdown/js/vgdropdown.js +104 -118
  12. package/app/modules/vgdropdown/scss/vgdropdown.scss +1 -2
  13. package/app/modules/vgformsender/js/hideshowpass.js +7 -4
  14. package/app/modules/vgformsender/js/vgformsender.js +343 -160
  15. package/app/modules/vgformsender/readme.md +250 -0
  16. package/app/modules/vgformsender/scss/vgformsender.scss +11 -3
  17. package/app/modules/vgnav/js/vgnav.js +98 -26
  18. package/app/modules/vgnav/scss/_placement.scss +8 -93
  19. package/app/modules/vgnav/scss/vgnav.scss +0 -1
  20. package/app/utils/js/components/ajax.js +237 -0
  21. package/app/utils/js/components/lang.js +108 -0
  22. package/app/utils/js/components/params.js +5 -0
  23. package/app/utils/js/components/placement.js +111 -108
  24. package/app/utils/js/components/templater.js +365 -33
  25. package/app/utils/js/functions.js +275 -143
  26. package/app/utils/scss/default.scss +1 -0
  27. package/app/utils/scss/placement.scss +72 -0
  28. package/app/utils/scss/variables.scss +10 -5
  29. package/build/vgapp.css +1 -1
  30. package/build/vgapp.css.map +1 -1
  31. package/index.scss +3 -0
  32. package/package.json +1 -1
  33. package/app/utils/js/components/alert.js +0 -8
@@ -0,0 +1,237 @@
1
+ import {mergeDeepObject, noop, normalizeData} from "../functions";
2
+
3
+ class Ajax {
4
+ /**
5
+ * Конфигурация запроса
6
+ * @param {Object} options
7
+ * @param {string} options.baseUrl - Базовый URL API (опционально)
8
+ * @param {Object} options.headers - Доп. заголовки (например, авторизация)
9
+ * @param {boolean} options.withCredentials - Отправлять ли куки (для авторизованных запросов)
10
+ * @param {string} options._token - Токен (авто-чтение из meta)
11
+ */
12
+ constructor(options = {}) {
13
+ this.baseUrl = options.baseUrl || '';
14
+ this.defaultHeaders = {
15
+ 'X-Requested-With': 'XMLHttpRequest',
16
+ ...options.headers
17
+ };
18
+ this.withCredentials = options.withCredentials || false;
19
+ this.csrfToken = options._token || this._getCsrfToken();
20
+ }
21
+
22
+ /**
23
+ * Получение csrf токена из тега meta
24
+ * @returns {string}
25
+ * @private
26
+ */
27
+ _getCsrfToken() {
28
+ const meta = document.querySelector('meta[name="csrf-token"]');
29
+ if (meta) return meta.getAttribute('content');
30
+ console.warn('CSRF-токен не найден в <meta name="csrf-token">');
31
+ return '';
32
+ }
33
+
34
+ /**
35
+ * Универсальный метод отправки запроса
36
+ * @param {string} url
37
+ * @param {Object} options
38
+ * @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)
42
+ * @param {Function} options.onSuccess
43
+ * @param {Function} options.onError
44
+ * @param {Function} options.onUploadStart
45
+ * @param {Function} options.onUploadEnd
46
+ */
47
+ request(url, {
48
+ method = 'GET',
49
+ body = null,
50
+ headers = {},
51
+ onProgress = null,
52
+ onSuccess = (data) => noop(),
53
+ onError = (error) => noop(),
54
+ onUploadStart = () => {},
55
+ onUploadEnd = () => {}
56
+ } = {}) {
57
+ const fullUrl = this.baseUrl + url;
58
+ const isFormData = body instanceof FormData;
59
+ const requestHeaders = { ...this.defaultHeaders, ...headers };
60
+ const token = {};
61
+
62
+ if (!isFormData && this.csrfToken) {
63
+ token.body = JSON.stringify({
64
+ _token: this.csrfToken
65
+ })
66
+ }
67
+
68
+ // Для JSON устанавливаем заголовок, для FormData — НЕЛЬЗЯ
69
+ if (!isFormData && !('Content-Type' in headers)) {
70
+ requestHeaders['Content-Type'] = 'application/json';
71
+ }
72
+
73
+ // Если это GET-запрос — тело игнорируется
74
+ if (method.toUpperCase() === 'GET') {
75
+ return this._makeFetch(fullUrl, {
76
+ method,
77
+ headers: requestHeaders,
78
+ withCredentials: this.withCredentials
79
+ }, onSuccess, onError);
80
+ }
81
+
82
+ // Для FormData — используем XMLHttpRequest, чтобы отслеживать прогресс
83
+ if (isFormData || onProgress) {
84
+ return this._makeXHR({
85
+ method,
86
+ url: fullUrl,
87
+ body,
88
+ headers: requestHeaders,
89
+ onProgress,
90
+ onSuccess,
91
+ onError,
92
+ onUploadStart,
93
+ onUploadEnd
94
+ });
95
+ }
96
+
97
+ // Остальные случаи — fetch
98
+ return this._makeFetch(fullUrl, mergeDeepObject({
99
+ method,
100
+ headers: requestHeaders,
101
+ withCredentials: this.withCredentials
102
+ }, token), onSuccess, onError);
103
+ }
104
+
105
+ /**
106
+ * Использование fetch (для JSON)
107
+ */
108
+ _makeFetch(url, config, onSuccess, onError) {
109
+ return fetch(url, config)
110
+ .then(response => {
111
+ if (!response.ok) {
112
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
113
+ }
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
+ };
120
+ }
121
+
122
+ return {
123
+ code: response.status,
124
+ response: response.text()
125
+ };
126
+ })
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
+ }
139
+ } else {
140
+ onSuccess(data)
141
+ }
142
+ })
143
+ .catch(error => onError(error));
144
+ }
145
+
146
+ /**
147
+ * Использование XHR (для FormData и прогресса)
148
+ */
149
+ _makeXHR({
150
+ method,
151
+ url,
152
+ body,
153
+ headers,
154
+ onProgress,
155
+ onSuccess,
156
+ onError,
157
+ onUploadStart,
158
+ onUploadEnd
159
+ }) {
160
+ return new Promise((resolve, reject) => {
161
+ const xhr = new XMLHttpRequest();
162
+
163
+ xhr.open(method, url, true);
164
+ xhr.withCredentials = this.withCredentials;
165
+
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]);
170
+ }
171
+ });
172
+
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
+ }
180
+ });
181
+ }
182
+
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
+ };
202
+
203
+ xhr.onerror = () => {
204
+ const error = new Error('Network Error');
205
+ onError(error);
206
+ reject(error);
207
+ onUploadEnd();
208
+ };
209
+
210
+ onUploadStart();
211
+ xhr.send(body);
212
+ });
213
+ }
214
+
215
+ // === Сокращённые методы ===
216
+ get(url, options = {}) {
217
+ return this.request(url, { method: 'GET', ...options });
218
+ }
219
+
220
+ post(url, body, options = {}) {
221
+ return this.request(url, { method: 'POST', body, ...options });
222
+ }
223
+
224
+ put(url, body, options = {}) {
225
+ return this.request(url, { method: 'PUT', body, ...options });
226
+ }
227
+
228
+ delete(url, options = {}) {
229
+ return this.request(url, { method: 'DELETE', ...options });
230
+ }
231
+
232
+ patch(url, body, options = {}) {
233
+ return this.request(url, { method: 'PATCH', body, ...options });
234
+ }
235
+ }
236
+
237
+ export default Ajax;
@@ -0,0 +1,108 @@
1
+ import {normalizeData} from "../functions";
2
+
3
+ const langs = {
4
+ ru: {
5
+ messages: {
6
+ errors: {
7
+ went_wrong: 'Что-то пошло не так, повторите позже',
8
+ "400": 'Неверный запрос',
9
+ "401": 'Не авторизован',
10
+ "403": 'Запрещено',
11
+ "404": 'Не найдено',
12
+ "413": 'Слишком большой запрос',
13
+ "419": 'Проблемы с токеном CSRF',
14
+ "422": 'Неверный запрос',
15
+ "500": 'Внутренняя ошибка сервера',
16
+ "504": 'Превышено время ожидания'
17
+ },
18
+ 'form-sender': {
19
+ 'bootstrap_not_found': 'VGApp не удалось найти bootstrap, модалки не будут закрыты, попробуйте сделать это через коллбек afterSend.'
20
+ },
21
+ alert: {
22
+ title: 'Заголовок по умолчанию',
23
+ description: 'Описание текущего действия',
24
+ reason: 'Алерт уже открыт'
25
+ }
26
+ },
27
+ titles: {
28
+ errors: {
29
+ title: 'Ошибка',
30
+ titles: 'Ошибки'
31
+ }
32
+ },
33
+ buttons: {
34
+ alert: {
35
+ agree: 'Да, согласен',
36
+ cancel: 'Отмена'
37
+ }
38
+ }
39
+ },
40
+ en: {
41
+ messages: {
42
+ errors: {
43
+ went_wrong: 'Something went wrong, please repeat later',
44
+ "400": 'Bad Request',
45
+ "401": 'Unauthorized',
46
+ "403": 'Forbidden',
47
+ "404": 'Not Found',
48
+ "413": 'Payload Too Large',
49
+ "419": 'Problems with the CSRF token',
50
+ "422": 'Unprocessable Entity',
51
+ "500": 'Internal Server Error',
52
+ "504": 'Gateway Timeout'
53
+ },
54
+ 'form-sender': {
55
+ 'bootstrap_not_found': 'VGApp could not find bootstrap, the modals will not be closed, try to do this through the afterSend callback.'
56
+ },
57
+ alert: {
58
+ title: 'Default header',
59
+ description: 'Description of the current action',
60
+ reason: 'Alert already open'
61
+ }
62
+ },
63
+ titles: {
64
+ errors: {
65
+ title: 'Error',
66
+ titles: 'Errors'
67
+ }
68
+ },
69
+ buttons: {
70
+ alert: {
71
+ agree: 'Yeah, I agree',
72
+ cancel: 'Cancel'
73
+ }
74
+ }
75
+ },
76
+ };
77
+
78
+ class Lang {
79
+ constructor(lang = 'en') {
80
+ this.lang = lang;
81
+ }
82
+
83
+ get() {
84
+ let data = langs[this.lang];
85
+
86
+ if (!data) data = langs['en'];
87
+
88
+ return normalizeData(data);
89
+ }
90
+ }
91
+
92
+ function lang(lg, mode, module) {
93
+ return new Lang(lg).get()[mode][module];
94
+ }
95
+
96
+ function lang_titles(lg, module) {
97
+ return lang(lg, 'titles', module) || {};
98
+ }
99
+
100
+ function lang_messages(lg, module) {
101
+ return lang(lg, 'messages', module) || {};
102
+ }
103
+
104
+ function lang_buttons(lg, module) {
105
+ return lang(lg, 'buttons', module) || {};
106
+ }
107
+
108
+ export {lang, lang_messages, lang_titles, lang_buttons};
@@ -37,6 +37,11 @@ class Params {
37
37
  }
38
38
 
39
39
  for (let key in mParams) {
40
+ if ('params' in mParams) {
41
+ mParams = mergeDeepObject(mParams, mParams.params);
42
+ delete mParams.params;
43
+ }
44
+
40
45
  if (key.indexOf('-') !== -1) {
41
46
  mParams = stringToNestedObjectWithValue(key, mParams[key], mParams);
42
47
  delete mParams[key];
@@ -3,138 +3,141 @@ import {Classes} from "../dom/manipulator";
3
3
 
4
4
  /**
5
5
  * Класс Placement, определяет и устанавливает местоположение элемента на странице.
6
- * TODO класс не дописан, не определяет сверху и снизу
7
6
  */
8
7
 
9
- const CLASS_NAME_RIGHT = 'right';
10
- const CLASS_NAME_LEFT = 'left';
11
- const CLASS_NAME_TOP = 'top';
12
- const CLASS_NAME_BOTTOM = 'bottom';
13
-
14
8
  class Placement {
15
- constructor(arg = {}) {
16
- this.params = mergeDeepObject({
17
- element: null,
18
- drop: null
19
- }, arg);
20
-
21
- this._drop = null;
22
- this.drop = this.params.drop;
23
-
24
- this._element = null;
25
- this.element = this.params.element;
26
-
27
- if (!this.drop) return false;
28
- }
29
-
30
- get drop() {
31
- return this._drop;
32
- }
33
-
34
- set drop(el) {
35
- if (!el) return;
36
- this._drop = el;
9
+ constructor(config) {
10
+ this.reference = config.reference;
11
+ this.drop = config.drop;
12
+ this.offset = config.offset || [0, 0];
13
+ this.boundary = config.boundary || 'viewport';
14
+ this.autoFlip = config.autoFlip !== false;
15
+ this.overflowProtection = config.overflowProtection !== false;
16
+ this.placement = config.placement || 'bottom';
17
+ this.fallbackPlacements = config.fallbackPlacements || [];
18
+
19
+ this._builtInPlacements = {
20
+ top: 'top',
21
+ 'top-start': 'top-start',
22
+ 'top-end': 'top-end',
23
+ bottom: 'bottom',
24
+ 'bottom-start': 'bottom-start',
25
+ 'bottom-end': 'bottom-end',
26
+ left: 'left',
27
+ 'left-start': 'left-start',
28
+ 'left-end': 'left-end',
29
+ right: 'right',
30
+ 'right-start': 'right-start',
31
+ 'right-end': 'right-end'
32
+ };
37
33
  }
38
34
 
39
- get element() {
40
- return this._element;
35
+ _getPlacementRect(element) {
36
+ return element.getBoundingClientRect();
41
37
  }
42
38
 
43
- set element(el) {
44
- if (!el) {
45
- if (this.drop) {
46
- this._element = this.drop.parentNode;
47
- }
48
- }
49
-
50
- this._element = el;
39
+ _getViewportRect() {
40
+ const doc = document.documentElement;
41
+ return {
42
+ width: doc.clientWidth,
43
+ height: doc.clientHeight,
44
+ left: 0,
45
+ top: 0,
46
+ right: doc.clientWidth,
47
+ bottom: doc.clientHeight
48
+ };
51
49
  }
52
50
 
53
- _setPlacement() {
54
- let rect = this._isElementInViewport(this.drop);
55
-
56
- if (!rect.isView) {
57
- if (!rect.isViewRight) {
58
- Classes.remove(this.drop, CLASS_NAME_LEFT);
59
- Classes.add(this.drop, CLASS_NAME_RIGHT);
60
- }
51
+ _getOverflowConstraints() {
52
+ const refRect = this._getPlacementRect(this.reference);
53
+ const dropRect = this._getPlacementRect(this.drop);
54
+ const viewRect = this._getViewportRect();
55
+ const [xOffset, yOffset] = this.offset;
61
56
 
62
- if (!rect.isViewLeft) {
63
- Classes.remove(this.drop, CLASS_NAME_RIGHT);
64
- Classes.add(this.drop, CLASS_NAME_LEFT);
65
- }
57
+ let placement = this.placement;
66
58
 
67
- if (!rect.isViewTop) {
68
- Classes.remove(this.drop, CLASS_NAME_BOTTOM);
69
- Classes.add(this.drop, CLASS_NAME_TOP);
70
- }
59
+ if (this.overflowProtection) {
60
+ const fallbacks = [this.placement, ...this.fallbackPlacements];
61
+ let best = null;
71
62
 
72
- if (!rect.isViewBottom) {
73
- Classes.remove(this.drop, CLASS_NAME_TOP);
74
- Classes.add(this.drop, CLASS_NAME_BOTTOM);
75
- }
76
- }
77
- }
63
+ for (let p of fallbacks) {
64
+ let pos = this._calculatePosition(p, refRect, dropRect, xOffset, yOffset);
65
+ let overflow = this._calculateOverflow(pos, viewRect);
78
66
 
79
- _getPlacement() {
80
- const _this = this;
81
- const _parent = (self) => {
82
- let parent = self.parentNode,
83
- overflow = getComputedStyle(parent).overflow;
84
-
85
- if (parent.tagName !== 'BODY') {
86
- if (overflow === 'visible') {
87
- _parent(parent)
88
- } else {
89
- return parent;
67
+ if (!best || overflow < best.overflow) {
68
+ best = { placement: p, position: pos, overflow };
69
+ if (overflow === 0) break;
90
70
  }
91
- } else {
92
- return null;
93
71
  }
94
- }
95
-
96
- let isFixed = false, top, left,
97
- bounds = _this.params.drop.getBoundingClientRect(),
98
- parent = _this.params.element.getBoundingClientRect();
99
72
 
100
- if (_parent(_this.params.element)) {
101
- isFixed = true;
102
- top = bounds.top;
103
- left = bounds.left;
73
+ placement = best.placement;
74
+ this._setStyles(best.position);
104
75
  } else {
105
- let styles = getComputedStyle(_this.params.drop);
106
- top = normalizeData(styles.top.slice(0, -2));
107
- left = normalizeData(styles.left.slice(0, -2));
76
+ const pos = this._calculatePosition(placement, refRect, dropRect, xOffset, yOffset);
77
+ this._setStyles(pos);
108
78
  }
109
79
 
110
- if ((bounds.left + bounds.width) > window.innerWidth) {
111
- left = parent.width - bounds.width;
112
- }
80
+ this.drop.setAttribute('data-vg-placement', placement);
81
+ }
113
82
 
114
- return {
115
- isFixed: isFixed,
116
- top: top,
117
- left: left
83
+ _calculatePosition(placement, refRect, dropRect, xOffset = 0, yOffset = 0) {
84
+ let top, left;
85
+
86
+ switch (placement) {
87
+ case 'top':
88
+ case 'top-start':
89
+ top = refRect.top - dropRect.height - yOffset;
90
+ left = placement === 'top-start' ? refRect.left + xOffset : refRect.left + (refRect.width - dropRect.width) / 2;
91
+ break;
92
+ case 'top-end':
93
+ top = refRect.top - dropRect.height - yOffset;
94
+ left = refRect.right - dropRect.width - xOffset;
95
+ break;
96
+ case 'bottom':
97
+ case 'bottom-start':
98
+ top = refRect.bottom + yOffset;
99
+ left = placement === 'bottom-start' ? refRect.left + xOffset : refRect.left + (refRect.width - dropRect.width) / 2;
100
+ break;
101
+ case 'bottom-end':
102
+ top = refRect.bottom + yOffset;
103
+ left = refRect.right - dropRect.width - xOffset;
104
+ break;
105
+ case 'left':
106
+ top = refRect.top + (refRect.height - dropRect.height) / 2;
107
+ left = refRect.left - dropRect.width - xOffset;
108
+ break;
109
+ case 'right':
110
+ top = refRect.top + (refRect.height - dropRect.height) / 2;
111
+ left = refRect.right + xOffset;
112
+ break;
113
+ default:
114
+ top = refRect.bottom + yOffset;
115
+ left = refRect.left + xOffset;
118
116
  }
117
+
118
+ return { top, left };
119
119
  }
120
120
 
121
- _isElementInViewport(element) {
122
- const rect = element.getBoundingClientRect();
123
- const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
124
- const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
121
+ _calculateOverflow(pos, viewRect) {
122
+ let overflow = 0;
123
+ if (pos.left < viewRect.left) overflow += viewRect.left - pos.left;
124
+ if (pos.top < viewRect.top) overflow += viewRect.top - pos.top;
125
+ if (pos.left + this.drop.offsetWidth > viewRect.right) overflow += (pos.left + this.drop.offsetWidth) - viewRect.right;
126
+ if (pos.top + this.drop.offsetHeight > viewRect.bottom) overflow += (pos.top + this.drop.offsetHeight) - viewRect.bottom;
127
+ return overflow;
128
+ }
125
129
 
126
- return {
127
- isView: (
128
- rect.top >= 0 &&
129
- rect.left >= 0 &&
130
- rect.bottom <= viewportHeight &&
131
- rect.right <= viewportWidth
132
- ),
133
- isViewRight: rect.right <= viewportWidth,
134
- isViewLeft: rect.left >= 0,
135
- isViewTop: rect.top >= 0,
136
- isViewBottom: rect.bottom <= viewportHeight,
137
- };
130
+ _setStyles(pos) {
131
+ mergeDeepObject(this.drop.style, {
132
+ position: 'absolute',
133
+ top: `${pos.top}px`,
134
+ left: `${pos.left}px`,
135
+ margin: 0
136
+ })
137
+ }
138
+
139
+ _setPlacement() {
140
+ this._getOverflowConstraints();
138
141
  }
139
142
  }
140
143