@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
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
  });
@@ -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
- mimeType: result.data.mime_type,
58
- formattedSize: result.data.formatted_size,
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = 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
  });
@@ -87,15 +130,21 @@ export class ImageUploadService {
87
130
  */
88
131
  async deleteImage(imageId) {
89
132
  try {
90
- const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
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: 12px;
531
- left: 16px;
532
- height: 50px;
533
- display: inline-flex;
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 6px;
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
- display: inline-flex;
547
- align-items: center;
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
- /* background: #f0f0f0; */
554
- /* color: #333; */
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
- /* Стили для SVG иконок в кнопках верхней панели */
561
- .moodboard-topbar__button svg {
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:hover svg {
570
- /* transform: scale(1.1); */
569
+ .moodboard-topbar__button--active {
570
+ background: #007ACC;
571
+ color: #fff;
571
572
  }
572
573
 
573
- .moodboard-topbar__button:hover {
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: 28px;
581
- background: #e5e7eb;
582
- margin: 0 6px 0 2px;
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
- // Это гарантирует работу в любом окружении (npm пакет, CDN, локальная разработка)
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
- loadBuiltInIcons() {
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
- for (const [iconName, svgContent] of Object.entries(builtInIcons)) {
76
- this.icons.set(iconName, svgContent);
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) {