@sequent-org/moodboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,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
|
+
}
|