@sequent-org/moodboard 1.2.119 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/assets/icons/rotate-icon.svg +1 -1
- package/src/core/HistoryManager.js +16 -16
- package/src/core/KeyboardManager.js +48 -539
- package/src/core/PixiEngine.js +9 -9
- package/src/core/SaveManager.js +56 -31
- package/src/core/bootstrap/CoreInitializer.js +65 -0
- package/src/core/commands/DeleteObjectCommand.js +8 -0
- package/src/core/commands/GroupDeleteCommand.js +75 -0
- package/src/core/commands/GroupRotateCommand.js +6 -0
- package/src/core/commands/UpdateContentCommand.js +52 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
- package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
- package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
- package/src/core/commands/UpdateTextStyleCommand.js +90 -0
- package/src/core/commands/index.js +6 -0
- package/src/core/events/Events.js +6 -0
- package/src/core/flows/ClipboardFlow.js +553 -0
- package/src/core/flows/LayerAndViewportFlow.js +283 -0
- package/src/core/flows/ObjectLifecycleFlow.js +336 -0
- package/src/core/flows/SaveFlow.js +34 -0
- package/src/core/flows/TransformFlow.js +277 -0
- package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
- package/src/core/index.js +41 -1773
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
- package/src/core/keyboard/KeyboardContextGuards.js +35 -0
- package/src/core/keyboard/KeyboardEventRouter.js +92 -0
- package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
- package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
- package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
- package/src/core/rendering/ObjectRenderer.js +3 -7
- package/src/grid/BaseGrid.js +26 -0
- package/src/grid/CrossGrid.js +7 -6
- package/src/grid/DotGrid.js +89 -33
- package/src/grid/DotGridZoomPhases.js +42 -0
- package/src/grid/LineGrid.js +22 -21
- package/src/moodboard/MoodBoard.js +31 -532
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
- package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
- package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
- package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
- package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
- package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
- package/src/objects/FileObject.js +17 -6
- package/src/objects/FrameObject.js +50 -10
- package/src/objects/NoteObject.js +5 -4
- package/src/services/BoardService.js +42 -2
- package/src/services/FrameService.js +83 -42
- package/src/services/ResizePolicyService.js +152 -0
- package/src/services/SettingsApplier.js +7 -2
- package/src/services/ZoomPanController.js +35 -9
- package/src/tools/ToolManager.js +30 -537
- package/src/tools/board-tools/PanTool.js +5 -11
- package/src/tools/manager/ToolActivationController.js +49 -0
- package/src/tools/manager/ToolEventRouter.js +396 -0
- package/src/tools/manager/ToolManagerGuards.js +33 -0
- package/src/tools/manager/ToolManagerLifecycle.js +110 -0
- package/src/tools/manager/ToolRegistry.js +33 -0
- package/src/tools/object-tools/DrawingTool.js +48 -14
- package/src/tools/object-tools/PlacementTool.js +50 -1049
- package/src/tools/object-tools/PlacementToolV2.js +88 -0
- package/src/tools/object-tools/SelectTool.js +174 -2681
- package/src/tools/object-tools/placement/GhostController.js +504 -0
- package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
- package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
- package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
- package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
- package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
- package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
- package/src/tools/object-tools/selection/CursorController.js +78 -0
- package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
- package/src/tools/object-tools/selection/HitTestService.js +102 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
- package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
- package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
- package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
- package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
- package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
- package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
- package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
- package/src/ui/FilePropertiesPanel.js +61 -32
- package/src/ui/FramePropertiesPanel.js +176 -101
- package/src/ui/HtmlHandlesLayer.js +121 -999
- package/src/ui/MapPanel.js +12 -7
- package/src/ui/NotePropertiesPanel.js +17 -2
- package/src/ui/TextPropertiesPanel.js +124 -738
- package/src/ui/Toolbar.js +71 -1180
- package/src/ui/Topbar.js +23 -25
- package/src/ui/ZoomPanel.js +16 -5
- package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
- package/src/ui/handles/HandlesDomRenderer.js +278 -0
- package/src/ui/handles/HandlesEventBridge.js +102 -0
- package/src/ui/handles/HandlesInteractionController.js +772 -0
- package/src/ui/handles/HandlesPositioningService.js +206 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
- package/src/ui/styles/toolbar.css +2 -0
- package/src/ui/styles/workspace.css +13 -6
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
- package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
- package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
- package/src/ui/toolbar/ToolbarRenderer.js +97 -0
- package/src/ui/toolbar/ToolbarStateController.js +79 -0
- package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
- package/src/utils/emojiLoaderNoBundler.js +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
import { registerEditorListeners } from './InlineEditorListenersRegistry.js';
|
|
3
|
+
|
|
4
|
+
export function createRegularTextAutoSize({
|
|
5
|
+
textarea,
|
|
6
|
+
wrapper,
|
|
7
|
+
minWBound,
|
|
8
|
+
minHBound,
|
|
9
|
+
effectiveFontPx,
|
|
10
|
+
computeLineHeightPx,
|
|
11
|
+
}) {
|
|
12
|
+
const MAX_AUTO_WIDTH = 360;
|
|
13
|
+
const BASELINE_FIX = 2;
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
textarea.style.width = 'auto';
|
|
17
|
+
textarea.style.height = 'auto';
|
|
18
|
+
|
|
19
|
+
const naturalW = textarea.scrollWidth + 1;
|
|
20
|
+
const targetW = Math.min(MAX_AUTO_WIDTH, Math.max(minWBound, naturalW));
|
|
21
|
+
textarea.style.width = `${targetW}px`;
|
|
22
|
+
wrapper.style.width = `${targetW}px`;
|
|
23
|
+
|
|
24
|
+
textarea.style.height = 'auto';
|
|
25
|
+
const adjust = BASELINE_FIX;
|
|
26
|
+
const computed = (typeof window !== 'undefined') ? window.getComputedStyle(textarea) : null;
|
|
27
|
+
const lineH = (computed ? parseFloat(computed.lineHeight) : computeLineHeightPx(effectiveFontPx)) + 10;
|
|
28
|
+
const rawH = textarea.scrollHeight;
|
|
29
|
+
const lines = lineH > 0 ? Math.max(1, Math.round(rawH / lineH)) : 1;
|
|
30
|
+
const targetH = lines <= 1
|
|
31
|
+
? Math.max(minHBound, Math.max(1, lineH - BASELINE_FIX))
|
|
32
|
+
: Math.max(minHBound, Math.max(1, rawH - adjust));
|
|
33
|
+
textarea.style.height = `${targetH}px`;
|
|
34
|
+
wrapper.style.height = `${targetH}px`;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createNoteEditorUpdater(controller, {
|
|
39
|
+
objectId,
|
|
40
|
+
position,
|
|
41
|
+
noteWidth,
|
|
42
|
+
noteHeight,
|
|
43
|
+
view,
|
|
44
|
+
textarea,
|
|
45
|
+
wrapper,
|
|
46
|
+
horizontalPadding,
|
|
47
|
+
computeLineHeightPx,
|
|
48
|
+
effectiveFontPx,
|
|
49
|
+
toScreen,
|
|
50
|
+
}) {
|
|
51
|
+
const minNoteEditorWidthPx = 20;
|
|
52
|
+
const minNoteEditorHeightPx = Math.max(1, computeLineHeightPx(effectiveFontPx));
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
try {
|
|
56
|
+
const posDataNow = { objectId, position: null };
|
|
57
|
+
const sizeDataNow = { objectId, size: null };
|
|
58
|
+
controller.eventBus.emit(Events.Tool.GetObjectPosition, posDataNow);
|
|
59
|
+
controller.eventBus.emit(Events.Tool.GetObjectSize, sizeDataNow);
|
|
60
|
+
const posNow = posDataNow.position || position;
|
|
61
|
+
const sizeNow = sizeDataNow.size || { width: noteWidth, height: noteHeight };
|
|
62
|
+
const screenNow = toScreen(posNow.x, posNow.y);
|
|
63
|
+
const viewRes = (controller.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
64
|
+
const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
|
|
65
|
+
const scaleX = worldLayerRef?.scale?.x || 1;
|
|
66
|
+
const scaleCss = scaleX / viewRes;
|
|
67
|
+
const maxWpx = Math.max(1, Math.round((sizeNow.width - (horizontalPadding * 2)) * scaleCss));
|
|
68
|
+
const maxHpx = Math.max(1, Math.round((sizeNow.height - (horizontalPadding * 2)) * scaleCss));
|
|
69
|
+
|
|
70
|
+
textarea.style.width = 'auto';
|
|
71
|
+
textarea.style.height = 'auto';
|
|
72
|
+
const naturalW = Math.ceil(textarea.scrollWidth + 1);
|
|
73
|
+
const naturalH = Math.ceil(textarea.scrollHeight);
|
|
74
|
+
const widthPx = Math.min(maxWpx, Math.max(minNoteEditorWidthPx, naturalW));
|
|
75
|
+
const heightPx = Math.min(maxHpx, Math.max(minNoteEditorHeightPx, naturalH));
|
|
76
|
+
|
|
77
|
+
textarea.style.width = `${widthPx}px`;
|
|
78
|
+
wrapper.style.width = `${widthPx}px`;
|
|
79
|
+
textarea.style.height = `${heightPx}px`;
|
|
80
|
+
wrapper.style.height = `${heightPx}px`;
|
|
81
|
+
|
|
82
|
+
const left = Math.round(screenNow.x + (sizeNow.width * scaleCss) / 2 - (widthPx / 2));
|
|
83
|
+
const top = Math.round(screenNow.y + (sizeNow.height * scaleCss) / 2 - (heightPx / 2));
|
|
84
|
+
wrapper.style.left = `${left}px`;
|
|
85
|
+
wrapper.style.top = `${top}px`;
|
|
86
|
+
textarea.style.width = `${widthPx}px`;
|
|
87
|
+
textarea.style.height = `${heightPx}px`;
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registerNoteEditorSync(controller, { objectId, updateNoteEditor }) {
|
|
93
|
+
const onZoom = () => updateNoteEditor();
|
|
94
|
+
const onPan = () => updateNoteEditor();
|
|
95
|
+
const onDrag = (e) => { if (e && e.object === objectId) updateNoteEditor(); };
|
|
96
|
+
const onResize = (e) => { if (e && e.object === objectId) updateNoteEditor(); };
|
|
97
|
+
const onRotate = (e) => { if (e && e.object === objectId) updateNoteEditor(); };
|
|
98
|
+
|
|
99
|
+
const listeners = [
|
|
100
|
+
[Events.UI.ZoomPercent, onZoom],
|
|
101
|
+
[Events.Tool.PanUpdate, onPan],
|
|
102
|
+
[Events.Tool.DragUpdate, onDrag],
|
|
103
|
+
[Events.Tool.ResizeUpdate, onResize],
|
|
104
|
+
[Events.Tool.RotateUpdate, onRotate],
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
registerEditorListeners(controller.eventBus, listeners);
|
|
108
|
+
controller.textEditor._listeners = listeners;
|
|
109
|
+
return listeners;
|
|
110
|
+
}
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { Events } from '../../../core/events/Events.js';
|
|
2
|
+
import {
|
|
3
|
+
createTextEditorToScreen,
|
|
4
|
+
positionRegularTextEditor,
|
|
5
|
+
syncCreatedTextEditorWorldPosition,
|
|
6
|
+
} from './TextEditorPositioningService.js';
|
|
7
|
+
import {
|
|
8
|
+
applyTextEditorCaretFromClick,
|
|
9
|
+
bindTextEditorInteractions,
|
|
10
|
+
closeTextEditorFromState,
|
|
11
|
+
createTextEditorFinalize,
|
|
12
|
+
} from './TextEditorInteractionController.js';
|
|
13
|
+
import {
|
|
14
|
+
hideGlobalTextEditorHandlesLayer,
|
|
15
|
+
hideNotePixiText,
|
|
16
|
+
hideStaticTextDuringEditing,
|
|
17
|
+
} from './TextEditorLifecycleRegistry.js';
|
|
18
|
+
import {
|
|
19
|
+
applyInitialTextEditorTextareaStyles,
|
|
20
|
+
attachTextEditorPlaceholderStyle,
|
|
21
|
+
computeTextEditorLineHeightPx,
|
|
22
|
+
createTextEditorTextarea,
|
|
23
|
+
createTextEditorWrapper,
|
|
24
|
+
measureTextEditorPlaceholderWidth,
|
|
25
|
+
} from './TextEditorDomFactory.js';
|
|
26
|
+
import { setupNoteInlineEditor } from './NoteInlineEditorController.js';
|
|
27
|
+
import { createRegularTextAutoSize } from './TextEditorSyncService.js';
|
|
28
|
+
|
|
29
|
+
export function openTextEditor(object, create = false) {
|
|
30
|
+
|
|
31
|
+
// Проверяем структуру объекта и извлекаем данные
|
|
32
|
+
let objectId, objectType, position, properties;
|
|
33
|
+
|
|
34
|
+
if (create) {
|
|
35
|
+
// Для создания нового объекта - данные в object.object
|
|
36
|
+
const objData = object.object || object;
|
|
37
|
+
objectId = objData.id || null;
|
|
38
|
+
objectType = objData.type || 'text';
|
|
39
|
+
position = objData.position;
|
|
40
|
+
properties = objData.properties || {};
|
|
41
|
+
} else {
|
|
42
|
+
// Для редактирования существующего объекта - данные в корне
|
|
43
|
+
objectId = object.id;
|
|
44
|
+
objectType = object.type || 'text';
|
|
45
|
+
position = object.position;
|
|
46
|
+
properties = object.properties || {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
let { fontSize = 32, content = '', initialSize } = properties;
|
|
51
|
+
|
|
52
|
+
// Определяем тип объекта
|
|
53
|
+
const isNote = objectType === 'note';
|
|
54
|
+
|
|
55
|
+
// Проверяем, что position существует
|
|
56
|
+
if (!position) {
|
|
57
|
+
console.error('❌ SelectTool: position is undefined in _openTextEditor', { object, create });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Закрываем предыдущий редактор, если он открыт
|
|
62
|
+
if (this.textEditor.active) this._closeTextEditor(true);
|
|
63
|
+
|
|
64
|
+
// Если это редактирование существующего объекта, получаем его данные
|
|
65
|
+
if (!create && objectId) {
|
|
66
|
+
const posData = { objectId, position: null };
|
|
67
|
+
const sizeData = { objectId, size: null };
|
|
68
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
69
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
70
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
71
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
72
|
+
|
|
73
|
+
// Обновляем данные из полученной информации
|
|
74
|
+
if (posData.position) position = posData.position;
|
|
75
|
+
if (sizeData.size) initialSize = sizeData.size;
|
|
76
|
+
|
|
77
|
+
const meta = pixiReq.pixiObject && pixiReq.pixiObject._mb ? pixiReq.pixiObject._mb.properties || {} : {};
|
|
78
|
+
if (meta.content) properties.content = meta.content;
|
|
79
|
+
if (meta.fontSize) properties.fontSize = meta.fontSize;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Уведомляем о начале редактирования (для разных типов отдельно)
|
|
83
|
+
if (objectType === 'note') {
|
|
84
|
+
this.eventBus.emit(Events.UI.NoteEditStart, { objectId: objectId || null });
|
|
85
|
+
} else {
|
|
86
|
+
this.eventBus.emit(Events.UI.TextEditStart, { objectId: objectId || null });
|
|
87
|
+
}
|
|
88
|
+
// Прячем глобальные HTML-ручки на время редактирования, чтобы не было второй рамки
|
|
89
|
+
hideGlobalTextEditorHandlesLayer();
|
|
90
|
+
|
|
91
|
+
const app = this.app;
|
|
92
|
+
const world = app?.stage?.getChildByName && app.stage.getChildByName('worldLayer');
|
|
93
|
+
this.textEditor.world = world || null;
|
|
94
|
+
const view = app?.view;
|
|
95
|
+
if (!view) return;
|
|
96
|
+
// Рассчитываем эффективный размер шрифта ДО вставки textarea в DOM, чтобы избежать скачка размера
|
|
97
|
+
const worldLayerEarly = world || (this.app?.stage);
|
|
98
|
+
const sEarly = worldLayerEarly?.scale?.x || 1;
|
|
99
|
+
const viewResEarly = (this.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
100
|
+
const sCssEarly = sEarly / viewResEarly;
|
|
101
|
+
let effectiveFontPx = Math.max(1, Math.round((fontSize || 14) * sCssEarly));
|
|
102
|
+
// Точное выравнивание размеров:
|
|
103
|
+
if (objectId) {
|
|
104
|
+
if (objectType === 'note') {
|
|
105
|
+
try {
|
|
106
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
107
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
108
|
+
const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
|
|
109
|
+
if (inst && inst.textField) {
|
|
110
|
+
const wt = inst.textField.worldTransform;
|
|
111
|
+
const scaleY = Math.max(0.0001, Math.hypot(wt.c || 0, wt.d || 0));
|
|
112
|
+
const baseFS = parseFloat(inst.textField.style?.fontSize || fontSize || 14) || (fontSize || 14);
|
|
113
|
+
effectiveFontPx = Math.max(1, Math.round(baseFS * (scaleY / viewResEarly)));
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
} else if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
117
|
+
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
118
|
+
if (el && typeof window.getComputedStyle === 'function') {
|
|
119
|
+
const cs = window.getComputedStyle(el);
|
|
120
|
+
const f = parseFloat(cs.fontSize);
|
|
121
|
+
if (isFinite(f) && f > 0) effectiveFontPx = Math.round(f);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Используем только HTML-ручки во время редактирования текста
|
|
126
|
+
// Обертка для рамки + textarea + ручек
|
|
127
|
+
const wrapper = createTextEditorWrapper();
|
|
128
|
+
|
|
129
|
+
// Базовые стили вынесены в CSS (.moodboard-text-editor)
|
|
130
|
+
|
|
131
|
+
const textarea = createTextEditorTextarea(content || '');
|
|
132
|
+
|
|
133
|
+
// Вычисляем межстрочный интервал; подгоняем к реальным значениям HTML-отображения
|
|
134
|
+
let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
|
|
135
|
+
try {
|
|
136
|
+
if (objectId) {
|
|
137
|
+
if (objectType === 'note') {
|
|
138
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
139
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
140
|
+
const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
|
|
141
|
+
if (inst && inst.textField) {
|
|
142
|
+
const wt = inst.textField.worldTransform;
|
|
143
|
+
const scaleY = Math.max(0.0001, Math.hypot(wt.c || 0, wt.d || 0));
|
|
144
|
+
const baseLH = parseFloat(inst.textField.style?.lineHeight || (fontSize * 1.2)) || (fontSize * 1.2);
|
|
145
|
+
lhInitial = Math.max(1, Math.round(baseLH * (scaleY / viewResEarly)));
|
|
146
|
+
}
|
|
147
|
+
} else if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
148
|
+
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
149
|
+
if (el) {
|
|
150
|
+
const cs = window.getComputedStyle(el);
|
|
151
|
+
const lh = parseFloat(cs.lineHeight);
|
|
152
|
+
if (isFinite(lh) && lh > 0) lhInitial = Math.round(lh);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
|
|
158
|
+
// Базовые стили вынесены в CSS (.moodboard-text-input); здесь — только динамика
|
|
159
|
+
// Подбираем актуальный font-family из объекта
|
|
160
|
+
try {
|
|
161
|
+
if (objectId) {
|
|
162
|
+
if (objectType === 'note') {
|
|
163
|
+
// Для записки читаем из PIXI-инстанса NoteObject
|
|
164
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
165
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
166
|
+
const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
|
|
167
|
+
const ff = (inst && inst.textField && inst.textField.style && inst.textField.style.fontFamily)
|
|
168
|
+
|| (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.properties && pixiReq.pixiObject._mb.properties.fontFamily)
|
|
169
|
+
|| null;
|
|
170
|
+
if (ff) textarea.style.fontFamily = ff;
|
|
171
|
+
} else if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
172
|
+
// Для обычного текста читаем из HTML-элемента
|
|
173
|
+
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
174
|
+
if (el) {
|
|
175
|
+
const cs = window.getComputedStyle(el);
|
|
176
|
+
const ff = cs && cs.fontFamily ? cs.fontFamily : null;
|
|
177
|
+
if (ff) textarea.style.fontFamily = ff;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (_) {}
|
|
182
|
+
applyInitialTextEditorTextareaStyles(textarea, {
|
|
183
|
+
effectiveFontPx,
|
|
184
|
+
lineHeightPx: lhInitial,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
wrapper.appendChild(textarea);
|
|
188
|
+
// Убрана зелёная рамка вокруг поля ввода по требованию
|
|
189
|
+
|
|
190
|
+
// В режиме input не показываем локальные ручки
|
|
191
|
+
|
|
192
|
+
// Не создаём локальные синие ручки: используем HtmlHandlesLayer (зелёные)
|
|
193
|
+
|
|
194
|
+
// Убираем ручки ресайза для всех типов объектов
|
|
195
|
+
// let handles = [];
|
|
196
|
+
// let placeHandles = () => {};
|
|
197
|
+
|
|
198
|
+
// if (!isNote) {
|
|
199
|
+
// // Ручки ресайза (8 штук) только для обычного текста
|
|
200
|
+
// handles = ['nw','n','ne','e','se','s','sw','w'].map(dir => {
|
|
201
|
+
// const h = document.createElement('div');
|
|
202
|
+
// h.dataset.dir = dir;
|
|
203
|
+
// Object.assign(h.style, {
|
|
204
|
+
// position: 'absolute', width: '12px', height: '12px', background: '#007ACC',
|
|
205
|
+
// border: '1px solid #fff', boxSizing: 'border-box', zIndex: 10001,
|
|
206
|
+
// });
|
|
207
|
+
// return h;
|
|
208
|
+
// });
|
|
209
|
+
//
|
|
210
|
+
// placeHandles = () => {
|
|
211
|
+
// const w = wrapper.offsetWidth;
|
|
212
|
+
// const h = wrapper.offsetHeight;
|
|
213
|
+
// handles.forEach(hd => {
|
|
214
|
+
// const dir = hd.dataset.dir;
|
|
215
|
+
// // default reset
|
|
216
|
+
// hd.style.left = '0px';
|
|
217
|
+
// hd.style.top = '0px';
|
|
218
|
+
// hd.style.right = '';
|
|
219
|
+
// hd.style.bottom = '';
|
|
220
|
+
// switch (dir) {
|
|
221
|
+
// case 'nw':
|
|
222
|
+
// hd.style.left = `${-6}px`;
|
|
223
|
+
// hd.style.top = `${-6}px`;
|
|
224
|
+
// hd.style.cursor = 'nwse-resize';
|
|
225
|
+
// break;
|
|
226
|
+
// case 'n':
|
|
227
|
+
// hd.style.left = `${Math.round(w / 2 - 6)}px`;
|
|
228
|
+
// hd.style.top = `${-6}px`;
|
|
229
|
+
// hd.style.cursor = 'n-resize';
|
|
230
|
+
// break;
|
|
231
|
+
// case 'ne':
|
|
232
|
+
// hd.style.left = `${Math.max(-6, w - 6)}px`;
|
|
233
|
+
// hd.style.top = `${-6}px`;
|
|
234
|
+
// hd.style.cursor = 'nesw-resize';
|
|
235
|
+
// break;
|
|
236
|
+
// case 'e':
|
|
237
|
+
// hd.style.left = `${Math.max(-6, w - 6)}px`;
|
|
238
|
+
// hd.style.top = `${Math.round(h / 2 - 6)}px`;
|
|
239
|
+
// hd.style.cursor = 'e-resize';
|
|
240
|
+
// break;
|
|
241
|
+
// case 'se':
|
|
242
|
+
// hd.style.left = `${Math.max(-6, w - 6)}px`;
|
|
243
|
+
// hd.style.top = `${Math.max(-6, h - 6)}px`;
|
|
244
|
+
// hd.style.cursor = 'nwse-resize';
|
|
245
|
+
// break;
|
|
246
|
+
// case 's':
|
|
247
|
+
// hd.style.left = `${Math.round(w / 2 - 6)}px`;
|
|
248
|
+
// hd.style.top = `${Math.max(-6, h - 6)}px`;
|
|
249
|
+
// hd.style.cursor = 's-resize';
|
|
250
|
+
// break;
|
|
251
|
+
// case 'sw':
|
|
252
|
+
// hd.style.left = `${-6}px`;
|
|
253
|
+
// hd.style.top = `${Math.max(-6, h - 6)}px`;
|
|
254
|
+
// hd.style.cursor = 'nesw-resize';
|
|
255
|
+
// break;
|
|
256
|
+
// case 'w':
|
|
257
|
+
// hd.style.left = `${-6}px`;
|
|
258
|
+
// hd.style.top = `${Math.round(h / 2 - 6)}px`;
|
|
259
|
+
// hd.style.cursor = 'w-resize';
|
|
260
|
+
// break;
|
|
261
|
+
// }
|
|
262
|
+
// });
|
|
263
|
+
// }
|
|
264
|
+
// }
|
|
265
|
+
|
|
266
|
+
// Добавляем в DOM
|
|
267
|
+
wrapper.appendChild(textarea);
|
|
268
|
+
view.parentElement.appendChild(wrapper);
|
|
269
|
+
|
|
270
|
+
// Позиция обертки по миру → экран
|
|
271
|
+
const toScreen = createTextEditorToScreen(this, view);
|
|
272
|
+
const screenPos = toScreen(position.x, position.y);
|
|
273
|
+
|
|
274
|
+
// Для записок позиционируем редактор внутри записки
|
|
275
|
+
let updateNoteEditor = null;
|
|
276
|
+
if (objectType === 'note') {
|
|
277
|
+
const noteSetup = setupNoteInlineEditor(this, {
|
|
278
|
+
objectId,
|
|
279
|
+
position,
|
|
280
|
+
initialSize,
|
|
281
|
+
view,
|
|
282
|
+
screenPos,
|
|
283
|
+
textarea,
|
|
284
|
+
wrapper,
|
|
285
|
+
computeLineHeightPx: computeTextEditorLineHeightPx,
|
|
286
|
+
effectiveFontPx,
|
|
287
|
+
toScreen,
|
|
288
|
+
});
|
|
289
|
+
updateNoteEditor = noteSetup.updateNoteEditor;
|
|
290
|
+
} else {
|
|
291
|
+
const {
|
|
292
|
+
leftPx,
|
|
293
|
+
topPx,
|
|
294
|
+
padTop,
|
|
295
|
+
padLeft,
|
|
296
|
+
lineHeightPx,
|
|
297
|
+
baseLeftPx,
|
|
298
|
+
baseTopPx,
|
|
299
|
+
} = positionRegularTextEditor({
|
|
300
|
+
create,
|
|
301
|
+
objectId,
|
|
302
|
+
screenPos,
|
|
303
|
+
textarea,
|
|
304
|
+
wrapper,
|
|
305
|
+
});
|
|
306
|
+
// Сохраняем CSS-позицию редактора для точной синхронизации при закрытии
|
|
307
|
+
this.textEditor._cssLeftPx = leftPx;
|
|
308
|
+
this.textEditor._cssTopPx = topPx;
|
|
309
|
+
// Диагностика: логируем позицию инпута и вычисленные параметры позиционирования
|
|
310
|
+
try {
|
|
311
|
+
console.log('🧭 Text input', {
|
|
312
|
+
input: { left: leftPx, top: topPx },
|
|
313
|
+
screenPos,
|
|
314
|
+
baseFromStatic: (!create && objectId) ? { left: baseLeftPx, top: baseTopPx } : null,
|
|
315
|
+
padding: { top: padTop, left: padLeft },
|
|
316
|
+
lineHeightPx,
|
|
317
|
+
caretCenterY: create ? (topPx + padTop + (lineHeightPx / 2)) : topPx,
|
|
318
|
+
create
|
|
319
|
+
});
|
|
320
|
+
} catch (_) {}
|
|
321
|
+
|
|
322
|
+
// Для новых текстов: синхронизируем мировую позицию объекта с фактической позицией wrapper,
|
|
323
|
+
// чтобы после закрытия редактора статичный текст встал ровно туда же без сдвига.
|
|
324
|
+
// Используем ту же систему координат, что и HtmlTextLayer/HtmlHandlesLayer:
|
|
325
|
+
// CSS ←→ world через toGlobal/toLocal БЕЗ умножения/деления на resolution.
|
|
326
|
+
syncCreatedTextEditorWorldPosition({
|
|
327
|
+
controller: this,
|
|
328
|
+
create,
|
|
329
|
+
objectId,
|
|
330
|
+
position,
|
|
331
|
+
leftPx,
|
|
332
|
+
topPx,
|
|
333
|
+
padTop,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Минимальные границы (зависят от текущего режима: новый объект или редактирование существующего)
|
|
337
|
+
const worldLayerRef = this.textEditor.world || (this.app?.stage);
|
|
338
|
+
const s = worldLayerRef?.scale?.x || 1;
|
|
339
|
+
const viewRes = (this.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
340
|
+
// Синхронизируем стартовый размер шрифта textarea с текущим зумом (как HtmlTextLayer)
|
|
341
|
+
// Используем ранее вычисленный effectiveFontPx (до вставки в DOM), если он есть в замыкании
|
|
342
|
+
textarea.style.fontSize = `${effectiveFontPx}px`;
|
|
343
|
+
const initialWpx = initialSize ? Math.max(1, (initialSize.width || 0) * s / viewRes) : null;
|
|
344
|
+
const initialHpx = initialSize ? Math.max(1, (initialSize.height || 0) * s / viewRes) : null;
|
|
345
|
+
|
|
346
|
+
// Определяем минимальные границы для всех типов объектов
|
|
347
|
+
let minWBound = initialWpx || 120; // базово близко к призраку
|
|
348
|
+
let minHBound = effectiveFontPx; // базовая высота
|
|
349
|
+
// Уменьшаем визуальный нижний запас, который браузеры добавляют к textarea
|
|
350
|
+
const BASELINE_FIX = 2; // px
|
|
351
|
+
if (!isNote) {
|
|
352
|
+
minHBound = Math.max(1, effectiveFontPx - BASELINE_FIX);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Если создаём новый текст — длина поля ровно как placeholder
|
|
356
|
+
if (create && !isNote) {
|
|
357
|
+
const startWidth = Math.max(1, measureTextEditorPlaceholderWidth(textarea, 'Напишите что-нибудь'));
|
|
358
|
+
const startHeight = Math.max(1, lhInitial - BASELINE_FIX + 10); // +5px сверху и +5px снизу
|
|
359
|
+
textarea.style.width = `${startWidth}px`;
|
|
360
|
+
textarea.style.height = `${startHeight}px`;
|
|
361
|
+
wrapper.style.width = `${startWidth}px`;
|
|
362
|
+
wrapper.style.height = `${startHeight}px`;
|
|
363
|
+
// Зафиксируем минимальные границы, чтобы авторазмер не схлопывал пустое поле
|
|
364
|
+
minWBound = startWidth;
|
|
365
|
+
minHBound = startHeight;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Для записок размеры уже установлены выше, пропускаем эту логику
|
|
369
|
+
if (!isNote) {
|
|
370
|
+
if (initialWpx) {
|
|
371
|
+
textarea.style.width = `${initialWpx}px`;
|
|
372
|
+
wrapper.style.width = `${initialWpx}px`;
|
|
373
|
+
}
|
|
374
|
+
if (initialHpx) {
|
|
375
|
+
textarea.style.height = `${initialHpx}px`;
|
|
376
|
+
wrapper.style.height = `${initialHpx}px`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Автоподгон
|
|
380
|
+
const autoSize = createRegularTextAutoSize({
|
|
381
|
+
textarea,
|
|
382
|
+
wrapper,
|
|
383
|
+
minWBound,
|
|
384
|
+
minHBound,
|
|
385
|
+
effectiveFontPx,
|
|
386
|
+
computeLineHeightPx: computeTextEditorLineHeightPx,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Вызываем autoSize только для обычного текста
|
|
390
|
+
if (!isNote) {
|
|
391
|
+
autoSize();
|
|
392
|
+
}
|
|
393
|
+
textarea.focus();
|
|
394
|
+
// Ручки скрыты в режиме input
|
|
395
|
+
// Локальная CSS-настройка placeholder (меньше базового шрифта)
|
|
396
|
+
const styleEl = attachTextEditorPlaceholderStyle(textarea, {
|
|
397
|
+
effectiveFontPx,
|
|
398
|
+
isNote,
|
|
399
|
+
});
|
|
400
|
+
this.textEditor = {
|
|
401
|
+
active: true,
|
|
402
|
+
objectId,
|
|
403
|
+
textarea,
|
|
404
|
+
wrapper,
|
|
405
|
+
world: this.textEditor.world,
|
|
406
|
+
position,
|
|
407
|
+
properties: { fontSize },
|
|
408
|
+
objectType,
|
|
409
|
+
_phStyle: styleEl,
|
|
410
|
+
initialContent: content,
|
|
411
|
+
isNewCreation: !!create,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Если переходим в редактирование существующего текста по двойному клику,
|
|
415
|
+
// устанавливаем каретку по координате клика между буквами
|
|
416
|
+
applyTextEditorCaretFromClick({
|
|
417
|
+
create,
|
|
418
|
+
objectId,
|
|
419
|
+
object,
|
|
420
|
+
textarea,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Если редактируем записку — скрываем PIXI-текст записки (чтобы не было дублирования)
|
|
424
|
+
if (objectType === 'note' && objectId) {
|
|
425
|
+
hideNotePixiText(this, objectId);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Скрываем статичный текст во время редактирования для всех типов объектов
|
|
429
|
+
hideStaticTextDuringEditing(this, objectId);
|
|
430
|
+
// Ресайз мышью только для обычного текста
|
|
431
|
+
// Не используем локальные ручки: ресайз обрабатывает HtmlHandlesLayer
|
|
432
|
+
// Завершение
|
|
433
|
+
const isNewCreation = !!create;
|
|
434
|
+
const finalize = createTextEditorFinalize(this, {
|
|
435
|
+
textarea,
|
|
436
|
+
wrapper,
|
|
437
|
+
view,
|
|
438
|
+
viewRes,
|
|
439
|
+
position,
|
|
440
|
+
fontSize,
|
|
441
|
+
objectId,
|
|
442
|
+
isNewCreation,
|
|
443
|
+
initialContent: content,
|
|
444
|
+
});
|
|
445
|
+
bindTextEditorInteractions(this, {
|
|
446
|
+
textarea,
|
|
447
|
+
isNewCreation,
|
|
448
|
+
isNote,
|
|
449
|
+
autoSize,
|
|
450
|
+
updateNoteEditor,
|
|
451
|
+
finalize,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function closeTextEditor(commit) {
|
|
456
|
+
return closeTextEditorFromState(this, commit);
|
|
457
|
+
}
|