@sequent-org/moodboard 1.0.17 → 1.0.19
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.
package/package.json
CHANGED
package/src/core/index.js
CHANGED
|
@@ -52,8 +52,14 @@ export class CoreMoodBoard {
|
|
|
52
52
|
this.saveManager = new SaveManager(this.eventBus, this.options);
|
|
53
53
|
this.history = new HistoryManager(this.eventBus);
|
|
54
54
|
this.apiClient = new ApiClient();
|
|
55
|
-
this.imageUploadService = new ImageUploadService(this.apiClient
|
|
56
|
-
|
|
55
|
+
this.imageUploadService = new ImageUploadService(this.apiClient, {
|
|
56
|
+
requireCsrf: this.options.requireCsrf !== false, // По умолчанию требуем CSRF
|
|
57
|
+
csrfToken: this.options.csrfToken
|
|
58
|
+
});
|
|
59
|
+
this.fileUploadService = new FileUploadService(this.apiClient, {
|
|
60
|
+
requireCsrf: this.options.requireCsrf !== false, // По умолчанию требуем CSRF
|
|
61
|
+
csrfToken: this.options.csrfToken
|
|
62
|
+
});
|
|
57
63
|
|
|
58
64
|
// Связываем SaveManager с ApiClient для правильной обработки изображений
|
|
59
65
|
this.saveManager.setApiClient(this.apiClient);
|
|
@@ -2,17 +2,54 @@
|
|
|
2
2
|
* Сервис для загрузки и управления файлами на сервере
|
|
3
3
|
*/
|
|
4
4
|
export class FileUploadService {
|
|
5
|
-
constructor(apiClient) {
|
|
5
|
+
constructor(apiClient, options = {}) {
|
|
6
6
|
this.apiClient = apiClient;
|
|
7
7
|
this.uploadEndpoint = '/api/files/upload';
|
|
8
8
|
this.deleteEndpoint = '/api/files';
|
|
9
|
+
this.options = {
|
|
10
|
+
csrfToken: null, // Можно передать токен напрямую
|
|
11
|
+
csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
|
|
12
|
+
requireCsrf: true, // Требовать ли CSRF токен
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Получает CSRF токен из различных источников
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
_getCsrfToken() {
|
|
22
|
+
// 1. Сначала проверяем токен, переданный в опциях
|
|
23
|
+
if (this.options.csrfToken) {
|
|
24
|
+
return this.options.csrfToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Ищем токен в DOM
|
|
28
|
+
if (typeof document !== 'undefined') {
|
|
29
|
+
const tokenElement = document.querySelector(this.options.csrfTokenSelector);
|
|
30
|
+
if (tokenElement) {
|
|
31
|
+
return tokenElement.getAttribute('content');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Проверяем глобальную переменную (для тестирования)
|
|
36
|
+
if (typeof window !== 'undefined' && window.csrfToken) {
|
|
37
|
+
return window.csrfToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 4. Если CSRF не требуется, возвращаем null
|
|
41
|
+
if (!this.options.requireCsrf) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
9
46
|
}
|
|
10
47
|
|
|
11
48
|
/**
|
|
12
49
|
* Загружает файл на сервер
|
|
13
|
-
* @param {File|Blob} file - файл
|
|
50
|
+
* @param {File|Blob} file - файл для загрузки
|
|
14
51
|
* @param {string} name - имя файла
|
|
15
|
-
* @returns {Promise<{id: string, url: string, size: number,
|
|
52
|
+
* @returns {Promise<{id: string, url: string, size: number, name: string}>}
|
|
16
53
|
*/
|
|
17
54
|
async uploadFile(file, name = null) {
|
|
18
55
|
try {
|
|
@@ -22,18 +59,24 @@ export class FileUploadService {
|
|
|
22
59
|
formData.append('name', name || file.name || 'file');
|
|
23
60
|
|
|
24
61
|
// Получаем CSRF токен
|
|
25
|
-
const csrfToken =
|
|
62
|
+
const csrfToken = this._getCsrfToken();
|
|
26
63
|
|
|
27
|
-
if (!csrfToken) {
|
|
28
|
-
throw new Error('CSRF токен не
|
|
64
|
+
if (this.options.requireCsrf && !csrfToken) {
|
|
65
|
+
throw new Error('CSRF токен не найден. Добавьте <meta name="csrf-token" content="{{ csrf_token() }}"> в HTML или передайте токен в опциях.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const headers = {
|
|
69
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Добавляем CSRF токен только если он есть
|
|
73
|
+
if (csrfToken) {
|
|
74
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
29
75
|
}
|
|
30
76
|
|
|
31
77
|
const response = await fetch(this.uploadEndpoint, {
|
|
32
78
|
method: 'POST',
|
|
33
|
-
headers
|
|
34
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
35
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
36
|
-
},
|
|
79
|
+
headers,
|
|
37
80
|
credentials: 'same-origin',
|
|
38
81
|
body: formData
|
|
39
82
|
});
|
|
@@ -54,9 +97,8 @@ export class FileUploadService {
|
|
|
54
97
|
fileId: result.data.fileId || result.data.id, // Добавляем fileId для явного доступа
|
|
55
98
|
url: result.data.url,
|
|
56
99
|
size: result.data.size,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
name: result.data.name
|
|
100
|
+
name: result.data.name,
|
|
101
|
+
type: result.data.type
|
|
60
102
|
};
|
|
61
103
|
|
|
62
104
|
} catch (error) {
|
|
@@ -73,20 +115,26 @@ export class FileUploadService {
|
|
|
73
115
|
*/
|
|
74
116
|
async updateFileMetadata(fileId, metadata) {
|
|
75
117
|
try {
|
|
76
|
-
const csrfToken =
|
|
118
|
+
const csrfToken = this._getCsrfToken();
|
|
77
119
|
|
|
78
|
-
if (!csrfToken) {
|
|
79
|
-
throw new Error('CSRF токен не
|
|
120
|
+
if (this.options.requireCsrf && !csrfToken) {
|
|
121
|
+
throw new Error('CSRF токен не найден. Добавьте <meta name="csrf-token" content="{{ csrf_token() }}"> в HTML или передайте токен в опциях.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headers = {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
127
|
+
'Accept': 'application/json'
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Добавляем CSRF токен только если он есть
|
|
131
|
+
if (csrfToken) {
|
|
132
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
83
136
|
method: 'PUT',
|
|
84
|
-
headers
|
|
85
|
-
'Content-Type': 'application/json',
|
|
86
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
87
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
88
|
-
'Accept': 'application/json'
|
|
89
|
-
},
|
|
137
|
+
headers,
|
|
90
138
|
credentials: 'same-origin',
|
|
91
139
|
body: JSON.stringify(metadata)
|
|
92
140
|
});
|
|
@@ -116,15 +164,21 @@ export class FileUploadService {
|
|
|
116
164
|
*/
|
|
117
165
|
async deleteFile(fileId) {
|
|
118
166
|
try {
|
|
119
|
-
const csrfToken =
|
|
167
|
+
const csrfToken = this._getCsrfToken();
|
|
120
168
|
|
|
169
|
+
const headers = {
|
|
170
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
171
|
+
'Accept': 'application/json'
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Добавляем CSRF токен только если он есть
|
|
175
|
+
if (csrfToken) {
|
|
176
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
177
|
+
}
|
|
178
|
+
|
|
121
179
|
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
122
180
|
method: 'DELETE',
|
|
123
|
-
headers
|
|
124
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
125
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
126
|
-
'Accept': 'application/json'
|
|
127
|
-
},
|
|
181
|
+
headers,
|
|
128
182
|
credentials: 'same-origin'
|
|
129
183
|
});
|
|
130
184
|
|
|
@@ -318,15 +372,21 @@ export class FileUploadService {
|
|
|
318
372
|
*/
|
|
319
373
|
async cleanupUnusedFiles() {
|
|
320
374
|
try {
|
|
321
|
-
const csrfToken =
|
|
375
|
+
const csrfToken = this._getCsrfToken();
|
|
322
376
|
|
|
377
|
+
const headers = {
|
|
378
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
379
|
+
'Accept': 'application/json'
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Добавляем CSRF токен только если он есть
|
|
383
|
+
if (csrfToken) {
|
|
384
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
385
|
+
}
|
|
386
|
+
|
|
323
387
|
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
324
388
|
method: 'POST',
|
|
325
|
-
headers
|
|
326
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
327
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
328
|
-
'Accept': 'application/json'
|
|
329
|
-
},
|
|
389
|
+
headers,
|
|
330
390
|
credentials: 'same-origin'
|
|
331
391
|
});
|
|
332
392
|
|
|
@@ -2,10 +2,47 @@
|
|
|
2
2
|
* Сервис для загрузки и управления изображениями на сервере
|
|
3
3
|
*/
|
|
4
4
|
export class ImageUploadService {
|
|
5
|
-
constructor(apiClient) {
|
|
5
|
+
constructor(apiClient, options = {}) {
|
|
6
6
|
this.apiClient = apiClient;
|
|
7
7
|
this.uploadEndpoint = '/api/images/upload';
|
|
8
8
|
this.deleteEndpoint = '/api/images';
|
|
9
|
+
this.options = {
|
|
10
|
+
csrfToken: null, // Можно передать токен напрямую
|
|
11
|
+
csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
|
|
12
|
+
requireCsrf: true, // Требовать ли CSRF токен
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Получает CSRF токен из различных источников
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
_getCsrfToken() {
|
|
22
|
+
// 1. Сначала проверяем токен, переданный в опциях
|
|
23
|
+
if (this.options.csrfToken) {
|
|
24
|
+
return this.options.csrfToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Ищем токен в DOM
|
|
28
|
+
if (typeof document !== 'undefined') {
|
|
29
|
+
const tokenElement = document.querySelector(this.options.csrfTokenSelector);
|
|
30
|
+
if (tokenElement) {
|
|
31
|
+
return tokenElement.getAttribute('content');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Проверяем глобальную переменную (для тестирования)
|
|
36
|
+
if (typeof window !== 'undefined' && window.csrfToken) {
|
|
37
|
+
return window.csrfToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 4. Если CSRF не требуется, возвращаем null
|
|
41
|
+
if (!this.options.requireCsrf) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
9
46
|
}
|
|
10
47
|
|
|
11
48
|
/**
|
|
@@ -27,18 +64,24 @@ export class ImageUploadService {
|
|
|
27
64
|
formData.append('height', dimensions.height.toString());
|
|
28
65
|
|
|
29
66
|
// Получаем CSRF токен
|
|
30
|
-
const csrfToken =
|
|
67
|
+
const csrfToken = this._getCsrfToken();
|
|
31
68
|
|
|
32
|
-
if (!csrfToken) {
|
|
33
|
-
throw new Error('CSRF токен не
|
|
69
|
+
if (this.options.requireCsrf && !csrfToken) {
|
|
70
|
+
throw new Error('CSRF токен не найден. Добавьте <meta name="csrf-token" content="{{ csrf_token() }}"> в HTML или передайте токен в опциях.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const headers = {
|
|
74
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Добавляем CSRF токен только если он есть
|
|
78
|
+
if (csrfToken) {
|
|
79
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
34
80
|
}
|
|
35
81
|
|
|
36
82
|
const response = await fetch(this.uploadEndpoint, {
|
|
37
83
|
method: 'POST',
|
|
38
|
-
headers
|
|
39
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
40
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
41
|
-
},
|
|
84
|
+
headers,
|
|
42
85
|
credentials: 'same-origin',
|
|
43
86
|
body: formData
|
|
44
87
|
});
|
|
@@ -87,15 +130,21 @@ export class ImageUploadService {
|
|
|
87
130
|
*/
|
|
88
131
|
async deleteImage(imageId) {
|
|
89
132
|
try {
|
|
90
|
-
const csrfToken =
|
|
133
|
+
const csrfToken = this._getCsrfToken();
|
|
91
134
|
|
|
135
|
+
const headers = {
|
|
136
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
137
|
+
'Accept': 'application/json'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Добавляем CSRF токен только если он есть
|
|
141
|
+
if (csrfToken) {
|
|
142
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
|
|
93
146
|
method: 'DELETE',
|
|
94
|
-
headers
|
|
95
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
96
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
97
|
-
'Accept': 'application/json'
|
|
98
|
-
},
|
|
147
|
+
headers,
|
|
99
148
|
credentials: 'same-origin'
|
|
100
149
|
});
|
|
101
150
|
|
|
@@ -119,15 +168,21 @@ export class ImageUploadService {
|
|
|
119
168
|
*/
|
|
120
169
|
async cleanupUnusedImages() {
|
|
121
170
|
try {
|
|
122
|
-
const csrfToken =
|
|
171
|
+
const csrfToken = this._getCsrfToken();
|
|
123
172
|
|
|
173
|
+
const headers = {
|
|
174
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
175
|
+
'Accept': 'application/json'
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Добавляем CSRF токен только если он есть
|
|
179
|
+
if (csrfToken) {
|
|
180
|
+
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
181
|
+
}
|
|
182
|
+
|
|
124
183
|
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
125
184
|
method: 'POST',
|
|
126
|
-
headers
|
|
127
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
128
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
129
|
-
'Accept': 'application/json'
|
|
130
|
-
},
|
|
185
|
+
headers,
|
|
131
186
|
credentials: 'same-origin'
|
|
132
187
|
});
|
|
133
188
|
|
|
@@ -524,16 +524,16 @@
|
|
|
524
524
|
line-height: 1.4;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
/* Topbar */
|
|
527
|
+
/* Topbar Styles */
|
|
528
528
|
.moodboard-topbar {
|
|
529
529
|
position: absolute;
|
|
530
|
-
top:
|
|
531
|
-
left:
|
|
532
|
-
|
|
533
|
-
display:
|
|
530
|
+
top: 16px;
|
|
531
|
+
left: 50%;
|
|
532
|
+
transform: translateX(-50%);
|
|
533
|
+
display: flex;
|
|
534
534
|
align-items: center;
|
|
535
535
|
gap: 8px;
|
|
536
|
-
padding: 8px
|
|
536
|
+
padding: 8px 12px;
|
|
537
537
|
background: #fff;
|
|
538
538
|
border: 1px solid #e0e0e0;
|
|
539
539
|
border-radius: 10px;
|
|
@@ -542,44 +542,53 @@
|
|
|
542
542
|
pointer-events: auto;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
.moodboard-topbar--dark {
|
|
546
|
+
background: #2a2a2a;
|
|
547
|
+
border-color: #444;
|
|
548
|
+
color: #fff;
|
|
549
|
+
}
|
|
550
|
+
|
|
545
551
|
.moodboard-topbar__button {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
justify-content: center;
|
|
549
|
-
width: 38px;
|
|
550
|
-
height: 38px;
|
|
552
|
+
width: 36px;
|
|
553
|
+
height: 36px;
|
|
551
554
|
border: none;
|
|
552
555
|
border-radius: 8px;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
font-size: 16px;
|
|
556
|
+
background: #f0f0f0;
|
|
557
|
+
color: #333;
|
|
556
558
|
cursor: pointer;
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
justify-content: center;
|
|
557
562
|
transition: all 0.2s ease;
|
|
558
563
|
}
|
|
559
564
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
width: 16px;
|
|
563
|
-
height: 16px;
|
|
564
|
-
/* fill: currentColor;
|
|
565
|
-
stroke: currentColor; */
|
|
566
|
-
transition: all 0.2s ease;
|
|
565
|
+
.moodboard-topbar__button:hover {
|
|
566
|
+
background: #e6e6e6;
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
-
.moodboard-topbar__button
|
|
570
|
-
|
|
569
|
+
.moodboard-topbar__button--active {
|
|
570
|
+
background: #007ACC;
|
|
571
|
+
color: #fff;
|
|
571
572
|
}
|
|
572
573
|
|
|
573
|
-
.moodboard-topbar__button
|
|
574
|
+
.moodboard-topbar__button--paint {
|
|
575
|
+
background: #f0f0f0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.moodboard-topbar__button--paint:hover {
|
|
574
579
|
background: #e6e6e6;
|
|
575
580
|
}
|
|
576
581
|
|
|
577
|
-
/* Divider for topbar */
|
|
578
582
|
.moodboard-topbar__divider {
|
|
579
583
|
width: 1px;
|
|
580
|
-
height:
|
|
581
|
-
background: #
|
|
582
|
-
margin: 0
|
|
584
|
+
height: 24px;
|
|
585
|
+
background: #e0e0e0;
|
|
586
|
+
margin: 0 4px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.moodboard-topbar svg {
|
|
590
|
+
width: 18px;
|
|
591
|
+
height: 18px;
|
|
583
592
|
}
|
|
584
593
|
|
|
585
594
|
/* Paint popover */
|
|
@@ -9,21 +9,69 @@ export class TopbarIconLoader {
|
|
|
9
9
|
|
|
10
10
|
async init() {
|
|
11
11
|
try {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
this.loadBuiltInIcons();
|
|
12
|
+
// Загружаем иконки из файлов в папке topbar
|
|
13
|
+
await this.loadTopbarIcons();
|
|
15
14
|
|
|
16
15
|
console.log('✅ Иконки верхней панели загружены успешно');
|
|
17
16
|
|
|
18
17
|
} catch (error) {
|
|
19
18
|
console.error('❌ Критическая ошибка загрузки иконок верхней панели:', error);
|
|
20
|
-
//
|
|
19
|
+
// В случае ошибки загружаем встроенные иконки как fallback
|
|
21
20
|
this.loadBuiltInIcons();
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
//
|
|
24
|
+
async loadTopbarIcons() {
|
|
25
|
+
// Список иконок, которые нужно загрузить
|
|
26
|
+
const iconNames = ['grid-line', 'grid-dot', 'grid-cross', 'grid-off', 'paint'];
|
|
27
|
+
|
|
28
|
+
for (const iconName of iconNames) {
|
|
29
|
+
try {
|
|
30
|
+
const svgContent = await this.loadIconFromFile(iconName);
|
|
31
|
+
this.icons.set(iconName, svgContent);
|
|
32
|
+
console.log(`✅ Загружена иконка: ${iconName}`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn(`⚠️ Не удалось загрузить иконку ${iconName}:`, error);
|
|
35
|
+
// Если не удалось загрузить из файла, используем встроенную версию
|
|
36
|
+
const builtInIcon = this.getBuiltInIcon(iconName);
|
|
37
|
+
if (builtInIcon) {
|
|
38
|
+
this.icons.set(iconName, builtInIcon);
|
|
39
|
+
console.log(`✅ Использована встроенная иконка для: ${iconName}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`📦 Загружено ${this.icons.size} иконок верхней панели`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async loadIconFromFile(iconName) {
|
|
48
|
+
// Пробуем несколько способов загрузки для разных окружений
|
|
49
|
+
const paths = [
|
|
50
|
+
`/src/assets/icons/topbar/${iconName}.svg`,
|
|
51
|
+
`./src/assets/icons/topbar/${iconName}.svg`,
|
|
52
|
+
`../assets/icons/topbar/${iconName}.svg`,
|
|
53
|
+
`assets/icons/topbar/${iconName}.svg`
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const path of paths) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(path);
|
|
59
|
+
if (response.ok) {
|
|
60
|
+
const svgContent = await response.text();
|
|
61
|
+
console.log(`✅ Иконка ${iconName} загружена с пути: ${path}`);
|
|
62
|
+
return svgContent;
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`⚠️ Не удалось загрузить ${iconName} с пути ${path}:`, error.message);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`Не удалось загрузить иконку ${iconName} ни с одного из путей`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getBuiltInIcon(iconName) {
|
|
74
|
+
// Встроенные иконки как fallback
|
|
27
75
|
const builtInIcons = {
|
|
28
76
|
'grid-line': `<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
29
77
|
<path d="M2 2H16V4H2V2Z" fill="currentColor"/>
|
|
@@ -71,12 +119,21 @@ export class TopbarIconLoader {
|
|
|
71
119
|
</svg>`
|
|
72
120
|
};
|
|
73
121
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
122
|
+
return builtInIcons[iconName];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
loadBuiltInIcons() {
|
|
126
|
+
// Загружаем только встроенные иконки как fallback
|
|
127
|
+
const iconNames = ['grid-line', 'grid-dot', 'grid-cross', 'grid-off', 'paint'];
|
|
128
|
+
|
|
129
|
+
for (const iconName of iconNames) {
|
|
130
|
+
const builtInIcon = this.getBuiltInIcon(iconName);
|
|
131
|
+
if (builtInIcon) {
|
|
132
|
+
this.icons.set(iconName, builtInIcon);
|
|
133
|
+
}
|
|
77
134
|
}
|
|
78
135
|
|
|
79
|
-
console.log(`📦 Загружено ${this.icons.size} встроенных иконок верхней
|
|
136
|
+
console.log(`📦 Загружено ${this.icons.size} встроенных иконок верхней панели (fallback)`);
|
|
80
137
|
}
|
|
81
138
|
|
|
82
139
|
getIcon(name) {
|