@sequent-org/moodboard 1.2.119 → 1.3.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 (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +6 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1773
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -999
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +71 -1180
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
package/src/ui/Toolbar.js CHANGED
@@ -3,7 +3,12 @@
3
3
  */
4
4
  import { Events } from '../core/events/Events.js';
5
5
  import { IconLoader } from '../utils/iconLoader.js';
6
- import { getInlinePngEmojiUrl, hasInlinePngEmoji } from '../utils/inlinePngEmojis.js';
6
+ import { ToolbarDialogsController } from './toolbar/ToolbarDialogsController.js';
7
+ import { ToolbarPopupsController } from './toolbar/ToolbarPopupsController.js';
8
+ import { ToolbarActionRouter } from './toolbar/ToolbarActionRouter.js';
9
+ import { ToolbarTooltipController } from './toolbar/ToolbarTooltipController.js';
10
+ import { ToolbarStateController } from './toolbar/ToolbarStateController.js';
11
+ import { ToolbarRenderer } from './toolbar/ToolbarRenderer.js';
7
12
 
8
13
  export class Toolbar {
9
14
  constructor(container, eventBus, theme = 'light', options = {}) {
@@ -19,6 +24,13 @@ export class Toolbar {
19
24
 
20
25
  // Кэш для SVG иконок
21
26
  this.icons = {};
27
+
28
+ this.dialogsController = new ToolbarDialogsController(this);
29
+ this.popupsController = new ToolbarPopupsController(this);
30
+ this.actionRouter = new ToolbarActionRouter(this);
31
+ this.tooltipController = new ToolbarTooltipController(this);
32
+ this.stateController = new ToolbarStateController(this);
33
+ this.renderer = new ToolbarRenderer(this);
22
34
 
23
35
  this.init();
24
36
  }
@@ -28,13 +40,13 @@ export class Toolbar {
28
40
  */
29
41
  async init() {
30
42
  try {
31
- // Инициализируем IconLoader и загружаем все иконки
32
43
  await this.iconLoader.init();
33
44
  this.icons = await this.iconLoader.loadAllIcons();
34
45
  } catch (error) {
35
46
  console.error('❌ Ошибка загрузки иконок:', error);
36
47
  }
37
-
48
+
49
+ this._toolActivatedHandler = ({ tool }) => this.setActiveToolbarButton(tool);
38
50
  this.createToolbar();
39
51
  this.attachEvents();
40
52
  this.setupHistoryEvents();
@@ -44,297 +56,54 @@ export class Toolbar {
44
56
  * Создает HTML структуру тулбара
45
57
  */
46
58
  createToolbar() {
47
- this.element = document.createElement('div');
48
- this.element.className = `moodboard-toolbar moodboard-toolbar--${this.theme}`;
49
-
50
- // Новые элементы интерфейса (без функционала)
51
- const newTools = [
52
- { id: 'select', iconName: 'select', title: 'Инструмент выделения (V)', type: 'activate-select' },
53
- { id: 'pan', iconName: 'pan', title: 'Панорамирование (Пробел)', type: 'activate-pan' },
54
- { id: 'divider', type: 'divider' },
55
- { id: 'text-add', iconName: 'text-add', title: 'Добавить текст', type: 'text-add' },
56
- { id: 'note', iconName: 'note', title: 'Добавить записку', type: 'note-add' },
57
- { id: 'image', iconName: 'image', title: 'Добавить картинку', type: 'image-add' },
58
- { id: 'shapes', iconName: 'shapes', title: 'Фигуры', type: 'custom-shapes' },
59
- { id: 'pencil', iconName: 'pencil', title: 'Рисование', type: 'custom-draw' },
60
- // { id: 'comments', iconName: 'comments', title: 'Комментарии', type: 'custom-comments' }, // Временно скрыто
61
- { id: 'attachments', iconName: 'attachments', title: 'Файлы', type: 'custom-attachments' },
62
- { id: 'emoji', iconName: 'emoji', title: 'Эмоджи', type: 'custom-emoji' }
63
- ];
64
-
65
- // Существующие элементы ниже новых
66
- // убрал { id: 'clear', iconName: 'clear', title: 'Очистить холст', type: 'clear' },
67
- const existingTools = [
68
- { id: 'frame', iconName: 'frame', title: 'Добавить фрейм', type: 'frame' },
69
- { id: 'divider', type: 'divider' },
70
- { id: 'undo', iconName: 'undo', title: 'Отменить (Ctrl+Z)', type: 'undo', disabled: true },
71
- { id: 'redo', iconName: 'redo', title: 'Повторить (Ctrl+Y)', type: 'redo', disabled: true }
72
- ];
73
-
74
- [...newTools, ...existingTools].forEach(tool => {
75
- if (tool.type === 'divider') {
76
- const divider = document.createElement('div');
77
- divider.className = 'moodboard-toolbar__divider';
78
- this.element.appendChild(divider);
79
- } else {
80
- const button = this.createButton(tool);
81
- this.element.appendChild(button);
82
- }
83
- });
84
-
85
- this.container.appendChild(this.element);
86
-
87
- // Создаем всплывающие панели (фигуры, рисование, эмоджи)
88
- this.createShapesPopup();
89
- this.createDrawPopup();
90
- this.createEmojiPopup();
91
- this.createFramePopup();
92
-
93
- // Подсветка активной кнопки на тулбаре по активному инструменту
94
- this.eventBus.on(Events.Tool.Activated, ({ tool }) => {
95
- this.setActiveToolbarButton(tool);
96
- });
97
-
98
- // Текущее состояние попапа рисования
99
- this.currentDrawTool = 'pencil';
59
+ return this.renderer.createToolbar();
100
60
  }
101
61
 
102
62
  createFramePopup() {
103
- this.framePopupEl = document.createElement('div');
104
- this.framePopupEl.className = 'moodboard-toolbar__popup frame-popup';
105
- this.framePopupEl.style.display = 'none';
106
-
107
- const makeBtn = (label, id, enabled, aspect, options = {}) => {
108
- const btn = document.createElement('button');
109
- btn.className = 'frame-popup__btn' + (enabled ? '' : ' is-disabled') + (options.header ? ' frame-popup__btn--header' : '');
110
- if (options.header) {
111
- // handled by CSS class
112
- }
113
- btn.dataset.id = id;
114
- // Внутри кнопки — превью (слева) и подпись (справа/ниже)
115
- const holder = document.createElement('div');
116
- holder.className = 'frame-popup__holder';
117
- let preview = document.createElement('div');
118
- if (options.header) {
119
- // Для «Произвольный» — горизонтальный пунктирный прямоугольник
120
- preview.className = 'frame-popup__preview frame-popup__preview--custom';
121
- } else {
122
- // Для пресетов — мини-превью с нужными пропорциями, слева от текста
123
- preview.className = 'frame-popup__preview';
124
- preview.style.aspectRatio = aspect || '1 / 1';
125
- }
126
- const caption = document.createElement('div');
127
- caption.textContent = label;
128
- caption.className = 'frame-popup__caption';
129
- holder.appendChild(preview);
130
- holder.appendChild(caption);
131
- btn.appendChild(holder);
132
- if (enabled) {
133
- btn.addEventListener('click', (e) => {
134
- e.stopPropagation();
135
- // Активируем place, устанавливаем pending для frame (А4)
136
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
137
- this.placeSelectedButtonId = 'frame';
138
- this.setActiveToolbarButton('place');
139
- if (id === 'custom') {
140
- // Рисовать фрейм вручную прямоугольником
141
- this.eventBus.emit(Events.Place.Set, { type: 'frame-draw', properties: {} });
142
- } else {
143
- // Подбираем размеры по пресету и увеличиваем площадь в 2 раза (масштаб по корню из 2)
144
- let width = 210, height = 297, titleText = 'A4';
145
- if (id === '1x1') { width = 300; height = 300; titleText = '1:1'; }
146
- else if (id === '4x3') { width = 320; height = 240; titleText = '4:3'; }
147
- else if (id === '16x9') { width = 320; height = 180; titleText = '16:9'; }
148
- const scale = 2; // х2 по сторонам = х4 по площади
149
- width = Math.round(width * scale);
150
- height = Math.round(height * scale);
151
- // Устанавливаем pending для размещения фрейма указанного размера
152
- this.eventBus.emit(Events.Place.Set, {
153
- type: 'frame',
154
- properties: {
155
- width,
156
- height,
157
- borderColor: 0x333333,
158
- fillColor: 0xFFFFFF,
159
- title: titleText,
160
- lockedAspect: true,
161
- type: id
162
- }
163
- });
164
- }
165
- this.closeFramePopup();
166
- });
167
- }
168
- this.framePopupEl.appendChild(btn);
169
- };
170
-
171
- // Верхний ряд: одна кнопка «Произвольный» (включаем рисование фрейма)
172
- makeBtn('Произвольный', 'custom', true, 'none', { header: true });
173
-
174
- makeBtn('A4', 'a4', true, '210 / 297');
175
- makeBtn('1:1', '1x1', true, '1 / 1');
176
- makeBtn('4:3', '4x3', true, '4 / 3');
177
- makeBtn('16:9', '16x9', true, '16 / 9');
178
-
179
- this.container.appendChild(this.framePopupEl);
63
+ return this.popupsController.createFramePopup();
180
64
  }
181
65
 
182
66
  toggleFramePopup(anchorBtn) {
183
- if (!this.framePopupEl) return;
184
- const visible = this.framePopupEl.style.display !== 'none';
185
- if (visible) {
186
- this.closeFramePopup();
187
- return;
188
- }
189
- const buttonRect = anchorBtn.getBoundingClientRect();
190
- const toolbarRect = this.container.getBoundingClientRect();
191
- // Сначала показываем невидимо, чтобы измерить размеры
192
- this.framePopupEl.style.display = 'grid';
193
- this.framePopupEl.style.visibility = 'hidden';
194
- const panelW = this.framePopupEl.offsetWidth || 120;
195
- const panelH = this.framePopupEl.offsetHeight || 120;
196
- // Горизонтально: как у панели фигур — от правого края тулбара + 8px
197
- const targetLeft = this.element.offsetWidth + 8;
198
- // Вертикально: центр панели на уровне центра кнопки, с тем же лёгким смещением -4px как у фигур
199
- const btnCenterY = buttonRect.top + buttonRect.height / 2;
200
- const targetTop = Math.max(0, Math.round(btnCenterY - toolbarRect.top - panelH / 2 - 4));
201
- this.framePopupEl.style.left = `${Math.round(targetLeft)}px`;
202
- this.framePopupEl.style.top = `${targetTop}px`;
203
- // Делаем видимой
204
- this.framePopupEl.style.visibility = '';
67
+ return this.popupsController.toggleFramePopup(anchorBtn);
205
68
  }
206
69
 
207
70
  closeFramePopup() {
208
- if (this.framePopupEl) this.framePopupEl.style.display = 'none';
71
+ return this.popupsController.closeFramePopup();
209
72
  }
210
73
 
211
74
  /**
212
75
  * Создает кнопку инструмента
213
76
  */
214
77
  createButton(tool) {
215
- const button = document.createElement('button');
216
- button.className = `moodboard-toolbar__button moodboard-toolbar__button--${tool.id}`;
217
- button.dataset.tool = tool.type;
218
- button.dataset.toolId = tool.id;
219
-
220
- // Устанавливаем disabled состояние если указано
221
- if (tool.disabled) {
222
- button.disabled = true;
223
- button.classList.add('moodboard-toolbar__button--disabled');
224
- }
225
-
226
- // Создаем tooltip если есть title
227
- if (tool.title) {
228
- this.createTooltip(button, tool.title);
229
- }
230
-
231
- // Создаем SVG иконку
232
- if (tool.iconName) {
233
- this.createSvgIcon(button, tool.iconName);
234
- }
235
-
236
- return button;
78
+ return this.renderer.createButton(tool);
237
79
  }
238
80
 
239
81
  /**
240
82
  * Создает SVG иконку для кнопки
241
83
  */
242
84
  createSvgIcon(button, iconName) {
243
- if (this.icons[iconName]) {
244
- // Создаем SVG элемент из загруженного содержимого
245
- const tempDiv = document.createElement('div');
246
- tempDiv.innerHTML = this.icons[iconName];
247
- const svg = tempDiv.querySelector('svg');
248
-
249
- if (svg) {
250
- // Убираем inline размеры, чтобы CSS мог их контролировать
251
- svg.removeAttribute('width');
252
- svg.removeAttribute('height');
253
- svg.style.display = 'block';
254
-
255
- // Добавляем SVG в кнопку
256
- button.appendChild(svg);
257
- }
258
- } else {
259
- // Fallback: создаем простую текстовую иконку
260
- const fallbackIcon = document.createElement('span');
261
- fallbackIcon.textContent = iconName.charAt(0).toUpperCase();
262
- fallbackIcon.style.fontSize = '14px';
263
- fallbackIcon.style.fontWeight = 'bold';
264
- button.appendChild(fallbackIcon);
265
- }
85
+ return this.renderer.createSvgIcon(button, iconName);
266
86
  }
267
87
 
268
88
  /**
269
89
  * Создает tooltip для кнопки
270
90
  */
271
91
  createTooltip(button, text) {
272
- // Создаем элемент tooltip
273
- const tooltip = document.createElement('div');
274
- tooltip.className = 'moodboard-tooltip';
275
- tooltip.textContent = text;
276
-
277
- // Добавляем tooltip в DOM
278
- document.body.appendChild(tooltip);
279
-
280
- // Переменные для управления tooltip
281
- let showTimeout;
282
- let hideTimeout;
283
-
284
- // Показываем tooltip при наведении
285
- button.addEventListener('mouseenter', () => {
286
- clearTimeout(hideTimeout);
287
- showTimeout = setTimeout(() => {
288
- this.showTooltip(tooltip, button);
289
- }, 300); // Задержка 300ms перед показом
290
- });
291
-
292
- // Скрываем tooltip при уходе мыши
293
- button.addEventListener('mouseleave', () => {
294
- clearTimeout(showTimeout);
295
- hideTimeout = setTimeout(() => {
296
- this.hideTooltip(tooltip);
297
- }, 100); // Задержка 100ms перед скрытием
298
- });
299
-
300
- // Скрываем tooltip при клике
301
- button.addEventListener('click', () => {
302
- clearTimeout(showTimeout);
303
- this.hideTooltip(tooltip);
304
- });
305
-
306
- // Сохраняем ссылку на tooltip в кнопке для очистки
307
- button._tooltip = tooltip;
92
+ return this.tooltipController.createTooltip(button, text);
308
93
  }
309
94
 
310
95
  /**
311
96
  * Показывает tooltip
312
97
  */
313
98
  showTooltip(tooltip, button) {
314
- // Получаем позицию кнопки
315
- const buttonRect = button.getBoundingClientRect();
316
- const toolbarRect = this.element.getBoundingClientRect();
317
-
318
- // Позиционируем tooltip справа от кнопки
319
- const left = buttonRect.right + 8; // 8px отступ справа от кнопки
320
- const top = buttonRect.top + (buttonRect.height / 2) - (tooltip.offsetHeight / 2); // центрируем по вертикали
321
-
322
- // Проверяем, чтобы tooltip не выходил за правую границу экрана
323
- const maxLeft = window.innerWidth - tooltip.offsetWidth - 8;
324
- const adjustedLeft = Math.min(left, maxLeft);
325
-
326
- tooltip.style.left = `${adjustedLeft}px`;
327
- tooltip.style.top = `${top}px`;
328
-
329
- // Показываем tooltip
330
- tooltip.classList.add('moodboard-tooltip--show');
99
+ return this.tooltipController.showTooltip(tooltip, button);
331
100
  }
332
101
 
333
102
  /**
334
103
  * Скрывает tooltip
335
104
  */
336
105
  hideTooltip(tooltip) {
337
- tooltip.classList.remove('moodboard-tooltip--show');
106
+ return this.tooltipController.hideTooltip(tooltip);
338
107
  }
339
108
 
340
109
  /**
@@ -344,219 +113,17 @@ export class Toolbar {
344
113
  this.element.addEventListener('click', (e) => {
345
114
  const button = e.target.closest('.moodboard-toolbar__button');
346
115
  if (!button || button.disabled) return;
347
-
116
+
348
117
  const toolType = button.dataset.tool;
349
118
  const toolId = button.dataset.toolId;
350
-
351
- // Обрабатываем undo/redo отдельно
352
- if (toolType === 'undo') {
353
- this.eventBus.emit(Events.Keyboard.Undo);
354
- this.animateButton(button);
355
- return;
356
- }
357
-
358
- if (toolType === 'redo') {
359
- this.eventBus.emit(Events.Keyboard.Redo);
360
- this.animateButton(button);
361
- return;
362
- }
363
-
364
- // Выбор инструмента выделения — отменяем режимы размещения и возвращаемся к select
365
- if (toolType === 'activate-select') {
366
- this.animateButton(button);
367
- this.closeShapesPopup();
368
- this.closeDrawPopup();
369
- this.closeEmojiPopup();
370
- // Сбрасываем отложенное размещение, активируем select
371
- this.eventBus.emit(Events.Place.Set, null);
372
- this.placeSelectedButtonId = null;
373
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
374
- this.setActiveToolbarButton('select');
375
- return;
376
- }
377
-
378
- // Временная активация панорамирования с панели
379
- if (toolType === 'activate-pan') {
380
- this.animateButton(button);
381
- this.closeShapesPopup();
382
- this.closeDrawPopup();
383
- this.closeEmojiPopup();
384
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'pan' });
385
- this.setActiveToolbarButton('pan');
386
- return;
387
- }
388
-
389
-
390
-
391
- // Добавление текста: включаем placement и ждём клика для выбора позиции
392
- if (toolType === 'text-add') {
393
- this.animateButton(button);
394
- this.closeShapesPopup();
395
- this.closeDrawPopup();
396
- this.closeEmojiPopup();
397
- // Переходим в универсальный placement tool и задаем pending конфигурацию
398
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
399
- this.placeSelectedButtonId = 'text';
400
- this.setActiveToolbarButton('place');
401
- this.eventBus.emit(Events.Place.Set, {
402
- type: 'text',
403
- // Специальный флаг: не создавать сразу объект, а открыть форму ввода на холсте
404
- properties: { editOnCreate: true, fontSize: 18 }
405
- });
406
- return;
407
- }
408
-
409
- // Добавление записки: включаем placement и ждём клика для выбора позиции
410
- if (toolType === 'note-add') {
411
- this.animateButton(button);
412
- this.closeShapesPopup();
413
- this.closeDrawPopup();
414
- this.closeEmojiPopup();
415
- // Активируем place, устанавливаем pending для note
416
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
417
- this.placeSelectedButtonId = 'note';
418
- this.setActiveToolbarButton('place');
419
- // Устанавливаем свойства записки по умолчанию
420
- this.eventBus.emit(Events.Place.Set, {
421
- type: 'note',
422
- properties: {
423
- content: 'Новая записка',
424
- fontFamily: 'Caveat, Arial, cursive',
425
- fontSize: 32,
426
- width: 250,
427
- height: 250
428
- }
429
- });
430
- return;
431
- }
432
-
433
- // Фрейм: показываем всплывающую панель с пресетами
434
- if (toolType === 'frame') {
435
- this.animateButton(button);
436
- this.toggleFramePopup(button);
437
- this.closeShapesPopup();
438
- this.closeDrawPopup();
439
- this.closeEmojiPopup();
440
- // Активируем place и подсвечиваем кнопку Frame
441
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
442
- this.placeSelectedButtonId = 'frame';
443
- this.setActiveToolbarButton('place');
444
- return;
445
- }
446
-
447
- // Добавление картинки — сразу открываем диалог выбора изображения
448
- if (toolType === 'image-add') {
449
- this.animateButton(button);
450
- this.closeShapesPopup();
451
- this.closeDrawPopup();
452
- this.closeEmojiPopup();
453
- // Открываем диалог выбора изображения
454
- this.openImageDialog();
455
- return;
456
- }
457
119
 
458
- // Комментарии — включаем режим размещения comment
459
- if (toolType === 'custom-comments') {
460
- this.animateButton(button);
461
- this.closeShapesPopup();
462
- this.closeDrawPopup();
463
- this.closeEmojiPopup();
464
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
465
- this.placeSelectedButtonId = 'comments';
466
- this.setActiveToolbarButton('place');
467
- // Увеличенный размер по умолчанию
468
- this.eventBus.emit(Events.Place.Set, { type: 'comment', properties: { width: 72, height: 72 } });
469
- return;
470
- }
471
-
472
- // Файлы — сразу открываем диалог выбора файла
473
- if (toolType === 'custom-attachments') {
474
- this.animateButton(button);
475
- this.closeShapesPopup();
476
- this.closeDrawPopup();
477
- this.closeEmojiPopup();
478
- // Открываем диалог выбора файла
479
- this.openFileDialog();
480
- return;
481
- }
482
-
483
- // Инструмент «Фрейм» — создаём через универсальный place-поток с размерами 200x300
484
- if (toolType === 'custom-frame') {
485
- this.animateButton(button);
486
- this.closeShapesPopup();
487
- this.closeDrawPopup();
488
- this.closeEmojiPopup();
489
- // Активируем режим размещения и устанавливаем pending
490
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
491
- this.placeSelectedButtonId = 'frame-tool';
492
- this.setActiveToolbarButton('place');
493
- this.eventBus.emit(Events.Place.Set, {
494
- type: 'frame',
495
- properties: { width: 200, height: 300 }
496
- });
497
- return;
498
- }
499
-
500
- // Тоггл всплывающей панели фигур
501
- if (toolType === 'custom-shapes') {
502
- this.animateButton(button);
503
- this.toggleShapesPopup(button);
504
- this.closeDrawPopup();
505
- this.closeEmojiPopup();
506
- // Активируем универсальный place tool для дальнейшего размещения
507
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
508
- this.placeSelectedButtonId = 'shapes';
509
- this.setActiveToolbarButton('place');
510
- return;
511
- }
512
-
513
- // Тоггл всплывающей панели рисования
514
- if (toolType === 'custom-draw') {
515
- this.animateButton(button);
516
- this.toggleDrawPopup(button);
517
- this.closeShapesPopup();
518
- this.closeEmojiPopup();
519
- // Выбираем инструмент рисования (последующее действие — на холсте)
520
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'draw' });
521
- this.setActiveToolbarButton('draw');
522
- return;
523
- }
524
-
525
- // Тоггл всплывающей панели эмоджи
526
- if (toolType === 'custom-emoji') {
527
- this.animateButton(button);
528
- this.toggleEmojiPopup(button);
529
- this.closeShapesPopup();
530
- this.closeDrawPopup();
531
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
532
- this.placeSelectedButtonId = 'emoji';
533
- this.setActiveToolbarButton('place'); // ← Исправление: подсвечиваем кнопку эмоджи
534
- return;
535
- }
536
-
537
- // Очистка холста - требует подтверждения
538
- if (toolType === 'clear') {
539
- this.animateButton(button);
540
- this.showClearConfirmation();
541
- return;
542
- }
543
-
544
- // Эмитим событие для других инструментов
545
- this.eventBus.emit(Events.UI.ToolbarAction, {
546
- type: toolType,
547
- id: toolId,
548
- position: this.getRandomPosition()
549
- });
550
-
551
- // Визуальная обратная связь
552
- this.animateButton(button);
120
+ this.actionRouter.routeToolbarAction(button, toolType, toolId);
553
121
  });
554
122
 
555
- // Клик вне попапов — закрыть
556
- document.addEventListener('click', (e) => {
557
- // ИСПРАВЛЕНИЕ: Защита от null элементов
123
+ // Клик вне попапов — закрыть (сохраняем handler для корректного removeEventListener)
124
+ this._documentClickHandler = (e) => {
558
125
  if (!e.target) return;
559
-
126
+
560
127
  const isInsideToolbar = this.element && this.element.contains(e.target);
561
128
  const isInsideShapesPopup = this.shapesPopupEl && this.shapesPopupEl.contains(e.target);
562
129
  const isInsideDrawPopup = this.drawPopupEl && this.drawPopupEl.contains(e.target);
@@ -572,57 +139,15 @@ export class Toolbar {
572
139
  this.closeEmojiPopup();
573
140
  this.closeFramePopup();
574
141
  }
575
- });
142
+ };
143
+ document.addEventListener('click', this._documentClickHandler);
576
144
  }
577
145
 
578
146
  /**
579
147
  * Подсвечивает активную кнопку на тулбаре в зависимости от активного инструмента
580
148
  */
581
149
  setActiveToolbarButton(toolName) {
582
- if (!this.element) return;
583
-
584
-
585
- // Сбрасываем активные классы
586
- this.element.querySelectorAll('.moodboard-toolbar__button--active').forEach(el => {
587
- el.classList.remove('moodboard-toolbar__button--active');
588
- });
589
-
590
- // Соответствие инструмент → кнопка
591
- const map = {
592
- select: 'select',
593
- pan: 'pan',
594
- draw: 'pencil',
595
- text: 'text-add' // Добавляем маппинг для text инструмента
596
- };
597
-
598
- let btnId = map[toolName];
599
-
600
- if (!btnId && toolName === 'place') {
601
- // Подсвечиваем тот источник place, который активен
602
- const placeButtonMap = {
603
- 'text': 'text-add',
604
- 'note': 'note',
605
- 'frame': 'frame',
606
- 'frame-tool': 'frame',
607
- 'comments': 'comments',
608
- 'attachments': 'attachments',
609
- 'shapes': 'shapes',
610
- 'emoji': 'emoji',
611
- null: 'image' // для изображений placeSelectedButtonId = null
612
- };
613
-
614
- btnId = placeButtonMap[this.placeSelectedButtonId] || 'shapes';
615
- }
616
-
617
- if (!btnId) {
618
- return;
619
- }
620
-
621
- const btn = this.element.querySelector(`.moodboard-toolbar__button--${btnId}`);
622
- if (btn) {
623
- btn.classList.add('moodboard-toolbar__button--active');
624
- } else {
625
- }
150
+ return this.stateController.setActiveToolbarButton(toolName);
626
151
  }
627
152
 
628
153
  /**
@@ -649,592 +174,71 @@ export class Toolbar {
649
174
  * Всплывающая панель с фигурами (UI)
650
175
  */
651
176
  createShapesPopup() {
652
- this.shapesPopupEl = document.createElement('div');
653
- this.shapesPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--shapes';
654
- this.shapesPopupEl.style.display = 'none';
655
-
656
- const grid = document.createElement('div');
657
- grid.className = 'moodboard-shapes__grid';
658
-
659
- const shapes = [
660
- // Перенесли кнопку "Добавить фигуру" сюда как первый элемент
661
- { id: 'shape', title: 'Добавить фигуру', isToolbarAction: true },
662
- { id: 'rounded-square', title: 'Скругленный квадрат' },
663
- { id: 'circle', title: 'Круг' },
664
- { id: 'triangle', title: 'Треугольник' },
665
- { id: 'diamond', title: 'Ромб' },
666
- { id: 'parallelogram', title: 'Параллелограмм' },
667
- { id: 'arrow', title: 'Стрелка' }
668
- ];
669
-
670
- shapes.forEach(s => {
671
- const btn = document.createElement('button');
672
- btn.className = `moodboard-shapes__btn moodboard-shapes__btn--${s.id}`;
673
- btn.title = s.title;
674
- const icon = document.createElement('span');
675
- if (s.isToolbarAction) {
676
- // Визуально как квадрат, действие — как старая кнопка "Добавить фигуру"
677
- icon.className = 'moodboard-shapes__icon shape-square';
678
- } else {
679
- icon.className = `moodboard-shapes__icon shape-${s.id}`;
680
- if (s.id === 'arrow') {
681
- // Залитая стрелка в стиле U+21E8 (прямоугольник + треугольник)
682
- 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>';
683
- }
684
- }
685
- btn.appendChild(icon);
686
- btn.addEventListener('click', () => {
687
- this.animateButton(btn);
688
- if (s.isToolbarAction) {
689
- // Режим: добавить дефолтную фигуру по клику на холсте
690
- this.eventBus.emit(Events.Place.Set, { type: 'shape', properties: { kind: 'square' } });
691
- this.closeShapesPopup();
692
- return;
693
- }
694
- // Для остальных фигур — запоминаем выбранную форму и ждём клика по холсту
695
- const propsMap = {
696
- 'rounded-square': { kind: 'rounded', cornerRadius: 10 },
697
- 'circle': { kind: 'circle' },
698
- 'triangle': { kind: 'triangle' },
699
- 'diamond': { kind: 'diamond' },
700
- 'parallelogram': { kind: 'parallelogram' },
701
- 'arrow': { kind: 'arrow' }
702
- };
703
- const props = propsMap[s.id] || { kind: 'square' };
704
- this.eventBus.emit(Events.Place.Set, { type: 'shape', properties: props });
705
- this.closeShapesPopup();
706
- });
707
- grid.appendChild(btn);
708
- });
709
-
710
- this.shapesPopupEl.appendChild(grid);
711
- // Добавляем попап внутрь контейнера тулбара
712
- this.container.appendChild(this.shapesPopupEl);
177
+ return this.popupsController.createShapesPopup();
713
178
  }
714
179
 
715
180
  toggleShapesPopup(anchorButton) {
716
- if (!this.shapesPopupEl) return;
717
- if (this.shapesPopupEl.style.display === 'none') {
718
- this.openShapesPopup(anchorButton);
719
- } else {
720
- this.closeShapesPopup();
721
- }
181
+ return this.popupsController.toggleShapesPopup(anchorButton);
722
182
  }
723
183
 
724
184
  openShapesPopup(anchorButton) {
725
- if (!this.shapesPopupEl) return;
726
- // Позиционируем справа от тулбара, по вертикали — напротив кнопки
727
- const toolbarRect = this.container.getBoundingClientRect();
728
- const buttonRect = anchorButton.getBoundingClientRect();
729
- const top = buttonRect.top - toolbarRect.top - 4; // легкое выравнивание
730
- const left = this.element.offsetWidth + 8; // отступ от тулбара
731
- this.shapesPopupEl.style.top = `${top}px`;
732
- this.shapesPopupEl.style.left = `${left}px`;
733
- this.shapesPopupEl.style.display = 'block';
185
+ return this.popupsController.openShapesPopup(anchorButton);
734
186
  }
735
187
 
736
188
  closeShapesPopup() {
737
- if (this.shapesPopupEl) {
738
- this.shapesPopupEl.style.display = 'none';
739
- }
189
+ return this.popupsController.closeShapesPopup();
740
190
  }
741
191
 
742
192
  /**
743
193
  * Всплывающая панель рисования (UI)
744
194
  */
745
195
  createDrawPopup() {
746
- this.drawPopupEl = document.createElement('div');
747
- this.drawPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--draw';
748
- this.drawPopupEl.style.display = 'none';
749
-
750
- const grid = document.createElement('div');
751
- grid.className = 'moodboard-draw__grid';
752
-
753
- // Первый ряд: карандаш, маркер, ластик (иконки SVG)
754
- const tools = [
755
- { id: 'pencil-tool', tool: 'pencil', title: 'Карандаш', svg: '<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M14.492 3.414 8.921 8.985a4.312 4.312 0 0 0 6.105 6.09l5.564-5.562 1.414 1.414-5.664 5.664a6.002 6.002 0 0 1-2.182 1.392L3.344 21.94 2.06 20.656 6.02 9.845c.3-.82.774-1.563 1.391-2.18l.093-.092.01-.01L13.077 2l1.415 1.414ZM4.68 19.32l4.486-1.64a6.305 6.305 0 0 1-1.651-1.19 6.306 6.306 0 0 1-1.192-1.655L4.68 19.32Z" clip-rule="evenodd"/></svg>' },
756
- { id: 'marker-tool', tool: 'marker', title: 'Маркер', svg: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" width="20" height="20" class="c-bxOhME c-bxOhME-dvzWZT-size-medium"><path fill="currentColor" fill-rule="evenodd" d="M12.737 2.676 8.531 7.264a1 1 0 0 0 .03 1.382l7.674 7.675a1 1 0 0 0 1.442-.029l4.589-4.97 1.468 1.357-4.588 4.97a3 3 0 0 1-3.46.689l-1.917 2.303-1.454.087-.63-.593-.828 1.38L10 22v-1l-.001-.001L10 22H1v-3l.18-.573 3.452-4.93-.817-.77.045-1.496 2.621-2.184a2.999 2.999 0 0 1 .577-3.134l4.205-4.589 1.474 1.352ZM3 19.315v.684h6.434l.76-1.268-4.09-3.85L3 19.314Zm3.007-7.27 6.904 6.498 1.217-1.46-6.667-6.25-1.454 1.212Z" clip-rule="evenodd"></path></svg>' },
757
- { id: 'eraser-tool', tool: 'eraser', title: 'Ластик', svg: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" width="20" height="20" class="c-bxOhME c-bxOhME-dvzWZT-size-medium"><path fill="currentColor" fill-rule="evenodd" d="M12.63 3.957 4.319 12.27a3 3 0 0 0 0 4.242L7.905 20.1 8.612 20.394H21v-2h-5.6l6.629-6.63a3 3 0 0 0 0-4.242L17.858 3.42a3 3 0 0 0-4.242 0ZM5.12 14.293a1 1 0 0 0 0 1.414L8.414 19h3.172l3-3L9 10.414l-3.879 3.88Zm10.336-8.922a1 1 0 0 0-1.414 0l-3.629 3.63L16 14.585l3.63-3.629a1 1 0 0 0 0-1.414L15.457 5.37Z" clip-rule="evenodd"></path></svg>' }
758
- ];
759
- const row1 = document.createElement('div');
760
- row1.className = 'moodboard-draw__row';
761
- this.drawRow1 = row1;
762
- tools.forEach(t => {
763
- const btn = document.createElement('button');
764
- btn.className = `moodboard-draw__btn moodboard-draw__btn--${t.id}`;
765
- btn.title = t.title;
766
- const icon = document.createElement('span');
767
- icon.className = 'draw-icon';
768
- icon.innerHTML = t.svg;
769
- btn.appendChild(icon);
770
- btn.addEventListener('click', () => {
771
- this.animateButton(btn);
772
- // Активируем инструмент рисования
773
- row1.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
774
- btn.classList.add('moodboard-draw__btn--active');
775
- this.currentDrawTool = t.tool;
776
- // Сообщаем текущий мод
777
- this.eventBus.emit(Events.Draw.BrushSet, { mode: t.tool });
778
- // Перестраиваем нижний ряд пресетов
779
- this.buildDrawPresets(row2);
780
- });
781
- row1.appendChild(btn);
782
- });
783
-
784
- // Второй ряд: толщина/цвет — круг + центральная точка
785
- const row2 = document.createElement('div');
786
- row2.className = 'moodboard-draw__row';
787
- this.drawRow2 = row2;
788
- this.buildDrawPresets = (container) => {
789
- container.innerHTML = '';
790
- if (this.currentDrawTool === 'pencil') {
791
- const sizes = [
792
- { id: 'size-thin-black', title: 'Тонкий черный', color: '#111827', dot: 4, width: 2 },
793
- { id: 'size-medium-red', title: 'Средний красный', color: '#ef4444', dot: 8, width: 4 },
794
- { id: 'size-thick-green', title: 'Толстый зеленый', color: '#16a34a', dot: 10, width: 6 }
795
- ];
796
- sizes.forEach(s => {
797
- const btn = document.createElement('button');
798
- btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
799
- btn.title = s.title;
800
- btn.dataset.brushWidth = String(s.width);
801
- btn.dataset.brushColor = s.color;
802
- const holder = document.createElement('span');
803
- holder.className = 'draw-size';
804
- const dot = document.createElement('span');
805
- dot.className = 'draw-dot';
806
- dot.style.background = s.color;
807
- dot.style.width = `${s.dot}px`;
808
- dot.style.height = `${s.dot}px`;
809
- holder.appendChild(dot);
810
- btn.appendChild(holder);
811
- btn.addEventListener('click', () => {
812
- this.animateButton(btn);
813
- container.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
814
- btn.classList.add('moodboard-draw__btn--active');
815
- const width = s.width;
816
- const color = parseInt(s.color.replace('#',''), 16);
817
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
818
- });
819
- container.appendChild(btn);
820
- });
821
- // Выставляем дефолт
822
- const first = container.querySelector('.moodboard-draw__btn');
823
- if (first) {
824
- first.classList.add('moodboard-draw__btn--active');
825
- const width = parseInt(first.dataset.brushWidth, 10) || 2;
826
- const color = parseInt((first.dataset.brushColor || '#111827').replace('#',''), 16);
827
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
828
- }
829
- } else if (this.currentDrawTool === 'marker') {
830
- const swatches = [
831
- { id: 'marker-yellow', title: 'Жёлтый', color: '#facc15' },
832
- { id: 'marker-green', title: 'Светло-зелёный', color: '#22c55e' },
833
- { id: 'marker-pink', title: 'Розовый', color: '#ec4899' }
834
- ];
835
- swatches.forEach(s => {
836
- const btn = document.createElement('button');
837
- btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
838
- btn.title = s.title;
839
- const sw = document.createElement('span');
840
- sw.className = 'draw-swatch';
841
- sw.style.background = s.color;
842
- btn.appendChild(sw);
843
- btn.addEventListener('click', () => {
844
- this.animateButton(btn);
845
- container.querySelectorAll('.moodboard-draw__btn--active').forEach(el => el.classList.remove('moodboard-draw__btn--active'));
846
- btn.classList.add('moodboard-draw__btn--active');
847
- const color = parseInt(s.color.replace('#',''), 16);
848
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
849
- });
850
- container.appendChild(btn);
851
- });
852
- // Дефолт — первый цвет
853
- const first = container.querySelector('.moodboard-draw__btn');
854
- if (first) {
855
- first.classList.add('moodboard-draw__btn--active');
856
- const color = parseInt(swatches[0].color.replace('#',''), 16);
857
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
858
- }
859
- } else if (this.currentDrawTool === 'eraser') {
860
- // Ластик — без пресетов
861
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'eraser' });
862
- }
863
- };
864
-
865
- grid.appendChild(row1);
866
- grid.appendChild(row2);
867
- this.drawPopupEl.appendChild(grid);
868
- this.container.appendChild(this.drawPopupEl);
869
- // Инициализируем верх/низ по умолчанию: активен карандаш и первый пресет
870
- const pencilBtn = row1.querySelector('.moodboard-draw__btn--pencil-tool');
871
- if (pencilBtn) pencilBtn.classList.add('moodboard-draw__btn--active');
872
- this.currentDrawTool = 'pencil';
873
- this.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil' });
874
- this.buildDrawPresets(row2);
196
+ return this.popupsController.createDrawPopup();
875
197
  }
876
198
 
877
199
  toggleDrawPopup(anchorButton) {
878
- if (!this.drawPopupEl) return;
879
- if (this.drawPopupEl.style.display === 'none') {
880
- this.openDrawPopup(anchorButton);
881
- } else {
882
- this.closeDrawPopup();
883
- }
200
+ return this.popupsController.toggleDrawPopup(anchorButton);
884
201
  }
885
202
 
886
203
  openDrawPopup(anchorButton) {
887
- if (!this.drawPopupEl) return;
888
- const toolbarRect = this.container.getBoundingClientRect();
889
- const buttonRect = anchorButton.getBoundingClientRect();
890
- const top = buttonRect.top - toolbarRect.top - 4;
891
- const left = this.element.offsetWidth + 8;
892
- this.drawPopupEl.style.top = `${top}px`;
893
- this.drawPopupEl.style.left = `${left}px`;
894
- this.drawPopupEl.style.display = 'block';
204
+ return this.popupsController.openDrawPopup(anchorButton);
895
205
  }
896
206
 
897
207
  closeDrawPopup() {
898
- if (this.drawPopupEl) {
899
- this.drawPopupEl.style.display = 'none';
900
- }
208
+ return this.popupsController.closeDrawPopup();
901
209
  }
902
210
 
903
211
  /**
904
212
  * Всплывающая панель эмоджи (UI)
905
213
  */
906
214
  createEmojiPopup() {
907
- this.emojiPopupEl = document.createElement('div');
908
- this.emojiPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--emoji';
909
- this.emojiPopupEl.style.display = 'none';
910
-
911
- // Загружаем файловые эмоджи и заменяем их на встроенные PNG data URL
912
- let groups = new Map();
913
- let convertedCount = 0;
914
-
915
- console.log('🎯 Создание EmojiPopup: заменяем файловые эмоджи на встроенные PNG...');
916
- if (typeof import.meta !== 'undefined' && import.meta.glob) {
917
- // Режим с bundler (Vite) - используем import.meta.glob
918
- const modules = import.meta.glob('../assets/emodji/**/*.{png,PNG,svg,SVG}', { eager: true, as: 'url' });
919
-
920
- // Группируем по подпапкам внутри emodji (категории)
921
- const entries = Object.entries(modules).sort(([a], [b]) => a.localeCompare(b));
922
- entries.forEach(([path, url]) => {
923
- const marker = '/emodji/';
924
- const idx = path.indexOf(marker);
925
- let category = 'Разное';
926
- if (idx >= 0) {
927
- const after = path.slice(idx + marker.length);
928
- const parts = after.split('/');
929
- category = parts.length > 1 ? parts[0] : 'Разное';
930
- }
931
-
932
- // Извлекаем код эмоджи из имени файла (например, "1f600.png" -> "1f600")
933
- const fileName = path.split('/').pop();
934
- const emojiCode = fileName.split('.')[0];
935
-
936
- // Заменяем на встроенный PNG data URL
937
- const inlineUrl = getInlinePngEmojiUrl(emojiCode);
938
-
939
- if (inlineUrl) {
940
- // Используем встроенный PNG
941
- if (!groups.has(category)) groups.set(category, []);
942
- groups.get(category).push({
943
- path: `inline:${emojiCode}`,
944
- url: inlineUrl,
945
- isInline: true,
946
- emojiCode: emojiCode
947
- });
948
- convertedCount++;
949
- } else {
950
- // Fallback на файловый URL (если встроенного нет)
951
- if (!groups.has(category)) groups.set(category, []);
952
- groups.get(category).push({ path, url, isInline: false });
953
- console.warn(`⚠️ Нет встроенного PNG для ${emojiCode}, используем файл`);
954
- }
955
- });
956
- } else {
957
- // Режим без bundler - используем статичный список
958
- const fallbackGroups = this.getFallbackEmojiGroups();
959
- fallbackGroups.forEach((items, category) => {
960
- if (!groups.has(category)) groups.set(category, []);
961
- groups.get(category).push(...items); // items уже содержат правильные isInline флаги
962
- convertedCount += items.filter(item => item.isInline).length;
963
- });
964
- }
965
-
966
- // Задаем желаемый порядок категорий (используем ваши оригинальные разделы)
967
- const ORDER = ['Смайлики', 'Жесты', 'Женские эмоции', 'Котики', 'Обезьянка', 'Разное'];
968
-
969
- console.log(`✅ Заменено ${convertedCount} файловых эмоджи на встроенные PNG`);
970
- const present = [...groups.keys()];
971
- const orderedFirst = ORDER.filter(name => groups.has(name));
972
- const theRest = present.filter(name => !ORDER.includes(name)).sort((a, b) => a.localeCompare(b));
973
- const orderedCategories = [...orderedFirst, ...theRest];
974
-
975
- // Рендерим секции по категориям в нужном порядке
976
- orderedCategories.forEach((cat) => {
977
- const section = document.createElement('div');
978
- section.className = 'moodboard-emoji__section';
979
-
980
- const title = document.createElement('div');
981
- title.className = 'moodboard-emoji__title';
982
- title.textContent = cat;
983
- section.appendChild(title);
984
-
985
- const grid = document.createElement('div');
986
- grid.className = 'moodboard-emoji__grid';
987
-
988
- groups.get(cat).forEach(({ url, isInline, emojiCode }) => {
989
- const btn = document.createElement('button');
990
- btn.className = 'moodboard-emoji__btn';
991
- btn.title = isInline ? `Встроенный PNG: ${emojiCode}` : 'Добавить изображение';
992
- const img = document.createElement('img');
993
- img.className = 'moodboard-emoji__img';
994
- img.src = url;
995
- img.alt = emojiCode || '';
996
- btn.appendChild(img);
997
-
998
- // Перетаскивание: начинаем только если был реальный drag (движение > 4px)
999
- btn.addEventListener('mousedown', (e) => {
1000
- // Блокируем одновременную обработку
1001
- if (btn.__clickProcessing || btn.__dragActive) return;
1002
-
1003
- const startX = e.clientX;
1004
- const startY = e.clientY;
1005
- let startedDrag = false;
1006
-
1007
- const onMove = (ev) => {
1008
- if (startedDrag) return;
1009
- const dx = Math.abs(ev.clientX - startX);
1010
- const dy = Math.abs(ev.clientY - startY);
1011
- if (dx > 4 || dy > 4) {
1012
- startedDrag = true;
1013
- btn.__dragActive = true;
1014
-
1015
- // Блокируем click handler
1016
- btn.__clickProcessing = true;
1017
-
1018
- const target = 64;
1019
- const targetW = target;
1020
- const targetH = target;
1021
- // Активируем инструмент размещения и включаем режим placeOnMouseUp
1022
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
1023
- this.eventBus.emit(Events.Place.Set, {
1024
- type: 'image',
1025
- properties: { src: url, width: targetW, height: targetH, isEmojiIcon: true },
1026
- size: { width: targetW, height: targetH },
1027
- placeOnMouseUp: true
1028
- });
1029
- // Закрываем поповер, чтобы не мешал курсору над холстом
1030
- this.closeEmojiPopup();
1031
- cleanup();
1032
- }
1033
- };
1034
- const onUp = () => {
1035
- cleanup();
1036
- // Снимаем флаги с задержкой
1037
- setTimeout(() => {
1038
- btn.__dragActive = false;
1039
- btn.__clickProcessing = false;
1040
- }, 50);
1041
- };
1042
- const cleanup = () => {
1043
- document.removeEventListener('mousemove', onMove);
1044
- document.removeEventListener('mouseup', onUp);
1045
- };
1046
- document.addEventListener('mousemove', onMove);
1047
- document.addEventListener('mouseup', onUp, { once: true });
1048
- });
1049
-
1050
- btn.addEventListener('click', (e) => {
1051
- // Блокируем обработку клика если был drag или если уже обрабатывается
1052
- if (btn.__dragActive || btn.__clickProcessing) return;
1053
-
1054
- btn.__clickProcessing = true;
1055
- setTimeout(() => { btn.__clickProcessing = false; }, 100);
1056
-
1057
- this.animateButton(btn);
1058
- const target = 64; // кратно 128 для лучшей четкости при даунскейле
1059
- const targetW = target;
1060
- const targetH = target;
1061
-
1062
- console.log(`🎯 Создаем эмоджи: ${isInline ? 'встроенный PNG' : 'файл'} (${emojiCode})`);
1063
-
1064
- this.eventBus.emit(Events.Place.Set, {
1065
- type: 'image',
1066
- properties: {
1067
- src: url,
1068
- width: targetW,
1069
- height: targetH,
1070
- isEmojiIcon: true,
1071
- isInlinePng: isInline || false,
1072
- emojiCode: emojiCode || null
1073
- },
1074
- size: { width: targetW, height: targetH }
1075
- });
1076
- this.closeEmojiPopup();
1077
- });
1078
-
1079
- grid.appendChild(btn);
1080
- });
1081
-
1082
- section.appendChild(grid);
1083
- this.emojiPopupEl.appendChild(section);
1084
- });
1085
- this.container.appendChild(this.emojiPopupEl);
215
+ return this.popupsController.createEmojiPopup();
1086
216
  }
1087
217
 
1088
218
  /**
1089
219
  * Возвращает fallback группы эмоджи для работы без bundler
1090
220
  */
1091
221
  getFallbackEmojiGroups() {
1092
- const groups = new Map();
1093
- let convertedCount = 0;
1094
-
1095
- console.log('🎯 Fallback режим: заменяем файловые эмоджи на встроенные PNG...');
1096
-
1097
- // Статичный список эмоджи с реальными именами файлов
1098
- const fallbackEmojis = {
1099
- 'Смайлики': [
1100
- '1f600', '1f601', '1f602', '1f603', '1f604', '1f605', '1f606', '1f607',
1101
- '1f609', '1f60a', '1f60b', '1f60c', '1f60d', '1f60e', '1f60f', '1f610',
1102
- '1f611', '1f612', '1f613', '1f614', '1f615', '1f616', '1f617', '1f618',
1103
- '1f619', '1f61a', '1f61b', '1f61c', '1f61d', '1f61e', '1f61f', '1f620',
1104
- '1f621', '1f622', '1f623', '1f624', '1f625', '1f626', '1f627', '1f628',
1105
- '1f629', '1f62a', '1f62b', '1f62c', '1f62d', '1f62e', '1f62f', '1f630',
1106
- '1f631', '1f632', '1f633', '1f635', '1f636', '1f641', '1f642', '2639', '263a'
1107
- ],
1108
- 'Жесты': [
1109
- '1f446', '1f447', '1f448', '1f449', '1f44a', '1f44b', '1f44c', '1f450',
1110
- '1f4aa', '1f590', '1f596', '1f64c', '1f64f', '261d', '270a', '270b', '270c', '270d'
1111
- ],
1112
- 'Женские эмоции': [
1113
- '1f645', '1f646', '1f64b', '1f64d', '1f64e'
1114
- ],
1115
- 'Котики': [
1116
- '1f638', '1f639', '1f63a', '1f63b', '1f63c', '1f63d', '1f63e', '1f63f', '1f640'
1117
- ],
1118
- 'Обезьянка': [
1119
- '1f435', '1f648', '1f649', '1f64a'
1120
- ],
1121
- 'Разное': [
1122
- '1f440', '1f441', '1f499', '1f4a1', '1f4a3', '1f4a9', '1f4ac', '1f4af', '203c', '26d4', '2764'
1123
- ]
1124
- };
1125
-
1126
- Object.entries(fallbackEmojis).forEach(([category, emojis]) => {
1127
- const emojiList = [];
1128
-
1129
- emojis.forEach(emojiCode => {
1130
- // Заменяем на встроенный PNG data URL
1131
- const inlineUrl = getInlinePngEmojiUrl(emojiCode);
1132
-
1133
- if (inlineUrl) {
1134
- emojiList.push({
1135
- path: `inline:${emojiCode}`,
1136
- url: inlineUrl,
1137
- isInline: true,
1138
- emojiCode: emojiCode
1139
- });
1140
- convertedCount++;
1141
- } else {
1142
- // Fallback на файловый URL (если встроенного нет)
1143
- const basePath = this.getEmojiBasePath();
1144
- emojiList.push({
1145
- path: `${basePath}${category}/${emojiCode}.png`,
1146
- url: `${basePath}${category}/${emojiCode}.png`,
1147
- isInline: false
1148
- });
1149
- console.warn(`⚠️ Нет встроенного PNG для ${emojiCode}, используем файл`);
1150
- }
1151
- });
1152
-
1153
- if (emojiList.length > 0) {
1154
- groups.set(category, emojiList);
1155
- }
1156
- });
1157
-
1158
- console.log(`✅ Fallback: заменено ${convertedCount} файловых эмоджи на встроенные PNG`);
1159
- return groups;
222
+ return this.popupsController.getFallbackEmojiGroups();
1160
223
  }
1161
224
 
1162
225
  /**
1163
226
  * Определяет базовый путь для эмоджи в зависимости от режима
1164
227
  */
1165
228
  getEmojiBasePath() {
1166
- // 1. Приоритет: опция basePath из конструктора
1167
- if (this.emojiBasePath) {
1168
- return this.emojiBasePath.endsWith('/') ? this.emojiBasePath : this.emojiBasePath + '/';
1169
- }
1170
-
1171
- // 2. Глобальная настройка (абсолютный URL)
1172
- if (window.MOODBOARD_BASE_PATH) {
1173
- const basePath = window.MOODBOARD_BASE_PATH.endsWith('/') ? window.MOODBOARD_BASE_PATH : window.MOODBOARD_BASE_PATH + '/';
1174
- return `${basePath}src/assets/emodji/`;
1175
- }
1176
-
1177
- // 3. Вычисление от URL текущего модуля (import.meta.url)
1178
- try {
1179
- // Используем import.meta.url для получения абсолютного пути к ассетам
1180
- const currentModuleUrl = import.meta.url;
1181
- // От текущего модуля (ui/Toolbar.js) поднимаемся к корню пакета и идем к assets
1182
- const emojiUrl = new URL('../assets/emodji/', currentModuleUrl).href;
1183
- return emojiUrl;
1184
- } catch (error) {
1185
- console.warn('⚠️ Не удалось определить путь через import.meta.url:', error);
1186
- }
1187
-
1188
- // 4. Fallback: поиск script тега для определения базового URL
1189
- try {
1190
- const currentScript = document.currentScript;
1191
- if (currentScript && currentScript.src) {
1192
- // Пытаемся определить от текущего скрипта
1193
- const scriptUrl = new URL(currentScript.src);
1194
- const baseUrl = new URL('../assets/emodji/', scriptUrl).href;
1195
- return baseUrl;
1196
- }
1197
- } catch (error) {
1198
- console.warn('⚠️ Не удалось определить путь через currentScript:', error);
1199
- }
1200
-
1201
- // 5. Последний fallback: абсолютный путь от корня домена
1202
- return '/src/assets/emodji/';
229
+ return this.popupsController.getEmojiBasePath();
1203
230
  }
1204
231
 
1205
232
  toggleEmojiPopup(anchorButton) {
1206
- if (!this.emojiPopupEl) return;
1207
- if (this.emojiPopupEl.style.display === 'none') {
1208
- this.openEmojiPopup(anchorButton);
1209
- } else {
1210
- this.closeEmojiPopup();
1211
- }
233
+ return this.popupsController.toggleEmojiPopup(anchorButton);
1212
234
  }
1213
235
 
1214
236
  openEmojiPopup(anchorButton) {
1215
- if (!this.emojiPopupEl) return;
1216
- const toolbarRect = this.container.getBoundingClientRect();
1217
- const buttonRect = anchorButton.getBoundingClientRect();
1218
- const left = this.element.offsetWidth + 8;
1219
- // Показать невидимо для вычисления размеров
1220
- this.emojiPopupEl.style.visibility = 'hidden';
1221
- this.emojiPopupEl.style.display = 'block';
1222
- // Рассчитать top так, чтобы попап не уходил за нижнюю границу
1223
- const desiredTop = buttonRect.top - toolbarRect.top - 4;
1224
- const popupHeight = this.emojiPopupEl.offsetHeight;
1225
- const containerHeight = this.container.clientHeight || toolbarRect.height;
1226
- const minTop = 8;
1227
- const maxTop = Math.max(minTop, containerHeight - popupHeight - 8);
1228
- const top = Math.min(Math.max(minTop, desiredTop), maxTop);
1229
- this.emojiPopupEl.style.top = `${top}px`;
1230
- this.emojiPopupEl.style.left = `${left}px`;
1231
- this.emojiPopupEl.style.visibility = 'visible';
237
+ return this.popupsController.openEmojiPopup(anchorButton);
1232
238
  }
1233
239
 
1234
240
  closeEmojiPopup() {
1235
- if (this.emojiPopupEl) {
1236
- this.emojiPopupEl.style.display = 'none';
1237
- }
241
+ return this.popupsController.closeEmojiPopup();
1238
242
  }
1239
243
 
1240
244
  /**
@@ -1281,179 +285,66 @@ export class Toolbar {
1281
285
  * Настройка обработчиков событий истории
1282
286
  */
1283
287
  setupHistoryEvents() {
1284
- // Слушаем изменения истории для обновления кнопок undo/redo
1285
- this.eventBus.on(Events.UI.UpdateHistoryButtons, (data) => {
1286
- this.updateHistoryButtons(data.canUndo, data.canRedo);
1287
- });
288
+ return this.stateController.setupHistoryEvents();
1288
289
  }
1289
290
 
1290
291
  /**
1291
292
  * Открывает диалог выбора файла и запускает режим "призрака"
1292
293
  */
1293
294
  async openFileDialog() {
1294
- const input = document.createElement('input');
1295
- input.type = 'file';
1296
- input.accept = '*/*'; // Принимаем любые файлы
1297
- input.style.display = 'none';
1298
- document.body.appendChild(input);
1299
-
1300
- input.addEventListener('change', async () => {
1301
- try {
1302
- const file = input.files && input.files[0];
1303
- if (!file) {
1304
- // Пользователь отменил выбор файла
1305
- this.eventBus.emit(Events.Place.FileCanceled);
1306
- return;
1307
- }
1308
-
1309
- // Файл выбран - запускаем режим "призрака"
1310
- this.eventBus.emit(Events.Place.FileSelected, {
1311
- file: file,
1312
- fileName: file.name,
1313
- fileSize: file.size,
1314
- mimeType: file.type,
1315
- properties: {
1316
- width: 120,
1317
- height: 140
1318
- }
1319
- });
1320
-
1321
- // Активируем инструмент размещения
1322
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
1323
- this.placeSelectedButtonId = 'attachments';
1324
- this.setActiveToolbarButton('place');
1325
-
1326
- } catch (error) {
1327
- console.error('Ошибка при выборе файла:', error);
1328
- alert('Ошибка при выборе файла: ' + error.message);
1329
- } finally {
1330
- input.remove();
1331
- }
1332
- }, { once: true });
1333
-
1334
- // Обработка отмены диалога (клик вне диалога или ESC)
1335
- const handleCancel = () => {
1336
- setTimeout(() => {
1337
- if (input.files.length === 0) {
1338
- this.eventBus.emit(Events.Place.FileCanceled);
1339
- input.remove();
1340
- }
1341
- window.removeEventListener('focus', handleCancel);
1342
- }, 100);
1343
- };
1344
-
1345
- window.addEventListener('focus', handleCancel, { once: true });
1346
- input.click();
295
+ return this.dialogsController.openFileDialog();
1347
296
  }
1348
297
 
1349
298
  /**
1350
299
  * Открывает диалог выбора изображения и запускает режим "призрака"
1351
300
  */
1352
301
  async openImageDialog() {
1353
- const input = document.createElement('input');
1354
- input.type = 'file';
1355
- input.accept = 'image/*'; // Принимаем только изображения
1356
- input.style.display = 'none';
1357
- document.body.appendChild(input);
1358
-
1359
- input.addEventListener('change', async () => {
1360
- try {
1361
- const file = input.files && input.files[0];
1362
- if (!file) {
1363
- // Пользователь отменил выбор изображения
1364
- this.eventBus.emit(Events.Place.ImageCanceled);
1365
- return;
1366
- }
1367
-
1368
- // Изображение выбрано - запускаем режим "призрака"
1369
- this.eventBus.emit(Events.Place.ImageSelected, {
1370
- file: file,
1371
- fileName: file.name,
1372
- fileSize: file.size,
1373
- mimeType: file.type,
1374
- properties: {
1375
- width: 300, // Дефолтная ширина для изображения
1376
- height: 200 // Дефолтная высота для изображения (будет пересчитана по пропорциям)
1377
- }
1378
- });
1379
-
1380
- // Активируем инструмент размещения
1381
- this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
1382
- this.placeSelectedButtonId = 'image';
1383
- this.setActiveToolbarButton('place');
1384
-
1385
- } catch (error) {
1386
- console.error('Ошибка при выборе изображения:', error);
1387
- alert('Ошибка при выборе изображения: ' + error.message);
1388
- } finally {
1389
- input.remove();
1390
- }
1391
- }, { once: true });
302
+ return this.dialogsController.openImageDialog();
303
+ }
1392
304
 
1393
- // Обработка отмены диалога (клик вне диалога или ESC)
1394
- const handleCancel = () => {
1395
- setTimeout(() => {
1396
- if (input.files.length === 0) {
1397
- this.eventBus.emit(Events.Place.ImageCanceled);
1398
- input.remove();
1399
- }
1400
- window.removeEventListener('focus', handleCancel);
1401
- }, 100);
1402
- };
1403
-
1404
- window.addEventListener('focus', handleCancel, { once: true });
1405
- input.click();
305
+ /**
306
+ * Открывает диалог выбора изображения для ImageObject2 (новая изолированная цепочка)
307
+ */
308
+ async openImageObject2Dialog() {
309
+ return this.dialogsController.openImageObject2Dialog();
1406
310
  }
1407
311
 
1408
312
  /**
1409
313
  * Обновление состояния кнопок undo/redo
1410
314
  */
1411
315
  updateHistoryButtons(canUndo, canRedo) {
1412
- const undoButton = this.element.querySelector('[data-tool="undo"]');
1413
- const redoButton = this.element.querySelector('[data-tool="redo"]');
1414
-
1415
- if (undoButton) {
1416
- undoButton.disabled = !canUndo;
1417
- if (canUndo) {
1418
- undoButton.classList.remove('moodboard-toolbar__button--disabled');
1419
- undoButton.title = 'Отменить последнее действие (Ctrl+Z)';
1420
- } else {
1421
- undoButton.classList.add('moodboard-toolbar__button--disabled');
1422
- undoButton.title = 'Нет действий для отмены';
1423
- }
1424
- }
1425
-
1426
- if (redoButton) {
1427
- redoButton.disabled = !canRedo;
1428
- if (canRedo) {
1429
- redoButton.classList.remove('moodboard-toolbar__button--disabled');
1430
- redoButton.title = 'Повторить отмененное действие (Ctrl+Y)';
1431
- } else {
1432
- redoButton.classList.add('moodboard-toolbar__button--disabled');
1433
- redoButton.title = 'Нет действий для повтора';
1434
- }
1435
- }
316
+ return this.stateController.updateHistoryButtons(canUndo, canRedo);
1436
317
  }
1437
318
 
1438
319
  /**
1439
320
  * Очистка ресурсов
1440
321
  */
1441
322
  destroy() {
323
+ // Удаляем document-level listener (предотвращение утечки памяти)
324
+ if (this._documentClickHandler) {
325
+ document.removeEventListener('click', this._documentClickHandler);
326
+ this._documentClickHandler = null;
327
+ }
328
+
329
+ // Отписываемся от Events.Tool.Activated (подписка в ToolbarRenderer)
330
+ if (this._toolActivatedHandler) {
331
+ this.eventBus.off(Events.Tool.Activated, this._toolActivatedHandler);
332
+ this._toolActivatedHandler = null;
333
+ }
334
+
1442
335
  if (this.element) {
1443
- // Очищаем все tooltips перед удалением элемента
1444
336
  const buttons = this.element.querySelectorAll('.moodboard-toolbar__button');
1445
- buttons.forEach(button => {
337
+ buttons.forEach((button) => {
1446
338
  if (button._tooltip) {
1447
339
  button._tooltip.remove();
1448
340
  button._tooltip = null;
1449
341
  }
1450
342
  });
1451
-
343
+
1452
344
  this.element.remove();
1453
345
  this.element = null;
1454
346
  }
1455
-
1456
- // Отписываемся от событий
347
+
1457
348
  this.eventBus.removeAllListeners(Events.UI.UpdateHistoryButtons);
1458
349
  }
1459
350