@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,184 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
import {
|
|
3
|
+
createFileNameEditorInput,
|
|
4
|
+
createFileNameEditorWrapper,
|
|
5
|
+
} from './InlineEditorDomFactory.js';
|
|
6
|
+
import { toScreenWithResolution } from './InlineEditorPositioningService.js';
|
|
7
|
+
|
|
8
|
+
export function openFileNameEditor(object, create = false) {
|
|
9
|
+
// Проверяем структуру объекта и извлекаем данные
|
|
10
|
+
let objectId, position, properties;
|
|
11
|
+
|
|
12
|
+
if (create) {
|
|
13
|
+
// Для создания нового объекта - данные в object.object
|
|
14
|
+
const objData = object.object || object;
|
|
15
|
+
objectId = objData.id || null;
|
|
16
|
+
position = objData.position;
|
|
17
|
+
properties = objData.properties || {};
|
|
18
|
+
} else {
|
|
19
|
+
// Для редактирования существующего объекта - данные в корне
|
|
20
|
+
objectId = object.id;
|
|
21
|
+
position = object.position;
|
|
22
|
+
properties = object.properties || {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fileName = properties.fileName || 'Untitled';
|
|
26
|
+
|
|
27
|
+
// Проверяем, что position существует
|
|
28
|
+
if (!position) {
|
|
29
|
+
console.error('❌ SelectTool: position is undefined in _openFileNameEditor', { object, create });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Закрываем предыдущий редактор, если он открыт
|
|
34
|
+
if (this.textEditor.active) {
|
|
35
|
+
if (this.textEditor.objectType === 'file') {
|
|
36
|
+
this._closeFileNameEditor(true);
|
|
37
|
+
} else {
|
|
38
|
+
this._closeTextEditor(true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Если это редактирование существующего объекта, получаем его данные
|
|
43
|
+
if (!create && objectId) {
|
|
44
|
+
const posData = { objectId, position: null };
|
|
45
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
46
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
47
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
48
|
+
|
|
49
|
+
// Обновляем данные из полученной информации
|
|
50
|
+
if (posData.position) position = posData.position;
|
|
51
|
+
|
|
52
|
+
// Скрываем текст файла на время редактирования
|
|
53
|
+
if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
|
|
54
|
+
const fileInstance = pixiReq.pixiObject._mb.instance;
|
|
55
|
+
if (typeof fileInstance.hideText === 'function') {
|
|
56
|
+
fileInstance.hideText();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Создаем wrapper для input
|
|
62
|
+
const wrapper = createFileNameEditorWrapper();
|
|
63
|
+
|
|
64
|
+
// Создаем input для редактирования названия
|
|
65
|
+
const input = createFileNameEditorInput(fileName);
|
|
66
|
+
|
|
67
|
+
wrapper.appendChild(input);
|
|
68
|
+
document.body.appendChild(wrapper);
|
|
69
|
+
|
|
70
|
+
// Позиционируем редактор (аналогично _openTextEditor)
|
|
71
|
+
const toScreen = (wx, wy) => toScreenWithResolution(this.textEditor.world || (this.app?.stage), this.app, wx, wy);
|
|
72
|
+
const screenPos = toScreen(position.x, position.y);
|
|
73
|
+
|
|
74
|
+
// Получаем размеры файлового объекта для точного позиционирования
|
|
75
|
+
let fileWidth = 120;
|
|
76
|
+
let fileHeight = 140;
|
|
77
|
+
|
|
78
|
+
if (objectId) {
|
|
79
|
+
const sizeData = { objectId, size: null };
|
|
80
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
81
|
+
if (sizeData.size) {
|
|
82
|
+
fileWidth = sizeData.size.width;
|
|
83
|
+
fileHeight = sizeData.size.height;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Позиционируем редактор в нижней части файла (где название)
|
|
88
|
+
// В FileObject название находится в позиции y = height - 40
|
|
89
|
+
const nameY = fileHeight - 40;
|
|
90
|
+
const centerX = fileWidth / 2;
|
|
91
|
+
|
|
92
|
+
wrapper.style.left = `${screenPos.x + centerX - 60}px`; // Центрируем относительно файла
|
|
93
|
+
wrapper.style.top = `${screenPos.y + nameY}px`; // Позиционируем на уровне названия
|
|
94
|
+
|
|
95
|
+
// Сохраняем состояние редактора
|
|
96
|
+
this.textEditor = {
|
|
97
|
+
active: true,
|
|
98
|
+
objectId: objectId,
|
|
99
|
+
textarea: input,
|
|
100
|
+
wrapper: wrapper,
|
|
101
|
+
position: position,
|
|
102
|
+
properties: properties,
|
|
103
|
+
objectType: 'file',
|
|
104
|
+
isResizing: false
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Фокусируем и выделяем весь текст
|
|
108
|
+
input.focus();
|
|
109
|
+
input.select();
|
|
110
|
+
|
|
111
|
+
// Функция завершения редактирования
|
|
112
|
+
const finalize = (commit) => {
|
|
113
|
+
this._closeFileNameEditor(commit);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Обработчики событий
|
|
117
|
+
input.addEventListener('blur', () => finalize(true));
|
|
118
|
+
input.addEventListener('keydown', (e) => {
|
|
119
|
+
if (e.key === 'Enter') {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
finalize(true);
|
|
122
|
+
} else if (e.key === 'Escape') {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
finalize(false);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function closeFileNameEditor(commit) {
|
|
130
|
+
// Проверяем, что редактор существует и не закрыт
|
|
131
|
+
if (!this.textEditor || !this.textEditor.textarea || this.textEditor.closing) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Устанавливаем флаг закрытия, чтобы избежать повторных вызовов
|
|
136
|
+
this.textEditor.closing = true;
|
|
137
|
+
|
|
138
|
+
const input = this.textEditor.textarea;
|
|
139
|
+
const value = input.value.trim();
|
|
140
|
+
const commitValue = commit && value.length > 0;
|
|
141
|
+
const objectId = this.textEditor.objectId;
|
|
142
|
+
|
|
143
|
+
// Убираем wrapper из DOM
|
|
144
|
+
if (this.textEditor.wrapper && this.textEditor.wrapper.parentNode) {
|
|
145
|
+
this.textEditor.wrapper.remove();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Показываем обратно текст файла
|
|
149
|
+
if (objectId) {
|
|
150
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
151
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
152
|
+
|
|
153
|
+
if (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance) {
|
|
154
|
+
const fileInstance = pixiReq.pixiObject._mb.instance;
|
|
155
|
+
if (typeof fileInstance.showText === 'function') {
|
|
156
|
+
fileInstance.showText();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Применяем изменения если нужно
|
|
160
|
+
if (commitValue && value !== this.textEditor.properties.fileName) {
|
|
161
|
+
// Создаем команду изменения названия файла
|
|
162
|
+
const oldName = this.textEditor.properties.fileName || 'Untitled';
|
|
163
|
+
this.eventBus.emit(Events.Object.FileNameChange, {
|
|
164
|
+
objectId: objectId,
|
|
165
|
+
oldName: oldName,
|
|
166
|
+
newName: value
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Сбрасываем состояние редактора
|
|
173
|
+
this.textEditor = {
|
|
174
|
+
active: false,
|
|
175
|
+
objectId: null,
|
|
176
|
+
textarea: null,
|
|
177
|
+
wrapper: null,
|
|
178
|
+
world: null,
|
|
179
|
+
position: null,
|
|
180
|
+
properties: null,
|
|
181
|
+
objectType: 'text',
|
|
182
|
+
isResizing: false
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
|
|
4
|
+
export function hitTest(x, y) {
|
|
5
|
+
// Проверяем, что инструмент не уничтожен
|
|
6
|
+
if (this.destroyed) {
|
|
7
|
+
return { type: 'empty' };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Сначала проверяем ручки изменения размера (они имеют приоритет)
|
|
11
|
+
if (this.resizeHandles) {
|
|
12
|
+
const pixiObjectAtPoint = this.getPixiObjectAt(x, y);
|
|
13
|
+
const handleInfo = this.resizeHandles.getHandleInfo(pixiObjectAtPoint);
|
|
14
|
+
if (handleInfo) {
|
|
15
|
+
// Определяем тип ручки
|
|
16
|
+
const hitType = handleInfo.type === 'rotate' ? 'rotate-handle' : 'resize-handle';
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
type: hitType,
|
|
20
|
+
handle: handleInfo.type,
|
|
21
|
+
object: handleInfo.targetObjectId,
|
|
22
|
+
pixiObject: handleInfo.handle
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Получаем объекты из системы через событие
|
|
28
|
+
const hitTestData = { x, y, result: null };
|
|
29
|
+
this.emit(Events.Tool.HitTest, hitTestData);
|
|
30
|
+
|
|
31
|
+
if (hitTestData.result && hitTestData.result.object) {
|
|
32
|
+
return hitTestData.result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { type: 'empty' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getPixiObjectAt(x, y) {
|
|
39
|
+
// Проверяем, что инструмент не уничтожен
|
|
40
|
+
if (this.destroyed) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!this.resizeHandles || !this.resizeHandles.app || !this.resizeHandles.container) return null;
|
|
45
|
+
|
|
46
|
+
const point = new PIXI.Point(x, y);
|
|
47
|
+
|
|
48
|
+
// Сначала ищем в контейнере ручек (приоритет)
|
|
49
|
+
if (this.resizeHandles.container && this.resizeHandles.container.visible) {
|
|
50
|
+
const container = this.resizeHandles.container;
|
|
51
|
+
if (!container || !container.children) return null;
|
|
52
|
+
|
|
53
|
+
for (let i = container.children.length - 1; i >= 0; i--) {
|
|
54
|
+
const child = container.children[i];
|
|
55
|
+
|
|
56
|
+
// Проверяем обычные объекты
|
|
57
|
+
if (child && child.containsPoint && typeof child.containsPoint === 'function') {
|
|
58
|
+
try {
|
|
59
|
+
if (child.containsPoint(point)) {
|
|
60
|
+
return child;
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {
|
|
63
|
+
// Игнорируем ошибки containsPoint
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Специальная проверка для контейнеров (ручка вращения)
|
|
68
|
+
if (child instanceof PIXI.Container && child.children && child.children.length > 0) {
|
|
69
|
+
// Проверяем границы контейнера
|
|
70
|
+
try {
|
|
71
|
+
const bounds = child.getBounds();
|
|
72
|
+
if (bounds && point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
|
|
73
|
+
point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
|
|
74
|
+
return child;
|
|
75
|
+
}
|
|
76
|
+
} catch (_) {
|
|
77
|
+
// Игнорируем ошибки getBounds
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Затем ищем в основной сцене
|
|
84
|
+
const stage = this.resizeHandles.app.stage;
|
|
85
|
+
if (!stage || !stage.children) return null;
|
|
86
|
+
|
|
87
|
+
for (let i = stage.children.length - 1; i >= 0; i--) {
|
|
88
|
+
const child = stage.children[i];
|
|
89
|
+
if (this.resizeHandles.container && child && child !== this.resizeHandles.container &&
|
|
90
|
+
child.containsPoint && typeof child.containsPoint === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
if (child.containsPoint(point)) {
|
|
93
|
+
return child;
|
|
94
|
+
}
|
|
95
|
+
} catch (_) {
|
|
96
|
+
// Игнорируем ошибки containsPoint
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeTextEditor as closeTextEditorViaController,
|
|
3
|
+
openTextEditor as openTextEditorViaController,
|
|
4
|
+
} from './TextInlineEditorController.js';
|
|
5
|
+
import {
|
|
6
|
+
closeFileNameEditor as closeFileNameEditorViaController,
|
|
7
|
+
openFileNameEditor as openFileNameEditorViaController,
|
|
8
|
+
} from './FileNameInlineEditorController.js';
|
|
9
|
+
|
|
10
|
+
export function openTextEditor(object, create = false) {
|
|
11
|
+
return openTextEditorViaController.call(this, object, create);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function openFileNameEditor(object, create = false) {
|
|
15
|
+
return openFileNameEditorViaController.call(this, object, create);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function closeFileNameEditor(commit) {
|
|
19
|
+
return closeFileNameEditorViaController.call(this, commit);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function closeTextEditor(commit) {
|
|
23
|
+
return closeTextEditorViaController.call(this, commit);
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function createTextEditorWrapper() {
|
|
2
|
+
const wrapper = document.createElement('div');
|
|
3
|
+
wrapper.className = 'moodboard-text-editor';
|
|
4
|
+
return wrapper;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createTextEditorTextarea(content) {
|
|
8
|
+
const textarea = document.createElement('textarea');
|
|
9
|
+
textarea.className = 'moodboard-text-input';
|
|
10
|
+
textarea.value = content || '';
|
|
11
|
+
textarea.placeholder = 'Напишите что-нибудь';
|
|
12
|
+
return textarea;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createFileNameEditorWrapper() {
|
|
16
|
+
const wrapper = document.createElement('div');
|
|
17
|
+
wrapper.className = 'moodboard-file-name-editor';
|
|
18
|
+
wrapper.style.cssText = `
|
|
19
|
+
position: absolute;
|
|
20
|
+
z-index: 1000;
|
|
21
|
+
background: white;
|
|
22
|
+
border: 2px solid #2563eb;
|
|
23
|
+
border-radius: 6px;
|
|
24
|
+
padding: 6px 8px;
|
|
25
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
26
|
+
min-width: 140px;
|
|
27
|
+
max-width: 200px;
|
|
28
|
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
29
|
+
`;
|
|
30
|
+
return wrapper;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createFileNameEditorInput(fileName) {
|
|
34
|
+
const input = document.createElement('input');
|
|
35
|
+
input.type = 'text';
|
|
36
|
+
input.value = fileName;
|
|
37
|
+
input.style.cssText = `
|
|
38
|
+
border: none;
|
|
39
|
+
outline: none;
|
|
40
|
+
background: transparent;
|
|
41
|
+
font-family: inherit;
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
text-align: center;
|
|
44
|
+
width: 100%;
|
|
45
|
+
padding: 2px 4px;
|
|
46
|
+
color: #1f2937;
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
`;
|
|
49
|
+
return input;
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function registerEditorListeners(eventBus, listeners) {
|
|
2
|
+
listeners.forEach(([eventName, handler]) => {
|
|
3
|
+
eventBus.on(eventName, handler);
|
|
4
|
+
});
|
|
5
|
+
return listeners;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function unregisterEditorListeners(eventBus, listeners) {
|
|
9
|
+
listeners.forEach(([eventName, handler]) => {
|
|
10
|
+
try {
|
|
11
|
+
eventBus.off(eventName, handler);
|
|
12
|
+
} catch (_) {}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as PIXI from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
export function toScreenWithContainerOffset(worldLayer, view, wx, wy) {
|
|
4
|
+
if (!worldLayer || !view || !view.parentElement) return { x: wx, y: wy };
|
|
5
|
+
|
|
6
|
+
const containerRect = view.parentElement.getBoundingClientRect();
|
|
7
|
+
const viewRect = view.getBoundingClientRect();
|
|
8
|
+
const offsetLeft = viewRect.left - containerRect.left;
|
|
9
|
+
const offsetTop = viewRect.top - containerRect.top;
|
|
10
|
+
const global = worldLayer.toGlobal(new PIXI.Point(wx, wy));
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
x: offsetLeft + global.x,
|
|
14
|
+
y: offsetTop + global.y,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toScreenWithResolution(worldLayer, app, wx, wy) {
|
|
19
|
+
if (!worldLayer) return { x: wx, y: wy };
|
|
20
|
+
|
|
21
|
+
const global = worldLayer.toGlobal(new PIXI.Point(wx, wy));
|
|
22
|
+
const view = app?.view || document.querySelector('canvas');
|
|
23
|
+
const viewRes = (app?.renderer?.resolution) || (view && view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
24
|
+
return { x: global.x / viewRes, y: global.y / viewRes };
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
import {
|
|
3
|
+
createNoteEditorUpdater,
|
|
4
|
+
registerNoteEditorSync,
|
|
5
|
+
} from './TextEditorSyncService.js';
|
|
6
|
+
|
|
7
|
+
export function setupNoteInlineEditor(controller, params) {
|
|
8
|
+
const {
|
|
9
|
+
objectId,
|
|
10
|
+
position,
|
|
11
|
+
initialSize,
|
|
12
|
+
view,
|
|
13
|
+
screenPos,
|
|
14
|
+
textarea,
|
|
15
|
+
wrapper,
|
|
16
|
+
computeLineHeightPx,
|
|
17
|
+
effectiveFontPx,
|
|
18
|
+
toScreen,
|
|
19
|
+
} = params;
|
|
20
|
+
|
|
21
|
+
// Получаем актуальные размеры записки
|
|
22
|
+
let noteWidth = 160;
|
|
23
|
+
let noteHeight = 100;
|
|
24
|
+
|
|
25
|
+
if (initialSize) {
|
|
26
|
+
noteWidth = initialSize.width;
|
|
27
|
+
noteHeight = initialSize.height;
|
|
28
|
+
} else if (objectId) {
|
|
29
|
+
// Если размер не передан, пытаемся получить его из объекта
|
|
30
|
+
const sizeData = { objectId, size: null };
|
|
31
|
+
controller.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
32
|
+
if (sizeData.size) {
|
|
33
|
+
noteWidth = sizeData.size.width;
|
|
34
|
+
noteHeight = sizeData.size.height;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Текст у записки центрирован по обеим осям; textarea тоже центрируем
|
|
39
|
+
const horizontalPadding = 16; // немного больше, чем раньше
|
|
40
|
+
// Преобразуем мировые размеры/отступы в CSS-пиксели с учётом текущего зума
|
|
41
|
+
const viewResLocal = (controller.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
42
|
+
const worldLayerRefForCss = controller.textEditor.world || (controller.app?.stage);
|
|
43
|
+
const sForCss = worldLayerRefForCss?.scale?.x || 1;
|
|
44
|
+
const sCssLocal = sForCss / viewResLocal;
|
|
45
|
+
const editorWidthWorld = Math.min(360, Math.max(1, noteWidth - (horizontalPadding * 2)));
|
|
46
|
+
const editorHeightWorld = Math.min(180, Math.max(1, noteHeight - (horizontalPadding * 2)));
|
|
47
|
+
const editorWidthPx = Math.max(1, Math.round(editorWidthWorld * sCssLocal));
|
|
48
|
+
const editorHeightPx = Math.max(1, Math.round(editorHeightWorld * sCssLocal));
|
|
49
|
+
const textCenterXWorld = noteWidth / 2;
|
|
50
|
+
const textCenterYWorld = noteHeight / 2;
|
|
51
|
+
const editorLeftWorld = textCenterXWorld - (editorWidthWorld / 2);
|
|
52
|
+
const editorTopWorld = textCenterYWorld - (editorHeightWorld / 2);
|
|
53
|
+
wrapper.style.left = `${Math.round(screenPos.x + editorLeftWorld * sCssLocal)}px`;
|
|
54
|
+
wrapper.style.top = `${Math.round(screenPos.y + editorTopWorld * sCssLocal)}px`;
|
|
55
|
+
// Устанавливаем размеры редактора (центрируем по контенту) в CSS-пикселях
|
|
56
|
+
textarea.style.width = `${editorWidthPx}px`;
|
|
57
|
+
textarea.style.height = `${editorHeightPx}px`;
|
|
58
|
+
wrapper.style.width = `${editorWidthPx}px`;
|
|
59
|
+
wrapper.style.height = `${editorHeightPx}px`;
|
|
60
|
+
|
|
61
|
+
// Для записок: авто-ресайз редактора под содержимое с сохранением центрирования
|
|
62
|
+
textarea.style.textAlign = 'center';
|
|
63
|
+
const maxEditorWidthPx = Math.max(1, Math.round((noteWidth - (horizontalPadding * 2)) * sCssLocal));
|
|
64
|
+
const maxEditorHeightPx = Math.max(1, Math.round((noteHeight - (horizontalPadding * 2)) * sCssLocal));
|
|
65
|
+
const MIN_NOTE_EDITOR_W = 20;
|
|
66
|
+
const MIN_NOTE_EDITOR_H = Math.max(1, computeLineHeightPx(effectiveFontPx));
|
|
67
|
+
|
|
68
|
+
const autoSizeNote = () => {
|
|
69
|
+
// Сначала сбрасываем размеры, чтобы измерить естественные
|
|
70
|
+
textarea.style.width = 'auto';
|
|
71
|
+
textarea.style.height = 'auto';
|
|
72
|
+
|
|
73
|
+
// Ширина по содержимому, но не шире границ записки (в CSS-пикселях)
|
|
74
|
+
const naturalW = Math.ceil(textarea.scrollWidth + 1);
|
|
75
|
+
const targetW = Math.min(maxEditorWidthPx, Math.max(MIN_NOTE_EDITOR_W, naturalW));
|
|
76
|
+
textarea.style.width = `${targetW}px`;
|
|
77
|
+
wrapper.style.width = `${targetW}px`;
|
|
78
|
+
|
|
79
|
+
// Высота по содержимому, c нижним пределом = одна строка
|
|
80
|
+
const computed = (typeof window !== 'undefined') ? window.getComputedStyle(textarea) : null;
|
|
81
|
+
const lineH = (computed ? parseFloat(computed.lineHeight) : computeLineHeightPx(effectiveFontPx));
|
|
82
|
+
const naturalH = Math.ceil(textarea.scrollHeight);
|
|
83
|
+
const targetH = Math.min(maxEditorHeightPx, Math.max(MIN_NOTE_EDITOR_H, naturalH));
|
|
84
|
+
textarea.style.height = `${targetH}px`;
|
|
85
|
+
wrapper.style.height = `${targetH}px`;
|
|
86
|
+
|
|
87
|
+
// Центрируем wrapper внутри записки после смены размеров (в CSS-пикселях)
|
|
88
|
+
const left = Math.round(screenPos.x + (noteWidth * sCssLocal) / 2 - (targetW / 2));
|
|
89
|
+
const top = Math.round(screenPos.y + (noteHeight * sCssLocal) / 2 - (targetH / 2));
|
|
90
|
+
wrapper.style.left = `${left}px`;
|
|
91
|
+
wrapper.style.top = `${top}px`;
|
|
92
|
+
};
|
|
93
|
+
// Первый вызов — синхронизировать с текущим содержимым
|
|
94
|
+
autoSizeNote();
|
|
95
|
+
|
|
96
|
+
// Динамическое обновление позиции/размера редактора при зуме/панорамировании/трансформациях
|
|
97
|
+
const updateNoteEditor = createNoteEditorUpdater(controller, {
|
|
98
|
+
objectId,
|
|
99
|
+
position,
|
|
100
|
+
noteWidth,
|
|
101
|
+
noteHeight,
|
|
102
|
+
view,
|
|
103
|
+
textarea,
|
|
104
|
+
wrapper,
|
|
105
|
+
horizontalPadding,
|
|
106
|
+
computeLineHeightPx,
|
|
107
|
+
effectiveFontPx,
|
|
108
|
+
toScreen,
|
|
109
|
+
});
|
|
110
|
+
registerNoteEditorSync(controller, { objectId, updateNoteEditor });
|
|
111
|
+
|
|
112
|
+
return { updateNoteEditor };
|
|
113
|
+
}
|