@sequent-org/moodboard 1.0.0

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.
Files changed (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. package/src/utils/topbarIconLoader.js +114 -0
@@ -0,0 +1,1118 @@
1
+ /**
2
+ * Панель инструментов для MoodBoard
3
+ */
4
+ import { Events } from '../core/events/Events.js';
5
+ import { IconLoader } from '../utils/iconLoader.js';
6
+
7
+ export class Toolbar {
8
+ constructor(container, eventBus, theme = 'light') {
9
+ this.container = container;
10
+ this.eventBus = eventBus;
11
+ this.theme = theme;
12
+
13
+ // Инициализируем IconLoader
14
+ this.iconLoader = new IconLoader();
15
+
16
+ // Кэш для SVG иконок
17
+ this.icons = {};
18
+
19
+ this.init();
20
+ }
21
+
22
+ /**
23
+ * Инициализация тулбара
24
+ */
25
+ async init() {
26
+ try {
27
+ // Инициализируем IconLoader и загружаем все иконки
28
+ await this.iconLoader.init();
29
+ this.icons = await this.iconLoader.loadAllIcons();
30
+ } catch (error) {
31
+ console.error('❌ Ошибка загрузки иконок:', error);
32
+ }
33
+
34
+ this.createToolbar();
35
+ this.attachEvents();
36
+ this.setupHistoryEvents();
37
+ }
38
+
39
+ /**
40
+ * Создает HTML структуру тулбара
41
+ */
42
+ createToolbar() {
43
+ this.element = document.createElement('div');
44
+ this.element.className = `moodboard-toolbar moodboard-toolbar--${this.theme}`;
45
+
46
+ // Новые элементы интерфейса (без функционала)
47
+ const newTools = [
48
+ { id: 'select', iconName: 'select', title: 'Инструмент выделения (V)', type: 'activate-select' },
49
+ { id: 'pan', iconName: 'pan', title: 'Панорамирование (Пробел)', type: 'activate-pan' },
50
+ { id: 'divider', type: 'divider' },
51
+ { id: 'text-add', iconName: 'text-add', title: 'Добавить текст', type: 'text-add' },
52
+ { id: 'note', iconName: 'note', title: 'Добавить записку', type: 'note-add' },
53
+ { id: 'image', iconName: 'image', title: 'Добавить картинку', type: 'image-add' },
54
+ { id: 'shapes', iconName: 'shapes', title: 'Фигуры', type: 'custom-shapes' },
55
+ { id: 'pencil', iconName: 'pencil', title: 'Рисование', type: 'custom-draw' },
56
+ { id: 'comments', iconName: 'comments', title: 'Комментарии', type: 'custom-comments' },
57
+ { id: 'attachments', iconName: 'attachments', title: 'Файлы', type: 'custom-attachments' },
58
+ { id: 'emoji', iconName: 'emoji', title: 'Эмоджи', type: 'custom-emoji' }
59
+ ];
60
+
61
+ // Существующие элементы ниже новых
62
+ const existingTools = [
63
+ { id: 'frame', iconName: 'frame', title: 'Добавить фрейм', type: 'frame' },
64
+ { id: 'divider', type: 'divider' },
65
+ { id: 'clear', iconName: 'clear', title: 'Очистить холст', type: 'clear' },
66
+ { id: 'divider', type: 'divider' },
67
+ { id: 'undo', iconName: 'undo', title: 'Отменить (Ctrl+Z)', type: 'undo', disabled: true },
68
+ { id: 'redo', iconName: 'redo', title: 'Повторить (Ctrl+Y)', type: 'redo', disabled: true }
69
+ ];
70
+
71
+ [...newTools, ...existingTools].forEach(tool => {
72
+ if (tool.type === 'divider') {
73
+ const divider = document.createElement('div');
74
+ divider.className = 'moodboard-toolbar__divider';
75
+ this.element.appendChild(divider);
76
+ } else {
77
+ const button = this.createButton(tool);
78
+ this.element.appendChild(button);
79
+ }
80
+ });
81
+
82
+ this.container.appendChild(this.element);
83
+
84
+ // Создаем всплывающие панели (фигуры, рисование, эмоджи)
85
+ this.createShapesPopup();
86
+ this.createDrawPopup();
87
+ this.createEmojiPopup();
88
+
89
+ // Подсветка активной кнопки на тулбаре по активному инструменту
90
+ this.eventBus.on(Events.Tool.Activated, ({ tool }) => {
91
+ this.setActiveToolbarButton(tool);
92
+ });
93
+
94
+ // Текущее состояние попапа рисования
95
+ this.currentDrawTool = 'pencil';
96
+ }
97
+
98
+ /**
99
+ * Создает кнопку инструмента
100
+ */
101
+ createButton(tool) {
102
+ const button = document.createElement('button');
103
+ button.className = `moodboard-toolbar__button moodboard-toolbar__button--${tool.id}`;
104
+ button.dataset.tool = tool.type;
105
+ button.dataset.toolId = tool.id;
106
+
107
+ // Устанавливаем disabled состояние если указано
108
+ if (tool.disabled) {
109
+ button.disabled = true;
110
+ button.classList.add('moodboard-toolbar__button--disabled');
111
+ }
112
+
113
+ // Создаем tooltip если есть title
114
+ if (tool.title) {
115
+ this.createTooltip(button, tool.title);
116
+ }
117
+
118
+ // Создаем SVG иконку
119
+ if (tool.iconName) {
120
+ this.createSvgIcon(button, tool.iconName);
121
+ }
122
+
123
+ return button;
124
+ }
125
+
126
+ /**
127
+ * Создает SVG иконку для кнопки
128
+ */
129
+ createSvgIcon(button, iconName) {
130
+ if (this.icons[iconName]) {
131
+ // Создаем SVG элемент из загруженного содержимого
132
+ const tempDiv = document.createElement('div');
133
+ tempDiv.innerHTML = this.icons[iconName];
134
+ const svg = tempDiv.querySelector('svg');
135
+
136
+ if (svg) {
137
+ // Убираем inline размеры, чтобы CSS мог их контролировать
138
+ svg.removeAttribute('width');
139
+ svg.removeAttribute('height');
140
+ svg.style.display = 'block';
141
+
142
+ // Добавляем SVG в кнопку
143
+ button.appendChild(svg);
144
+ }
145
+ } else {
146
+ // Fallback: создаем простую текстовую иконку
147
+ const fallbackIcon = document.createElement('span');
148
+ fallbackIcon.textContent = iconName.charAt(0).toUpperCase();
149
+ fallbackIcon.style.fontSize = '14px';
150
+ fallbackIcon.style.fontWeight = 'bold';
151
+ button.appendChild(fallbackIcon);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Создает tooltip для кнопки
157
+ */
158
+ createTooltip(button, text) {
159
+ // Создаем элемент tooltip
160
+ const tooltip = document.createElement('div');
161
+ tooltip.className = 'moodboard-tooltip';
162
+ tooltip.textContent = text;
163
+
164
+ // Добавляем tooltip в DOM
165
+ document.body.appendChild(tooltip);
166
+
167
+ // Переменные для управления tooltip
168
+ let showTimeout;
169
+ let hideTimeout;
170
+
171
+ // Показываем tooltip при наведении
172
+ button.addEventListener('mouseenter', () => {
173
+ clearTimeout(hideTimeout);
174
+ showTimeout = setTimeout(() => {
175
+ this.showTooltip(tooltip, button);
176
+ }, 300); // Задержка 300ms перед показом
177
+ });
178
+
179
+ // Скрываем tooltip при уходе мыши
180
+ button.addEventListener('mouseleave', () => {
181
+ clearTimeout(showTimeout);
182
+ hideTimeout = setTimeout(() => {
183
+ this.hideTooltip(tooltip);
184
+ }, 100); // Задержка 100ms перед скрытием
185
+ });
186
+
187
+ // Скрываем tooltip при клике
188
+ button.addEventListener('click', () => {
189
+ clearTimeout(showTimeout);
190
+ this.hideTooltip(tooltip);
191
+ });
192
+
193
+ // Сохраняем ссылку на tooltip в кнопке для очистки
194
+ button._tooltip = tooltip;
195
+ }
196
+
197
+ /**
198
+ * Показывает tooltip
199
+ */
200
+ showTooltip(tooltip, button) {
201
+ // Получаем позицию кнопки
202
+ const buttonRect = button.getBoundingClientRect();
203
+ const toolbarRect = this.element.getBoundingClientRect();
204
+
205
+ // Позиционируем tooltip справа от кнопки
206
+ const left = buttonRect.right + 8; // 8px отступ справа от кнопки
207
+ const top = buttonRect.top + (buttonRect.height / 2) - (tooltip.offsetHeight / 2); // центрируем по вертикали
208
+
209
+ // Проверяем, чтобы tooltip не выходил за правую границу экрана
210
+ const maxLeft = window.innerWidth - tooltip.offsetWidth - 8;
211
+ const adjustedLeft = Math.min(left, maxLeft);
212
+
213
+ tooltip.style.left = `${adjustedLeft}px`;
214
+ tooltip.style.top = `${top}px`;
215
+
216
+ // Показываем tooltip
217
+ tooltip.classList.add('moodboard-tooltip--show');
218
+ }
219
+
220
+ /**
221
+ * Скрывает tooltip
222
+ */
223
+ hideTooltip(tooltip) {
224
+ tooltip.classList.remove('moodboard-tooltip--show');
225
+ }
226
+
227
+ /**
228
+ * Подключает обработчики событий
229
+ */
230
+ attachEvents() {
231
+ this.element.addEventListener('click', (e) => {
232
+ const button = e.target.closest('.moodboard-toolbar__button');
233
+ if (!button || button.disabled) return;
234
+
235
+ const toolType = button.dataset.tool;
236
+ const toolId = button.dataset.toolId;
237
+
238
+ // Обрабатываем undo/redo отдельно
239
+ if (toolType === 'undo') {
240
+ this.eventBus.emit(Events.Keyboard.Undo);
241
+ this.animateButton(button);
242
+ return;
243
+ }
244
+
245
+ if (toolType === 'redo') {
246
+ this.eventBus.emit(Events.Keyboard.Redo);
247
+ this.animateButton(button);
248
+ return;
249
+ }
250
+
251
+ // Выбор инструмента выделения — отменяем режимы размещения и возвращаемся к select
252
+ if (toolType === 'activate-select') {
253
+ this.animateButton(button);
254
+ this.closeShapesPopup();
255
+ this.closeDrawPopup();
256
+ this.closeEmojiPopup();
257
+ // Сбрасываем отложенное размещение, активируем select
258
+ this.eventBus.emit(Events.Place.Set, null);
259
+ this.placeSelectedButtonId = null;
260
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
261
+ this.setActiveToolbarButton('select');
262
+ return;
263
+ }
264
+
265
+ // Временная активация панорамирования с панели
266
+ if (toolType === 'activate-pan') {
267
+ this.animateButton(button);
268
+ this.closeShapesPopup();
269
+ this.closeDrawPopup();
270
+ this.closeEmojiPopup();
271
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'pan' });
272
+ this.setActiveToolbarButton('pan');
273
+ return;
274
+ }
275
+
276
+
277
+
278
+ // Добавление текста: включаем placement и ждём клика для выбора позиции
279
+ if (toolType === 'text-add') {
280
+ this.animateButton(button);
281
+ this.closeShapesPopup();
282
+ this.closeDrawPopup();
283
+ this.closeEmojiPopup();
284
+ // Переходим в универсальный placement tool и задаем pending конфигурацию
285
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
286
+ this.placeSelectedButtonId = 'text';
287
+ this.setActiveToolbarButton('place');
288
+ this.eventBus.emit(Events.Place.Set, {
289
+ type: 'text',
290
+ // Специальный флаг: не создавать сразу объект, а открыть форму ввода на холсте
291
+ properties: { editOnCreate: true, fontSize: 18 }
292
+ });
293
+ return;
294
+ }
295
+
296
+ // Добавление записки: включаем placement и ждём клика для выбора позиции
297
+ if (toolType === 'note-add') {
298
+ this.animateButton(button);
299
+ this.closeShapesPopup();
300
+ this.closeDrawPopup();
301
+ this.closeEmojiPopup();
302
+ // Активируем place, устанавливаем pending для note
303
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
304
+ this.placeSelectedButtonId = 'note';
305
+ this.setActiveToolbarButton('place');
306
+ // Устанавливаем свойства записки по умолчанию
307
+ this.eventBus.emit(Events.Place.Set, {
308
+ type: 'note',
309
+ properties: {
310
+ content: 'Новая записка',
311
+ fontSize: 16,
312
+ width: 160,
313
+ height: 100
314
+ }
315
+ });
316
+ return;
317
+ }
318
+
319
+ // Добавление фрейма: включаем placement и ждём клика для выбора позиции
320
+ if (toolType === 'frame') {
321
+ this.animateButton(button);
322
+ this.closeShapesPopup();
323
+ this.closeDrawPopup();
324
+ this.closeEmojiPopup();
325
+ // Активируем place, устанавливаем pending для frame
326
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
327
+ this.placeSelectedButtonId = 'frame';
328
+ this.setActiveToolbarButton('place');
329
+ // Устанавливаем свойства фрейма по умолчанию
330
+ this.eventBus.emit(Events.Place.Set, {
331
+ type: 'frame',
332
+ properties: {
333
+ width: 200,
334
+ height: 300,
335
+ borderColor: 0x333333,
336
+ fillColor: 0xFFFFFF,
337
+ title: 'Новый' // Название по умолчанию
338
+ }
339
+ });
340
+ return;
341
+ }
342
+
343
+ // Добавление картинки — сразу открываем диалог выбора изображения
344
+ if (toolType === 'image-add') {
345
+ this.animateButton(button);
346
+ this.closeShapesPopup();
347
+ this.closeDrawPopup();
348
+ this.closeEmojiPopup();
349
+ // Открываем диалог выбора изображения
350
+ this.openImageDialog();
351
+ return;
352
+ }
353
+
354
+ // Комментарии — включаем режим размещения comment
355
+ if (toolType === 'custom-comments') {
356
+ this.animateButton(button);
357
+ this.closeShapesPopup();
358
+ this.closeDrawPopup();
359
+ this.closeEmojiPopup();
360
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
361
+ this.placeSelectedButtonId = 'comments';
362
+ this.setActiveToolbarButton('place');
363
+ // Увеличенный размер по умолчанию
364
+ this.eventBus.emit(Events.Place.Set, { type: 'comment', properties: { width: 72, height: 72 } });
365
+ return;
366
+ }
367
+
368
+ // Файлы — сразу открываем диалог выбора файла
369
+ if (toolType === 'custom-attachments') {
370
+ this.animateButton(button);
371
+ this.closeShapesPopup();
372
+ this.closeDrawPopup();
373
+ this.closeEmojiPopup();
374
+ // Открываем диалог выбора файла
375
+ this.openFileDialog();
376
+ return;
377
+ }
378
+
379
+ // Инструмент «Фрейм» — создаём через универсальный place-поток с размерами 200x300
380
+ if (toolType === 'custom-frame') {
381
+ this.animateButton(button);
382
+ this.closeShapesPopup();
383
+ this.closeDrawPopup();
384
+ this.closeEmojiPopup();
385
+ // Активируем режим размещения и устанавливаем pending
386
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
387
+ this.placeSelectedButtonId = 'frame-tool';
388
+ this.setActiveToolbarButton('place');
389
+ this.eventBus.emit(Events.Place.Set, {
390
+ type: 'frame',
391
+ properties: { width: 200, height: 300 }
392
+ });
393
+ return;
394
+ }
395
+
396
+ // Тоггл всплывающей панели фигур
397
+ if (toolType === 'custom-shapes') {
398
+ this.animateButton(button);
399
+ this.toggleShapesPopup(button);
400
+ this.closeDrawPopup();
401
+ this.closeEmojiPopup();
402
+ // Активируем универсальный place tool для дальнейшего размещения
403
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
404
+ this.placeSelectedButtonId = 'shapes';
405
+ this.setActiveToolbarButton('place');
406
+ return;
407
+ }
408
+
409
+ // Тоггл всплывающей панели рисования
410
+ if (toolType === 'custom-draw') {
411
+ this.animateButton(button);
412
+ this.toggleDrawPopup(button);
413
+ this.closeShapesPopup();
414
+ this.closeEmojiPopup();
415
+ // Выбираем инструмент рисования (последующее действие — на холсте)
416
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'draw' });
417
+ this.setActiveToolbarButton('draw');
418
+ return;
419
+ }
420
+
421
+ // Тоггл всплывающей панели эмоджи
422
+ if (toolType === 'custom-emoji') {
423
+ this.animateButton(button);
424
+ this.toggleEmojiPopup(button);
425
+ this.closeShapesPopup();
426
+ this.closeDrawPopup();
427
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
428
+ this.placeSelectedButtonId = 'emoji';
429
+ this.setActiveToolbarButton('place'); // ← Исправление: подсвечиваем кнопку эмоджи
430
+ return;
431
+ }
432
+
433
+ // Эмитим событие для других инструментов
434
+ this.eventBus.emit(Events.UI.ToolbarAction, {
435
+ type: toolType,
436
+ id: toolId,
437
+ position: this.getRandomPosition()
438
+ });
439
+
440
+ // Визуальная обратная связь
441
+ this.animateButton(button);
442
+ });
443
+
444
+ // Клик вне попапов — закрыть
445
+ document.addEventListener('click', (e) => {
446
+ const isInsideToolbar = this.element.contains(e.target);
447
+ const isInsideShapesPopup = this.shapesPopupEl && this.shapesPopupEl.contains(e.target);
448
+ const isInsideDrawPopup = this.drawPopupEl && this.drawPopupEl.contains(e.target);
449
+ const isInsideEmojiPopup = this.emojiPopupEl && this.emojiPopupEl.contains(e.target);
450
+ const isShapesButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--shapes');
451
+ const isDrawButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--pencil');
452
+ const isEmojiButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--emoji');
453
+ if (!isInsideToolbar && !isInsideShapesPopup && !isShapesButton && !isInsideDrawPopup && !isDrawButton && !isInsideEmojiPopup && !isEmojiButton) {
454
+ this.closeShapesPopup();
455
+ this.closeDrawPopup();
456
+ this.closeEmojiPopup();
457
+ }
458
+ });
459
+ }
460
+
461
+ /**
462
+ * Подсвечивает активную кнопку на тулбаре в зависимости от активного инструмента
463
+ */
464
+ setActiveToolbarButton(toolName) {
465
+ if (!this.element) return;
466
+
467
+ console.log('🎯 Toolbar: Установка активной кнопки для инструмента:', toolName, 'placeSelectedButtonId:', this.placeSelectedButtonId);
468
+
469
+ // Сбрасываем активные классы
470
+ this.element.querySelectorAll('.moodboard-toolbar__button--active').forEach(el => {
471
+ console.log('🔄 Deactivating button:', el.dataset.toolId);
472
+ el.classList.remove('moodboard-toolbar__button--active');
473
+ });
474
+
475
+ // Соответствие инструмент → кнопка
476
+ const map = {
477
+ select: 'select',
478
+ pan: 'pan',
479
+ draw: 'pencil',
480
+ text: 'text-add' // Добавляем маппинг для text инструмента
481
+ };
482
+
483
+ let btnId = map[toolName];
484
+
485
+ if (!btnId && toolName === 'place') {
486
+ // Подсвечиваем тот источник place, который активен
487
+ const placeButtonMap = {
488
+ 'text': 'text-add',
489
+ 'note': 'note',
490
+ 'frame': 'frame',
491
+ 'frame-tool': 'frame',
492
+ 'comments': 'comments',
493
+ 'attachments': 'attachments',
494
+ 'shapes': 'shapes',
495
+ 'emoji': 'emoji',
496
+ null: 'image' // для изображений placeSelectedButtonId = null
497
+ };
498
+
499
+ btnId = placeButtonMap[this.placeSelectedButtonId] || 'shapes';
500
+ }
501
+
502
+ if (!btnId) {
503
+ console.warn('⚠️ Toolbar: Не найден btnId для инструмента:', toolName);
504
+ return;
505
+ }
506
+
507
+ const btn = this.element.querySelector(`.moodboard-toolbar__button--${btnId}`);
508
+ if (btn) {
509
+ btn.classList.add('moodboard-toolbar__button--active');
510
+ console.log('✅ Toolbar: Активирована кнопка:', btnId);
511
+ } else {
512
+ console.warn('⚠️ Toolbar: Не найдена кнопка с селектором:', `.moodboard-toolbar__button--${btnId}`);
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Генерирует случайную позицию для нового объекта
518
+ */
519
+ getRandomPosition() {
520
+ return {
521
+ x: Math.random() * 300 + 50,
522
+ y: Math.random() * 200 + 50
523
+ };
524
+ }
525
+
526
+ /**
527
+ * Анимация нажатия кнопки
528
+ */
529
+ animateButton(button) {
530
+ button.style.transform = 'scale(0.95)';
531
+ setTimeout(() => {
532
+ button.style.transform = 'scale(1)';
533
+ }, 100);
534
+ }
535
+
536
+ /**
537
+ * Всплывающая панель с фигурами (UI)
538
+ */
539
+ createShapesPopup() {
540
+ this.shapesPopupEl = document.createElement('div');
541
+ this.shapesPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--shapes';
542
+ this.shapesPopupEl.style.display = 'none';
543
+
544
+ const grid = document.createElement('div');
545
+ grid.className = 'moodboard-shapes__grid';
546
+
547
+ const shapes = [
548
+ // Перенесли кнопку "Добавить фигуру" сюда как первый элемент
549
+ { id: 'shape', title: 'Добавить фигуру', isToolbarAction: true },
550
+ { id: 'rounded-square', title: 'Скругленный квадрат' },
551
+ { id: 'circle', title: 'Круг' },
552
+ { id: 'triangle', title: 'Треугольник' },
553
+ { id: 'diamond', title: 'Ромб' },
554
+ { id: 'parallelogram', title: 'Параллелограмм' },
555
+ { id: 'arrow', title: 'Стрелка' }
556
+ ];
557
+
558
+ shapes.forEach(s => {
559
+ const btn = document.createElement('button');
560
+ btn.className = `moodboard-shapes__btn moodboard-shapes__btn--${s.id}`;
561
+ btn.title = s.title;
562
+ const icon = document.createElement('span');
563
+ if (s.isToolbarAction) {
564
+ // Визуально как квадрат, действие — как старая кнопка "Добавить фигуру"
565
+ icon.className = 'moodboard-shapes__icon shape-square';
566
+ } else {
567
+ icon.className = `moodboard-shapes__icon shape-${s.id}`;
568
+ if (s.id === 'arrow') {
569
+ // Залитая стрелка в стиле U+21E8 (прямоугольник + треугольник)
570
+ icon.innerHTML = '<svg width="18" height="12" viewBox="0 0 18 12" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="0" y="5" width="12" height="2" rx="1" fill="#1d4ed8"/><path d="M12 0 L18 6 L12 12 Z" fill="#1d4ed8"/></svg>';
571
+ }
572
+ }
573
+ btn.appendChild(icon);
574
+ btn.addEventListener('click', () => {
575
+ this.animateButton(btn);
576
+ if (s.isToolbarAction) {
577
+ // Режим: добавить дефолтную фигуру по клику на холсте
578
+ this.eventBus.emit(Events.Place.Set, { type: 'shape', properties: { kind: 'square' } });
579
+ this.closeShapesPopup();
580
+ return;
581
+ }
582
+ // Для остальных фигур — запоминаем выбранную форму и ждём клика по холсту
583
+ const propsMap = {
584
+ 'rounded-square': { kind: 'rounded', cornerRadius: 10 },
585
+ 'circle': { kind: 'circle' },
586
+ 'triangle': { kind: 'triangle' },
587
+ 'diamond': { kind: 'diamond' },
588
+ 'parallelogram': { kind: 'parallelogram' },
589
+ 'arrow': { kind: 'arrow' }
590
+ };
591
+ const props = propsMap[s.id] || { kind: 'square' };
592
+ this.eventBus.emit(Events.Place.Set, { type: 'shape', properties: props });
593
+ this.closeShapesPopup();
594
+ });
595
+ grid.appendChild(btn);
596
+ });
597
+
598
+ this.shapesPopupEl.appendChild(grid);
599
+ // Добавляем попап внутрь контейнера тулбара
600
+ this.container.appendChild(this.shapesPopupEl);
601
+ }
602
+
603
+ toggleShapesPopup(anchorButton) {
604
+ if (!this.shapesPopupEl) return;
605
+ if (this.shapesPopupEl.style.display === 'none') {
606
+ this.openShapesPopup(anchorButton);
607
+ } else {
608
+ this.closeShapesPopup();
609
+ }
610
+ }
611
+
612
+ openShapesPopup(anchorButton) {
613
+ if (!this.shapesPopupEl) return;
614
+ // Позиционируем справа от тулбара, по вертикали — напротив кнопки
615
+ const toolbarRect = this.container.getBoundingClientRect();
616
+ const buttonRect = anchorButton.getBoundingClientRect();
617
+ const top = buttonRect.top - toolbarRect.top - 4; // легкое выравнивание
618
+ const left = this.element.offsetWidth + 8; // отступ от тулбара
619
+ this.shapesPopupEl.style.top = `${top}px`;
620
+ this.shapesPopupEl.style.left = `${left}px`;
621
+ this.shapesPopupEl.style.display = 'block';
622
+ }
623
+
624
+ closeShapesPopup() {
625
+ if (this.shapesPopupEl) {
626
+ this.shapesPopupEl.style.display = 'none';
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Всплывающая панель рисования (UI)
632
+ */
633
+ createDrawPopup() {
634
+ this.drawPopupEl = document.createElement('div');
635
+ this.drawPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--draw';
636
+ this.drawPopupEl.style.display = 'none';
637
+
638
+ const grid = document.createElement('div');
639
+ grid.className = 'moodboard-draw__grid';
640
+
641
+ // Первый ряд: карандаш, маркер, ластик (иконки SVG)
642
+ const tools = [
643
+ { id: 'pencil-tool', tool: 'pencil', title: 'Карандаш', svg: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M2 14 L14 2 L18 6 L6 18 L2 18 Z" fill="#1f2937"/><path d="M12 4 L16 8" stroke="#e5e7eb" stroke-width="2"/></svg>' },
644
+ { id: 'marker-tool', tool: 'marker', title: 'Маркер', svg: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="3" y="3" width="10" height="6" rx="2" fill="#1f2937"/><path d="M13 4 L17 8 L12 13 L8 9 Z" fill="#374151"/></svg>' },
645
+ { id: 'eraser-tool', tool: 'eraser', title: 'Ластик', svg: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="4" y="10" width="10" height="6" rx="2" transform="rotate(-45 4 10)" fill="#9ca3af"/><rect x="9" y="5" width="6" height="4" rx="1" transform="rotate(-45 9 5)" fill="#d1d5db"/></svg>' }
646
+ ];
647
+ const row1 = document.createElement('div');
648
+ row1.className = 'moodboard-draw__row';
649
+ this.drawRow1 = row1;
650
+ tools.forEach(t => {
651
+ const btn = document.createElement('button');
652
+ btn.className = `moodboard-draw__btn moodboard-draw__btn--${t.id}`;
653
+ btn.title = t.title;
654
+ const icon = document.createElement('span');
655
+ icon.className = 'draw-icon';
656
+ icon.innerHTML = t.svg;
657
+ btn.appendChild(icon);
658
+ btn.addEventListener('click', () => {
659
+ this.animateButton(btn);
660
+ // Активируем инструмент рисования
661
+ row1.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
662
+ btn.classList.add('moodboard-draw__btn--active');
663
+ this.currentDrawTool = t.tool;
664
+ // Сообщаем текущий мод
665
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: t.tool });
666
+ // Перестраиваем нижний ряд пресетов
667
+ this.buildDrawPresets(row2);
668
+ });
669
+ row1.appendChild(btn);
670
+ });
671
+
672
+ // Второй ряд: толщина/цвет — круг + центральная точка
673
+ const row2 = document.createElement('div');
674
+ row2.className = 'moodboard-draw__row';
675
+ this.drawRow2 = row2;
676
+ this.buildDrawPresets = (container) => {
677
+ container.innerHTML = '';
678
+ if (this.currentDrawTool === 'pencil') {
679
+ const sizes = [
680
+ { id: 'size-thin-black', title: 'Тонкий черный', color: '#111827', dot: 4, width: 2 },
681
+ { id: 'size-medium-red', title: 'Средний красный', color: '#ef4444', dot: 7, width: 4 },
682
+ { id: 'size-thick-green', title: 'Толстый зеленый', color: '#16a34a', dot: 10, width: 6 }
683
+ ];
684
+ sizes.forEach(s => {
685
+ const btn = document.createElement('button');
686
+ btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
687
+ btn.title = s.title;
688
+ btn.dataset.brushWidth = String(s.width);
689
+ btn.dataset.brushColor = s.color;
690
+ const holder = document.createElement('span');
691
+ holder.className = 'draw-size';
692
+ const dot = document.createElement('span');
693
+ dot.className = 'draw-dot';
694
+ dot.style.background = s.color;
695
+ dot.style.width = `${s.dot}px`;
696
+ dot.style.height = `${s.dot}px`;
697
+ holder.appendChild(dot);
698
+ btn.appendChild(holder);
699
+ btn.addEventListener('click', () => {
700
+ this.animateButton(btn);
701
+ container.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
702
+ btn.classList.add('moodboard-draw__btn--active');
703
+ const width = s.width;
704
+ const color = parseInt(s.color.replace('#',''), 16);
705
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
706
+ });
707
+ container.appendChild(btn);
708
+ });
709
+ // Выставляем дефолт
710
+ const first = container.querySelector('.moodboard-draw__btn');
711
+ if (first) {
712
+ first.classList.add('moodboard-draw__btn--active');
713
+ const width = parseInt(first.dataset.brushWidth, 10) || 2;
714
+ const color = parseInt((first.dataset.brushColor || '#111827').replace('#',''), 16);
715
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
716
+ }
717
+ } else if (this.currentDrawTool === 'marker') {
718
+ const swatches = [
719
+ { id: 'marker-yellow', title: 'Жёлтый', color: '#facc15' },
720
+ { id: 'marker-green', title: 'Светло-зелёный', color: '#22c55e' },
721
+ { id: 'marker-pink', title: 'Розовый', color: '#ec4899' }
722
+ ];
723
+ swatches.forEach(s => {
724
+ const btn = document.createElement('button');
725
+ btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
726
+ btn.title = s.title;
727
+ const sw = document.createElement('span');
728
+ sw.className = 'draw-swatch';
729
+ sw.style.background = s.color;
730
+ btn.appendChild(sw);
731
+ btn.addEventListener('click', () => {
732
+ this.animateButton(btn);
733
+ container.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
734
+ btn.classList.add('moodboard-draw__btn--active');
735
+ const color = parseInt(s.color.replace('#',''), 16);
736
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
737
+ });
738
+ container.appendChild(btn);
739
+ });
740
+ // Дефолт — первый цвет
741
+ const first = container.querySelector('.moodboard-draw__btn');
742
+ if (first) {
743
+ first.classList.add('moodboard-draw__btn--active');
744
+ const color = parseInt(swatches[0].color.replace('#',''), 16);
745
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
746
+ }
747
+ } else if (this.currentDrawTool === 'eraser') {
748
+ // Ластик — без пресетов
749
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'eraser' });
750
+ }
751
+ };
752
+
753
+ grid.appendChild(row1);
754
+ grid.appendChild(row2);
755
+ this.drawPopupEl.appendChild(grid);
756
+ this.container.appendChild(this.drawPopupEl);
757
+ // Инициализируем верх/низ по умолчанию: активен карандаш и первый пресет
758
+ const pencilBtn = row1.querySelector('.moodboard-draw__btn--pencil-tool');
759
+ if (pencilBtn) pencilBtn.classList.add('moodboard-draw__btn--active');
760
+ this.currentDrawTool = 'pencil';
761
+ this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil' });
762
+ this.buildDrawPresets(row2);
763
+ }
764
+
765
+ toggleDrawPopup(anchorButton) {
766
+ if (!this.drawPopupEl) return;
767
+ if (this.drawPopupEl.style.display === 'none') {
768
+ this.openDrawPopup(anchorButton);
769
+ } else {
770
+ this.closeDrawPopup();
771
+ }
772
+ }
773
+
774
+ openDrawPopup(anchorButton) {
775
+ if (!this.drawPopupEl) return;
776
+ const toolbarRect = this.container.getBoundingClientRect();
777
+ const buttonRect = anchorButton.getBoundingClientRect();
778
+ const top = buttonRect.top - toolbarRect.top - 4;
779
+ const left = this.element.offsetWidth + 8;
780
+ this.drawPopupEl.style.top = `${top}px`;
781
+ this.drawPopupEl.style.left = `${left}px`;
782
+ this.drawPopupEl.style.display = 'block';
783
+ }
784
+
785
+ closeDrawPopup() {
786
+ if (this.drawPopupEl) {
787
+ this.drawPopupEl.style.display = 'none';
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Всплывающая панель эмоджи (UI)
793
+ */
794
+ createEmojiPopup() {
795
+ this.emojiPopupEl = document.createElement('div');
796
+ this.emojiPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--emoji';
797
+ this.emojiPopupEl.style.display = 'none';
798
+
799
+ const categories = [
800
+ { title: 'Смайлики', items: ['😀','😁','😂','🤣','🙂','😊','😍','😘','😎','🤔','😴','😡','😭','😇','🤩','🤨','😐','😅','😏','🤗','🤫','😤','🤯','🤪'] },
801
+ { title: 'Жесты', items: ['👍','👎','👌','✌️','🤘','🤙','👏','🙌','🙏','💪','☝️','👋','🖐️','✋'] },
802
+ { title: 'Предметы', items: ['💡','📌','📎','📝','🖌️','🖼️','🗂️','📁','📷','🎥','🎯','🧩','🔒','🔑'] },
803
+ { title: 'Символы', items: ['⭐','🌟','✨','🔥','💥','⚡','❗','❓','✅','❌','💯','🔔','🌀'] },
804
+ { title: 'Животные', items: ['🐶','🐱','🦊','🐼','🐨','🐵','🐸','🐧','🐤','🦄','🐙'] }
805
+ ];
806
+
807
+ categories.forEach(cat => {
808
+ const section = document.createElement('div');
809
+ section.className = 'moodboard-emoji__section';
810
+ const title = document.createElement('div');
811
+ title.className = 'moodboard-emoji__title';
812
+ title.textContent = cat.title;
813
+ const grid = document.createElement('div');
814
+ grid.className = 'moodboard-emoji__grid';
815
+ cat.items.forEach(ch => {
816
+ const btn = document.createElement('button');
817
+ btn.className = 'moodboard-emoji__btn';
818
+ btn.title = ch;
819
+ btn.textContent = ch;
820
+ btn.addEventListener('click', () => {
821
+ this.animateButton(btn);
822
+ // Устанавливаем pending для размещения emoji кликом по холсту
823
+ const size = 48; // базовый размер
824
+ this.eventBus.emit(Events.Place.Set, {
825
+ type: 'emoji',
826
+ properties: { content: ch, fontSize: size, width: size, height: size },
827
+ size: { width: size, height: size },
828
+ // anchorCentered не используем, позиция ставится как топ-левт со смещением на половину размера
829
+ });
830
+ this.closeEmojiPopup();
831
+ });
832
+ grid.appendChild(btn);
833
+ });
834
+ section.appendChild(title);
835
+ section.appendChild(grid);
836
+ this.emojiPopupEl.appendChild(section);
837
+ });
838
+
839
+ // Разделительная линия
840
+ const divider = document.createElement('div');
841
+ divider.className = 'moodboard-emoji__divider';
842
+ this.emojiPopupEl.appendChild(divider);
843
+
844
+ // Стикеры (простые крупные эмодзи или пиктограммы)
845
+ const stickersTitle = document.createElement('div');
846
+ stickersTitle.className = 'moodboard-stickers__title';
847
+ stickersTitle.textContent = 'Стикеры';
848
+ const stickersGrid = document.createElement('div');
849
+ stickersGrid.className = 'moodboard-stickers__grid';
850
+
851
+ const stickers = ['📌','📎','🗂️','📁','🧩','🎯','💡','⭐','🔥','🚀','🎉','🧠'];
852
+ stickers.forEach(s => {
853
+ const btn = document.createElement('button');
854
+ btn.className = 'moodboard-sticker__btn';
855
+ btn.title = s;
856
+ btn.textContent = s;
857
+ btn.addEventListener('click', () => this.animateButton(btn));
858
+ stickersGrid.appendChild(btn);
859
+ });
860
+ this.emojiPopupEl.appendChild(stickersTitle);
861
+ this.emojiPopupEl.appendChild(stickersGrid);
862
+ this.container.appendChild(this.emojiPopupEl);
863
+ }
864
+
865
+ toggleEmojiPopup(anchorButton) {
866
+ if (!this.emojiPopupEl) return;
867
+ if (this.emojiPopupEl.style.display === 'none') {
868
+ this.openEmojiPopup(anchorButton);
869
+ } else {
870
+ this.closeEmojiPopup();
871
+ }
872
+ }
873
+
874
+ openEmojiPopup(anchorButton) {
875
+ if (!this.emojiPopupEl) return;
876
+ const toolbarRect = this.container.getBoundingClientRect();
877
+ const buttonRect = anchorButton.getBoundingClientRect();
878
+ const left = this.element.offsetWidth + 8;
879
+ // Показать невидимо для вычисления размеров
880
+ this.emojiPopupEl.style.visibility = 'hidden';
881
+ this.emojiPopupEl.style.display = 'block';
882
+ // Рассчитать top так, чтобы попап не уходил за нижнюю границу
883
+ const desiredTop = buttonRect.top - toolbarRect.top - 4;
884
+ const popupHeight = this.emojiPopupEl.offsetHeight;
885
+ const containerHeight = this.container.clientHeight || toolbarRect.height;
886
+ const minTop = 8;
887
+ const maxTop = Math.max(minTop, containerHeight - popupHeight - 8);
888
+ const top = Math.min(Math.max(minTop, desiredTop), maxTop);
889
+ this.emojiPopupEl.style.top = `${top}px`;
890
+ this.emojiPopupEl.style.left = `${left}px`;
891
+ this.emojiPopupEl.style.visibility = 'visible';
892
+ }
893
+
894
+ closeEmojiPopup() {
895
+ if (this.emojiPopupEl) {
896
+ this.emojiPopupEl.style.display = 'none';
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Изменение темы
902
+ */
903
+ setTheme(theme) {
904
+ this.theme = theme;
905
+ this.element.className = `moodboard-toolbar moodboard-toolbar--${theme}`;
906
+ }
907
+
908
+ /**
909
+ * Настройка обработчиков событий истории
910
+ */
911
+ setupHistoryEvents() {
912
+ // Слушаем изменения истории для обновления кнопок undo/redo
913
+ this.eventBus.on(Events.UI.UpdateHistoryButtons, (data) => {
914
+ this.updateHistoryButtons(data.canUndo, data.canRedo);
915
+ });
916
+ }
917
+
918
+ /**
919
+ * Открывает диалог выбора файла и запускает режим "призрака"
920
+ */
921
+ async openFileDialog() {
922
+ const input = document.createElement('input');
923
+ input.type = 'file';
924
+ input.accept = '*/*'; // Принимаем любые файлы
925
+ input.style.display = 'none';
926
+ document.body.appendChild(input);
927
+
928
+ input.addEventListener('change', async () => {
929
+ try {
930
+ const file = input.files && input.files[0];
931
+ if (!file) {
932
+ // Пользователь отменил выбор файла
933
+ this.eventBus.emit(Events.Place.FileCanceled);
934
+ return;
935
+ }
936
+
937
+ // Файл выбран - запускаем режим "призрака"
938
+ this.eventBus.emit(Events.Place.FileSelected, {
939
+ file: file,
940
+ fileName: file.name,
941
+ fileSize: file.size,
942
+ mimeType: file.type,
943
+ properties: {
944
+ width: 120,
945
+ height: 140
946
+ }
947
+ });
948
+
949
+ // Активируем инструмент размещения
950
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
951
+ this.placeSelectedButtonId = 'attachments';
952
+ this.setActiveToolbarButton('place');
953
+
954
+ } catch (error) {
955
+ console.error('Ошибка при выборе файла:', error);
956
+ alert('Ошибка при выборе файла: ' + error.message);
957
+ } finally {
958
+ input.remove();
959
+ }
960
+ }, { once: true });
961
+
962
+ // Обработка отмены диалога (клик вне диалога или ESC)
963
+ const handleCancel = () => {
964
+ setTimeout(() => {
965
+ if (input.files.length === 0) {
966
+ this.eventBus.emit(Events.Place.FileCanceled);
967
+ input.remove();
968
+ }
969
+ window.removeEventListener('focus', handleCancel);
970
+ }, 100);
971
+ };
972
+
973
+ window.addEventListener('focus', handleCancel, { once: true });
974
+ input.click();
975
+ }
976
+
977
+ /**
978
+ * Открывает диалог выбора изображения и запускает режим "призрака"
979
+ */
980
+ async openImageDialog() {
981
+ const input = document.createElement('input');
982
+ input.type = 'file';
983
+ input.accept = 'image/*'; // Принимаем только изображения
984
+ input.style.display = 'none';
985
+ document.body.appendChild(input);
986
+
987
+ input.addEventListener('change', async () => {
988
+ try {
989
+ const file = input.files && input.files[0];
990
+ if (!file) {
991
+ // Пользователь отменил выбор изображения
992
+ this.eventBus.emit(Events.Place.ImageCanceled);
993
+ return;
994
+ }
995
+
996
+ // Изображение выбрано - запускаем режим "призрака"
997
+ this.eventBus.emit(Events.Place.ImageSelected, {
998
+ file: file,
999
+ fileName: file.name,
1000
+ fileSize: file.size,
1001
+ mimeType: file.type,
1002
+ properties: {
1003
+ width: 300, // Дефолтная ширина для изображения
1004
+ height: 200 // Дефолтная высота для изображения (будет пересчитана по пропорциям)
1005
+ }
1006
+ });
1007
+
1008
+ // Активируем инструмент размещения
1009
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
1010
+ this.placeSelectedButtonId = 'image';
1011
+ this.setActiveToolbarButton('place');
1012
+
1013
+ } catch (error) {
1014
+ console.error('Ошибка при выборе изображения:', error);
1015
+ alert('Ошибка при выборе изображения: ' + error.message);
1016
+ } finally {
1017
+ input.remove();
1018
+ }
1019
+ }, { once: true });
1020
+
1021
+ // Обработка отмены диалога (клик вне диалога или ESC)
1022
+ const handleCancel = () => {
1023
+ setTimeout(() => {
1024
+ if (input.files.length === 0) {
1025
+ this.eventBus.emit(Events.Place.ImageCanceled);
1026
+ input.remove();
1027
+ }
1028
+ window.removeEventListener('focus', handleCancel);
1029
+ }, 100);
1030
+ };
1031
+
1032
+ window.addEventListener('focus', handleCancel, { once: true });
1033
+ input.click();
1034
+ }
1035
+
1036
+ /**
1037
+ * Обновление состояния кнопок undo/redo
1038
+ */
1039
+ updateHistoryButtons(canUndo, canRedo) {
1040
+ const undoButton = this.element.querySelector('[data-tool="undo"]');
1041
+ const redoButton = this.element.querySelector('[data-tool="redo"]');
1042
+
1043
+ if (undoButton) {
1044
+ undoButton.disabled = !canUndo;
1045
+ if (canUndo) {
1046
+ undoButton.classList.remove('moodboard-toolbar__button--disabled');
1047
+ undoButton.title = 'Отменить последнее действие (Ctrl+Z)';
1048
+ } else {
1049
+ undoButton.classList.add('moodboard-toolbar__button--disabled');
1050
+ undoButton.title = 'Нет действий для отмены';
1051
+ }
1052
+ }
1053
+
1054
+ if (redoButton) {
1055
+ redoButton.disabled = !canRedo;
1056
+ if (canRedo) {
1057
+ redoButton.classList.remove('moodboard-toolbar__button--disabled');
1058
+ redoButton.title = 'Повторить отмененное действие (Ctrl+Y)';
1059
+ } else {
1060
+ redoButton.classList.add('moodboard-toolbar__button--disabled');
1061
+ redoButton.title = 'Нет действий для повтора';
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ /**
1067
+ * Очистка ресурсов
1068
+ */
1069
+ destroy() {
1070
+ if (this.element) {
1071
+ // Очищаем все tooltips перед удалением элемента
1072
+ const buttons = this.element.querySelectorAll('.moodboard-toolbar__button');
1073
+ buttons.forEach(button => {
1074
+ if (button._tooltip) {
1075
+ button._tooltip.remove();
1076
+ button._tooltip = null;
1077
+ }
1078
+ });
1079
+
1080
+ this.element.remove();
1081
+ this.element = null;
1082
+ }
1083
+
1084
+ // Отписываемся от событий
1085
+ this.eventBus.removeAllListeners(Events.UI.UpdateHistoryButtons);
1086
+ }
1087
+
1088
+ /**
1089
+ * Принудительно обновляет иконку (для отладки)
1090
+ * @param {string} iconName - имя иконки
1091
+ */
1092
+ async reloadToolbarIcon(iconName) {
1093
+ console.log(`🔄 Начинаем обновление иконки ${iconName} в тулбаре...`);
1094
+ try {
1095
+ // Перезагружаем иконку
1096
+ const newSvgContent = await this.iconLoader.reloadIcon(iconName);
1097
+ this.icons[iconName] = newSvgContent;
1098
+
1099
+ // Находим кнопку с этой иконкой и обновляем её
1100
+ const button = this.element.querySelector(`[data-tool-id="${iconName}"]`);
1101
+ if (button) {
1102
+ // Очищаем старый SVG
1103
+ const oldSvg = button.querySelector('svg');
1104
+ if (oldSvg) {
1105
+ oldSvg.remove();
1106
+ }
1107
+
1108
+ // Добавляем новый SVG
1109
+ this.createSvgIcon(button, iconName);
1110
+ console.log(`✅ Иконка ${iconName} обновлена в интерфейсе!`);
1111
+ } else {
1112
+ console.warn(`⚠️ Кнопка с иконкой ${iconName} не найдена`);
1113
+ }
1114
+ } catch (error) {
1115
+ console.error(`❌ Ошибка обновления иконки ${iconName}:`, error);
1116
+ }
1117
+ }
1118
+ }