@sequent-org/moodboard 1.2.21 → 1.2.23

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.2.21",
3
+ "version": "1.2.23",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -2,428 +2,28 @@
2
2
  export { MoodBoard } from './moodboard/MoodBoard.js';
3
3
 
4
4
  /**
5
- * БЫСТРОЕ ИСПРАВЛЕНИЕ эмоджи для Vite/bundled версий
6
- * Устанавливает правильный базовый путь к ассетам пакета
5
+ * АВТОМАТИЧЕСКАЯ настройка эмоджи при импорте пакета
6
+ * Больше НЕ нужно вызывать fixEmojiPaths() вручную
7
7
  */
8
- export function fixEmojiPaths(packageName = null) {
9
- if (packageName) {
10
- // Если указано имя пакета - используем его
11
- window.MOODBOARD_BASE_PATH = `${window.location.origin}/node_modules/${packageName}/`;
12
- console.log('🔧 Установлен базовый путь для эмоджи:', window.MOODBOARD_BASE_PATH);
13
- } else {
14
- // Автоопределение - используем стандартное имя пакета
8
+ function autoSetupEmojiPaths() {
9
+ if (typeof window !== 'undefined' && !window.MOODBOARD_BASE_PATH) {
15
10
  const detectedPackage = '@sequent-org/moodboard';
16
11
  window.MOODBOARD_BASE_PATH = `${window.location.origin}/node_modules/${detectedPackage}/`;
17
- console.log('🔧 Автоопределен базовый путь для эмоджи:', window.MOODBOARD_BASE_PATH);
12
+ console.log('🔧 Мудборд: автоматически установлен базовый путь для эмоджи:', window.MOODBOARD_BASE_PATH);
18
13
  }
19
-
20
- return window.MOODBOARD_BASE_PATH;
21
14
  }
22
15
 
23
- /**
24
- * Диагностика конфликтов CSS для панелей
25
- * Находит что именно переопределяет ширину панелей
26
- */
27
- export function diagnosePanelConflicts() {
28
- console.log('🔍 ДИАГНОСТИКА: поиск конфликтов стилей панелей...');
29
-
30
- const panel = document.querySelector('.text-properties-panel, .frame-properties-panel');
31
- if (!panel) {
32
- console.log('❌ Панели не найдены. Создайте объект и выберите его.');
33
- return;
34
- }
35
-
36
- console.log('📋 Найдена панель:', panel.className);
37
-
38
- // Получаем все применяемые стили
39
- const computedStyle = getComputedStyle(panel);
40
- console.log('📏 Текущие размеры панели:');
41
- console.log(' - width:', computedStyle.width);
42
- console.log(' - min-width:', computedStyle.minWidth);
43
- console.log(' - max-width:', computedStyle.maxWidth);
44
- console.log(' - height:', computedStyle.height);
45
- console.log(' - padding:', computedStyle.padding);
46
- console.log(' - display:', computedStyle.display);
47
- console.log(' - position:', computedStyle.position);
48
-
49
- // Проверяем inline стили
50
- if (panel.style.cssText) {
51
- console.log('⚠️ НАЙДЕНЫ inline стили на панели:', panel.style.cssText);
52
- } else {
53
- console.log('✅ Inline стилей нет');
54
- }
55
-
56
- // Ищем все CSS правила, которые могут влиять на панель
57
- console.log('🔍 Поиск CSS правил, влияющих на панель...');
58
-
59
- // Проверяем основные подозрительные свойства
60
- const suspiciousProperties = ['width', 'min-width', 'max-width', 'height', 'padding', 'display'];
61
-
62
- suspiciousProperties.forEach(prop => {
63
- const value = computedStyle.getPropertyValue(prop);
64
- console.log(`📌 ${prop}: ${value}`);
65
-
66
- // Проверяем приоритет
67
- const priority = computedStyle.getPropertyPriority(prop);
68
- if (priority) {
69
- console.log(` ⚡ Приоритет: ${priority}`);
70
- }
71
- });
72
-
73
- // Проверяем классы родительских элементов
74
- let parent = panel.parentElement;
75
- let level = 1;
76
- console.log('🔗 Родительские элементы:');
77
- while (parent && level <= 5) {
78
- const parentStyles = getComputedStyle(parent);
79
- console.log(` ${level}. ${parent.tagName}${parent.className ? '.' + parent.className : ''}`);
80
- console.log(` width: ${parentStyles.width}, display: ${parentStyles.display}`);
81
- parent = parent.parentElement;
82
- level++;
83
- }
84
-
85
- // Ищем потенциальные конфликтующие CSS классы
86
- console.log('⚠️ Поиск потенциальных конфликтов:');
87
-
88
- const possibleConflicts = [
89
- 'bootstrap', 'tailwind', 'flex', 'grid', 'container', 'row', 'col',
90
- 'w-', 'width', 'min-w', 'max-w', 'panel', 'modal', 'popup'
91
- ];
92
-
93
- const allClasses = panel.className.split(' ');
94
- const parentClasses = panel.parentElement?.className?.split(' ') || [];
95
-
96
- [...allClasses, ...parentClasses].forEach(cls => {
97
- possibleConflicts.forEach(conflict => {
98
- if (cls.includes(conflict)) {
99
- console.log(`🚨 Подозрительный класс: "${cls}" (содержит "${conflict}")`);
100
- }
101
- });
102
- });
103
-
104
- return {
105
- element: panel,
106
- computedStyle: computedStyle,
107
- currentWidth: computedStyle.width,
108
- currentMinWidth: computedStyle.minWidth,
109
- hasInlineStyles: !!panel.style.cssText
110
- };
111
- }
16
+ // Автоматически выполняем настройку при импорте пакета
17
+ autoSetupEmojiPaths();
112
18
 
113
- /**
114
- * Хирургическое исправление конкретных свойств панелей
115
- * Исправляет только width и min-width, не трогая остальное
116
- */
117
- export function surgicalPanelFix() {
118
- console.log('🔧 ХИРУРГИЧЕСКОЕ исправление размеров панелей...');
119
-
120
- const targetPanels = document.querySelectorAll(`
121
- .text-properties-panel,
122
- .frame-properties-panel,
123
- .note-properties-panel,
124
- .file-properties-panel,
125
- .moodboard-file-properties-panel
126
- `);
127
-
128
- if (targetPanels.length === 0) {
129
- console.log('❌ Панели не найдены');
130
- return;
131
- }
132
-
133
- targetPanels.forEach((panel, index) => {
134
- console.log(`🔧 Исправляем панель ${index + 1}: ${panel.className}`);
135
-
136
- // Запоминаем текущие значения для диагностики
137
- const beforeWidth = getComputedStyle(panel).width;
138
- const beforeMinWidth = getComputedStyle(panel).minWidth;
139
-
140
- // Применяем ТОЛЬКО минимально необходимые исправления
141
- if (panel.classList.contains('text-properties-panel') ||
142
- panel.classList.contains('frame-properties-panel')) {
143
- panel.style.setProperty('min-width', '320px', 'important');
144
- panel.style.setProperty('width', 'auto', 'important');
145
- } else if (panel.classList.contains('note-properties-panel')) {
146
- panel.style.setProperty('min-width', '280px', 'important');
147
- panel.style.setProperty('width', 'auto', 'important');
148
- } else if (panel.classList.contains('file-properties-panel') ||
149
- panel.classList.contains('moodboard-file-properties-panel')) {
150
- panel.style.setProperty('min-width', '250px', 'important');
151
- panel.style.setProperty('width', 'auto', 'important');
152
- }
153
-
154
- // Проверяем результат
155
- setTimeout(() => {
156
- const afterWidth = getComputedStyle(panel).width;
157
- const afterMinWidth = getComputedStyle(panel).minWidth;
158
-
159
- console.log(`📏 Панель ${index + 1} результат:`);
160
- console.log(` До: width: ${beforeWidth}, min-width: ${beforeMinWidth}`);
161
- console.log(` После: width: ${afterWidth}, min-width: ${afterMinWidth}`);
162
-
163
- if (parseInt(afterMinWidth) >= 250) {
164
- console.log(`✅ Панель ${index + 1} исправлена успешно!`);
165
- } else {
166
- console.log(`❌ Панель ${index + 1} все еще имеет проблемы`);
167
- }
168
- }, 50);
169
- });
170
- }
19
+ // ПРИМЕЧАНИЕ: Стили должны загружаться через bundler (Vite/Webpack)
20
+ // import '@sequent-org/moodboard/style.css' в вашем приложении
171
21
 
172
- // Дополнительные экспорты для работы без bundler
173
- export { initMoodBoardNoBundler, quickInitMoodBoard, injectCriticalStyles, forceInjectPanelStyles } from './initNoBundler.js';
174
- export { StyleLoader } from './utils/styleLoader.js';
175
- export { EmojiLoaderNoBundler } from './utils/emojiLoaderNoBundler.js';
22
+ // Дополнительные экспорты (только для специальных случаев)
23
+ export { initMoodBoardNoBundler } from './initNoBundler.js';
176
24
 
177
- // Экспорт встроенных эмоджи (PNG data URL)
25
+ // Основные утилиты для эмоджи (если нужны)
178
26
  export {
179
27
  getInlinePngEmojiUrl,
180
- getAvailableInlinePngEmojis,
181
- hasInlinePngEmoji
182
- } from './utils/inlinePngEmojis.js';
183
-
184
- // Экспорт встроенных SVG эмоджи (для пользователей, которые хотят добавить свои)
185
- export {
186
- addInlineSvgEmoji,
187
- bulkAddInlineSvgEmojis,
188
- getAvailableInlineEmojis,
189
- isInlineSvgEmoji,
190
- getInlineEmojiByCode
191
- } from './utils/inlineSvgEmojis.js';
192
-
193
- /**
194
- * СУПЕР-АГРЕССИВНАЯ функция для исправления панелей в проектах с конфликтами CSS
195
- */
196
- export function forceFixPanelStyles() {
197
- console.log('💪 СУПЕР-АГРЕССИВНОЕ исправление панелей (для проектов с конфликтами)...');
198
-
199
- const superForcedCSS = `
200
- /* МАКСИМАЛЬНО АГРЕССИВНЫЕ стили панелей */
201
- .text-properties-panel,
202
- div.text-properties-panel,
203
- [class*="text-properties-panel"] {
204
- min-width: 320px !important;
205
- max-width: none !important;
206
- width: auto !important;
207
- height: 36px !important;
208
- padding: 12px 22px !important;
209
- margin: 0 !important;
210
- background: #ffffff !important;
211
- background-color: #ffffff !important;
212
- border: 1px solid #e0e0e0 !important;
213
- border-radius: 9999px !important;
214
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12) !important;
215
- display: flex !important;
216
- flex-direction: row !important;
217
- align-items: center !important;
218
- gap: 8px !important;
219
- font-size: 13px !important;
220
- font-family: 'Roboto', Arial, sans-serif !important;
221
- position: absolute !important;
222
- pointer-events: auto !important;
223
- z-index: 10001 !important;
224
- box-sizing: border-box !important;
225
- transform: none !important;
226
- opacity: 1 !important;
227
- visibility: visible !important;
228
- }
229
-
230
- .frame-properties-panel,
231
- div.frame-properties-panel,
232
- [class*="frame-properties-panel"] {
233
- min-width: 320px !important;
234
- max-width: none !important;
235
- width: auto !important;
236
- height: 36px !important;
237
- padding: 12px 32px !important;
238
- margin: 0 !important;
239
- background: #ffffff !important;
240
- background-color: #ffffff !important;
241
- border: 1px solid #e0e0e0 !important;
242
- border-radius: 9999px !important;
243
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12) !important;
244
- display: inline-flex !important;
245
- align-items: center !important;
246
- gap: 8px !important;
247
- font-size: 13px !important;
248
- font-family: 'Roboto', Arial, sans-serif !important;
249
- position: absolute !important;
250
- pointer-events: auto !important;
251
- z-index: 10001 !important;
252
- box-sizing: border-box !important;
253
- transform: none !important;
254
- opacity: 1 !important;
255
- visibility: visible !important;
256
- }
257
-
258
- .note-properties-panel,
259
- div.note-properties-panel,
260
- [class*="note-properties-panel"] {
261
- min-width: 280px !important;
262
- max-width: none !important;
263
- width: auto !important;
264
- height: 40px !important;
265
- padding: 8px 40px !important;
266
- margin: 0 !important;
267
- background: white !important;
268
- background-color: white !important;
269
- border: 1px solid #e0e0e0 !important;
270
- border-radius: 9999px !important;
271
- box-shadow: 0 6px 24px rgba(0, 0, 0, 0.16) !important;
272
- display: inline-flex !important;
273
- flex-direction: row !important;
274
- align-items: center !important;
275
- gap: 8px !important;
276
- font-size: 12px !important;
277
- font-family: Arial, sans-serif !important;
278
- position: absolute !important;
279
- pointer-events: auto !important;
280
- z-index: 10000 !important;
281
- box-sizing: border-box !important;
282
- backdrop-filter: blur(4px) !important;
283
- transform: none !important;
284
- opacity: 1 !important;
285
- visibility: visible !important;
286
- }
287
- `;
288
-
289
- // Удаляем предыдущие версии
290
- const existingStyles = document.querySelectorAll('#moodboard-universal-panel-fix, #moodboard-super-force-panel-fix');
291
- existingStyles.forEach(style => style.remove());
292
-
293
- const style = document.createElement('style');
294
- style.id = 'moodboard-super-force-panel-fix';
295
- style.textContent = superForcedCSS;
296
-
297
- // Вставляем в самый конец head для максимального приоритета
298
- document.head.appendChild(style);
299
-
300
- console.log('💪 Супер-агрессивные стили применены');
301
-
302
- // Проверяем все панели
303
- setTimeout(() => {
304
- const panels = document.querySelectorAll('.text-properties-panel, .frame-properties-panel, .note-properties-panel');
305
- panels.forEach((panel, index) => {
306
- const computedStyle = getComputedStyle(panel);
307
- const width = parseInt(computedStyle.minWidth);
308
- console.log(`📏 Панель ${index + 1}: minWidth = ${width}px`);
309
- });
310
- }, 200);
311
- }
312
-
313
- /**
314
- * Универсальная функция для исправления стилей панелей
315
- * Работает с любой версией MoodBoard (bundled и no-bundler)
316
- */
317
- export function fixPanelStyles() {
318
- console.log('🔧 Исправляем стили панелей MoodBoard (универсальная версия)...');
319
-
320
- const forcedPanelCSS = `
321
- /* УНИВЕРСАЛЬНЫЕ принудительные стили панелей */
322
- .text-properties-panel {
323
- min-width: 320px !important;
324
- width: auto !important;
325
- height: 36px !important;
326
- padding: 12px 22px !important;
327
- background-color: #ffffff !important;
328
- border: 1px solid #e0e0e0 !important;
329
- border-radius: 9999px !important;
330
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12) !important;
331
- display: flex !important;
332
- flex-direction: row !important;
333
- align-items: center !important;
334
- gap: 8px !important;
335
- font-size: 13px !important;
336
- font-family: 'Roboto', Arial, sans-serif !important;
337
- position: absolute !important;
338
- pointer-events: auto !important;
339
- z-index: 1001 !important;
340
- }
341
-
342
- .frame-properties-panel {
343
- min-width: 320px !important;
344
- width: auto !important;
345
- height: 36px !important;
346
- padding: 12px 32px !important;
347
- background-color: #ffffff !important;
348
- border: 1px solid #e0e0e0 !important;
349
- border-radius: 9999px !important;
350
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12) !important;
351
- display: inline-flex !important;
352
- align-items: center !important;
353
- gap: 8px !important;
354
- font-size: 13px !important;
355
- font-family: 'Roboto', Arial, sans-serif !important;
356
- position: absolute !important;
357
- pointer-events: auto !important;
358
- z-index: 1001 !important;
359
- }
360
-
361
- .note-properties-panel {
362
- min-width: 280px !important;
363
- width: auto !important;
364
- height: 36px !important;
365
- padding: 12px 22px !important;
366
- background-color: #ffffff !important;
367
- border: 1px solid #e0e0e0 !important;
368
- border-radius: 9999px !important;
369
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12) !important;
370
- display: inline-flex !important;
371
- align-items: center !important;
372
- gap: 8px !important;
373
- font-size: 13px !important;
374
- font-family: 'Roboto', Arial, sans-serif !important;
375
- position: absolute !important;
376
- pointer-events: auto !important;
377
- z-index: 1001 !important;
378
- }
379
-
380
- .file-properties-panel, .moodboard-file-properties-panel {
381
- min-width: 250px !important;
382
- width: auto !important;
383
- height: 36px !important;
384
- padding: 8px 12px !important;
385
- background-color: #ffffff !important;
386
- border: 1px solid #e5e7eb !important;
387
- border-radius: 8px !important;
388
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
389
- display: flex !important;
390
- flex-direction: row !important;
391
- align-items: center !important;
392
- gap: 8px !important;
393
- font-size: 14px !important;
394
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
395
- position: absolute !important;
396
- pointer-events: auto !important;
397
- z-index: 1000 !important;
398
- }
399
- `;
400
-
401
- // Проверяем, не добавлены ли уже стили
402
- if (document.getElementById('moodboard-universal-panel-fix')) {
403
- console.log('⚠️ Универсальные стили панелей уже добавлены');
404
- return;
405
- }
406
-
407
- const style = document.createElement('style');
408
- style.id = 'moodboard-universal-panel-fix';
409
- style.textContent = forcedPanelCSS;
410
- document.head.appendChild(style);
411
-
412
- console.log('✅ Универсальные стили панелей добавлены');
413
-
414
- // Проверяем результат через 100ms
415
- setTimeout(() => {
416
- const panel = document.querySelector('.text-properties-panel, .frame-properties-panel, .note-properties-panel');
417
- if (panel) {
418
- const computedStyle = getComputedStyle(panel);
419
- const width = parseInt(computedStyle.minWidth);
420
- console.log(`📏 Проверка панели: minWidth = ${width}px`);
421
-
422
- if (width >= 250) {
423
- console.log('✅ Стили панелей исправлены успешно!');
424
- } else {
425
- console.log('⚠️ Панель все еще узкая, возможны конфликты CSS');
426
- }
427
- }
428
- }, 100);
429
- }
28
+ getAvailableInlinePngEmojis
29
+ } from './utils/inlinePngEmojis.js';
@@ -37,6 +37,16 @@ export class DataManager {
37
37
  this.loadViewport(data.viewport);
38
38
  }
39
39
 
40
+ // ИСПРАВЛЕНИЕ: Принудительно пересоздаем HTML элементы после загрузки данных
41
+ // Это нужно потому что createObjectFromData() НЕ генерирует Events.Object.Created
42
+ // чтобы не запускать автосохранение, но HtmlTextLayer нуждается в этих событиях
43
+ setTimeout(() => {
44
+ // Ищем htmlTextLayer через глобальную переменную (установленную в MoodBoard.js)
45
+ if (window.moodboardHtmlTextLayer) {
46
+ window.moodboardHtmlTextLayer.rebuildFromState();
47
+ window.moodboardHtmlTextLayer.updateAll();
48
+ }
49
+ }, 100);
40
50
 
41
51
  }
