@sequent-org/moodboard 1.2.118 → 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 +7 -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 -1765
  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 -976
  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
@@ -1,4 +1,39 @@
1
1
  import { Events } from '../core/events/Events.js';
2
+ import {
3
+ bindTextPropertiesPanelControls,
4
+ unbindTextPropertiesPanelControls,
5
+ } from './text-properties/TextPropertiesPanelBindings.js';
6
+ import {
7
+ attachTextPropertiesPanelEventBridge,
8
+ detachTextPropertiesPanelEventBridge,
9
+ } from './text-properties/TextPropertiesPanelEventBridge.js';
10
+ import {
11
+ applyTextAppearanceToDom,
12
+ buildBackgroundColorUpdate,
13
+ buildFontFamilyUpdate,
14
+ buildFontSizeUpdate,
15
+ buildTextColorUpdate,
16
+ getControlValuesFromProperties,
17
+ getFallbackControlValues,
18
+ getObjectGeometry,
19
+ getObjectProperties,
20
+ getSelectedTextObjectId,
21
+ syncPixiTextProperties,
22
+ } from './text-properties/TextPropertiesPanelMapper.js';
23
+ import {
24
+ createTextPropertiesPanelRenderer,
25
+ hideBgColorDropdown,
26
+ hideColorDropdown,
27
+ toggleBgColorDropdown,
28
+ toggleColorDropdown,
29
+ updateCurrentBgColorButton,
30
+ updateCurrentColorButton,
31
+ } from './text-properties/TextPropertiesPanelRenderer.js';
32
+ import {
33
+ clearTextPropertiesPanelState,
34
+ createTextPropertiesPanelState,
35
+ resetCurrentSelection,
36
+ } from './text-properties/TextPropertiesPanelState.js';
2
37
 
