@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.
Files changed (123) hide show
  1. package/package.json +44 -0
  2. package/src/assets/icons/README.md +105 -0
  3. package/src/assets/icons/attachments.svg +3 -0
  4. package/src/assets/icons/clear.svg +5 -0
  5. package/src/assets/icons/comments.svg +3 -0
  6. package/src/assets/icons/emoji.svg +6 -0
  7. package/src/assets/icons/frame.svg +3 -0
  8. package/src/assets/icons/image.svg +3 -0
  9. package/src/assets/icons/note.svg +3 -0
  10. package/src/assets/icons/pan.svg +3 -0
  11. package/src/assets/icons/pencil.svg +3 -0
  12. package/src/assets/icons/redo.svg +3 -0
  13. package/src/assets/icons/select.svg +9 -0
  14. package/src/assets/icons/shapes.svg +3 -0
  15. package/src/assets/icons/text-add.svg +3 -0
  16. package/src/assets/icons/topbar/README.md +39 -0
  17. package/src/assets/icons/topbar/grid-cross.svg +6 -0
  18. package/src/assets/icons/topbar/grid-dot.svg +3 -0
  19. package/src/assets/icons/topbar/grid-line.svg +3 -0
  20. package/src/assets/icons/topbar/grid-off.svg +3 -0
  21. package/src/assets/icons/topbar/paint.svg +3 -0
  22. package/src/assets/icons/undo.svg +3 -0
  23. package/src/core/ApiClient.js +309 -0
  24. package/src/core/EventBus.js +42 -0
  25. package/src/core/HistoryManager.js +261 -0
  26. package/src/core/KeyboardManager.js +710 -0
  27. package/src/core/PixiEngine.js +439 -0
  28. package/src/core/SaveManager.js +381 -0
  29. package/src/core/StateManager.js +64 -0
  30. package/src/core/commands/BaseCommand.js +68 -0
  31. package/src/core/commands/CopyObjectCommand.js +44 -0
  32. package/src/core/commands/CreateObjectCommand.js +46 -0
  33. package/src/core/commands/DeleteObjectCommand.js +146 -0
  34. package/src/core/commands/EditFileNameCommand.js +107 -0
  35. package/src/core/commands/GroupMoveCommand.js +47 -0
  36. package/src/core/commands/GroupReorderZCommand.js +74 -0
  37. package/src/core/commands/GroupResizeCommand.js +37 -0
  38. package/src/core/commands/GroupRotateCommand.js +41 -0
  39. package/src/core/commands/MoveObjectCommand.js +89 -0
  40. package/src/core/commands/PasteObjectCommand.js +103 -0
  41. package/src/core/commands/ReorderZCommand.js +45 -0
  42. package/src/core/commands/ResizeObjectCommand.js +135 -0
  43. package/src/core/commands/RotateObjectCommand.js +70 -0
  44. package/src/core/commands/index.js +14 -0
  45. package/src/core/events/Events.js +147 -0
  46. package/src/core/index.js +1632 -0
  47. package/src/core/rendering/GeometryUtils.js +89 -0
  48. package/src/core/rendering/HitTestManager.js +186 -0
  49. package/src/core/rendering/LayerManager.js +137 -0
  50. package/src/core/rendering/ObjectRenderer.js +363 -0
  51. package/src/core/rendering/PixiRenderer.js +140 -0
  52. package/src/core/rendering/index.js +9 -0
  53. package/src/grid/BaseGrid.js +164 -0
  54. package/src/grid/CrossGrid.js +75 -0
  55. package/src/grid/DotGrid.js +148 -0
  56. package/src/grid/GridFactory.js +173 -0
  57. package/src/grid/LineGrid.js +115 -0
  58. package/src/index.js +2 -0
  59. package/src/moodboard/ActionHandler.js +114 -0
  60. package/src/moodboard/DataManager.js +114 -0
  61. package/src/moodboard/MoodBoard.js +359 -0
  62. package/src/moodboard/WorkspaceManager.js +103 -0
  63. package/src/objects/BaseObject.js +1 -0
  64. package/src/objects/CommentObject.js +115 -0
  65. package/src/objects/DrawingObject.js +114 -0
  66. package/src/objects/EmojiObject.js +98 -0
  67. package/src/objects/FileObject.js +318 -0
  68. package/src/objects/FrameObject.js +127 -0
  69. package/src/objects/ImageObject.js +72 -0
  70. package/src/objects/NoteObject.js +227 -0
  71. package/src/objects/ObjectFactory.js +61 -0
  72. package/src/objects/ShapeObject.js +134 -0
  73. package/src/objects/StampObject.js +0 -0
  74. package/src/objects/StickerObject.js +0 -0
  75. package/src/objects/TextObject.js +123 -0
  76. package/src/services/BoardService.js +85 -0
  77. package/src/services/FileUploadService.js +398 -0
  78. package/src/services/FrameService.js +138 -0
  79. package/src/services/ImageUploadService.js +246 -0
  80. package/src/services/ZOrderManager.js +50 -0
  81. package/src/services/ZoomPanController.js +78 -0
  82. package/src/src.7z +0 -0
  83. package/src/src.zip +0 -0
  84. package/src/src2.zip +0 -0
  85. package/src/tools/AlignmentGuides.js +326 -0
  86. package/src/tools/BaseTool.js +257 -0
  87. package/src/tools/ResizeHandles.js +381 -0
  88. package/src/tools/ToolManager.js +580 -0
  89. package/src/tools/board-tools/PanTool.js +43 -0
  90. package/src/tools/board-tools/ZoomTool.js +393 -0
  91. package/src/tools/object-tools/DrawingTool.js +404 -0
  92. package/src/tools/object-tools/PlacementTool.js +1005 -0
  93. package/src/tools/object-tools/SelectTool.js +2183 -0
  94. package/src/tools/object-tools/TextTool.js +416 -0
  95. package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
  96. package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
  97. package/src/tools/object-tools/selection/GroupDragController.js +61 -0
  98. package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
  99. package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
  100. package/src/tools/object-tools/selection/HandlesSync.js +96 -0
  101. package/src/tools/object-tools/selection/ResizeController.js +68 -0
  102. package/src/tools/object-tools/selection/RotateController.js +58 -0
  103. package/src/tools/object-tools/selection/SelectionModel.js +42 -0
  104. package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
  105. package/src/ui/CommentPopover.js +187 -0
  106. package/src/ui/ContextMenu.js +340 -0
  107. package/src/ui/FilePropertiesPanel.js +298 -0
  108. package/src/ui/FramePropertiesPanel.js +462 -0
  109. package/src/ui/HtmlHandlesLayer.js +778 -0
  110. package/src/ui/HtmlTextLayer.js +279 -0
  111. package/src/ui/MapPanel.js +290 -0
  112. package/src/ui/NotePropertiesPanel.js +502 -0
  113. package/src/ui/SaveStatus.js +250 -0
  114. package/src/ui/TextPropertiesPanel.js +911 -0
  115. package/src/ui/Toolbar.js +1118 -0
  116. package/src/ui/Topbar.js +220 -0
  117. package/src/ui/ZoomPanel.js +116 -0
  118. package/src/ui/styles/workspace.css +854 -0
  119. package/src/utils/colors.js +0 -0
  120. package/src/utils/geometry.js +0 -0
  121. package/src/utils/iconLoader.js +270 -0
  122. package/src/utils/objectIdGenerator.js +17 -0
  123. package/src/utils/topbarIconLoader.js +114 -0
