@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -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
- this.fileUploadService = new FileUploadService(this.apiClient);
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, mimeType: string, formattedSize: string}>}
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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
- mimeType: result.data.mime_type,
57
- formattedSize: result.data.formatted_size,
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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
 
@@ -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);