@sequent-org/moodboard 1.2.119 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Менеджер клавиатуры для обработки горячих клавиш
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
/**
|