@@ -0,0 +1,710 @@
1
+ /**
2
+ * Менеджер клавиатуры для обработки горячих клавиш
3
+ */
4
+ import { Events } from './events/Events.js';
5
+ export class KeyboardManager {
6
+ constructor(eventBus, targetElement = document, core = null) {
7
+ this.eventBus = eventBus;
8
+ this.targetElement = targetElement;
9
+ this.core = core;
10
+ this.shortcuts = new Map();
11
+ this.isListening = false;
12
+
13
+ // Привязываем контекст методов
14
+ this.handleKeyDown = this.handleKeyDown.bind(this);
15
+ this.handleKeyUp = this.handleKeyUp.bind(this);
16
+ }
17
+
18
+ /**
19
+ * Обрабатывает загрузку изображения на сервер
20
+ * @private
21
+ */
22
+ async _handleImageUpload(dataUrl, fileName) {
23
+ try {
24
+ if (this.core && this.core.imageUploadService) {
25
+ // Загружаем на сервер
26
+ const uploadResult = await this.core.imageUploadService.uploadFromDataUrl(dataUrl, fileName);
27
+ this.eventBus.emit(Events.UI.PasteImage, {
28
+ src: uploadResult.url,
29
+ name: uploadResult.name,
30
+ imageId: uploadResult.id
31
+ });
32
+ } else {
33
+ // Fallback к старому способу
34
+ this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
35
+ }
36
+ } catch (error) {
37
+ console.error('Ошибка загрузки изображения:', error);
38
+ // В случае ошибки используем base64 как fallback
39
+ this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Обработка загрузки файла изображения (более эффективно чем DataURL)
45
+ * @param {File} file - файл изображения
46
+ * @param {string} fileName - имя файла
47
+ * @private
48
+ */
49
+ async _handleImageFileUpload(file, fileName) {
50
+ try {
51
+ if (this.core && this.core.imageUploadService) {
52
+ // Прямая загрузка файла на сервер (более эффективно)
53
+ const uploadResult = await this.core.imageUploadService.uploadImage(file, fileName);
54
+ this.eventBus.emit(Events.UI.PasteImage, {
55
+ src: uploadResult.url,
56
+ name: uploadResult.name,
57
+ imageId: uploadResult.id
58
+ });
59
+ } else {
60
+ // Fallback к старому способу: конвертируем в DataURL
61
+ const reader = new FileReader();
62
+ reader.onload = () => {
63
+ this.eventBus.emit(Events.UI.PasteImage, {
64
+ src: reader.result,
65
+ name: fileName
66
+ });
67
+ };
68
+ reader.readAsDataURL(file);
69
+ }
70
+ } catch (error) {
71
+ console.error('Ошибка загрузки файла изображения:', error);
72
+ // Fallback к DataURL при ошибке
73
+ try {
74
+ const reader = new FileReader();
75
+ reader.onload = () => {
76
+ this.eventBus.emit(Events.UI.PasteImage, {
77
+ src: reader.result,
78
+ name: fileName
79
+ });
80
+ };
81
+ reader.readAsDataURL(file);
82
+ } catch (fallbackError) {
83
+ console.error('Критическая ошибка при чтении файла:', fallbackError);
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Начать прослушивание клавиатуры
90
+ */
91
+ startListening() {
92
+ if (this.isListening) return;
93
+
94
+ this.targetElement.addEventListener('keydown', this.handleKeyDown);
95
+ this.targetElement.addEventListener('keyup', this.handleKeyUp);
96
+ // Вставка изображений из буфера обмена
97
+ this.targetElement.addEventListener('paste', async (e) => {
98
+ try {
99
+ const cd = e.clipboardData;
100
+ if (!cd) return;
101
+ let handled = false;
102
+ // 1) items API
103
+ const items = cd.items ? Array.from(cd.items) : [];
104
+ const imageItem = items.find(i => i.type && i.type.startsWith('image/'));
105
+ if (imageItem) {
106
+ e.preventDefault();
107
+ const file = imageItem.getAsFile();
108
+ if (file) {
109
+ await this._handleImageFileUpload(file, file.name || 'clipboard-image.png');
110
+ handled = true;
111
+ }
112
+ }
113
+ if (handled) return;
114
+ // 2) files API
115
+ const files = cd.files ? Array.from(cd.files) : [];
116
+ const imgFile = files.find(f => f.type && f.type.startsWith('image/'));
117
+ if (imgFile) {
118
+ e.preventDefault();
119
+ await this._handleImageFileUpload(imgFile, imgFile.name || 'clipboard-image.png');
120
+ return;
121
+ }
122
+ // 3) text/html with <img src="...">
123
+ const html = cd.getData && cd.getData('text/html');
124
+ if (html && html.includes('<img')) {
125
+ const m = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
126
+ if (m && m[1]) {
127
+ const srcInHtml = m[1];
128
+ if (/^data:image\//i.test(srcInHtml)) {
129
+ e.preventDefault();
130
+ this._handleImageUpload(srcInHtml, 'clipboard-image.png');
131
+ return;
132
+ }
133
+ if (/^https?:\/\//i.test(srcInHtml)) {
134
+ e.preventDefault();
135
+ try {
136
+ const resp = await fetch(srcInHtml, { mode: 'cors' });
137
+ const blob = await resp.blob();
138
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
139
+ this._handleImageUpload(dataUrl, srcInHtml.split('/').pop() || 'image');
140
+ } catch (_) {
141
+ // как fallback, попробуем напрямую URL
142
+ this.eventBus.emit(Events.UI.PasteImage, { src: srcInHtml, name: srcInHtml.split('/').pop() || 'image' });
143
+ }
144
+ return;
145
+ }
146
+ if (/^blob:/i.test(srcInHtml)) {
147
+ // Попробуем прочитать из системного буфера, если браузер разрешит
148
+ try {
149
+ if (navigator.clipboard && navigator.clipboard.read) {
150
+ const itemsFromAPI = await navigator.clipboard.read();
151
+ for (const it of itemsFromAPI) {
152
+ const imgType = (it.types || []).find(t => t.startsWith('image/'));
153
+ if (!imgType) continue;
154
+ const blob = await it.getType(imgType);
155
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
156
+ e.preventDefault();
157
+ this._handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
158
+ return;
159
+ }
160
+ }
161
+ } catch (_) {}
162
+ }
163
+ }
164
+ }
165
+ // 4) text/plain with image URL or data URL
166
+ const text = cd.getData && cd.getData('text/plain');
167
+ if (text) {
168
+ const trimmed = text.trim();
169
+ const isDataUrl = /^data:image\//i.test(trimmed);
170
+ const isHttpUrl = /^https?:\/\//i.test(trimmed);
171
+ const looksLikeImage = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
172
+ if (isDataUrl) {
173
+ e.preventDefault();
174
+ this._handleImageUpload(trimmed, 'clipboard-image.png');
175
+ return;
176
+ }
177
+ if (isHttpUrl && looksLikeImage) {
178
+ e.preventDefault();
179
+ try {
180
+ const resp = await fetch(trimmed, { mode: 'cors' });
181
+ const blob = await resp.blob();
182
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
183
+ this._handleImageUpload(dataUrl, trimmed.split('/').pop() || 'image');
184
+ return;
185
+ } catch (_) {
186
+ // Если не удалось из-за CORS, попробуем напрямую URL (PIXI загрузит)
187
+ this.eventBus.emit(Events.UI.PasteImage, { src: trimmed, name: trimmed.split('/').pop() || 'image' });
188
+ return;
189
+ }
190
+ }
191
+ }
192
+ // 5) Fallback: попробовать Clipboard API напрямую
193
+ try {
194
+ if (!handled && navigator.clipboard && navigator.clipboard.read) {
195
+ const itemsFromAPI = await navigator.clipboard.read();
196
+ for (const it of itemsFromAPI) {
197
+ const imgType = (it.types || []).find(t => t.startsWith('image/'));
198
+ if (!imgType) continue;
199
+ const blob = await it.getType(imgType);
200
+ const dataUrl = await new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(blob); });
201
+ e.preventDefault();
202
+ this._handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
203
+ return;
204
+ }
205
+ }
206
+ } catch(_) {}
207
+ } catch (err) {
208
+ console.error('Error in paste handler:', err);
209
+ }
210
+ }, { capture: true });
211
+ this.isListening = true;
212
+
213
+ // Регистрируем стандартные горячие клавиши
214
+ this.registerDefaultShortcuts();
215
+ }
216
+
217
+ /**
218
+ * Остановить прослушивание клавиатуры
219
+ */
220
+ stopListening() {
221
+ if (!this.isListening) return;
222
+
223
+ this.targetElement.removeEventListener('keydown', this.handleKeyDown);
224
+ this.targetElement.removeEventListener('keyup', this.handleKeyUp);
225
+ this.isListening = false;
226
+ }
227
+
228
+ /**
229
+ * Регистрация горячей клавиши
230
+ * @param {string} combination - Комбинация клавиш (например: 'ctrl+a', 'delete', 'escape')
231
+ * @param {Function} handler - Обработчик события
232
+ * @param {Object} options - Дополнительные опции
233
+ */
234
+ registerShortcut(combination, handler, options = {}) {
235
+ const normalizedCombo = this.normalizeShortcut(combination);
236
+
237
+ if (!this.shortcuts.has(normalizedCombo)) {
238
+ this.shortcuts.set(normalizedCombo, []);
239
+ }
240
+
241
+ this.shortcuts.get(normalizedCombo).push({
242
+ handler,
243
+ preventDefault: options.preventDefault !== false, // По умолчанию true
244
+ stopPropagation: options.stopPropagation !== false, // По умолчанию true
245
+ description: options.description || ''
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Удаление горячей клавиши
251
+ */
252
+ unregisterShortcut(combination, handler = null) {
253
+ const normalizedCombo = this.normalizeShortcut(combination);
254
+
255
+ if (!this.shortcuts.has(normalizedCombo)) return;
256
+
257
+ if (handler) {
258
+ // Удаляем конкретный обработчик
259
+ const handlers = this.shortcuts.get(normalizedCombo);
260
+ const filtered = handlers.filter(item => item.handler !== handler);
261
+
262
+ if (filtered.length === 0) {
263
+ this.shortcuts.delete(normalizedCombo);
264
+ } else {
265
+ this.shortcuts.set(normalizedCombo, filtered);
266
+ }
267
+ } else {
268
+ // Удаляем все обработчики для комбинации
269
+ this.shortcuts.delete(normalizedCombo);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Обработка нажатия клавиши
275
+ */
276
+ handleKeyDown(event) {
277
+ // Пропускаем события в полях ввода
278
+ if (this.isInputElement(event.target)) {
279
+ return;
280
+ }
281
+
282
+ const combination = this.eventToShortcut(event);
283
+ const handlers = this.shortcuts.get(combination);
284
+
285
+ if (handlers && handlers.length > 0) {
286
+ // Выполняем все обработчики для данной комбинации
287
+ handlers.forEach(({ handler, preventDefault, stopPropagation }) => {
288
+ if (preventDefault) event.preventDefault();
289
+ if (stopPropagation) event.stopPropagation();
290
+
291
+ handler(event);
292
+ });
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Обработка отпускания клавиши
298
+ */
299
+ handleKeyUp(event) {
300
+ // Можно использовать для отслеживания длительных нажатий
301
+ const combination = this.eventToShortcut(event, 'keyup');
302
+
303
+ // Эмитируем событие для инструментов
304
+ this.eventBus.emit(Events.Keyboard.KeyUp, {
305
+ key: event.key,
306
+ code: event.code,
307
+ combination,
308
+ originalEvent: event
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Нормализация комбинации клавиш
314
+ */
315
+ normalizeShortcut(combination) {
316
+ return combination
317
+ .toLowerCase()
318
+ .split('+')
319
+ .map(key => key.trim())
320
+ .sort((a, b) => {
321
+ // Сортируем модификаторы в определенном порядке
322
+ const order = ['ctrl', 'alt', 'shift', 'meta'];
323
+ const aIndex = order.indexOf(a);
324
+ const bIndex = order.indexOf(b);
325
+
326
+ if (aIndex !== -1 && bIndex !== -1) {
327
+ return aIndex - bIndex;
328
+ }
329
+ if (aIndex !== -1) return -1;
330
+ if (bIndex !== -1) return 1;
331
+ return a.localeCompare(b);
332
+ })
333
+ .join('+');
334
+ }
335
+
336
+ /**
337
+ * Преобразование события клавиатуры в строку комбинации
338
+ */
339
+ eventToShortcut(event, eventType = 'keydown') {
340
+ const parts = [];
341
+
342
+ if (event.ctrlKey) parts.push('ctrl');
343
+ if (event.altKey) parts.push('alt');
344
+ if (event.shiftKey) parts.push('shift');
345
+ if (event.metaKey) parts.push('meta');
346
+
347
+ // Нормализуем ключ
348
+ let key = event.key.toLowerCase();
349
+
350
+ // Специальные клавиши
351
+ const specialKeys = {
352
+ ' ': 'space',
353
+ 'enter': 'enter',
354
+ 'escape': 'escape',
355
+ 'backspace': 'backspace',
356
+ 'delete': 'delete',
357
+ 'tab': 'tab',
358
+ 'arrowup': 'arrowup',
359
+ 'arrowdown': 'arrowdown',
360
+ 'arrowleft': 'arrowleft',
361
+ 'arrowright': 'arrowright'
362
+ };
363
+
364
+ if (specialKeys[key]) {
365
+ key = specialKeys[key];
366
+ }
367
+
368
+ // Не добавляем модификаторы как основную клавишу
369
+ if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
370
+ parts.push(key);
371
+ }
372
+
373
+ return parts.join('+');
374
+ }
375
+
376
+ /**
377
+ * Проверка, является ли элемент полем ввода
378
+ */
379
+ isInputElement(element) {
380
+ const inputTags = ['input', 'textarea', 'select'];
381
+ const isInput = inputTags.includes(element.tagName.toLowerCase());
382
+ const isContentEditable = element.contentEditable === 'true';
383
+
384
+ return isInput || isContentEditable;
385
+ }
386
+
387
+ /**
388
+ * Регистрация стандартных горячих клавиш для MoodBoard
389
+ */
390
+ registerDefaultShortcuts() {
391
+ // Выделение всех объектов
392
+ this.registerShortcut('ctrl+a', () => {
393
+ this.eventBus.emit(Events.Keyboard.SelectAll);
394
+ }, { description: 'Выделить все объекты' });
395
+
396
+ // Удаление выделенных объектов
397
+ this.registerShortcut('delete', () => {
398
+ // Проверяем, не активен ли какой-либо текстовый редактор
399
+ if (this._isTextEditorActive()) {
400
+ console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
401
+ return;
402
+ }
403
+ this.eventBus.emit(Events.Keyboard.Delete);
404
+ }, { description: 'Удалить выделенные объекты' });
405
+
406
+ this.registerShortcut('backspace', () => {
407
+ // Проверяем, не активен ли какой-либо текстовый редактор
408
+ if (this._isTextEditorActive()) {
409
+ console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
410
+ return;
411
+ }
412
+ this.eventBus.emit(Events.Keyboard.Delete);
413
+ }, { description: 'Удалить выделенные объекты' });
414
+
415
+ // Отмена выделения
416
+ this.registerShortcut('escape', () => {
417
+ this.eventBus.emit(Events.Keyboard.Escape);
418
+ }, { description: 'Отменить выделение' });
419
+
420
+ // Копирование
421
+ this.registerShortcut('ctrl+c', () => {
422
+ this.eventBus.emit(Events.Keyboard.Copy);
423
+ }, { description: 'Копировать выделенные объекты' });
424
+
425
+ // Вставка
426
+ this.registerShortcut('ctrl+v', () => {
427
+ this.eventBus.emit(Events.Keyboard.Paste);
428
+ }, { description: 'Вставить объекты' });
429
+
430
+ // Отмена действия
431
+ this.registerShortcut('ctrl+z', () => {
432
+ this.eventBus.emit(Events.Keyboard.Undo);
433
+ }, { description: 'Отменить действие' });
434
+
435
+ // Повтор действия
436
+ this.registerShortcut('ctrl+y', () => {
437
+ this.eventBus.emit(Events.Keyboard.Redo);
438
+ }, { description: 'Повторить действие' });
439
+
440
+ this.registerShortcut('ctrl+shift+z', () => {
441
+ this.eventBus.emit(Events.Keyboard.Redo);
442
+ }, { description: 'Повторить действие' });
443
+
444
+ // Переключение инструментов
445
+ this.registerShortcut('v', () => {
446
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
447
+ }, { description: 'Инструмент выделения' });
448
+
449
+ this.registerShortcut('t', () => {
450
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'text' });
451
+ }, { description: 'Инструмент текста' });
452
+
453
+ this.registerShortcut('r', () => {
454
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'frame' });
455
+ }, { description: 'Инструмент рамки' });
456
+
457
+ // Перемещение объектов стрелками
458
+ this.registerShortcut('arrowup', () => {
459
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'up', step: 1 });
460
+ }, { description: 'Переместить объект вверх' });
461
+
462
+ this.registerShortcut('arrowdown', () => {
463
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'down', step: 1 });
464
+ }, { description: 'Переместить объект вниз' });
465
+
466
+ this.registerShortcut('arrowleft', () => {
467
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'left', step: 1 });
468
+ }, { description: 'Переместить объект влево' });
469
+
470
+ this.registerShortcut('arrowright', () => {
471
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'right', step: 1 });
472
+ }, { description: 'Переместить объект вправо' });
473
+
474
+ // Перемещение с шагом 10px при зажатом Shift
475
+ this.registerShortcut('shift+arrowup', () => {
476
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'up', step: 10 });
477
+ }, { description: 'Переместить объект вверх на 10px' });
478
+
479
+ this.registerShortcut('shift+arrowdown', () => {
480
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'down', step: 10 });
481
+ }, { description: 'Переместить объект вниз на 10px' });
482
+
483
+ this.registerShortcut('shift+arrowleft', () => {
484
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'left', step: 10 });
485
+ }, { description: 'Переместить объект влево на 10px' });
486
+
487
+ this.registerShortcut('shift+arrowright', () => {
488
+ this.eventBus.emit(Events.Keyboard.Move, { direction: 'right', step: 10 });
489
+ }, { description: 'Переместить объект вправо на 10px' });
490
+ }
491
+
492
+ /**
493
+ * Получить список всех зарегистрированных горячих клавиш
494
+ */
495
+ getShortcuts() {
496
+ const result = [];
497
+
498
+ for (const [combination, handlers] of this.shortcuts.entries()) {
499
+ handlers.forEach(({ description }) => {
500
+ result.push({
501
+ combination,
502
+ description
503
+ });
504
+ });
505
+ }
506
+
507
+ return result.sort((a, b) => a.combination.localeCompare(b.combination));
508
+ }
509
+
510
+ /**
511
+ * Регистрация стандартных горячих клавиш
512
+ */
513
+ registerDefaultShortcuts() {
514
+ // Undo/Redo (латиница и кириллица)
515
+ this.registerShortcut('ctrl+z', () => {
516
+ this.eventBus.emit(Events.Keyboard.Undo);
517
+ }, { description: 'Отменить действие', preventDefault: true });
518
+
519
+ this.registerShortcut('ctrl+я', () => { // русская 'я' на той же клавише что и 'z'
520
+ this.eventBus.emit(Events.Keyboard.Undo);
521
+ }, { description: 'Отменить действие (рус)', preventDefault: true });
522
+
523
+ this.registerShortcut('ctrl+shift+z', () => {
524
+ this.eventBus.emit(Events.Keyboard.Redo);
525
+ }, { description: 'Повторить действие', preventDefault: true });
526
+
527
+ this.registerShortcut('ctrl+shift+я', () => {
528
+ this.eventBus.emit(Events.Keyboard.Redo);
529
+ }, { description: 'Повторить действие (рус)', preventDefault: true });
530
+
531
+ this.registerShortcut('ctrl+y', () => {
532
+ this.eventBus.emit(Events.Keyboard.Redo);
533
+ }, { description: 'Повторить действие (альтернативный)', preventDefault: true });
534
+
535
+ this.registerShortcut('ctrl+н', () => { // русская 'н' на той же клавише что и 'y'
536
+ this.eventBus.emit(Events.Keyboard.Redo);
537
+ }, { description: 'Повторить действие (рус альт)', preventDefault: true });
538
+
539
+ // Выделение (латиница и кириллица)
540
+ this.registerShortcut('ctrl+a', () => {
541
+ this.eventBus.emit(Events.Keyboard.SelectAll);
542
+ }, { description: 'Выделить все', preventDefault: true });
543
+
544
+ this.registerShortcut('ctrl+ф', () => { // русская 'ф' на той же клавише что и 'a'
545
+ this.eventBus.emit(Events.Keyboard.SelectAll);
546
+ }, { description: 'Выделить все (рус)', preventDefault: true });
547
+
548
+ // Копирование/Вставка (латиница и кириллица)
549
+ this.registerShortcut('ctrl+c', () => {
550
+ this.eventBus.emit(Events.Keyboard.Copy);
551
+ }, { description: 'Копировать', preventDefault: true });
552
+
553
+ this.registerShortcut('ctrl+с', () => { // русская 'с' на той же клавише что и 'c'
554
+ this.eventBus.emit(Events.Keyboard.Copy);
555
+ }, { description: 'Копировать (рус)', preventDefault: true });
556
+
557
+ this.registerShortcut('ctrl+v', () => {
558
+ this.eventBus.emit(Events.Keyboard.Paste);
559
+ }, { description: 'Вставить', preventDefault: false });
560
+
561
+ this.registerShortcut('ctrl+м', () => { // русская 'м' на той же клавише что и 'v'
562
+ this.eventBus.emit(Events.Keyboard.Paste);
563
+ }, { description: 'Вставить (рус)', preventDefault: false });
564
+
565
+ // Слойность (латиница и русская раскладка)
566
+ this.registerShortcut(']', () => {
567
+ const data = { selection: [] };
568
+ this.eventBus.emit(Events.Tool.GetSelection, data);
569
+ const id = data.selection?.[0];
570
+ if (id) this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: id });
571
+ }, { description: 'На передний план', preventDefault: true });
572
+ this.registerShortcut('ctrl+]', () => {
573
+ const data = { selection: [] };
574
+ this.eventBus.emit(Events.Tool.GetSelection, data);
575
+ const id = data.selection?.[0];
576
+ if (id) this.eventBus.emit(Events.UI.LayerBringForward, { objectId: id });
577
+ }, { description: 'Перенести вперёд', preventDefault: true });
578
+ this.registerShortcut('[', () => {
579
+ const data = { selection: [] };
580
+ this.eventBus.emit(Events.Tool.GetSelection, data);
581
+ const id = data.selection?.[0];
582
+ if (id) this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: id });
583
+ }, { description: 'На задний план', preventDefault: true });
584
+ this.registerShortcut('ctrl+[', () => {
585
+ const data = { selection: [] };
586
+ this.eventBus.emit(Events.Tool.GetSelection, data);
587
+ const id = data.selection?.[0];
588
+ if (id) this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: id });
589
+ }, { description: 'Перенести назад', preventDefault: true });
590
+
591
+ // Удаление
592
+ this.registerShortcut('delete', () => {
593
+ // Проверяем, не активен ли какой-либо текстовый редактор
594
+ if (this._isTextEditorActive()) {
595
+ console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
596
+ return;
597
+ }
598
+ this.eventBus.emit(Events.Keyboard.Delete);
599
+ }, { description: 'Удалить объект', preventDefault: true });
600
+
601
+ this.registerShortcut('backspace', () => {
602
+ // Проверяем, не активен ли какой-либо текстовый редактор
603
+ if (this._isTextEditorActive()) {
604
+ console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
605
+ return;
606
+ }
607
+ this.eventBus.emit(Events.Keyboard.Delete);
608
+ }, { description: 'Удалить объект', preventDefault: true });
609
+
610
+ // Отмена выделения
611
+ this.registerShortcut('escape', () => {
612
+ this.eventBus.emit(Events.Keyboard.Escape);
613
+ }, { description: 'Отменить выделение', preventDefault: true });
614
+
615
+ // Инструменты (латиница и кириллица)
616
+ this.registerShortcut('v', () => {
617
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
618
+ }, { description: 'Выбрать инструмент выделения' });
619
+
620
+ this.registerShortcut('м', () => { // русская 'м' на той же клавише что и 'v'
621
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
622
+ }, { description: 'Выбрать инструмент выделения (рус)' });
623
+
624
+ this.registerShortcut('t', () => {
625
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'text' });
626
+ }, { description: 'Выбрать инструмент текста' });
627
+
628
+ this.registerShortcut('е', () => { // русская 'е' на той же клавише что и 't'
629
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'text' });
630
+ }, { description: 'Выбрать инструмент текста (рус)' });
631
+
632
+ this.registerShortcut('r', () => {
633
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'frame' });
634
+ }, { description: 'Выбрать инструмент рамки' });
635
+
636
+ this.registerShortcut('к', () => { // русская 'к' на той же клавише что и 'r'
637
+ this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'frame' });
638
+ }, { description: 'Выбрать инструмент рамки (рус)' });
639
+
640
+ // Перемещение стрелками
641
+ this.registerShortcut('arrowup', (event) => {
642
+ this.eventBus.emit(Events.Keyboard.Move, {
643
+ direction: 'up',
644
+ step: event.shiftKey ? 10 : 1
645
+ });
646
+ }, { description: 'Переместить вверх', preventDefault: true });
647
+
648
+ this.registerShortcut('arrowdown', (event) => {
649
+ this.eventBus.emit(Events.Keyboard.Move, {
650
+ direction: 'down',
651
+ step: event.shiftKey ? 10 : 1
652
+ });
653
+ }, { description: 'Переместить вниз', preventDefault: true });
654
+
655
+ this.registerShortcut('arrowleft', (event) => {
656
+ this.eventBus.emit(Events.Keyboard.Move, {
657
+ direction: 'left',
658
+ step: event.shiftKey ? 10 : 1
659
+ });
660
+ }, { description: 'Переместить влево', preventDefault: true });
661
+
662
+ this.registerShortcut('arrowright', (event) => {
663
+ this.eventBus.emit(Events.Keyboard.Move, {
664
+ direction: 'right',
665
+ step: event.shiftKey ? 10 : 1
666
+ });
667
+ }, { description: 'Переместить вправо', preventDefault: true });
668
+
669
+
670
+ }
671
+
672
+ /**
673
+ * Проверяет, активен ли какой-либо текстовый редактор
674
+ * @private
675
+ */
676
+ _isTextEditorActive() {
677
+ // Проверяем фокус на стандартных HTML элементах ввода
678
+ const activeElement = document.activeElement;
679
+
680
+ if (activeElement && (
681
+ activeElement.tagName === 'INPUT' ||
682
+ activeElement.tagName === 'TEXTAREA' ||
683
+ activeElement.contentEditable === 'true'
684
+ )) {
685
+ return true;
686
+ }
687
+
688
+ // Проверяем наличие активных редакторов названий файлов
689
+ const fileNameEditor = document.querySelector('.moodboard-file-name-editor');
690
+ if (fileNameEditor && fileNameEditor.style.display !== 'none') {
691
+ return true;
692
+ }
693
+
694
+ // Проверяем наличие активных редакторов текста
695
+ const textEditor = document.querySelector('.moodboard-text-editor');
696
+ if (textEditor && textEditor.style.display !== 'none') {
697
+ return true;
698
+ }
699
+
700
+ return false;
701
+ }
702
+
703
+ /**
704
+ * Очистка ресурсов
705
+ */
706
+ destroy() {
707
+ this.stopListening();
708
+ this.shortcuts.clear();
709
+ }
710
+ }