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