42
52
 
@@ -36,6 +36,12 @@ export class MoodBoard {
36
36
  // Настройки по умолчанию
37
37
  this.options = {
38
38
  theme: 'light',
39
+ boardId: null,
40
+ apiUrl: '/api/moodboard',
41
+ autoLoad: true,
42
+ onSave: null,
43
+ onLoad: null,
44
+ onDestroy: null,
39
45
  ...options
40
46
  };
41
47
 
@@ -63,6 +69,11 @@ export class MoodBoard {
63
69
  */
64
70
  async init() {
65
71
  try {
72
+ // Добавляем корневой класс к контейнеру для изоляции стилей
73
+ if (this.container) {
74
+ this.container.classList.add('moodboard-root');
75
+ }
76
+
66
77
  // Создаем HTML структуру
67
78
  const { workspace, toolbar, canvas, topbar } = this.workspaceManager.createWorkspaceStructure();
68
79
  this.workspaceElement = workspace;
@@ -121,8 +132,13 @@ export class MoodBoard {
121
132
  // Предоставляем доступ к сервису через core
122
133
  this.coreMoodboard.imageUploadService = this.imageUploadService;
123
134
 
124
- // Загружаем данные (сначала пробуем загрузить с сервера, потом дефолтные)
125
- await this.loadExistingBoard();
135
+ // Настраиваем коллбеки событий
136
+ this.setupEventCallbacks();
137
+
138
+ // Автоматически загружаем данные если включено
139
+ if (this.options.autoLoad) {
140
+ await this.loadExistingBoard();
141
+ }
126
142
 
127
143
  } catch (error) {
128
144
  console.error('MoodBoard init failed:', error);
@@ -326,34 +342,65 @@ export class MoodBoard {
326
342
  try {
327
343
  const boardId = this.options.boardId;
328
344
 
329
- if (!boardId || !this.options.loadEndpoint) {
330
- this.dataManager.loadData(this.data);
345
+ if (!boardId || !this.options.apiUrl) {
346
+ console.log('📦 MoodBoard: нет boardId или apiUrl, загружаем пустую доску');
347
+ this.dataManager.loadData(this.data || { objects: [] });
348
+
349
+ // Вызываем коллбек onLoad
350
+ if (typeof this.options.onLoad === 'function') {
351
+ this.options.onLoad({ success: true, data: this.data || { objects: [] } });
352
+ }
331
353
  return;
332
354
  }
333
355
 
334
- // Пытаемся загрузить с сервера
335
- const boardData = await this.coreMoodboard.saveManager.loadBoardData(boardId);
356
+ console.log(`📦 MoodBoard: загружаем доску ${boardId} с ${this.options.apiUrl}`);
336
357
 
337
- if (boardData && boardData.objects) {
338
- // Восстанавливаем URL изображений и файлов перед загрузкой (если метод доступен)
339
- let restoredData = boardData;
340
- if (this.coreMoodboard.apiClient && typeof this.coreMoodboard.apiClient.restoreObjectUrls === 'function') {
341
- try {
342
- restoredData = await this.coreMoodboard.apiClient.restoreObjectUrls(boardData);
343
- } catch (error) {
344
- console.warn('Не удалось восстановить URL объектов:', error);
345
- restoredData = boardData; // Используем исходные данные
346
- }
358
+ // Формируем URL для загрузки
359
+ const loadUrl = this.options.apiUrl.endsWith('/')
360
+ ? `${this.options.apiUrl}load/${boardId}`
361
+ : `${this.options.apiUrl}/load/${boardId}`;
362
+
363
+ // Загружаем с сервера через fetch
364
+ const response = await fetch(loadUrl, {
365
+ method: 'GET',
366
+ headers: {
367
+ 'Content-Type': 'application/json',
368
+ 'X-CSRF-TOKEN': this.getCsrfToken()
369
+ }
370
+ });
371
+
372
+ if (!response.ok) {
373
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
374
+ }
375
+
376
+ const boardData = await response.json();
377
+
378
+ if (boardData && boardData.data) {
379
+ console.log('✅ MoodBoard: данные загружены с сервера', boardData.data);
380
+ this.dataManager.loadData(boardData.data);
381
+
382
+ // Вызываем коллбек onLoad
383
+ if (typeof this.options.onLoad === 'function') {
384
+ this.options.onLoad({ success: true, data: boardData.data });
347
385
  }
348
- this.dataManager.loadData(restoredData);
349
386
  } else {
350
- this.dataManager.loadData(this.data);
387
+ console.log('📦 MoodBoard: нет данных с сервера, загружаем пустую доску');
388
+ this.dataManager.loadData(this.data || { objects: [] });
389
+
390
+ // Вызываем коллбек onLoad
391
+ if (typeof this.options.onLoad === 'function') {
392
+ this.options.onLoad({ success: true, data: this.data || { objects: [] } });
393
+ }
351
394
  }
352
395
 
353
396
  } catch (error) {
354
- console.warn('⚠️ Ошибка загрузки доски, создаем новую:', error.message);
355
- // Если загрузка не удалась, используем дефолтные данные
356
- this.dataManager.loadData(this.data);
397
+ console.warn('⚠️ MoodBoard: ошибка загрузки доски, создаем новую:', error.message);
398
+ this.dataManager.loadData(this.data || { objects: [] });
399
+
400
+ // Вызываем коллбек onLoad с ошибкой
401
+ if (typeof this.options.onLoad === 'function') {
402
+ this.options.onLoad({ success: false, error: error.message, data: this.data || { objects: [] } });
403
+ }
357
404
  }
358
405
  }
359
406
 
@@ -432,8 +479,212 @@ export class MoodBoard {
432
479
  this.dataManager = null;
433
480
  this.actionHandler = null;
434
481
 
435
- // Очищаем ссылку на контейнер
482
+ // Удаляем корневой класс и очищаем ссылку на контейнер
483
+ if (this.container) {
484
+ this.container.classList.remove('moodboard-root');
485
+ }
436
486
  this.container = null;
437
487
 
488
+ // Вызываем коллбек onDestroy
489
+ if (typeof this.options.onDestroy === 'function') {
490
+ try {
491
+ this.options.onDestroy();
492
+ } catch (error) {
493
+ console.warn('⚠️ Ошибка в коллбеке onDestroy:', error);
494
+ }
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Настройка коллбеков событий
500
+ */
501
+ setupEventCallbacks() {
502
+ if (!this.coreMoodboard || !this.coreMoodboard.eventBus) return;
503
+
504
+ // Коллбек для успешного сохранения
505
+ if (typeof this.options.onSave === 'function') {
506
+ this.coreMoodboard.eventBus.on('save:success', (data) => {
507
+ try {
508
+ // Создаем объединенный скриншот с HTML текстом
509
+ let screenshot = null;
510
+ if (this.coreMoodboard.pixi && this.coreMoodboard.pixi.app && this.coreMoodboard.pixi.app.view) {
511
+ screenshot = this.createCombinedScreenshot('image/jpeg', 0.6);
512
+ }
513
+
514
+ this.options.onSave({
515
+ success: true,
516
+ data: data,
517
+ screenshot: screenshot,
518
+ boardId: this.options.boardId
519
+ });
520
+ } catch (error) {
521
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
522
+ }
523
+ });
524
+
525
+ // Коллбек для ошибки сохранения
526
+ this.coreMoodboard.eventBus.on('save:error', (data) => {
527
+ try {
528
+ this.options.onSave({
529
+ success: false,
530
+ error: data.error,
531
+ boardId: this.options.boardId
532
+ });
533
+ } catch (error) {
534
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
535
+ }
536
+ });
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Получение CSRF токена из всех возможных источников
542
+ */
543
+ getCsrfToken() {
544
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ||
545
+ window.csrfToken ||
546
+ this.options.csrfToken ||
547
+ '';
548
+ }
549
+
550
+ /**
551
+ * Публичный метод для загрузки данных из API
552
+ */
553
+ async loadFromApi(boardId = null) {
554
+ const targetBoardId = boardId || this.options.boardId;
555
+ if (!targetBoardId) {
556
+ throw new Error('boardId не указан');
557
+ }
558
+
559
+ // Временно меняем boardId для загрузки
560
+ const originalBoardId = this.options.boardId;
561
+ this.options.boardId = targetBoardId;
562
+
563
+ try {
564
+ await this.loadExistingBoard();
565
+ } finally {
566
+ // Восстанавливаем оригинальный boardId
567
+ this.options.boardId = originalBoardId;
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Публичный метод для экспорта скриншота с HTML текстом
573
+ */
574
+ exportScreenshot(format = 'image/jpeg', quality = 0.6) {
575
+ return this.createCombinedScreenshot(format, quality);
576
+ }
577
+
578
+ /**
579
+ * Разбивает текст на строки с учетом ширины элемента (имитирует HTML word-break: break-word)
580
+ */
581
+ wrapText(ctx, text, maxWidth) {
582
+ const lines = [];
583
+
584
+ if (!text || maxWidth <= 0) {
585
+ return [text];
586
+ }
587
+
588
+ // Разбиваем по символам если не помещается (имитирует word-break: break-word)
589
+ let currentLine = '';
590
+
591
+ for (let i = 0; i < text.length; i++) {
592
+ const char = text[i];
593
+ const testLine = currentLine + char;
594
+ const metrics = ctx.measureText(testLine);
595
+
596
+ if (metrics.width > maxWidth && currentLine !== '') {
597
+ // Текущая строка не помещается, сохраняем предыдущую
598
+ lines.push(currentLine);
599
+ currentLine = char;
600
+ } else {
601
+ currentLine = testLine;
602
+ }
603
+ }
604
+
605
+ // Добавляем последнюю строку
606
+ if (currentLine) {
607
+ lines.push(currentLine);
608
+ }
609
+
610
+ return lines.length > 0 ? lines : [text];
611
+ }
612
+
613
+ /**
614
+ * Создает объединенный скриншот: PIXI canvas + HTML текстовые элементы
615
+ */
616
+ createCombinedScreenshot(format = 'image/jpeg', quality = 0.6) {
617
+ if (!this.coreMoodboard || !this.coreMoodboard.pixi || !this.coreMoodboard.pixi.app || !this.coreMoodboard.pixi.app.view) {
618
+ throw new Error('Canvas не найден');
619
+ }
620
+
621
+ try {
622
+ // Получаем PIXI canvas
623
+ const pixiCanvas = this.coreMoodboard.pixi.app.view;
624
+ const pixiWidth = pixiCanvas.width;
625
+ const pixiHeight = pixiCanvas.height;
626
+
627
+ // Создаем временный canvas для объединения
628
+ const combinedCanvas = document.createElement('canvas');
629
+ combinedCanvas.width = pixiWidth;
630
+ combinedCanvas.height = pixiHeight;
631
+ const ctx = combinedCanvas.getContext('2d');
632
+
633
+ // 1. Рисуем PIXI canvas как основу
634
+ ctx.drawImage(pixiCanvas, 0, 0);
635
+
636
+ // 2. Рисуем HTML текстовые элементы поверх
637
+ const textElements = document.querySelectorAll('.mb-text');
638
+
639
+ textElements.forEach((textEl, index) => {
640
+ try {
641
+ // Получаем стили и позицию элемента
642
+ const computedStyle = window.getComputedStyle(textEl);
643
+ const text = textEl.textContent || '';
644
+
645
+ // Проверяем видимость
646
+ if (computedStyle.visibility === 'hidden' || computedStyle.opacity === '0' || !text.trim()) {
647
+ return;
648
+ }
649
+
650
+ // Используем CSS позицию (абсолютная позиция)
651
+ const left = parseInt(textEl.style.left) || 0;
652
+ const top = parseInt(textEl.style.top) || 0;
653
+
654
+ // Настраиваем стили текста
655
+ const fontSize = parseInt(computedStyle.fontSize) || 18;
656
+ const fontFamily = computedStyle.fontFamily || 'Arial, sans-serif';
657
+ const color = computedStyle.color || '#000000';
658
+
659
+ ctx.font = `${fontSize}px ${fontFamily}`;
660
+ ctx.fillStyle = color;
661
+ ctx.textAlign = 'left';
662
+ ctx.textBaseline = 'top';
663
+
664
+ // Получаем размеры элемента
665
+ const elementWidth = parseInt(textEl.style.width) || 182;
666
+
667
+ // Разбиваем текст на строки и рисуем каждую строку
668
+ const lines = this.wrapText(ctx, text, elementWidth);
669
+ const lineHeight = fontSize * 1.3; // Межстрочный интервал
670
+
671
+ lines.forEach((line, lineIndex) => {
672
+ const yPos = top + (lineIndex * lineHeight) + 2;
673
+ ctx.fillText(line, left, yPos);
674
+ });
675
+ } catch (error) {
676
+ console.warn(`⚠️ Ошибка при рисовании текста ${index + 1}:`, error);
677
+ }
678
+ });
679
+
680
+ // 3. Экспортируем объединенный результат
681
+ return combinedCanvas.toDataURL(format, quality);
682
+
683
+ } catch (error) {
684
+ console.warn('⚠️ Ошибка при создании объединенного скриншота, используем только PIXI canvas:', error);
685
+ // Fallback: только PIXI canvas
686
+ const canvas = this.coreMoodboard.pixi.app.view;
687
+ return canvas.toDataURL(format, quality);
688
+ }
438
689
  }
439
690
  }
@@ -41,16 +41,14 @@ export class BaseTool {
41
41
 
42
42
  /**
43
43
  * Устанавливает курсор для инструмента
44
+ * Базовая реализация не устанавливает глобальный курсор
45
+ * Конкретные инструменты должны переопределить этот метод
46
+ * и устанавливать курсор на canvas или соответствующий элемент
44
47
  */
45
48
  setCursor() {
46
- if (typeof document !== 'undefined' && document.body) {
47
- // Для 'default' не ставим инлайн-стиль, чтобы сработал глобальный CSS-курсор
48
- if (!this.cursor || this.cursor === 'default') {
49
- document.body.style.cursor = '';
50
- } else {
51
- document.body.style.cursor = this.cursor;
52
- }
53
- }
49
+ // Базовая реализация ничего не делает
50
+ // Избегаем установки глобального курсора на document.body
51
+ // Конкретные инструменты устанавливают курсор на canvas
54
52
  }
55
53
 
56
54
  /**
@@ -14,8 +14,10 @@
14
14
  font-display: swap;
15
15
  }
16
16
 
17
- /* Global default cursor */
18
- html, body, .moodboard-workspace, .moodboard-container, .moodboard-canvas {
17
+ /* Default cursor inside moodboard root only */
18
+ .moodboard-root .moodboard-workspace,
19
+ .moodboard-root .moodboard-container,
20
+ .moodboard-root .moodboard-canvas {
19
21
  cursor: url('../../assets/icons/cursor-default-custom.svg') 0 0, default;
20
22
  }
21
23