@sequent-org/moodboard 1.2.118 → 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 +7 -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 -1765
- 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 -976
- 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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Events } from '../events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class KeyboardClipboardImagePaste {
|
|
4
|
+
constructor(eventBus, core = null) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.core = core;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async handleImageUpload(dataUrl, fileName) {
|
|
10
|
+
try {
|
|
11
|
+
if (this.core && this.core.imageUploadService) {
|
|
12
|
+
const uploadResult = await this.core.imageUploadService.uploadFromDataUrl(dataUrl, fileName);
|
|
13
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
14
|
+
src: uploadResult.url,
|
|
15
|
+
name: uploadResult.name,
|
|
16
|
+
imageId: uploadResult.imageId || uploadResult.id
|
|
17
|
+
});
|
|
18
|
+
} else {
|
|
19
|
+
this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Ошибка загрузки изображения:', error);
|
|
23
|
+
this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handleImageFileUpload(file, fileName) {
|
|
28
|
+
try {
|
|
29
|
+
if (this.core && this.core.imageUploadService) {
|
|
30
|
+
const uploadResult = await this.core.imageUploadService.uploadImage(file, fileName);
|
|
31
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
32
|
+
src: uploadResult.url,
|
|
33
|
+
name: uploadResult.name,
|
|
34
|
+
imageId: uploadResult.imageId || uploadResult.id
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
const reader = new FileReader();
|
|
38
|
+
reader.onload = () => {
|
|
39
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
40
|
+
src: reader.result,
|
|
41
|
+
name: fileName
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
reader.readAsDataURL(file);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Ошибка загрузки файла изображения:', error);
|
|
48
|
+
try {
|
|
49
|
+
const reader = new FileReader();
|
|
50
|
+
reader.onload = () => {
|
|
51
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
52
|
+
src: reader.result,
|
|
53
|
+
name: fileName
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
reader.readAsDataURL(file);
|
|
57
|
+
} catch (fallbackError) {
|
|
58
|
+
console.error('Критическая ошибка при чтении файла:', fallbackError);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
createPasteHandler() {
|
|
64
|
+
return async (e) => {
|
|
65
|
+
try {
|
|
66
|
+
const cd = e.clipboardData;
|
|
67
|
+
if (!cd) return;
|
|
68
|
+
let handled = false;
|
|
69
|
+
|
|
70
|
+
const items = cd.items ? Array.from(cd.items) : [];
|
|
71
|
+
const imageItem = items.find(i => i.type && i.type.startsWith('image/'));
|
|
72
|
+
if (imageItem) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const file = imageItem.getAsFile();
|
|
75
|
+
if (file) {
|
|
76
|
+
await this.handleImageFileUpload(file, file.name || 'clipboard-image.png');
|
|
77
|
+
handled = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (handled) return;
|
|
81
|
+
|
|
82
|
+
const files = cd.files ? Array.from(cd.files) : [];
|
|
83
|
+
const imgFile = files.find(f => f.type && f.type.startsWith('image/'));
|
|
84
|
+
if (imgFile) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
await this.handleImageFileUpload(imgFile, imgFile.name || 'clipboard-image.png');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const html = cd.getData && cd.getData('text/html');
|
|
91
|
+
if (html && html.includes('<img')) {
|
|
92
|
+
const match = html.match(/<img[^>]*src\s*=\s*"([^"]+)"/i);
|
|
93
|
+
if (match && match[1]) {
|
|
94
|
+
const srcInHtml = match[1];
|
|
95
|
+
if (/^data:image\//i.test(srcInHtml)) {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
this.handleImageUpload(srcInHtml, 'clipboard-image.png');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (/^https?:\/\//i.test(srcInHtml)) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
try {
|
|
103
|
+
const resp = await fetch(srcInHtml, { mode: 'cors' });
|
|
104
|
+
const blob = await resp.blob();
|
|
105
|
+
const dataUrl = await this._blobToDataUrl(blob);
|
|
106
|
+
this.handleImageUpload(dataUrl, srcInHtml.split('/').pop() || 'image');
|
|
107
|
+
} catch (_) {
|
|
108
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
109
|
+
src: srcInHtml,
|
|
110
|
+
name: srcInHtml.split('/').pop() || 'image'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (/^blob:/i.test(srcInHtml)) {
|
|
116
|
+
try {
|
|
117
|
+
if (navigator.clipboard && navigator.clipboard.read) {
|
|
118
|
+
const itemsFromAPI = await navigator.clipboard.read();
|
|
119
|
+
for (const item of itemsFromAPI) {
|
|
120
|
+
const imgType = (item.types || []).find(t => t.startsWith('image/'));
|
|
121
|
+
if (!imgType) continue;
|
|
122
|
+
const blob = await item.getType(imgType);
|
|
123
|
+
const dataUrl = await this._blobToDataUrl(blob);
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
this.handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const text = cd.getData && cd.getData('text/plain');
|
|
135
|
+
if (text) {
|
|
136
|
+
const trimmed = text.trim();
|
|
137
|
+
const isDataUrl = /^data:image\//i.test(trimmed);
|
|
138
|
+
const isHttpUrl = /^https?:\/\//i.test(trimmed);
|
|
139
|
+
const looksLikeImage = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(trimmed);
|
|
140
|
+
if (isDataUrl) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
this.handleImageUpload(trimmed, 'clipboard-image.png');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (isHttpUrl && looksLikeImage) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
try {
|
|
148
|
+
const resp = await fetch(trimmed, { mode: 'cors' });
|
|
149
|
+
const blob = await resp.blob();
|
|
150
|
+
const dataUrl = await this._blobToDataUrl(blob);
|
|
151
|
+
this.handleImageUpload(dataUrl, trimmed.split('/').pop() || 'image');
|
|
152
|
+
return;
|
|
153
|
+
} catch (_) {
|
|
154
|
+
this.eventBus.emit(Events.UI.PasteImage, {
|
|
155
|
+
src: trimmed,
|
|
156
|
+
name: trimmed.split('/').pop() || 'image'
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (!handled && navigator.clipboard && navigator.clipboard.read) {
|
|
165
|
+
const itemsFromAPI = await navigator.clipboard.read();
|
|
166
|
+
for (const item of itemsFromAPI) {
|
|
167
|
+
const imgType = (item.types || []).find(t => t.startsWith('image/'));
|
|
168
|
+
if (!imgType) continue;
|
|
169
|
+
const blob = await item.getType(imgType);
|
|
170
|
+
const dataUrl = await this._blobToDataUrl(blob);
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
this.handleImageUpload(dataUrl, `clipboard.${imgType.split('/')[1] || 'png'}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (_) {}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('Error in paste handler:', err);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_blobToDataUrl(blob) {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const reader = new FileReader();
|
|
186
|
+
reader.onload = () => resolve(reader.result);
|
|
187
|
+
reader.readAsDataURL(blob);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isInputElement(element) {
|
|
2
|
+
if (!element || !element.tagName) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const inputTags = ['input', 'textarea', 'select'];
|
|
7
|
+
const isInput = inputTags.includes(element.tagName.toLowerCase());
|
|
8
|
+
const isContentEditable = element.contentEditable === 'true';
|
|
9
|
+
|
|
10
|
+
return isInput || isContentEditable;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isTextEditorActive(doc = document) {
|
|
14
|
+
const activeElement = doc.activeElement;
|
|
15
|
+
|
|
16
|
+
if (activeElement && (
|
|
17
|
+
activeElement.tagName === 'INPUT' ||
|
|
18
|
+
activeElement.tagName === 'TEXTAREA' ||
|
|
19
|
+
activeElement.contentEditable === 'true'
|
|
20
|
+
)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fileNameEditor = doc.querySelector('.moodboard-file-name-editor');
|
|
25
|
+
if (fileNameEditor && fileNameEditor.style.display !== 'none') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const textEditor = doc.querySelector('.moodboard-text-editor');
|
|
30
|
+
if (textEditor && textEditor.style.display !== 'none') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Events } from '../events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class KeyboardEventRouter {
|
|
4
|
+
constructor(eventBus, shortcuts, isInputElementGuard) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.shortcuts = shortcuts;
|
|
7
|
+
this.isInputElementGuard = isInputElementGuard;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
handleKeyDown(event) {
|
|
11
|
+
if (this.isInputElementGuard(event.target)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const combination = this.eventToShortcut(event);
|
|
16
|
+
const handlers = this.shortcuts.get(combination);
|
|
17
|
+
|
|
18
|
+
if (handlers && handlers.length > 0) {
|
|
19
|
+
handlers.forEach(({ handler, preventDefault, stopPropagation }) => {
|
|
20
|
+
if (preventDefault) event.preventDefault();
|
|
21
|
+
if (stopPropagation) event.stopPropagation();
|
|
22
|
+
|
|
23
|
+
handler(event);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleKeyUp(event) {
|
|
29
|
+
const combination = this.eventToShortcut(event, 'keyup');
|
|
30
|
+
|
|
31
|
+
this.eventBus.emit(Events.Keyboard.KeyUp, {
|
|
32
|
+
key: event.key,
|
|
33
|
+
code: event.code,
|
|
34
|
+
combination,
|
|
35
|
+
originalEvent: event
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
normalizeShortcut(combination) {
|
|
40
|
+
return combination
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.split('+')
|
|
43
|
+
.map(key => key.trim())
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
const order = ['ctrl', 'alt', 'shift', 'meta'];
|
|
46
|
+
const aIndex = order.indexOf(a);
|
|
47
|
+
const bIndex = order.indexOf(b);
|
|
48
|
+
|
|
49
|
+
if (aIndex !== -1 && bIndex !== -1) {
|
|
50
|
+
return aIndex - bIndex;
|
|
51
|
+
}
|
|
52
|
+
if (aIndex !== -1) return -1;
|
|
53
|
+
if (bIndex !== -1) return 1;
|
|
54
|
+
return a.localeCompare(b);
|
|
55
|
+
})
|
|
56
|
+
.join('+');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
eventToShortcut(event, eventType = 'keydown') {
|
|
60
|
+
const parts = [];
|
|
61
|
+
|
|
62
|
+
if (event.ctrlKey) parts.push('ctrl');
|
|
63
|
+
if (event.altKey) parts.push('alt');
|
|
64
|
+
if (event.shiftKey) parts.push('shift');
|
|
65
|
+
if (event.metaKey) parts.push('meta');
|
|
66
|
+
|
|
67
|
+
let key = event.key.toLowerCase();
|
|
68
|
+
|
|
69
|
+
const specialKeys = {
|
|
70
|
+
' ': 'space',
|
|
71
|
+
'enter': 'enter',
|
|
72
|
+
'escape': 'escape',
|
|
73
|
+
'backspace': 'backspace',
|
|
74
|
+
'delete': 'delete',
|
|
75
|
+
'tab': 'tab',
|
|
76
|
+
'arrowup': 'arrowup',
|
|
77
|
+
'arrowdown': 'arrowdown',
|
|
78
|
+
'arrowleft': 'arrowleft',
|
|
79
|
+
'arrowright': 'arrowright'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (specialKeys[key]) {
|
|
83
|
+
key = specialKeys[key];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
|
|
87
|
+
parts.push(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parts.join('+');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Events } from '../events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class KeyboardSelectionActions {
|
|
4
|
+
constructor(eventBus, isTextEditorActive) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.isTextEditorActive = isTextEditorActive;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
createHandler(actionId) {
|
|
10
|
+
switch (actionId) {
|
|
11
|
+
case 'undo':
|
|
12
|
+
return () => {
|
|
13
|
+
this.eventBus.emit(Events.Keyboard.Undo);
|
|
14
|
+
};
|
|
15
|
+
case 'redo':
|
|
16
|
+
return () => {
|
|
17
|
+
this.eventBus.emit(Events.Keyboard.Redo);
|
|
18
|
+
};
|
|
19
|
+
case 'select-all':
|
|
20
|
+
return () => {
|
|
21
|
+
this.eventBus.emit(Events.Keyboard.SelectAll);
|
|
22
|
+
};
|
|
23
|
+
case 'copy':
|
|
24
|
+
return () => {
|
|
25
|
+
this.eventBus.emit(Events.Keyboard.Copy);
|
|
26
|
+
};
|
|
27
|
+
case 'paste':
|
|
28
|
+
return () => {
|
|
29
|
+
this.eventBus.emit(Events.Keyboard.Paste);
|
|
30
|
+
};
|
|
31
|
+
case 'layer-bring-to-front':
|
|
32
|
+
return () => {
|
|
33
|
+
const data = { selection: [] };
|
|
34
|
+
this.eventBus.emit(Events.Tool.GetSelection, data);
|
|
35
|
+
const id = data.selection?.[0];
|
|
36
|
+
if (id) this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: id });
|
|
37
|
+
};
|
|
38
|
+
case 'layer-bring-forward':
|
|
39
|
+
return () => {
|
|
40
|
+
const data = { selection: [] };
|
|
41
|
+
this.eventBus.emit(Events.Tool.GetSelection, data);
|
|
42
|
+
const id = data.selection?.[0];
|
|
43
|
+
if (id) this.eventBus.emit(Events.UI.LayerBringForward, { objectId: id });
|
|
44
|
+
};
|
|
45
|
+
case 'layer-send-to-back':
|
|
46
|
+
return () => {
|
|
47
|
+
const data = { selection: [] };
|
|
48
|
+
this.eventBus.emit(Events.Tool.GetSelection, data);
|
|
49
|
+
const id = data.selection?.[0];
|
|
50
|
+
if (id) this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: id });
|
|
51
|
+
};
|
|
52
|
+
case 'layer-send-backward':
|
|
53
|
+
return () => {
|
|
54
|
+
const data = { selection: [] };
|
|
55
|
+
this.eventBus.emit(Events.Tool.GetSelection, data);
|
|
56
|
+
const id = data.selection?.[0];
|
|
57
|
+
if (id) this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: id });
|
|
58
|
+
};
|
|
59
|
+
case 'delete':
|
|
60
|
+
return () => {
|
|
61
|
+
if (this.isTextEditorActive()) {
|
|
62
|
+
console.log('🔒 KeyboardManager: Текстовый редактор активен, пропускаем удаление объектов');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.eventBus.emit(Events.Keyboard.Delete);
|
|
66
|
+
};
|
|
67
|
+
case 'escape':
|
|
68
|
+
return () => {
|
|
69
|
+
this.eventBus.emit(Events.Keyboard.Escape);
|
|
70
|
+
};
|
|
71
|
+
case 'move-up':
|
|
72
|
+
return (event) => {
|
|
73
|
+
this.eventBus.emit(Events.Keyboard.Move, {
|
|
74
|
+
direction: 'up',
|
|
75
|
+
step: event.shiftKey ? 10 : 1
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
case 'move-down':
|
|
79
|
+
return (event) => {
|
|
80
|
+
this.eventBus.emit(Events.Keyboard.Move, {
|
|
81
|
+
direction: 'down',
|
|
82
|
+
step: event.shiftKey ? 10 : 1
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
case 'move-left':
|
|
86
|
+
return (event) => {
|
|
87
|
+
this.eventBus.emit(Events.Keyboard.Move, {
|
|
88
|
+
direction: 'left',
|
|
89
|
+
step: event.shiftKey ? 10 : 1
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
case 'move-right':
|
|
93
|
+
return (event) => {
|
|
94
|
+
this.eventBus.emit(Events.Keyboard.Move, {
|
|
95
|
+
direction: 'right',
|
|
96
|
+
step: event.shiftKey ? 10 : 1
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_KEYBOARD_SHORTCUTS = [
|
|
2
|
+
{ combination: 'ctrl+z', actionId: 'undo', description: 'Отменить действие', preventDefault: true },
|
|
3
|
+
{ combination: 'ctrl+я', actionId: 'undo', description: 'Отменить действие (рус)', preventDefault: true },
|
|
4
|
+
{ combination: 'ctrl+shift+z', actionId: 'redo', description: 'Повторить действие', preventDefault: true },
|
|
5
|
+
{ combination: 'ctrl+shift+я', actionId: 'redo', description: 'Повторить действие (рус)', preventDefault: true },
|
|
6
|
+
{ combination: 'ctrl+y', actionId: 'redo', description: 'Повторить действие (альтернативный)', preventDefault: true },
|
|
7
|
+
{ combination: 'ctrl+н', actionId: 'redo', description: 'Повторить действие (рус альт)', preventDefault: true },
|
|
8
|
+
{ combination: 'ctrl+a', actionId: 'select-all', description: 'Выделить все', preventDefault: true },
|
|
9
|
+
{ combination: 'ctrl+ф', actionId: 'select-all', description: 'Выделить все (рус)', preventDefault: true },
|
|
10
|
+
{ combination: 'ctrl+c', actionId: 'copy', description: 'Копировать', preventDefault: true },
|
|
11
|
+
{ combination: 'ctrl+с', actionId: 'copy', description: 'Копировать (рус)', preventDefault: true },
|
|
12
|
+
{ combination: 'ctrl+v', actionId: 'paste', description: 'Вставить', preventDefault: false },
|
|
13
|
+
{ combination: 'ctrl+м', actionId: 'paste', description: 'Вставить (рус)', preventDefault: false },
|
|
14
|
+
{ combination: ']', actionId: 'layer-bring-to-front', description: 'На передний план', preventDefault: true },
|
|
15
|
+
{ combination: 'ctrl+]', actionId: 'layer-bring-forward', description: 'Перенести вперёд', preventDefault: true },
|
|
16
|
+
{ combination: '[', actionId: 'layer-send-to-back', description: 'На задний план', preventDefault: true },
|
|
17
|
+
{ combination: 'ctrl+[', actionId: 'layer-send-backward', description: 'Перенести назад', preventDefault: true },
|
|
18
|
+
{ combination: 'delete', actionId: 'delete', description: 'Удалить объект', preventDefault: true },
|
|
19
|
+
{ combination: 'backspace', actionId: 'delete', description: 'Удалить объект', preventDefault: true },
|
|
20
|
+
{ combination: 'escape', actionId: 'escape', description: 'Отменить выделение', preventDefault: true },
|
|
21
|
+
{ combination: 'v', actionId: 'tool-select', description: 'Выбрать инструмент выделения' },
|
|
22
|
+
{ combination: 'м', actionId: 'tool-select', description: 'Выбрать инструмент выделения (рус)' },
|
|
23
|
+
{ combination: 't', actionId: 'tool-text', description: 'Выбрать инструмент текста' },
|
|
24
|
+
{ combination: 'е', actionId: 'tool-text', description: 'Выбрать инструмент текста (рус)' },
|
|
25
|
+
{ combination: 'r', actionId: 'tool-frame', description: 'Выбрать инструмент рамки' },
|
|
26
|
+
{ combination: 'к', actionId: 'tool-frame', description: 'Выбрать инструмент рамки (рус)' },
|
|
27
|
+
{ combination: 'arrowup', actionId: 'move-up', description: 'Переместить вверх', preventDefault: true },
|
|
28
|
+
{ combination: 'arrowdown', actionId: 'move-down', description: 'Переместить вниз', preventDefault: true },
|
|
29
|
+
{ combination: 'arrowleft', actionId: 'move-left', description: 'Переместить влево', preventDefault: true },
|
|
30
|
+
{ combination: 'arrowright', actionId: 'move-right', description: 'Переместить вправо', preventDefault: true },
|
|
31
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Events } from '../events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class KeyboardToolSwitching {
|
|
4
|
+
constructor(eventBus) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
createHandler(actionId) {
|
|
9
|
+
switch (actionId) {
|
|
10
|
+
case 'tool-select':
|
|
11
|
+
return () => {
|
|
12
|
+
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
|
|
13
|
+
};
|
|
14
|
+
case 'tool-text':
|
|
15
|
+
return () => {
|
|
16
|
+
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'text' });
|
|
17
|
+
};
|
|
18
|
+
case 'tool-frame':
|
|
19
|
+
return () => {
|
|
20
|
+
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'frame' });
|
|
21
|
+
};
|
|
22
|
+
default:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -327,14 +327,10 @@ export class ObjectRenderer {
|
|
|
327
327
|
*/
|
|
328
328
|
setFrameFill(objectId, width, height, fillColor = 0xFFFFFF) {
|
|
329
329
|
const pixiObject = this.objects.get(objectId);
|
|
330
|
-
if (!pixiObject
|
|
331
|
-
|
|
330
|
+
if (!pixiObject) return;
|
|
332
331
|
const meta = pixiObject._mb || {};
|
|
333
|
-
if (meta.type !== 'frame') return;
|
|
334
|
-
|
|
335
|
-
if (meta.instance) {
|
|
336
|
-
meta.instance.setFill(fillColor);
|
|
337
|
-
}
|
|
332
|
+
if (meta.type !== 'frame' || !meta.instance) return;
|
|
333
|
+
meta.instance.setFill(fillColor);
|
|
338
334
|
}
|
|
339
335
|
|
|
340
336
|
/**
|
package/src/grid/BaseGrid.js
CHANGED
|
@@ -17,6 +17,8 @@ export class BaseGrid {
|
|
|
17
17
|
// Размеры области отрисовки
|
|
18
18
|
this.width = options.width || 1920;
|
|
19
19
|
this.height = options.height || 1080;
|
|
20
|
+
/** @type {{ left: number, top: number, right: number, bottom: number } | null} */
|
|
21
|
+
this.viewportBounds = null;
|
|
20
22
|
|
|
21
23
|
// PIXI графика
|
|
22
24
|
this.graphics = new PIXI.Graphics();
|
|
@@ -133,8 +135,32 @@ export class BaseGrid {
|
|
|
133
135
|
resize(width, height) {
|
|
134
136
|
this.width = width;
|
|
135
137
|
this.height = height;
|
|
138
|
+
this.viewportBounds = null;
|
|
136
139
|
this.updateVisual();
|
|
137
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Устанавливает видимую область для непрерывной отрисовки при паннинге.
|
|
144
|
+
* @param {number} left
|
|
145
|
+
* @param {number} top
|
|
146
|
+
* @param {number} right
|
|
147
|
+
* @param {number} bottom
|
|
148
|
+
*/
|
|
149
|
+
setVisibleBounds(left, top, right, bottom) {
|
|
150
|
+
this.viewportBounds = { left, top, right, bottom };
|
|
151
|
+
this.updateVisual();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Возвращает границы отрисовки: viewportBounds или (0,0,width,height)
|
|
156
|
+
* @returns {{ left: number, top: number, right: number, bottom: number }}
|
|
157
|
+
*/
|
|
158
|
+
getDrawBounds() {
|
|
159
|
+
if (this.viewportBounds) {
|
|
160
|
+
return this.viewportBounds;
|
|
161
|
+
}
|
|
162
|
+
return { left: 0, top: 0, right: this.width, bottom: this.height };
|
|
163
|
+
}
|
|
138
164
|
|
|
139
165
|
/**
|
|
140
166
|
* Возвращает PIXI объект для рендеринга
|
package/src/grid/CrossGrid.js
CHANGED
|
@@ -34,16 +34,17 @@ export class CrossGrid extends BaseGrid {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const hs = this.crossHalfSize;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const b = this.getDrawBounds();
|
|
38
|
+
const startX = Math.floor(b.left / this.size) * this.size;
|
|
39
|
+
const startY = Math.floor(b.top / this.size) * this.size;
|
|
40
|
+
const endX = Math.ceil(b.right / this.size) * this.size;
|
|
41
|
+
const endY = Math.ceil(b.bottom / this.size) * this.size;
|
|
42
|
+
for (let x = startX; x <= endX; x += this.size) {
|
|
43
|
+
for (let y = startY; y <= endY; y += this.size) {
|
|
41
44
|
const px = Math.round(x) + 0.5;
|
|
42
45
|
const py = Math.round(y) + 0.5;
|
|
43
|
-
// Горизонтальная часть креста
|
|
44
46
|
g.moveTo(px - hs, py);
|
|
45
47
|
g.lineTo(px + hs, py);
|
|
46
|
-
// Вертикальная часть креста
|
|
47
48
|
g.moveTo(px, py - hs);
|
|
48
49
|
g.lineTo(px, py + hs);
|
|
49
50
|
}
|