3
38
  /**
4
39
  * TextPropertiesPanel — всплывающая панель свойств для текстовых объектов
@@ -8,11 +43,8 @@ export class TextPropertiesPanel {
8
43
  this.container = container;
9
44
  this.eventBus = eventBus;
10
45
  this.core = core;
11
- this.layer = null;
12
- this.panel = null;
13
- this.currentId = null;
14
- this.isTextEditing = false; // Флаг режима редактирования текста
15
-
46
+ Object.assign(this, createTextPropertiesPanelState());
47
+
16
48
  this._onDocMouseDown = this._onDocMouseDown.bind(this);
17
49
  }
18
50
 
@@ -20,387 +52,79 @@ export class TextPropertiesPanel {
20
52
  this.layer = document.createElement('div');
21
53
  this.layer.className = 'text-properties-layer';
22
54
  Object.assign(this.layer.style, {
23
- position: 'absolute',
24
- inset: '0',
25
- pointerEvents: 'none',
26
- zIndex: 20 // Меньше чем у комментариев, но выше основного контента
55
+ position: 'absolute',
56
+ inset: '0',
57
+ pointerEvents: 'none',
58
+ zIndex: 20,
27
59
  });
28
60
  this.container.appendChild(this.layer);
29
61
 
30
- // Подписки на события
31
- this.eventBus.on(Events.Tool.SelectionAdd, () => this.updateFromSelection());
32
- this.eventBus.on(Events.Tool.SelectionRemove, () => this.updateFromSelection());
33
- this.eventBus.on(Events.Tool.SelectionClear, () => this.hide());
34
- this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
35
- this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
36
- this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
37
- this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
38
- this.eventBus.on(Events.UI.ZoomPercent, () => this.reposition());
39
- this.eventBus.on(Events.Tool.PanUpdate, () => this.reposition());
40
- this.eventBus.on(Events.Object.Deleted, ({ objectId }) => {
41
- if (this.currentId && objectId === this.currentId) this.hide();
42
- });
43
-
44
- // Во время редактирования текста скрываем панель
45
- this.eventBus.on(Events.UI.TextEditStart, () => {
46
- this.isTextEditing = true;
47
- this.hide();
48
- });
49
- this.eventBus.on(Events.UI.TextEditEnd, () => {
50
- this.isTextEditing = false;
51
- // Небольшая задержка, чтобы не появлялась сразу после завершения редактирования
52
- setTimeout(() => this.updateFromSelection(), 100);
53
- });
62
+ attachTextPropertiesPanelEventBridge(this);
54
63
  }
55
64
 
56
65
  destroy() {
57
66
  this.hide();
58
- if (this.layer) this.layer.remove();
59
- this.layer = null;
67
+ unbindTextPropertiesPanelControls(this);
68
+ detachTextPropertiesPanelEventBridge(this);
69
+
70
+ if (this.layer) {
71
+ this.layer.remove();
72
+ }
73
+
74
+ clearTextPropertiesPanelState(this);
60
75
  }
61
76
 
62
77
  updateFromSelection() {
63
- // Не показываем панель во время редактирования текста
64
78
  if (this.isTextEditing) {
65
79
  this.hide();
66
80
  return;
67
81
  }
68
82
 
69
- // Показываем только для одиночного выделения текстового объекта
70
- const ids = this.core?.selectTool ? Array.from(this.core.selectTool.selectedObjects || []) : [];
71
- if (!ids || ids.length !== 1) {
72
- this.hide();
73
- return;
74
- }
75
-
76
- const id = ids[0];
77
- const pixi = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(id) : null;
78
- if (!pixi) {
79
- this.hide();
80
- return;
81
- }
82
-
83
- const mb = pixi._mb || {};
84
- if (mb.type !== 'text') {
85
- this.hide();
86
- return;
83
+ const id = getSelectedTextObjectId(this.core);
84
+ if (!id) {
85
+ this.hide();
86
+ return;
87
87
  }
88
-
88
+
89
89
  this.currentId = id;
90
90
  this.showFor(id);
91
91
  }
92
92
 
93
93
  showFor(id) {
94
- if (!this.layer) return;
95
-
94
+ if (!this.layer) {
95
+ return;
96
+ }
97
+
96
98
  if (!this.panel) {
97
- this.panel = this._createPanel();
99
+ this.panel = createTextPropertiesPanelRenderer(this);
98
100
  this.layer.appendChild(this.panel);
101
+ bindTextPropertiesPanelControls(this);
99
102
  document.addEventListener('mousedown', this._onDocMouseDown, true);
100
103
  }
101
-
104
+
102
105
  this.panel.style.display = 'flex';
103
106
  this.reposition();
104
-
105
- // Обновляем контролы в соответствии с текущими свойствами объекта
106
107
  this._updateControlsFromObject();
107
108
  }
108
109
 
109
110
  hide() {
110
- this.currentId = null;
111
+ resetCurrentSelection(this);
112
+
111
113
  if (this.panel) {
112
114
  this.panel.style.display = 'none';
113
115
  }
114
- this._hideColorDropdown(); // Закрываем выпадающую панель цветов
115
- this._hideBgColorDropdown(); // Закрываем выпадающую панель фона
116
- document.removeEventListener('mousedown', this._onDocMouseDown, true);
117
- }
118
-
119
- _createPanel() {
120
- const panel = document.createElement('div');
121
- panel.className = 'text-properties-panel';
122
- // Основные стили панели вынесены в CSS (.text-properties-panel)
123
-
124
- // Создаем контролы
125
- this._createFontControls(panel);
126
-
127
- return panel;
128
- }
129
-
130
- _createFontControls(panel) {
131
- // Лейбл для шрифта
132
- const fontLabel = document.createElement('span');
133
- fontLabel.textContent = 'Шрифт:';
134
- fontLabel.className = 'tpp-label';
135
- panel.appendChild(fontLabel);
136
-
137
- // Выпадающий список шрифтов
138
- this.fontSelect = document.createElement('select');
139
- this.fontSelect.className = 'font-select';
140
- this.fontSelect.className = 'font-select';
141
-
142
- // Список популярных шрифтов
143
- const fonts = [
144
- { value: 'Roboto, Arial, sans-serif', name: 'Roboto' },
145
- { value: 'Oswald, Arial, sans-serif', name: 'Oswald' },
146
- { value: '"Playfair Display", Georgia, serif', name: 'Playfair Display' },
147
- { value: '"Roboto Slab", Georgia, serif', name: 'Roboto Slab' },
148
- { value: '"Noto Serif", Georgia, serif', name: 'Noto Serif' },
149
- { value: 'Lobster, "Comic Sans MS", cursive', name: 'Lobster' },
150
- { value: 'Caveat, "Comic Sans MS", cursive', name: 'Caveat' },
151
- { value: '"Rubik Mono One", "Courier New", monospace', name: 'Rubik Mono One' },
152
- { value: '"Great Vibes", "Comic Sans MS", cursive', name: 'Great Vibes' },
153
- { value: '"Amatic SC", "Comic Sans MS", cursive', name: 'Amatic SC' },
154
- { value: '"Poiret One", Arial, sans-serif', name: 'Poiret One' },
155
- { value: 'Pacifico, "Comic Sans MS", cursive', name: 'Pacifico' }
156
- ];
157
-
158
- fonts.forEach(font => {
159
- const option = document.createElement('option');
160
- option.value = font.value;
161
- option.textContent = font.name;
162
- option.style.fontFamily = font.value;
163
- this.fontSelect.appendChild(option);
164
- });
165
-
166
- // Обработчик изменения шрифта
167
- this.fontSelect.addEventListener('change', (e) => {
168
- this._changeFontFamily(e.target.value);
169
- });
170
-
171
- panel.appendChild(this.fontSelect);
172
-
173
- // Лейбл для размера
174
- const sizeLabel = document.createElement('span');
175
- sizeLabel.textContent = 'Размер:';
176
- sizeLabel.className = 'tpp-label tpp-label--spaced';
177
- panel.appendChild(sizeLabel);
178
-
179
- // Выпадающий список размеров шрифта
180
- this.fontSizeSelect = document.createElement('select');
181
- this.fontSizeSelect.className = 'font-size-select';
182
- this.fontSizeSelect.className = 'font-size-select';
183
-
184
- // Популярные размеры шрифта
185
- const fontSizes = [8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 60, 72];
186
-
187
- fontSizes.forEach(size => {
188
- const option = document.createElement('option');
189
- option.value = size;
190
- option.textContent = `${size}px`;
191
- this.fontSizeSelect.appendChild(option);
192
- });
193
-
194
- // Обработчик изменения размера шрифта
195
- this.fontSizeSelect.addEventListener('change', (e) => {
196
- this._changeFontSize(parseInt(e.target.value));
197
- });
198
-
199
- panel.appendChild(this.fontSizeSelect);
200
-
201
- // Лейбл для цвета
202
- const colorLabel = document.createElement('span');
203
- colorLabel.textContent = 'Цвет:';
204
- colorLabel.className = 'tpp-label tpp-label--spaced';
205
- panel.appendChild(colorLabel);
206
-
207
- // Создаем компактный селектор цвета текста
208
- this._createCompactColorSelector(panel);
209
-
210
- // Лейбл для фона
211
- const bgColorLabel = document.createElement('span');
212
- bgColorLabel.textContent = 'Фон:';
213
- bgColorLabel.className = 'tpp-label tpp-label--spaced';
214
- panel.appendChild(bgColorLabel);
215
-
216
- // Создаем компактный селектор цвета фона
217
- this._createCompactBackgroundSelector(panel);
218
- }
219
-
220
- _createCompactColorSelector(panel) {
221
- // Контейнер для селектора цвета
222
- const colorSelectorContainer = document.createElement('div');
223
- colorSelectorContainer.style.cssText = `
224
- position: relative;
225
- display: inline-block;
226
- margin-left: 4px;
227
- `;
228
-
229
- // Кнопка показывающая текущий цвет
230
- this.currentColorButton = document.createElement('button');
231
- this.currentColorButton.type = 'button';
232
- this.currentColorButton.title = 'Выбрать цвет';
233
- this.currentColorButton.className = 'current-color-button';
234
-
235
- // Создаем выпадающую панель с цветами
236
- this.colorDropdown = document.createElement('div');
237
- this.colorDropdown.style.cssText = `
238
- position: absolute;
239
- top: 100%;
240
- left: 0;
241
- background: white;
242
- border: 1px solid #ddd;
243
- border-radius: 6px;
244
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
245
- padding: 8px;
246
- display: none;
247
- z-index: 10000;
248
- min-width: 200px;
249
- `;
250
-
251
- // Создаем сетку цветов
252
- this._createColorGrid(this.colorDropdown);
253
-
254
- // Обработчик клика по кнопке
255
- this.currentColorButton.addEventListener('click', (e) => {
256
- e.stopPropagation();
257
- this._toggleColorDropdown();
258
- });
259
-
260
- // Закрываем панель при клике вне её
261
- document.addEventListener('click', (e) => {
262
- // ИСПРАВЛЕНИЕ: Защита от null элементов
263
- if (!colorSelectorContainer || !e.target || !colorSelectorContainer.contains(e.target)) {
264
- this._hideColorDropdown();
265
- }
266
- });
267
-
268
- colorSelectorContainer.appendChild(this.currentColorButton);
269
- colorSelectorContainer.appendChild(this.colorDropdown);
270
- panel.appendChild(colorSelectorContainer);
271
- }
272
-
273
- _createColorGrid(container) {
274
- // Популярные цвета для текста
275
- const presetColors = [
276
- { color: '#000000', name: '#000000' },
277
- { color: '#404040', name: '#404040' },
278
- { color: '#999999', name: '#999999' },
279
- { color: '#FF2D55', name: '#FF2D55' },
280
- { color: '#CB30E0', name: '#CB30E0' },
281
- { color: '#6155F5', name: '#6155F5' },
282
- { color: '#00C0E8', name: '#00C0E8' },
283
- { color: '#34C759', name: '#34C759' },
284
- { color: '#FF8D28', name: '#FF8D28' },
285
- { color: '#FFCC00', name: '#FFCC00' }
286
- ];
287
-
288
- // Сетка заготовленных цветов
289
- const presetsGrid = document.createElement('div');
290
- presetsGrid.style.cssText = `
291
- display: grid;
292
- grid-template-columns: repeat(6, 28px);
293
- gap: 6px;
294
- margin-bottom: 8px;
295
- align-items: center;
296
- justify-items: center;
297
- `;
298
-
299
- presetColors.forEach(preset => {
300
- const colorButton = document.createElement('button');
301
- colorButton.type = 'button';
302
- colorButton.title = preset.name;
303
- colorButton.style.cssText = `
304
- width: 28px;
305
- height: 28px;
306
- border: 1px solid #ddd;
307
- border-radius: 50%;
308
- background-color: ${preset.color};
309
- cursor: pointer;
310
- margin: 0;
311
- padding: 0;
312
- display: block;
313
- box-sizing: border-box;
314
- ${preset.color === '#ffffff' ? 'border-color: #ccc;' : ''}
315
- position: relative;
316
- `;
317
- // Галочка по центру активного пресета
318
- const tick = document.createElement('i');
319
- tick.style.cssText = `
320
- position: absolute;
321
- left: 50%;
322
- top: 50%;
323
- width: 8px;
324
- height: 5px;
325
- transform: translate(-50%, -50%) rotate(315deg) scaleX(-1);
326
- border-right: 2px solid #111;
327
- border-bottom: 2px solid #111;
328
- display: none;
329
- pointer-events: none;
330
- `;
331
- colorButton.appendChild(tick);
332
-
333
- colorButton.addEventListener('click', () => {
334
- // Снимаем активность с других и ставим на текущий
335
- Array.from(presetsGrid.children).forEach((el) => {
336
- const i = el.querySelector('i');
337
- if (i) i.style.display = 'none';
338
- });
339
- tick.style.display = 'block';
340
- this._selectColor(preset.color);
341
- });
342
-
343
- presetsGrid.appendChild(colorButton);
344
- });
345
-
346
- container.appendChild(presetsGrid);
347
-
348
- // Разделитель
349
- const separator = document.createElement('div');
350
- separator.style.cssText = `
351
- height: 1px;
352
- background: #eee;
353
- margin: 8px 0;
354
- `;
355
- container.appendChild(separator);
356
-
357
- // Кастомный color picker
358
- const customContainer = document.createElement('div');
359
- customContainer.style.cssText = `
360
- display: flex;
361
- align-items: center;
362
- gap: 8px;
363
- `;
364
-
365
- const customLabel = document.createElement('span');
366
- customLabel.textContent = 'Свой цвет:';
367
- customLabel.style.cssText = `
368
- font-size: 12px;
369
- color: #666;
370
- `;
371
-
372
- this.colorInput = document.createElement('input');
373
- this.colorInput.type = 'color';
374
- this.colorInput.style.cssText = `
375
- width: 32px;
376
- height: 24px;
377
- border: 1px solid #ddd;
378
- border-radius: 3px;
379
- cursor: pointer;
380
- padding: 0;
381
- `;
382
-
383
- this.colorInput.addEventListener('change', (e) => {
384
- this._selectColor(e.target.value);
385
- });
386
116
 
387
- customContainer.appendChild(customLabel);
388
- customContainer.appendChild(this.colorInput);
389
- container.appendChild(customContainer);
117
+ this._hideColorDropdown();
118
+ this._hideBgColorDropdown();
119
+ document.removeEventListener('mousedown', this._onDocMouseDown, true);
390
120
  }
391
121
 
392
122
  _toggleColorDropdown() {
393
- if (this.colorDropdown.style.display === 'none') {
394
- this.colorDropdown.style.display = 'block';
395
- } else {
396
- this.colorDropdown.style.display = 'none';
397
- }
123
+ toggleColorDropdown(this);
398
124
  }
399
125
 
400
126
  _hideColorDropdown() {
401
- if (this.colorDropdown) {
402
- this.colorDropdown.style.display = 'none';
403
- }
127
+ hideColorDropdown(this);
404
128
  }
405
129
 
406
130
  _selectColor(color) {
@@ -410,232 +134,15 @@ export class TextPropertiesPanel {
410
134
  }
411
135
 
412
136
  _updateCurrentColorButton(color) {
413
- if (this.currentColorButton) {
414
- this.currentColorButton.style.backgroundColor = color;
415
- this.currentColorButton.title = `Текущий цвет: ${color}`;
416
- }
417
- if (this.colorInput) {
418
- this.colorInput.value = color;
419
- }
420
- }
421
-
422
- _createCompactBackgroundSelector(panel) {
423
- // Контейнер для селектора фона
424
- const bgSelectorContainer = document.createElement('div');
425
- bgSelectorContainer.style.cssText = `
426
- position: relative;
427
- display: inline-block;
428
- margin-left: 4px;
429
- `;
430
-
431
- // Кнопка показывающая текущий цвет фона
432
- this.currentBgColorButton = document.createElement('button');
433
- this.currentBgColorButton.type = 'button';
434
- this.currentBgColorButton.title = 'Выбрать цвет выделения';
435
- this.currentBgColorButton.className = 'current-bgcolor-button';
436
-
437
- // Создаем выпадающую панель с цветами фона
438
- this.bgColorDropdown = document.createElement('div');
439
- this.bgColorDropdown.style.cssText = `
440
- position: absolute;
441
- top: 100%;
442
- left: 0;
443
- background: white;
444
- border: 1px solid #ddd;
445
- border-radius: 6px;
446
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
447
- padding: 8px;
448
- display: none;
449
- z-index: 10000;
450
- min-width: 200px;
451
- `;
452
-
453
- // Создаем сетку цветов фона
454
- this._createBackgroundColorGrid(this.bgColorDropdown);
455
-
456
- // Обработчик клика по кнопке
457
- this.currentBgColorButton.addEventListener('click', (e) => {
458
- e.stopPropagation();
459
- this._toggleBgColorDropdown();
460
- });
461
-
462
- // Закрываем панель при клике вне её
463
- document.addEventListener('click', (e) => {
464
- // ИСПРАВЛЕНИЕ: Защита от null элементов
465
- if (!bgSelectorContainer || !e.target || !bgSelectorContainer.contains(e.target)) {
466
- this._hideBgColorDropdown();
467
- }
468
- });
469
-
470
- bgSelectorContainer.appendChild(this.currentBgColorButton);
471
- bgSelectorContainer.appendChild(this.bgColorDropdown);
472
- panel.appendChild(bgSelectorContainer);
473
- }
474
-
475
- _createBackgroundColorGrid(container) {
476
- // Цвета для выделения текста (включая прозрачный)
477
- const bgColors = [
478
- { color: 'transparent', name: 'Без выделения' },
479
- { color: '#ffff99', name: 'Желтый' },
480
- { color: '#ffcc99', name: 'Оранжевый' },
481
- { color: '#ff9999', name: 'Розовый' },
482
- { color: '#ccffcc', name: 'Зеленый' },
483
- { color: '#99ccff', name: 'Голубой' },
484
- { color: '#cc99ff', name: 'Фиолетовый' },
485
- { color: '#f0f0f0', name: 'Светло-серый' },
486
- { color: '#d0d0d0', name: 'Серый' },
487
- { color: '#ffffff', name: 'Белый' },
488
- { color: '#000000', name: 'Черный' },
489
- { color: '#333333', name: 'Темно-серый' }
490
- ];
491
-
492
- // Сетка заготовленных цветов фона
493
- const presetsGrid = document.createElement('div');
494
- presetsGrid.style.cssText = `
495
- display: grid;
496
- grid-template-columns: repeat(6, 28px);
497
- gap: 6px;
498
- margin-bottom: 8px;
499
- align-items: center;
500
- justify-items: center;
501
- `;
502
-
503
- bgColors.forEach(preset => {
504
- const colorButton = document.createElement('button');
505
- colorButton.type = 'button';
506
- colorButton.title = preset.name;
507
-
508
- if (preset.color === 'transparent') {
509
- // Специальная кнопка для "без выделения"
510
- colorButton.style.cssText = `
511
- width: 28px;
512
- height: 28px;
513
- border: 1px solid #ddd;
514
- border-radius: 50%;
515
- background: white;
516
- cursor: pointer;
517
- margin: 0;
518
- padding: 0;
519
- display: flex;
520
- align-items: center;
521
- justify-content: center;
522
- box-sizing: border-box;
523
- position: relative;
524
- `;
525
-
526
- // Добавляем диагональную линию для обозначения "нет"
527
- const line = document.createElement('div');
528
- line.style.cssText = `
529
- width: 20px;
530
- height: 1px;
531
- background: #ff0000;
532
- transform: rotate(45deg);
533
- `;
534
- colorButton.appendChild(line);
535
- } else {
536
- colorButton.style.cssText = `
537
- width: 28px;
538
- height: 28px;
539
- border: 1px solid #ddd;
540
- border-radius: 50%;
541
- background-color: ${preset.color};
542
- cursor: pointer;
543
- margin: 0;
544
- padding: 0;
545
- display: block;
546
- box-sizing: border-box;
547
- ${preset.color === '#ffffff' ? 'border-color: #ccc;' : ''}
548
- position: relative;
549
- `;
550
- // Галочка по центру активного пресета
551
- const tick = document.createElement('i');
552
- tick.style.cssText = `
553
- position: absolute;
554
- left: 50%;
555
- top: 50%;
556
- width: 8px;
557
- height: 5px;
558
- transform: translate(-50%, -50%) rotate(315deg) scaleX(-1);
559
- border-right: 2px solid #111;
560
- border-bottom: 2px solid #111;
561
- display: none;
562
- pointer-events: none;
563
- `;
564
- colorButton.appendChild(tick);
565
- }
566
-
567
- colorButton.addEventListener('click', () => {
568
- // Снимаем активность с других и ставим на текущий
569
- Array.from(presetsGrid.children).forEach((el) => {
570
- const i = el.querySelector('i');
571
- if (i) i.style.display = 'none';
572
- });
573
- const selfTick = colorButton.querySelector('i');
574
- if (selfTick) selfTick.style.display = 'block';
575
- this._selectBgColor(preset.color);
576
- });
577
-
578
- presetsGrid.appendChild(colorButton);
579
- });
580
-
581
- container.appendChild(presetsGrid);
582
-
583
- // Разделитель
584
- const separator = document.createElement('div');
585
- separator.style.cssText = `
586
- height: 1px;
587
- background: #eee;
588
- margin: 8px 0;
589
- `;
590
- container.appendChild(separator);
591
-
592
- // Кастомный color picker для фона
593
- const customContainer = document.createElement('div');
594
- customContainer.style.cssText = `
595
- display: flex;
596
- align-items: center;
597
- gap: 8px;
598
- `;
599
-
600
- const customLabel = document.createElement('span');
601
- customLabel.textContent = 'Свой цвет:';
602
- customLabel.style.cssText = `
603
- font-size: 12px;
604
- color: #666;
605
- `;
606
-
607
- this.bgColorInput = document.createElement('input');
608
- this.bgColorInput.type = 'color';
609
- this.bgColorInput.style.cssText = `
610
- width: 32px;
611
- height: 24px;
612
- border: 1px solid #ddd;
613
- border-radius: 3px;
614
- cursor: pointer;
615
- padding: 0;
616
- `;
617
-
618
- this.bgColorInput.addEventListener('change', (e) => {
619
- this._selectBgColor(e.target.value);
620
- });
621
-
622
- customContainer.appendChild(customLabel);
623
- customContainer.appendChild(this.bgColorInput);
624
- container.appendChild(customContainer);
137
+ updateCurrentColorButton(this, color);
625
138
  }
626
139
 
627
140
  _toggleBgColorDropdown() {
628
- if (this.bgColorDropdown.style.display === 'none') {
629
- this.bgColorDropdown.style.display = 'block';
630
- } else {
631
- this.bgColorDropdown.style.display = 'none';
632
- }
141
+ toggleBgColorDropdown(this);
633
142
  }
634
143
 
635
144
  _hideBgColorDropdown() {
636
- if (this.bgColorDropdown) {
637
- this.bgColorDropdown.style.display = 'none';
638
- }
145
+ hideBgColorDropdown(this);
639
146
  }
640
147
 
641
148
  _selectBgColor(color) {
@@ -645,225 +152,108 @@ export class TextPropertiesPanel {
645
152
  }
646
153
 
647
154
  _updateCurrentBgColorButton(color) {
648
- if (this.currentBgColorButton) {
649
- if (color === 'transparent') {
650
- this.currentBgColorButton.style.backgroundColor = 'white';
651
- this.currentBgColorButton.title = 'Без выделения';
652
- // Добавляем диагональную линию если её нет
653
- if (!this.currentBgColorButton.querySelector('div')) {
654
- const line = document.createElement('div');
655
- line.style.cssText = `
656
- width: 20px;
657
- height: 1px;
658
- background: #ff0000;
659
- transform: rotate(45deg);
660
- position: absolute;
661
- top: 50%;
662
- left: 50%;
663
- transform-origin: center;
664
- transform: translate(-50%, -50%) rotate(45deg);
665
- `;
666
- this.currentBgColorButton.appendChild(line);
667
- }
668
- } else {
669
- this.currentBgColorButton.style.backgroundColor = color;
670
- this.currentBgColorButton.title = `Цвет выделения: ${color}`;
671
- // Убираем диагональную линию если есть
672
- const line = this.currentBgColorButton.querySelector('div');
673
- if (line) {
674
- line.remove();
675
- }
676
- }
677
- }
678
- if (this.bgColorInput) {
679
- this.bgColorInput.value = color === 'transparent' ? '#ffff99' : color;
680
- }
155
+ updateCurrentBgColorButton(this, color);
681
156
  }
682
157
 
683
158
  _changeFontFamily(fontFamily) {
684
- if (!this.currentId) return;
685
-
159
+ if (!this.currentId) {
160
+ return;
161
+ }
686
162
 
687
- // Обновляем свойства объекта через StateManager (в properties)
688
163
  this.eventBus.emit(Events.Object.StateChanged, {
689
164
  objectId: this.currentId,
690
- updates: {
691
- properties: { fontFamily }
692
- }
165
+ updates: buildFontFamilyUpdate(fontFamily),
693
166
  });
694
167
 
695
- // Также обновляем визуальное отображение
696
168
  this._updateTextAppearance(this.currentId, { fontFamily });
697
169
  }
698
170
 
699
171
  _changeFontSize(fontSize) {
700
- if (!this.currentId) return;
701
-
172
+ if (!this.currentId) {
173
+ return;
174
+ }
702
175
 
703
- // Обновляем свойства объекта через StateManager
704
176
  this.eventBus.emit(Events.Object.StateChanged, {
705
177
  objectId: this.currentId,
706
- updates: {
707
- fontSize: fontSize
708
- }
178
+ updates: buildFontSizeUpdate(fontSize),
709
179
  });
710
180
 
711
- // Также обновляем визуальное отображение
712
181
  this._updateTextAppearance(this.currentId, { fontSize });
713
182
  }
714
183
 
715
184
  _changeTextColor(color) {
716
- if (!this.currentId) return;
717
-
185
+ if (!this.currentId) {
186
+ return;
187
+ }
718
188
 
719
- // Обновляем свойства объекта через StateManager
720
189
  this.eventBus.emit(Events.Object.StateChanged, {
721
190
  objectId: this.currentId,
722
- updates: {
723
- color: color
724
- }
191
+ updates: buildTextColorUpdate(color),
725
192
  });
726
193
 
727
- // Также обновляем визуальное отображение
728
194
  this._updateTextAppearance(this.currentId, { color });
729
195
  }
730
196
 
731
197
  _changeBackgroundColor(backgroundColor) {
732
- if (!this.currentId) return;
733
-
198
+ if (!this.currentId) {
199
+ return;
200
+ }
734
201
 
735
- // Обновляем свойства объекта через StateManager
736
202
  this.eventBus.emit(Events.Object.StateChanged, {
737
203
  objectId: this.currentId,
738
- updates: {
739
- backgroundColor: backgroundColor
740
- }
204
+ updates: buildBackgroundColorUpdate(backgroundColor),
741
205
  });
742
206
 
743
- // Также обновляем визуальное отображение
744
207
  this._updateTextAppearance(this.currentId, { backgroundColor });
745
208
  }
746
209
 
747
210
  _updateTextAppearance(objectId, properties) {
748
- // Обновляем HTML текст через HtmlTextLayer
749
- const htmlElement = document.querySelector(`[data-id="${objectId}"]`);
750
- if (htmlElement) {
751
- if (properties.fontFamily) {
752
- htmlElement.style.fontFamily = properties.fontFamily;
753
- }
754
- if (properties.fontSize) {
755
- htmlElement.style.fontSize = `${properties.fontSize}px`;
756
- }
757
- if (properties.color) {
758
- htmlElement.style.color = properties.color;
759
- }
760
- if (properties.backgroundColor !== undefined) {
761
- if (properties.backgroundColor === 'transparent') {
762
- htmlElement.style.backgroundColor = '';
763
- } else {
764
- htmlElement.style.backgroundColor = properties.backgroundColor;
765
- }
766
- }
767
- }
211
+ applyTextAppearanceToDom(objectId, properties);
212
+ syncPixiTextProperties(this.eventBus, objectId, properties);
768
213
 
769
- // Обновляем PIXI объект и его метаданные
770
- const pixiData = { objectId, pixiObject: null };
771
- this.eventBus.emit(Events.Tool.GetObjectPixi, pixiData);
772
- const pixiObject = pixiData.pixiObject;
773
-
774
- if (pixiObject && pixiObject._mb) {
775
- if (!pixiObject._mb.properties) {
776
- pixiObject._mb.properties = {};
777
- }
778
-
779
- // Обновляем свойства в метаданных объекта
780
- Object.assign(pixiObject._mb.properties, properties);
781
- }
782
-
783
- // Помечаем изменения для автосохранения
784
214
  if (this.core && this.core.state) {
785
215
  this.core.state.markDirty();
786
216
  }
787
217
  }
788
218
 
789
219
  _updateControlsFromObject() {
790
- if (!this.currentId || !this.fontSelect || !this.fontSizeSelect) return;
791
-
792
- // Получаем текущие свойства объекта
793
- const pixiData = { objectId: this.currentId, pixiObject: null };
794
- this.eventBus.emit(Events.Tool.GetObjectPixi, pixiData);
795
- const pixiObject = pixiData.pixiObject;
796
-
797
- if (pixiObject && pixiObject._mb && pixiObject._mb.properties) {
798
- const properties = pixiObject._mb.properties;
799
-
800
- // Устанавливаем выбранный шрифт в селекте
801
- if (properties.fontFamily) {
802
- this.fontSelect.value = properties.fontFamily;
803
- } else {
804
- // Устанавливаем дефолтный шрифт
805
- this.fontSelect.value = 'Roboto, Arial, sans-serif';
806
- }
807
-
808
- // Устанавливаем размер шрифта в селекте
809
- if (properties.fontSize) {
810
- this.fontSizeSelect.value = properties.fontSize;
811
- } else {
812
- // Устанавливаем дефолтный размер
813
- this.fontSizeSelect.value = '18';
814
- }
815
-
816
- // Устанавливаем цвет текста
817
- if (properties.color) {
818
- this._updateCurrentColorButton(properties.color);
819
- } else {
820
- // Устанавливаем дефолтный цвет (черный)
821
- this._updateCurrentColorButton('#000000');
822
- }
823
-
824
- // Устанавливаем цвет фона
825
- if (properties.backgroundColor !== undefined) {
826
- this._updateCurrentBgColorButton(properties.backgroundColor);
827
- } else {
828
- // Устанавливаем дефолтный фон (прозрачный)
829
- this._updateCurrentBgColorButton('transparent');
830
- }
831
- } else {
832
- // Дефолтные значения
833
- this.fontSelect.value = 'Arial, sans-serif';
834
- this.fontSizeSelect.value = '18';
835
- this._updateCurrentColorButton('#000000');
836
- this._updateCurrentBgColorButton('transparent');
220
+ if (!this.currentId || !this.fontSelect || !this.fontSizeSelect) {
221
+ return;
837
222
  }
223
+
224
+ const properties = getObjectProperties(this.eventBus, this.currentId);
225
+ const values = properties
226
+ ? getControlValuesFromProperties(properties)
227
+ : getFallbackControlValues();
228
+
229
+ this.fontSelect.value = values.fontFamily;
230
+ this.fontSizeSelect.value = values.fontSize;
231
+ this._updateCurrentColorButton(values.color);
232
+ this._updateCurrentBgColorButton(values.backgroundColor);
838
233
  }
839
234
 
840
235
  reposition() {
841
- if (!this.panel || !this.currentId || this.panel.style.display === 'none') return;
842
-
843
- // Получаем позицию и размеры объекта
844
- const posData = { objectId: this.currentId, position: null };
845
- const sizeData = { objectId: this.currentId, size: null };
846
- this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
847
- this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
236
+ if (!this.panel || !this.currentId || this.panel.style.display === 'none') {
237
+ return;
238
+ }
848
239
 
849
- if (!posData.position || !sizeData.size) return;
240
+ const geometry = getObjectGeometry(this.eventBus, this.currentId);
241
+ if (!geometry.position || !geometry.size) {
242
+ return;
243
+ }
850
244
 
851
- // Получаем зум и позицию мира
852
245
  const worldLayer = this.core?.pixi?.worldLayer;
853
246
  const scale = worldLayer?.scale?.x || 1;
854
247
  const worldX = worldLayer?.x || 0;
855
248
  const worldY = worldLayer?.y || 0;
856
249
 
857
- // Преобразуем координаты объекта в экранные координаты
858
- const screenX = posData.position.x * scale + worldX;
859
- const screenY = posData.position.y * scale + worldY;
860
- const objectWidth = sizeData.size.width * scale;
250
+ const screenX = geometry.position.x * scale + worldX;
251
+ const screenY = geometry.position.y * scale + worldY;
252
+ const objectWidth = geometry.size.width * scale;
861
253
 
862
- // Позиционируем панель над объектом
863
254
  const panelX = screenX + (objectWidth / 2) - (this.panel.offsetWidth / 2);
864
- const panelY = screenY - this.panel.offsetHeight - 20; // поднимем выше ещё
255
+ const panelY = screenY - this.panel.offsetHeight - 20;
865
256
 
866
- // Проверяем границы контейнера
867
257
  const containerRect = this.container.getBoundingClientRect();
868
258
  const finalX = Math.max(10, Math.min(panelX, containerRect.width - this.panel.offsetWidth - 10));
869
259
  const finalY = Math.max(10, panelY);
@@ -872,20 +262,16 @@ export class TextPropertiesPanel {
872
262
  this.panel.style.top = `${finalY}px`;
873
263
  }
874
264
 
875
- _onDocMouseDown(e) {
876
- // ИСПРАВЛЕНИЕ: Защита от null элементов + скрываем панель при клике вне неё
877
- if (!this.panel || !e.target) return;
878
-
879
- // Если клик внутри панели - не скрываем
880
- if (this.panel.contains(e.target)) return;
881
-
882
- // Проверяем, не кликнули ли по текущему текстовому объекту
883
- const rect = this.container.getBoundingClientRect();
884
- const x = e.clientX - rect.left;
885
- const y = e.clientY - rect.top;
886
-
887
- // Здесь можно добавить проверку попадания в текстовый объект
888
- // Пока просто скрываем панель
265
+ _onDocMouseDown(event) {
266
+ if (!this.panel || !event.target) {
267
+ return;
268
+ }
269
+
270
+ if (this.panel.contains(event.target)) {
271
+ return;
272
+ }
273
+
274
+ this.container.getBoundingClientRect();
889
275
  this.hide();
890
276
  }
891
277
  }