@sequent-org/moodboard 1.2.119 → 1.3.1
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 +82 -1181
- 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 +665 -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,19 @@ 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 }) => {
|
|
50
|
+
this.setActiveToolbarButton(tool);
|
|
51
|
+
// Draw palette must stay open only while draw tool is active.
|
|
52
|
+
if (tool !== 'draw') {
|
|
53
|
+
this.closeDrawPopup();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
38
56
|
this.createToolbar();
|
|
39
57
|
this.attachEvents();
|
|
40
58
|
this.setupHistoryEvents();
|
|
@@ -44,297 +62,54 @@ export class Toolbar {
|
|
|
44
62
|
* Создает HTML структуру тулбара
|
|
45
63
|
*/
|
|
46
64
|
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';
|
|
65
|
+
return this.renderer.createToolbar();
|
|
100
66
|
}
|
|
101
67
|
|
|
102
68
|
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);
|
|
69
|
+
return this.popupsController.createFramePopup();
|
|
180
70
|
}
|
|
181
71
|
|
|
182
72
|
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 = '';
|
|
73
|
+
return this.popupsController.toggleFramePopup(anchorBtn);
|
|
205
74
|
}
|
|
206
75
|
|
|
207
76
|
closeFramePopup() {
|
|
208
|
-
|
|
77
|
+
return this.popupsController.closeFramePopup();
|
|
209
78
|
}
|
|
210
79
|
|
|
211
80
|
/**
|
|
212
81
|
* Создает кнопку инструмента
|
|
213
82
|
*/
|
|
214
83
|
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;
|
|
84
|
+
return this.renderer.createButton(tool);
|
|
237
85
|
}
|
|
238
86
|
|
|
239
87
|
/**
|
|
240
88
|
* Создает SVG иконку для кнопки
|
|
241
89
|
*/
|
|
242
90
|
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
|
-
}
|
|
91
|
+
return this.renderer.createSvgIcon(button, iconName);
|
|
266
92
|
}
|
|
267
93
|
|
|
268
94
|
/**
|
|
269
95
|
* Создает tooltip для кнопки
|
|
270
96
|
*/
|
|
271
97
|
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;
|
|
98
|
+
return this.tooltipController.createTooltip(button, text);
|
|
308
99
|
}
|
|
309
100
|
|
|
310
101
|
/**
|
|
311
102
|
* Показывает tooltip
|
|
312
103
|
*/
|
|
313
104
|
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');
|
|
105
|
+
return this.tooltipController.showTooltip(tooltip, button);
|
|
331
106
|
}
|
|
332
107
|
|
|
333
108
|
/**
|
|
334
109
|
* Скрывает tooltip
|
|
335
110
|
*/
|
|
336
111
|
hideTooltip(tooltip) {
|
|
337
|
-
|
|
112
|
+
return this.tooltipController.hideTooltip(tooltip);
|
|
338
113
|
}
|
|
339
114
|
|
|
340
115
|
/**
|
|
@@ -344,219 +119,17 @@ export class Toolbar {
|
|
|
344
119
|
this.element.addEventListener('click', (e) => {
|
|
345
120
|
const button = e.target.closest('.moodboard-toolbar__button');
|
|
346
121
|
if (!button || button.disabled) return;
|
|
347
|
-
|
|
122
|
+
|
|
348
123
|
const toolType = button.dataset.tool;
|
|
349
124
|
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
125
|
|
|
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
|
-
|
|
458
|
-
// Комментарии — включаем режим размещения comment
|
|
459
|
-
if (toolType === 'custom-comments') {
|
|
460
|
-
this.animateButton(button);
|
|
461
|
-
this.closeShapesPopup();
|
|
462
|
-
this.closeDrawPopup();
|
|
463
|
-
this.closeEmojiPopup();
|
|
464
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
|
|
465
|
-
this.placeSelectedButtonId = 'comments';
|
|
466
|
-
this.setActiveToolbarButton('place');
|
|
467
|
-
// Увеличенный размер по умолчанию
|
|
468
|
-
this.eventBus.emit(Events.Place.Set, { type: 'comment', properties: { width: 72, height: 72 } });
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Файлы — сразу открываем диалог выбора файла
|
|
473
|
-
if (toolType === 'custom-attachments') {
|
|
474
|
-
this.animateButton(button);
|
|
475
|
-
this.closeShapesPopup();
|
|
476
|
-
this.closeDrawPopup();
|
|
477
|
-
this.closeEmojiPopup();
|
|
478
|
-
// Открываем диалог выбора файла
|
|
479
|
-
this.openFileDialog();
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Инструмент «Фрейм» — создаём через универсальный place-поток с размерами 200x300
|
|
484
|
-
if (toolType === 'custom-frame') {
|
|
485
|
-
this.animateButton(button);
|
|
486
|
-
this.closeShapesPopup();
|
|
487
|
-
this.closeDrawPopup();
|
|
488
|
-
this.closeEmojiPopup();
|
|
489
|
-
// Активируем режим размещения и устанавливаем pending
|
|
490
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
|
|
491
|
-
this.placeSelectedButtonId = 'frame-tool';
|
|
492
|
-
this.setActiveToolbarButton('place');
|
|
493
|
-
this.eventBus.emit(Events.Place.Set, {
|
|
494
|
-
type: 'frame',
|
|
495
|
-
properties: { width: 200, height: 300 }
|
|
496
|
-
});
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Тоггл всплывающей панели фигур
|
|
501
|
-
if (toolType === 'custom-shapes') {
|
|
502
|
-
this.animateButton(button);
|
|
503
|
-
this.toggleShapesPopup(button);
|
|
504
|
-
this.closeDrawPopup();
|
|
505
|
-
this.closeEmojiPopup();
|
|
506
|
-
// Активируем универсальный place tool для дальнейшего размещения
|
|
507
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
|
|
508
|
-
this.placeSelectedButtonId = 'shapes';
|
|
509
|
-
this.setActiveToolbarButton('place');
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Тоггл всплывающей панели рисования
|
|
514
|
-
if (toolType === 'custom-draw') {
|
|
515
|
-
this.animateButton(button);
|
|
516
|
-
this.toggleDrawPopup(button);
|
|
517
|
-
this.closeShapesPopup();
|
|
518
|
-
this.closeEmojiPopup();
|
|
519
|
-
// Выбираем инструмент рисования (последующее действие — на холсте)
|
|
520
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'draw' });
|
|
521
|
-
this.setActiveToolbarButton('draw');
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Тоггл всплывающей панели эмоджи
|
|
526
|
-
if (toolType === 'custom-emoji') {
|
|
527
|
-
this.animateButton(button);
|
|
528
|
-
this.toggleEmojiPopup(button);
|
|
529
|
-
this.closeShapesPopup();
|
|
530
|
-
this.closeDrawPopup();
|
|
531
|
-
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
|
|
532
|
-
this.placeSelectedButtonId = 'emoji';
|
|
533
|
-
this.setActiveToolbarButton('place'); // ← Исправление: подсвечиваем кнопку эмоджи
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Очистка холста - требует подтверждения
|
|
538
|
-
if (toolType === 'clear') {
|
|
539
|
-
this.animateButton(button);
|
|
540
|
-
this.showClearConfirmation();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Эмитим событие для других инструментов
|
|
545
|
-
this.eventBus.emit(Events.UI.ToolbarAction, {
|
|
546
|
-
type: toolType,
|
|
547
|
-
id: toolId,
|
|
548
|
-
position: this.getRandomPosition()
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// Визуальная обратная связь
|
|
552
|
-
this.animateButton(button);
|
|
126
|
+
this.actionRouter.routeToolbarAction(button, toolType, toolId);
|
|
553
127
|
});
|
|
554
128
|
|
|
555
|
-
// Клик вне попапов — закрыть
|
|
556
|
-
|
|
557
|
-
// ИСПРАВЛЕНИЕ: Защита от null элементов
|
|
129
|
+
// Клик вне попапов — закрыть (сохраняем handler для корректного removeEventListener)
|
|
130
|
+
this._documentClickHandler = (e) => {
|
|
558
131
|
if (!e.target) return;
|
|
559
|
-
|
|
132
|
+
|
|
560
133
|
const isInsideToolbar = this.element && this.element.contains(e.target);
|
|
561
134
|
const isInsideShapesPopup = this.shapesPopupEl && this.shapesPopupEl.contains(e.target);
|
|
562
135
|
const isInsideDrawPopup = this.drawPopupEl && this.drawPopupEl.contains(e.target);
|
|
@@ -566,63 +139,25 @@ export class Toolbar {
|
|
|
566
139
|
const isDrawButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--pencil');
|
|
567
140
|
const isEmojiButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--emoji');
|
|
568
141
|
const isFrameButton = e.target.closest && e.target.closest('.moodboard-toolbar__button--frame');
|
|
142
|
+
const isDrawActive = !!(this.element && this.element.querySelector('.moodboard-toolbar__button--pencil.moodboard-toolbar__button--active'));
|
|
143
|
+
|
|
569
144
|
if (!isInsideToolbar && !isInsideShapesPopup && !isShapesButton && !isInsideDrawPopup && !isDrawButton && !isInsideEmojiPopup && !isEmojiButton && !isInsideFramePopup && !isFrameButton) {
|
|
570
145
|
this.closeShapesPopup();
|
|
571
|
-
|
|
146
|
+
if (!isDrawActive) {
|
|
147
|
+
this.closeDrawPopup();
|
|
148
|
+
}
|
|
572
149
|
this.closeEmojiPopup();
|
|
573
150
|
this.closeFramePopup();
|
|
574
151
|
}
|
|
575
|
-
}
|
|
152
|
+
};
|
|
153
|
+
document.addEventListener('click', this._documentClickHandler);
|
|
576
154
|
}
|
|
577
155
|
|
|
578
156
|
/**
|
|
579
157
|
* Подсвечивает активную кнопку на тулбаре в зависимости от активного инструмента
|
|
580
158
|
*/
|
|
581
159
|
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
|
-
}
|
|
160
|
+
return this.stateController.setActiveToolbarButton(toolName);
|
|
626
161
|
}
|
|
627
162
|
|
|
628
163
|
/**
|
|
@@ -649,592 +184,71 @@ export class Toolbar {
|
|
|
649
184
|
* Всплывающая панель с фигурами (UI)
|
|
650
185
|
*/
|
|
651
186
|
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);
|
|
187
|
+
return this.popupsController.createShapesPopup();
|
|
713
188
|
}
|
|
714
189
|
|
|
715
190
|
toggleShapesPopup(anchorButton) {
|
|
716
|
-
|
|
717
|
-
if (this.shapesPopupEl.style.display === 'none') {
|
|
718
|
-
this.openShapesPopup(anchorButton);
|
|
719
|
-
} else {
|
|
720
|
-
this.closeShapesPopup();
|
|
721
|
-
}
|
|
191
|
+
return this.popupsController.toggleShapesPopup(anchorButton);
|
|
722
192
|
}
|
|
723
193
|
|
|
724
194
|
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';
|
|
195
|
+
return this.popupsController.openShapesPopup(anchorButton);
|
|
734
196
|
}
|
|
735
197
|
|
|
736
198
|
closeShapesPopup() {
|
|
737
|
-
|
|
738
|
-
this.shapesPopupEl.style.display = 'none';
|
|
739
|
-
}
|
|
199
|
+
return this.popupsController.closeShapesPopup();
|
|
740
200
|
}
|
|
741
201
|
|
|
742
202
|
/**
|
|
743
203
|
* Всплывающая панель рисования (UI)
|
|
744
204
|
*/
|
|
745
205
|
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);
|
|
206
|
+
return this.popupsController.createDrawPopup();
|
|
875
207
|
}
|
|
876
208
|
|
|
877
209
|
toggleDrawPopup(anchorButton) {
|
|
878
|
-
|
|
879
|
-
if (this.drawPopupEl.style.display === 'none') {
|
|
880
|
-
this.openDrawPopup(anchorButton);
|
|
881
|
-
} else {
|
|
882
|
-
this.closeDrawPopup();
|
|
883
|
-
}
|
|
210
|
+
return this.popupsController.toggleDrawPopup(anchorButton);
|
|
884
211
|
}
|
|
885
212
|
|
|
886
213
|
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';
|
|
214
|
+
return this.popupsController.openDrawPopup(anchorButton);
|
|
895
215
|
}
|
|
896
216
|
|
|
897
217
|
closeDrawPopup() {
|
|
898
|
-
|
|
899
|
-
this.drawPopupEl.style.display = 'none';
|
|
900
|
-
}
|
|
218
|
+
return this.popupsController.closeDrawPopup();
|
|
901
219
|
}
|
|
902
220
|
|
|
903
221
|
/**
|
|
904
222
|
* Всплывающая панель эмоджи (UI)
|
|
905
223
|
*/
|
|
906
224
|
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);
|
|
225
|
+
return this.popupsController.createEmojiPopup();
|
|
1086
226
|
}
|
|
1087
227
|
|
|
1088
228
|
/**
|
|
1089
229
|
* Возвращает fallback группы эмоджи для работы без bundler
|
|
1090
230
|
*/
|
|
1091
231
|
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;
|
|
232
|
+
return this.popupsController.getFallbackEmojiGroups();
|
|
1160
233
|
}
|
|
1161
234
|
|
|
1162
235
|
/**
|
|
1163
236
|
* Определяет базовый путь для эмоджи в зависимости от режима
|
|
1164
237
|
*/
|
|
1165
238
|
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/';
|
|
239
|
+
return this.popupsController.getEmojiBasePath();
|
|
1203
240
|
}
|
|
1204
241
|
|
|
1205
242
|
toggleEmojiPopup(anchorButton) {
|
|
1206
|
-
|
|
1207
|
-
if (this.emojiPopupEl.style.display === 'none') {
|
|
1208
|
-
this.openEmojiPopup(anchorButton);
|
|
1209
|
-
} else {
|
|
1210
|
-
this.closeEmojiPopup();
|
|
1211
|
-
}
|
|
243
|
+
return this.popupsController.toggleEmojiPopup(anchorButton);
|
|
1212
244
|
}
|
|
1213
245
|
|
|
1214
246
|
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';
|
|
247
|
+
return this.popupsController.openEmojiPopup(anchorButton);
|
|
1232
248
|
}
|
|
1233
249
|
|
|
1234
250
|
closeEmojiPopup() {
|
|
1235
|
-
|
|
1236
|
-
this.emojiPopupEl.style.display = 'none';
|
|
1237
|
-
}
|
|
251
|
+
return this.popupsController.closeEmojiPopup();
|
|
1238
252
|
}
|
|
1239
253
|
|
|
1240
254
|
/**
|
|
@@ -1281,179 +295,66 @@ export class Toolbar {
|
|
|
1281
295
|
* Настройка обработчиков событий истории
|
|
1282
296
|
*/
|
|
1283
297
|
setupHistoryEvents() {
|
|
1284
|
-
|
|
1285
|
-
this.eventBus.on(Events.UI.UpdateHistoryButtons, (data) => {
|
|
1286
|
-
this.updateHistoryButtons(data.canUndo, data.canRedo);
|
|
1287
|
-
});
|
|
298
|
+
return this.stateController.setupHistoryEvents();
|
|
1288
299
|
}
|
|
1289
300
|
|
|
1290
301
|
/**
|
|
1291
302
|
* Открывает диалог выбора файла и запускает режим "призрака"
|
|
1292
303
|
*/
|
|
1293
304
|
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();
|
|
305
|
+
return this.dialogsController.openFileDialog();
|
|
1347
306
|
}
|
|
1348
307
|
|
|
1349
308
|
/**
|
|
1350
309
|
* Открывает диалог выбора изображения и запускает режим "призрака"
|
|
1351
310
|
*/
|
|
1352
311
|
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 });
|
|
312
|
+
return this.dialogsController.openImageDialog();
|
|
313
|
+
}
|
|
1392
314
|
|
|
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();
|
|
315
|
+
/**
|
|
316
|
+
* Открывает диалог выбора изображения для ImageObject2 (новая изолированная цепочка)
|
|
317
|
+
*/
|
|
318
|
+
async openImageObject2Dialog() {
|
|
319
|
+
return this.dialogsController.openImageObject2Dialog();
|
|
1406
320
|
}
|
|
1407
321
|
|
|
1408
322
|
/**
|
|
1409
323
|
* Обновление состояния кнопок undo/redo
|
|
1410
324
|
*/
|
|
1411
325
|
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
|
-
}
|
|
326
|
+
return this.stateController.updateHistoryButtons(canUndo, canRedo);
|
|
1436
327
|
}
|
|
1437
328
|
|
|
1438
329
|
/**
|
|
1439
330
|
* Очистка ресурсов
|
|
1440
331
|
*/
|
|
1441
332
|
destroy() {
|
|
333
|
+
// Удаляем document-level listener (предотвращение утечки памяти)
|
|
334
|
+
if (this._documentClickHandler) {
|
|
335
|
+
document.removeEventListener('click', this._documentClickHandler);
|
|
336
|
+
this._documentClickHandler = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Отписываемся от Events.Tool.Activated (подписка в ToolbarRenderer)
|
|
340
|
+
if (this._toolActivatedHandler) {
|
|
341
|
+
this.eventBus.off(Events.Tool.Activated, this._toolActivatedHandler);
|
|
342
|
+
this._toolActivatedHandler = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
1442
345
|
if (this.element) {
|
|
1443
|
-
// Очищаем все tooltips перед удалением элемента
|
|
1444
346
|
const buttons = this.element.querySelectorAll('.moodboard-toolbar__button');
|
|
1445
|
-
buttons.forEach(button => {
|
|
347
|
+
buttons.forEach((button) => {
|
|
1446
348
|
if (button._tooltip) {
|
|
1447
349
|
button._tooltip.remove();
|
|
1448
350
|
button._tooltip = null;
|
|
1449
351
|
}
|
|
1450
352
|
});
|
|
1451
|
-
|
|
353
|
+
|
|
1452
354
|
this.element.remove();
|
|
1453
355
|
this.element = null;
|
|
1454
356
|
}
|
|
1455
|
-
|
|
1456
|
-
// Отписываемся от событий
|
|
357
|
+
|
|
1457
358
|
this.eventBus.removeAllListeners(Events.UI.UpdateHistoryButtons);
|
|
1458
359
|
}
|
|
1459
360
|
|