@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.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- 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 {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
return this.popupsController.closeFramePopup();
|
|
209
72
|
}
|
|
210
73
|
|
|
211
74
|
/**
|
|
212
75
|
* Создает кнопку инструмента
|
|
213
76
|
*/
|
|
214
77
|
createButton(tool) {
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
|