@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.
Files changed (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +6 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1773
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -999
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +82 -1181
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +665 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Менеджер клавиатуры для обработки горячих клавиш
3
3
  */
4
- import { Events } from './events/Events.js';
4
+ import { KeyboardClipboardImagePaste } from './keyboard/KeyboardClipboardImagePaste.js';
5
+ import { KeyboardEventRouter } from './keyboard/KeyboardEventRouter.js';
6
+ import { isInputElement, isTextEditorActive } from './keyboard/KeyboardContextGuards.js';
7
+ import { KeyboardSelectionActions } from './keyboard/KeyboardSelectionActions.js';
8
+ import { DEFAULT_KEYBOARD_SHORTCUTS } from './keyboard/KeyboardShortcutMap.js';
9
+ import { KeyboardToolSwitching } from './keyboard/KeyboardToolSwitching.js';
5
10
  export class KeyboardManager {
6
11
  constructor(eventBus, targetElement = document, core = null) {
7
12
  this.eventBus = eventBus;
@@ -9,6 +14,18 @@ export class KeyboardManager {
9
14
  this.core = core;
10
15
  this.shortcuts = new Map();
11
16
  this.isListening = false;
17
+ this.handlePaste = null;
18
+ this.selectionActions = new KeyboardSelectionActions(
19
+ this.eventBus,
20
+ () => this._isTextEditorActive()
21
+ );
22
+ this.toolSwitching = new KeyboardToolSwitching(this.eventBus);
23
+ this.clipboardImagePaste = new KeyboardClipboardImagePaste(this.eventBus, this.core);
24
+ this.eventRouter = new KeyboardEventRouter(
25
+ this.eventBus,
26
+ this.shortcuts,
27
+ (element) => this.isInputElement(element)
28
+ );
12
29
 
13
30
  // Привязываем контекст методов
14
31
  this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -20,24 +37,7 @@ export class KeyboardManager {
20
37
  * @private
21
38
  */
22
39
  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.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
- }
40
+ return this.clipboardImagePaste.handleImageUpload(dataUrl, fileName);
41
41
  }
42
42
 
43
43
  /**
@@ -47,42 +47,7 @@ export class KeyboardManager {
47
47
  * @private
48
48
  */
49
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.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
- }
50
+ return this.clipboardImagePaste.handleImageFileUpload(file, fileName);
86
51
  }
87
52
 
88
53
  /**
@@ -90,124 +55,14 @@ export class KeyboardManager {
90
55
  */
91
56
  startListening() {
92
57
  if (this.isListening) return;
58
+
59
+ if (!this.handlePaste) {
60
+ this.handlePaste = this.clipboardImagePaste.createPasteHandler();
61
+ }
93
62
 
94
63
  this.targetElement.addEventListener('keydown', this.handleKeyDown);
95
64
  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 });
65
+ this.targetElement.addEventListener('paste', this.handlePaste, { capture: true });
211
66
  this.isListening = true;
212
67
 
213
68
  // Регистрируем стандартные горячие клавиши
@@ -222,6 +77,10 @@ export class KeyboardManager {
222
77
 
223
78
  this.targetElement.removeEventListener('keydown', this.handleKeyDown);
224
79
  this.targetElement.removeEventListener('keyup', this.handleKeyUp);
80
+ if (this.handlePaste) {
81
+ this.targetElement.removeEventListener('paste', this.handlePaste, { capture: true });
82
+ this.handlePaste = null;
83
+ }
225
84
  this.isListening = false;
226
85
  }
227
86
 
@@ -274,219 +133,35 @@ export class KeyboardManager {
274
133
  * Обработка нажатия клавиши
275
134
  */
276
135
  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
- }
136
+ this.eventRouter.handleKeyDown(event);
294
137
  }
295
138
 
296
139
  /**
297
140
  * Обработка отпускания клавиши
298
141
  */
299
142
  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
- });
143
+ this.eventRouter.handleKeyUp(event);
310
144
  }
311
145
 
312
146
  /**
313
147
  * Нормализация комбинации клавиш
314
148
  */
315
149
  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('+');
150
+ return this.eventRouter.normalizeShortcut(combination);
334
151
  }
335
152
 
336
153
  /**
337
154
  * Преобразование события клавиатуры в строку комбинации
338
155
  */
339
156
  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('+');
157
+ return this.eventRouter.eventToShortcut(event, eventType);
374
158
  }
375
159
 
376
160
  /**
377
161
  * Проверка, является ли элемент полем ввода
378
162
  */
379
163
  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' });
164
+ return isInputElement(element);
490
165
  }
491
166
 
492
167
  /**
@@ -506,167 +181,24 @@ export class KeyboardManager {
506
181
 
507
182
  return result.sort((a, b) => a.combination.localeCompare(b.combination));
508
183
  }
184
+
185
+ _createDefaultShortcutHandler(actionId) {
186
+ return this.toolSwitching.createHandler(actionId)
187
+ || this.selectionActions.createHandler(actionId)
188
+ || (() => {});
189
+ }
509
190
 
510
191
  /**
511
192
  * Регистрация стандартных горячих клавиш
512
193
  */
513
194
  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
-
195
+ DEFAULT_KEYBOARD_SHORTCUTS.forEach(({ combination, actionId, description, preventDefault }) => {
196
+ this.registerShortcut(
197
+ combination,
198
+ this._createDefaultShortcutHandler(actionId),
199
+ { description, preventDefault }
200
+ );
201
+ });
670
202
  }
671
203
 
672
204
  /**
@@ -674,30 +206,7 @@ export class KeyboardManager {
674
206
  * @private
675
207
  */
676
208
  _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;
209
+ return isTextEditorActive(document);
701
210
  }
702
211
 
703
212
  /**