@sequent-org/moodboard 1.0.16 → 1.0.18
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 +1 -1
- package/src/core/KeyboardManager.js +2 -2
- package/src/core/index.js +8 -2
- package/src/services/FileUploadService.js +96 -35
- package/src/services/ImageUploadService.js +77 -21
- package/src/tools/ToolManager.js +1 -1
- package/src/tools/object-tools/PlacementTool.js +4 -4
- package/src/ui/HtmlHandlesLayer.js +1 -0
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@ export class KeyboardManager {
|
|
|
27
27
|
this.eventBus.emit(Events.UI.PasteImage, {
|
|
28
28
|
src: uploadResult.url,
|
|
29
29
|
name: uploadResult.name,
|
|
30
|
-
imageId: uploadResult.id
|
|
30
|
+
imageId: uploadResult.imageId || uploadResult.id
|
|
31
31
|
});
|
|
32
32
|
} else {
|
|
33
33
|
// Fallback к старому способу
|
|
@@ -54,7 +54,7 @@ export class KeyboardManager {
|
|
|
54
54
|
this.eventBus.emit(Events.UI.PasteImage, {
|
|
55
55
|
src: uploadResult.url,
|
|
56
56
|
name: uploadResult.name,
|
|
57
|
-
imageId: uploadResult.id
|
|
57
|
+
imageId: uploadResult.imageId || uploadResult.id
|
|
58
58
|
});
|
|
59
59
|
} else {
|
|
60
60
|
// Fallback к старому способу: конвертируем в DataURL
|
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
|
});
|
|
@@ -50,12 +93,12 @@ export class FileUploadService {
|
|
|
50
93
|
}
|
|
51
94
|
|
|
52
95
|
return {
|
|
53
|
-
id: result.data.id,
|
|
96
|
+
id: result.data.fileId || result.data.id, // Используем fileId как основное поле, id для обратной совместимости
|
|
97
|
+
fileId: result.data.fileId || result.data.id, // Добавляем fileId для явного доступа
|
|
54
98
|
url: result.data.url,
|
|
55
99
|
size: result.data.size,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name: result.data.name
|
|
100
|
+
name: result.data.name,
|
|
101
|
+
type: result.data.type
|
|
59
102
|
};
|
|
60
103
|
|
|
61
104
|
} catch (error) {
|
|
@@ -72,20 +115,26 @@ export class FileUploadService {
|
|
|
72
115
|
*/
|
|
73
116
|
async updateFileMetadata(fileId, metadata) {
|
|
74
117
|
try {
|
|
75
|
-
const csrfToken =
|
|
118
|
+
const csrfToken = this._getCsrfToken();
|
|
76
119
|
|
|
77
|
-
if (!csrfToken) {
|
|
78
|
-
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;
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
82
136
|
method: 'PUT',
|
|
83
|
-
headers
|
|
84
|
-
'Content-Type': 'application/json',
|
|
85
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
86
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
87
|
-
'Accept': 'application/json'
|
|
88
|
-
},
|
|
137
|
+
headers,
|
|
89
138
|
credentials: 'same-origin',
|
|
90
139
|
body: JSON.stringify(metadata)
|
|
91
140
|
});
|
|
@@ -115,15 +164,21 @@ export class FileUploadService {
|
|
|
115
164
|
*/
|
|
116
165
|
async deleteFile(fileId) {
|
|
117
166
|
try {
|
|
118
|
-
const csrfToken =
|
|
167
|
+
const csrfToken = this._getCsrfToken();
|
|
119
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
|
+
|
|
120
179
|
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
121
180
|
method: 'DELETE',
|
|
122
|
-
headers
|
|
123
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
124
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
125
|
-
'Accept': 'application/json'
|
|
126
|
-
},
|
|
181
|
+
headers,
|
|
127
182
|
credentials: 'same-origin'
|
|
128
183
|
});
|
|
129
184
|
|
|
@@ -317,15 +372,21 @@ export class FileUploadService {
|
|
|
317
372
|
*/
|
|
318
373
|
async cleanupUnusedFiles() {
|
|
319
374
|
try {
|
|
320
|
-
const csrfToken =
|
|
375
|
+
const csrfToken = this._getCsrfToken();
|
|
321
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
|
+
|
|
322
387
|
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
323
388
|
method: 'POST',
|
|
324
|
-
headers
|
|
325
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
326
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
327
|
-
'Accept': 'application/json'
|
|
328
|
-
},
|
|
389
|
+
headers,
|
|
329
390
|
credentials: 'same-origin'
|
|
330
391
|
});
|
|
331
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
|
});
|
|
@@ -55,7 +98,8 @@ export class ImageUploadService {
|
|
|
55
98
|
}
|
|
56
99
|
|
|
57
100
|
return {
|
|
58
|
-
id: result.data.id,
|
|
101
|
+
id: result.data.imageId || result.data.id, // Используем imageId как основное поле, id для обратной совместимости
|
|
102
|
+
imageId: result.data.imageId || result.data.id, // Добавляем imageId для явного доступа
|
|
59
103
|
url: result.data.url,
|
|
60
104
|
width: result.data.width,
|
|
61
105
|
height: result.data.height,
|
|
@@ -86,15 +130,21 @@ export class ImageUploadService {
|
|
|
86
130
|
*/
|
|
87
131
|
async deleteImage(imageId) {
|
|
88
132
|
try {
|
|
89
|
-
const csrfToken =
|
|
133
|
+
const csrfToken = this._getCsrfToken();
|
|
90
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
|
+
|
|
91
145
|
const response = await fetch(`${this.deleteEndpoint}/${imageId}`, {
|
|
92
146
|
method: 'DELETE',
|
|
93
|
-
headers
|
|
94
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
95
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
96
|
-
'Accept': 'application/json'
|
|
97
|
-
},
|
|
147
|
+
headers,
|
|
98
148
|
credentials: 'same-origin'
|
|
99
149
|
});
|
|
100
150
|
|
|
@@ -118,15 +168,21 @@ export class ImageUploadService {
|
|
|
118
168
|
*/
|
|
119
169
|
async cleanupUnusedImages() {
|
|
120
170
|
try {
|
|
121
|
-
const csrfToken =
|
|
171
|
+
const csrfToken = this._getCsrfToken();
|
|
122
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
|
+
|
|
123
183
|
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
124
184
|
method: 'POST',
|
|
125
|
-
headers
|
|
126
|
-
'X-CSRF-TOKEN': csrfToken,
|
|
127
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
128
|
-
'Accept': 'application/json'
|
|
129
|
-
},
|
|
185
|
+
headers,
|
|
130
186
|
credentials: 'same-origin'
|
|
131
187
|
});
|
|
132
188
|
|
package/src/tools/ToolManager.js
CHANGED
|
@@ -372,7 +372,7 @@ export class ToolManager {
|
|
|
372
372
|
// Пытаемся загрузить изображение на сервер
|
|
373
373
|
if (this.core && this.core.imageUploadService) {
|
|
374
374
|
const uploadResult = await this.core.imageUploadService.uploadImage(file, file.name || 'image');
|
|
375
|
-
emitAt(uploadResult.url, uploadResult.name, uploadResult.id, index++);
|
|
375
|
+
emitAt(uploadResult.url, uploadResult.name, uploadResult.imageId || uploadResult.id, index++);
|
|
376
376
|
} else {
|
|
377
377
|
// Fallback к старому способу (base64)
|
|
378
378
|
await new Promise((resolve) => {
|
|
@@ -233,7 +233,7 @@ export class PlacementTool extends BaseTool {
|
|
|
233
233
|
width: targetW,
|
|
234
234
|
height: targetH
|
|
235
235
|
},
|
|
236
|
-
imageId: uploadResult.id // Сохраняем ID изображения
|
|
236
|
+
imageId: uploadResult.imageId || uploadResult.id // Сохраняем ID изображения
|
|
237
237
|
});
|
|
238
238
|
} catch (error) {
|
|
239
239
|
console.error('Ошибка загрузки изображения:', error);
|
|
@@ -274,7 +274,7 @@ export class PlacementTool extends BaseTool {
|
|
|
274
274
|
width: props.width || 120,
|
|
275
275
|
height: props.height || 140
|
|
276
276
|
},
|
|
277
|
-
fileId: uploadResult.id // Сохраняем ID файла
|
|
277
|
+
fileId: uploadResult.fileId || uploadResult.id // Сохраняем ID файла
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
// Возвращаемся к инструменту выделения после создания файла
|
|
@@ -465,7 +465,7 @@ export class PlacementTool extends BaseTool {
|
|
|
465
465
|
width: props.width || 120,
|
|
466
466
|
height: props.height || 140
|
|
467
467
|
},
|
|
468
|
-
fileId: uploadResult.id // Сохраняем ID файла
|
|
468
|
+
fileId: uploadResult.fileId || uploadResult.id // Сохраняем ID файла
|
|
469
469
|
});
|
|
470
470
|
|
|
471
471
|
} catch (uploadError) {
|
|
@@ -961,7 +961,7 @@ export class PlacementTool extends BaseTool {
|
|
|
961
961
|
width: targetW,
|
|
962
962
|
height: targetH
|
|
963
963
|
},
|
|
964
|
-
imageId: uploadResult.id // Сохраняем ID изображения
|
|
964
|
+
imageId: uploadResult.imageId || uploadResult.id // Сохраняем ID изображения
|
|
965
965
|
});
|
|
966
966
|
|
|
967
967
|
} catch (uploadError) {
|
|
@@ -222,6 +222,7 @@ export class HtmlHandlesLayer {
|
|
|
222
222
|
position: 'absolute', pointerEvents: 'auto', cursor,
|
|
223
223
|
zIndex: 5, // Меньше чем у ручек (10)
|
|
224
224
|
background: 'transparent' // невидимые области
|
|
225
|
+
|
|
225
226
|
});
|
|
226
227
|
e.addEventListener('mousedown', (evt) => this._onEdgeResizeDown(evt));
|
|
227
228
|
box.appendChild(e);
|