@sequent-org/moodboard 1.2.21 → 1.2.22

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.22",
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
 
@@ -121,8 +127,13 @@ export class MoodBoard {
121
127
  // Предоставляем доступ к сервису через core
122
128
  this.coreMoodboard.imageUploadService = this.imageUploadService;
123
129
 
124
- // Загружаем данные (сначала пробуем загрузить с сервера, потом дефолтные)
125
- await this.loadExistingBoard();
130
+ // Настраиваем коллбеки событий
131
+ this.setupEventCallbacks();
132
+
133
+ // Автоматически загружаем данные если включено
134
+ if (this.options.autoLoad) {
135
+ await this.loadExistingBoard();
136
+ }
126
137
 
127
138
  } catch (error) {
128
139
  console.error('MoodBoard init failed:', error);
@@ -326,34 +337,65 @@ export class MoodBoard {
326
337
  try {
327
338
  const boardId = this.options.boardId;
328
339
 
329
- if (!boardId || !this.options.loadEndpoint) {
330
- this.dataManager.loadData(this.data);
340
+ if (!boardId || !this.options.apiUrl) {
341
+ console.log('📦 MoodBoard: нет boardId или apiUrl, загружаем пустую доску');
342
+ this.dataManager.loadData(this.data || { objects: [] });
343
+
344
+ // Вызываем коллбек onLoad
345
+ if (typeof this.options.onLoad === 'function') {
346
+ this.options.onLoad({ success: true, data: this.data || { objects: [] } });
347
+ }
331
348
  return;
332
349
  }
333
350
 
334
- // Пытаемся загрузить с сервера
335
- const boardData = await this.coreMoodboard.saveManager.loadBoardData(boardId);
351
+ console.log(`📦 MoodBoard: загружаем доску ${boardId} с ${this.options.apiUrl}`);
336
352
 
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
- }
353
+ // Формируем URL для загрузки
354
+ const loadUrl = this.options.apiUrl.endsWith('/')
355
+ ? `${this.options.apiUrl}load/${boardId}`
356
+ : `${this.options.apiUrl}/load/${boardId}`;
357
+
358
+ // Загружаем с сервера через fetch
359
+ const response = await fetch(loadUrl, {
360
+ method: 'GET',
361
+ headers: {
362
+ 'Content-Type': 'application/json',
363
+ 'X-CSRF-TOKEN': this.getCsrfToken()
364
+ }
365
+ });
366
+
367
+ if (!response.ok) {
368
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
369
+ }
370
+
371
+ const boardData = await response.json();
372
+
373
+ if (boardData && boardData.data) {
374
+ console.log('✅ MoodBoard: данные загружены с сервера', boardData.data);
375
+ this.dataManager.loadData(boardData.data);
376
+
377
+ // Вызываем коллбек onLoad
378
+ if (typeof this.options.onLoad === 'function') {
379
+ this.options.onLoad({ success: true, data: boardData.data });
347
380
  }
348
- this.dataManager.loadData(restoredData);
349
381
  } else {
350
- this.dataManager.loadData(this.data);
382
+ console.log('📦 MoodBoard: нет данных с сервера, загружаем пустую доску');
383
+ this.dataManager.loadData(this.data || { objects: [] });
384
+
385
+ // Вызываем коллбек onLoad
386
+ if (typeof this.options.onLoad === 'function') {
387
+ this.options.onLoad({ success: true, data: this.data || { objects: [] } });
388
+ }
351
389
  }
352
390
 
353
391
  } catch (error) {
354
- console.warn('⚠️ Ошибка загрузки доски, создаем новую:', error.message);
355
- // Если загрузка не удалась, используем дефолтные данные
356
- this.dataManager.loadData(this.data);
392
+ console.warn('⚠️ MoodBoard: ошибка загрузки доски, создаем новую:', error.message);
393
+ this.dataManager.loadData(this.data || { objects: [] });
394
+
395
+ // Вызываем коллбек onLoad с ошибкой
396
+ if (typeof this.options.onLoad === 'function') {
397
+ this.options.onLoad({ success: false, error: error.message, data: this.data || { objects: [] } });
398
+ }
357
399
  }
358
400
  }
359
401
 
@@ -435,5 +477,206 @@ export class MoodBoard {
435
477
  // Очищаем ссылку на контейнер
436
478
  this.container = null;
437
479
 
480
+ // Вызываем коллбек onDestroy
481
+ if (typeof this.options.onDestroy === 'function') {
482
+ try {
483
+ this.options.onDestroy();
484
+ } catch (error) {
485
+ console.warn('⚠️ Ошибка в коллбеке onDestroy:', error);
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Настройка коллбеков событий
492
+ */
493
+ setupEventCallbacks() {
494
+ if (!this.coreMoodboard || !this.coreMoodboard.eventBus) return;
495
+
496
+ // Коллбек для успешного сохранения
497
+ if (typeof this.options.onSave === 'function') {
498
+ this.coreMoodboard.eventBus.on('save:success', (data) => {
499
+ try {
500
+ // Создаем объединенный скриншот с HTML текстом
501
+ let screenshot = null;
502
+ if (this.coreMoodboard.pixi && this.coreMoodboard.pixi.app && this.coreMoodboard.pixi.app.view) {
503
+ screenshot = this.createCombinedScreenshot('image/jpeg', 0.6);
504
+ }
505
+
506
+ this.options.onSave({
507
+ success: true,
508
+ data: data,
509
+ screenshot: screenshot,
510
+ boardId: this.options.boardId
511
+ });
512
+ } catch (error) {
513
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
514
+ }
515
+ });
516
+
517
+ // Коллбек для ошибки сохранения
518
+ this.coreMoodboard.eventBus.on('save:error', (data) => {
519
+ try {
520
+ this.options.onSave({
521
+ success: false,
522
+ error: data.error,
523
+ boardId: this.options.boardId
524
+ });
525
+ } catch (error) {
526
+ console.warn('⚠️ Ошибка в коллбеке onSave:', error);
527
+ }
528
+ });
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Получение CSRF токена из всех возможных источников
534
+ */
535
+ getCsrfToken() {
536
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ||
537
+ window.csrfToken ||
538
+ this.options.csrfToken ||
539
+ '';
540
+ }
541
+
542
+ /**
543
+ * Публичный метод для загрузки данных из API
544
+ */
545
+ async loadFromApi(boardId = null) {
546
+ const targetBoardId = boardId || this.options.boardId;
547
+ if (!targetBoardId) {
548
+ throw new Error('boardId не указан');
549
+ }
550
+
551
+ // Временно меняем boardId для загрузки
552
+ const originalBoardId = this.options.boardId;
553
+ this.options.boardId = targetBoardId;
554
+
555
+ try {
556
+ await this.loadExistingBoard();
557
+ } finally {
558
+ // Восстанавливаем оригинальный boardId
559
+ this.options.boardId = originalBoardId;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Публичный метод для экспорта скриншота с HTML текстом
565
+ */
566
+ exportScreenshot(format = 'image/jpeg', quality = 0.6) {
567
+ return this.createCombinedScreenshot(format, quality);
568
+ }
569
+
570
+ /**
571
+ * Разбивает текст на строки с учетом ширины элемента (имитирует HTML word-break: break-word)
572
+ */
573
+ wrapText(ctx, text, maxWidth) {
574
+ const lines = [];
575
+
576
+ if (!text || maxWidth <= 0) {
577
+ return [text];
578
+ }
579
+
580
+ // Разбиваем по символам если не помещается (имитирует word-break: break-word)
581
+ let currentLine = '';
582
+
583
+ for (let i = 0; i < text.length; i++) {
584
+ const char = text[i];
585
+ const testLine = currentLine + char;
586
+ const metrics = ctx.measureText(testLine);
587
+
588
+ if (metrics.width > maxWidth && currentLine !== '') {
589
+ // Текущая строка не помещается, сохраняем предыдущую
590
+ lines.push(currentLine);
591
+ currentLine = char;
592
+ } else {
593
+ currentLine = testLine;
594
+ }
595
+ }
596
+
597
+ // Добавляем последнюю строку
598
+ if (currentLine) {
599
+ lines.push(currentLine);
600
+ }
601
+
602
+ return lines.length > 0 ? lines : [text];
603
+ }
604
+
605
+ /**
606
+ * Создает объединенный скриншот: PIXI canvas + HTML текстовые элементы
607
+ */
608
+ createCombinedScreenshot(format = 'image/jpeg', quality = 0.6) {
609
+ if (!this.coreMoodboard || !this.coreMoodboard.pixi || !this.coreMoodboard.pixi.app || !this.coreMoodboard.pixi.app.view) {
610
+ throw new Error('Canvas не найден');
611
+ }
612
+
613
+ try {
614
+ // Получаем PIXI canvas
615
+ const pixiCanvas = this.coreMoodboard.pixi.app.view;
616
+ const pixiWidth = pixiCanvas.width;
617
+ const pixiHeight = pixiCanvas.height;
618
+
619
+ // Создаем временный canvas для объединения
620
+ const combinedCanvas = document.createElement('canvas');
621
+ combinedCanvas.width = pixiWidth;
622
+ combinedCanvas.height = pixiHeight;
623
+ const ctx = combinedCanvas.getContext('2d');
624
+
625
+ // 1. Рисуем PIXI canvas как основу
626
+ ctx.drawImage(pixiCanvas, 0, 0);
627
+
628
+ // 2. Рисуем HTML текстовые элементы поверх
629
+ const textElements = document.querySelectorAll('.mb-text');
630
+
631
+ textElements.forEach((textEl, index) => {
632
+ try {
633
+ // Получаем стили и позицию элемента
634
+ const computedStyle = window.getComputedStyle(textEl);
635
+ const text = textEl.textContent || '';
636
+
637
+ // Проверяем видимость
638
+ if (computedStyle.visibility === 'hidden' || computedStyle.opacity === '0' || !text.trim()) {
639
+ return;
640
+ }
641
+
642
+ // Используем CSS позицию (абсолютная позиция)
643
+ const left = parseInt(textEl.style.left) || 0;
644
+ const top = parseInt(textEl.style.top) || 0;
645
+
646
+ // Настраиваем стили текста
647
+ const fontSize = parseInt(computedStyle.fontSize) || 18;
648
+ const fontFamily = computedStyle.fontFamily || 'Arial, sans-serif';
649
+ const color = computedStyle.color || '#000000';
650
+
651
+ ctx.font = `${fontSize}px ${fontFamily}`;
652
+ ctx.fillStyle = color;
653
+ ctx.textAlign = 'left';
654
+ ctx.textBaseline = 'top';
655
+
656
+ // Получаем размеры элемента
657
+ const elementWidth = parseInt(textEl.style.width) || 182;
658
+
659
+ // Разбиваем текст на строки и рисуем каждую строку
660
+ const lines = this.wrapText(ctx, text, elementWidth);
661
+ const lineHeight = fontSize * 1.3; // Межстрочный интервал
662
+
663
+ lines.forEach((line, lineIndex) => {
664
+ const yPos = top + (lineIndex * lineHeight) + 2;
665
+ ctx.fillText(line, left, yPos);
666
+ });
667
+ } catch (error) {
668
+ console.warn(`⚠️ Ошибка при рисовании текста ${index + 1}:`, error);
669
+ }
670
+ });
671
+
672
+ // 3. Экспортируем объединенный результат
673
+ return combinedCanvas.toDataURL(format, quality);
674
+
675
+ } catch (error) {
676
+ console.warn('⚠️ Ошибка при создании объединенного скриншота, используем только PIXI canvas:', error);
677
+ // Fallback: только PIXI canvas
678
+ const canvas = this.coreMoodboard.pixi.app.view;
679
+ return canvas.toDataURL(format, quality);
680
+ }
438
681
  }
439
682
  }