@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.
Files changed (122) hide show
  1. package/package.json +11 -1
  2. package/src/assets/icons/rotate-icon.svg +1 -1
  3. package/src/core/HistoryManager.js +16 -16
  4. package/src/core/KeyboardManager.js +48 -539
  5. package/src/core/PixiEngine.js +9 -9
  6. package/src/core/SaveManager.js +56 -31
  7. package/src/core/bootstrap/CoreInitializer.js +65 -0
  8. package/src/core/commands/DeleteObjectCommand.js +8 -0
  9. package/src/core/commands/GroupDeleteCommand.js +75 -0
  10. package/src/core/commands/GroupRotateCommand.js +6 -0
  11. package/src/core/commands/UpdateContentCommand.js +52 -0
  12. package/src/core/commands/UpdateFramePropertiesCommand.js +98 -0
  13. package/src/core/commands/UpdateFrameTypeCommand.js +85 -0
  14. package/src/core/commands/UpdateNoteStyleCommand.js +88 -0
  15. package/src/core/commands/UpdateTextStyleCommand.js +90 -0
  16. package/src/core/commands/index.js +6 -0
  17. package/src/core/events/Events.js +6 -0
  18. package/src/core/flows/ClipboardFlow.js +553 -0
  19. package/src/core/flows/LayerAndViewportFlow.js +283 -0
  20. package/src/core/flows/ObjectLifecycleFlow.js +336 -0
  21. package/src/core/flows/SaveFlow.js +34 -0
  22. package/src/core/flows/TransformFlow.js +277 -0
  23. package/src/core/flows/TransformFlowResizeHelpers.js +83 -0
  24. package/src/core/index.js +41 -1773
  25. package/src/core/keyboard/KeyboardClipboardImagePaste.js +190 -0
  26. package/src/core/keyboard/KeyboardContextGuards.js +35 -0
  27. package/src/core/keyboard/KeyboardEventRouter.js +92 -0
  28. package/src/core/keyboard/KeyboardSelectionActions.js +103 -0
  29. package/src/core/keyboard/KeyboardShortcutMap.js +31 -0
  30. package/src/core/keyboard/KeyboardToolSwitching.js +26 -0
  31. package/src/core/rendering/ObjectRenderer.js +3 -7
  32. package/src/grid/BaseGrid.js +26 -0
  33. package/src/grid/CrossGrid.js +7 -6
  34. package/src/grid/DotGrid.js +89 -33
  35. package/src/grid/DotGridZoomPhases.js +42 -0
  36. package/src/grid/LineGrid.js +22 -21
  37. package/src/moodboard/MoodBoard.js +31 -532
  38. package/src/moodboard/bootstrap/MoodBoardInitializer.js +47 -0
  39. package/src/moodboard/bootstrap/MoodBoardManagersFactory.js +38 -0
  40. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +109 -0
  41. package/src/moodboard/integration/MoodBoardEventBindings.js +65 -0
  42. package/src/moodboard/integration/MoodBoardLoadApi.js +82 -0
  43. package/src/moodboard/integration/MoodBoardScreenshotApi.js +33 -0
  44. package/src/moodboard/integration/MoodBoardScreenshotCanvas.js +98 -0
  45. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +97 -0
  46. package/src/objects/FileObject.js +17 -6
  47. package/src/objects/FrameObject.js +50 -10
  48. package/src/objects/NoteObject.js +5 -4
  49. package/src/services/BoardService.js +42 -2
  50. package/src/services/FrameService.js +83 -42
  51. package/src/services/ResizePolicyService.js +152 -0
  52. package/src/services/SettingsApplier.js +7 -2
  53. package/src/services/ZoomPanController.js +35 -9
  54. package/src/tools/ToolManager.js +30 -537
  55. package/src/tools/board-tools/PanTool.js +5 -11
  56. package/src/tools/manager/ToolActivationController.js +49 -0
  57. package/src/tools/manager/ToolEventRouter.js +396 -0
  58. package/src/tools/manager/ToolManagerGuards.js +33 -0
  59. package/src/tools/manager/ToolManagerLifecycle.js +110 -0
  60. package/src/tools/manager/ToolRegistry.js +33 -0
  61. package/src/tools/object-tools/DrawingTool.js +48 -14
  62. package/src/tools/object-tools/PlacementTool.js +50 -1049
  63. package/src/tools/object-tools/PlacementToolV2.js +88 -0
  64. package/src/tools/object-tools/SelectTool.js +174 -2681
  65. package/src/tools/object-tools/placement/GhostController.js +504 -0
  66. package/src/tools/object-tools/placement/PlacementCoordinateResolver.js +20 -0
  67. package/src/tools/object-tools/placement/PlacementEventsBridge.js +91 -0
  68. package/src/tools/object-tools/placement/PlacementInputRouter.js +267 -0
  69. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +111 -0
  70. package/src/tools/object-tools/placement/PlacementSessionStore.js +18 -0
  71. package/src/tools/object-tools/selection/BoxSelectController.js +0 -5
  72. package/src/tools/object-tools/selection/CloneFlowController.js +71 -0
  73. package/src/tools/object-tools/selection/CoordinateMapper.js +10 -0
  74. package/src/tools/object-tools/selection/CursorController.js +78 -0
  75. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +184 -0
  76. package/src/tools/object-tools/selection/HitTestService.js +102 -0
  77. package/src/tools/object-tools/selection/InlineEditorController.js +24 -0
  78. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +50 -0
  79. package/src/tools/object-tools/selection/InlineEditorListenersRegistry.js +14 -0
  80. package/src/tools/object-tools/selection/InlineEditorPositioningService.js +25 -0
  81. package/src/tools/object-tools/selection/NoteInlineEditorController.js +113 -0
  82. package/src/tools/object-tools/selection/SelectInputRouter.js +267 -0
  83. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +128 -0
  84. package/src/tools/object-tools/selection/SelectToolSetup.js +134 -0
  85. package/src/tools/object-tools/selection/SelectionOverlayService.js +81 -0
  86. package/src/tools/object-tools/selection/SelectionStateController.js +91 -0
  87. package/src/tools/object-tools/selection/TextEditorDomFactory.js +65 -0
  88. package/src/tools/object-tools/selection/TextEditorInteractionController.js +266 -0
  89. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +90 -0
  90. package/src/tools/object-tools/selection/TextEditorPositioningService.js +158 -0
  91. package/src/tools/object-tools/selection/TextEditorSyncService.js +110 -0
  92. package/src/tools/object-tools/selection/TextInlineEditorController.js +457 -0
  93. package/src/tools/object-tools/selection/TransformInteractionController.js +466 -0
  94. package/src/ui/FilePropertiesPanel.js +61 -32
  95. package/src/ui/FramePropertiesPanel.js +176 -101
  96. package/src/ui/HtmlHandlesLayer.js +121 -999
  97. package/src/ui/MapPanel.js +12 -7
  98. package/src/ui/NotePropertiesPanel.js +17 -2
  99. package/src/ui/TextPropertiesPanel.js +124 -738
  100. package/src/ui/Toolbar.js +71 -1180
  101. package/src/ui/Topbar.js +23 -25
  102. package/src/ui/ZoomPanel.js +16 -5
  103. package/src/ui/handles/GroupSelectionHandlesController.js +29 -0
  104. package/src/ui/handles/HandlesDomRenderer.js +278 -0
  105. package/src/ui/handles/HandlesEventBridge.js +102 -0
  106. package/src/ui/handles/HandlesInteractionController.js +772 -0
  107. package/src/ui/handles/HandlesPositioningService.js +206 -0
  108. package/src/ui/handles/SingleSelectionHandlesController.js +22 -0
  109. package/src/ui/styles/toolbar.css +2 -0
  110. package/src/ui/styles/workspace.css +13 -6
  111. package/src/ui/text-properties/TextPropertiesPanelBindings.js +92 -0
  112. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +77 -0
  113. package/src/ui/text-properties/TextPropertiesPanelMapper.js +173 -0
  114. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +434 -0
  115. package/src/ui/text-properties/TextPropertiesPanelState.js +39 -0
  116. package/src/ui/toolbar/ToolbarActionRouter.js +193 -0
  117. package/src/ui/toolbar/ToolbarDialogsController.js +186 -0
  118. package/src/ui/toolbar/ToolbarPopupsController.js +662 -0
  119. package/src/ui/toolbar/ToolbarRenderer.js +97 -0
  120. package/src/ui/toolbar/ToolbarStateController.js +79 -0
  121. package/src/ui/toolbar/ToolbarTooltipController.js +52 -0
  122. package/src/utils/emojiLoaderNoBundler.js +1 -1
@@ -0,0 +1,91 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+
3
+ export function addToSelection(object) {
4
+ this.selection.add(object);
5
+ this.emit(Events.Tool.SelectionAdd, { object });
6
+ this.updateResizeHandles();
7
+ }
8
+
9
+ export function removeFromSelection(object) {
10
+ this.selection.remove(object);
11
+ this.emit(Events.Tool.SelectionRemove, { object });
12
+ this.updateResizeHandles();
13
+ }
14
+
15
+ export function clearSelection() {
16
+ // Проверяем, что инструмент не уничтожен
17
+ if (this.destroyed) {
18
+ return;
19
+ }
20
+
21
+ const objects = this.selection.toArray();
22
+ this.selection.clear();
23
+ this.emit(Events.Tool.SelectionClear, { objects });
24
+ this.updateResizeHandles();
25
+ }
26
+
27
+ export function selectAll() {
28
+ // TODO: Выделить все объекты на доске
29
+ this.emit(Events.Tool.SelectionAll);
30
+ }
31
+
32
+ export function deleteSelectedObjects() {
33
+ const objects = this.selection.toArray();
34
+ this.clearSelection();
35
+ this.emit(Events.Tool.ObjectsDelete, { objects });
36
+ }
37
+
38
+ export function editObject(object) {
39
+ this.emit(Events.Tool.ObjectEdit, { object });
40
+ }
41
+
42
+ export function getSelection() {
43
+ return this.selection.toArray();
44
+ }
45
+
46
+ export function hasSelection() {
47
+ return this.selection.size() > 0;
48
+ }
49
+
50
+ export function setSelection(objectIds) {
51
+ const prev = this.selection.toArray();
52
+ this.selection.clear();
53
+ this.selection.addMany(objectIds);
54
+ // Эмитим события для совместимости
55
+ if (prev.length > 0) {
56
+ this.emit(Events.Tool.SelectionClear, { objects: prev });
57
+ }
58
+ for (const id of objectIds) {
59
+ this.emit(Events.Tool.SelectionAdd, { object: id });
60
+ }
61
+ this.updateResizeHandles();
62
+ }
63
+
64
+ export function updateResizeHandles() {
65
+ // Проверяем, что инструмент не уничтожен
66
+ if (this.destroyed) {
67
+ return;
68
+ }
69
+
70
+ // Используем HTML-ручки (HtmlHandlesLayer). Прячем Pixi-ручки и групповые графики.
71
+ try {
72
+ if (this.resizeHandles && typeof this.resizeHandles.hideHandles === 'function') {
73
+ this.resizeHandles.hideHandles();
74
+ }
75
+ const stage = this.app?.stage;
76
+ const world = stage?.getChildByName && stage.getChildByName('worldLayer');
77
+ const rh = world && world.getChildByName && world.getChildByName('resize-handles');
78
+ if (rh) rh.visible = false;
79
+ const gb = stage && stage.getChildByName && stage.getChildByName('group-bounds');
80
+ if (gb) gb.visible = false;
81
+ } catch (_) {
82
+ // noop
83
+ }
84
+ }
85
+
86
+ export function onActivateSelection() {
87
+ // Подписка безопасна: EventBus простая шина, а вызов синхронный
88
+ this.eventBus.on(Events.Tool.GetSelection, (data) => {
89
+ data.selection = this.getSelection();
90
+ });
91
+ }
@@ -0,0 +1,65 @@
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 computeTextEditorLineHeightPx(fontSizePx) {
16
+ if (fontSizePx <= 12) return Math.round(fontSizePx * 1.40);
17
+ if (fontSizePx <= 18) return Math.round(fontSizePx * 1.34);
18
+ if (fontSizePx <= 36) return Math.round(fontSizePx * 1.26);
19
+ if (fontSizePx <= 48) return Math.round(fontSizePx * 1.24);
20
+ if (fontSizePx <= 72) return Math.round(fontSizePx * 1.22);
21
+ if (fontSizePx <= 96) return Math.round(fontSizePx * 1.20);
22
+ return Math.round(fontSizePx * 1.18);
23
+ }
24
+
25
+ export function applyInitialTextEditorTextareaStyles(textarea, { effectiveFontPx, lineHeightPx }) {
26
+ textarea.style.fontSize = `${effectiveFontPx}px`;
27
+ textarea.style.lineHeight = `${lineHeightPx}px`;
28
+
29
+ const initialHeightPx = Math.max(1, lineHeightPx);
30
+ textarea.style.minHeight = `${initialHeightPx}px`;
31
+ textarea.style.height = `${initialHeightPx}px`;
32
+ textarea.setAttribute('rows', '1');
33
+ textarea.style.overflowY = 'hidden';
34
+ textarea.style.whiteSpace = 'pre-wrap';
35
+ textarea.style.wordBreak = 'break-word';
36
+ textarea.style.letterSpacing = '0px';
37
+ textarea.style.fontKerning = 'normal';
38
+ }
39
+
40
+ export function measureTextEditorPlaceholderWidth(textarea, placeholder = 'Напишите что-нибудь') {
41
+ const measureEl = document.createElement('span');
42
+ measureEl.style.position = 'absolute';
43
+ measureEl.style.visibility = 'hidden';
44
+ measureEl.style.whiteSpace = 'pre';
45
+ measureEl.style.fontFamily = textarea.style.fontFamily;
46
+ measureEl.style.fontSize = textarea.style.fontSize;
47
+ measureEl.textContent = placeholder;
48
+ document.body.appendChild(measureEl);
49
+
50
+ const width = Math.ceil(measureEl.getBoundingClientRect().width);
51
+ measureEl.remove();
52
+ return width;
53
+ }
54
+
55
+ export function attachTextEditorPlaceholderStyle(textarea, { effectiveFontPx, isNote }) {
56
+ const uid = 'mbti-' + Math.random().toString(36).slice(2);
57
+ textarea.classList.add(uid);
58
+
59
+ const styleEl = document.createElement('style');
60
+ const placeholderOpacity = isNote ? '0.4' : '0.6';
61
+ styleEl.textContent = `.${uid}::placeholder{font-size:${effectiveFontPx}px;opacity:${placeholderOpacity};line-height:${computeTextEditorLineHeightPx(effectiveFontPx)}px;white-space:nowrap;}`;
62
+ document.head.appendChild(styleEl);
63
+
64
+ return styleEl;
65
+ }
@@ -0,0 +1,266 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+ import { alignStaticTextToEditorCssPosition } from './TextEditorPositioningService.js';
3
+ import {
4
+ cleanupActiveTextEditor,
5
+ showNotePixiText,
6
+ showStaticTextAfterEditing,
7
+ updateGlobalTextEditorHandlesLayer,
8
+ } from './TextEditorLifecycleRegistry.js';
9
+
10
+ export function applyTextEditorCaretFromClick({ create, objectId, object, textarea }) {
11
+ try {
12
+ const click = (object && object.caretClick) ? object.caretClick : null;
13
+ if (!create && objectId && click && typeof window !== 'undefined') {
14
+ setTimeout(() => {
15
+ try {
16
+ const el = window.moodboardHtmlTextLayer ? window.moodboardHtmlTextLayer.idToEl.get(objectId) : null;
17
+ const fullText = (typeof textarea.value === 'string') ? textarea.value : '';
18
+ if (!el || !fullText || !el.firstChild) return;
19
+ const textNode = el.firstChild;
20
+ const len = textNode.textContent.length;
21
+ if (len === 0) {
22
+ textarea.selectionStart = textarea.selectionEnd = 0;
23
+ return;
24
+ }
25
+ const doc = el.ownerDocument || document;
26
+ let bestIdx = 0;
27
+ let bestDist = Infinity;
28
+ for (let i = 0; i <= len; i++) {
29
+ const range = doc.createRange();
30
+ range.setStart(textNode, i);
31
+ range.setEnd(textNode, i);
32
+ const rects = range.getClientRects();
33
+ const rect = rects && rects.length > 0 ? rects[0] : range.getBoundingClientRect();
34
+ if (rect && isFinite(rect.left) && isFinite(rect.top)) {
35
+ if (click.clientX >= rect.left && click.clientX <= rect.right &&
36
+ click.clientY >= rect.top && click.clientY <= rect.bottom) {
37
+ bestIdx = i;
38
+ bestDist = 0;
39
+ break;
40
+ }
41
+ const cx = Math.max(rect.left, Math.min(click.clientX, rect.right));
42
+ const cy = Math.max(rect.top, Math.min(click.clientY, rect.bottom));
43
+ const dx = click.clientX - cx;
44
+ const dy = click.clientY - cy;
45
+ const d2 = dx * dx + dy * dy;
46
+ if (d2 < bestDist) {
47
+ bestDist = d2;
48
+ bestIdx = i;
49
+ }
50
+ }
51
+ }
52
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
53
+ const caret = clamp(bestIdx, 0, fullText.length);
54
+ textarea.selectionStart = textarea.selectionEnd = caret;
55
+ if (typeof textarea.scrollTop === 'number') textarea.scrollTop = 0;
56
+ console.log('🧭 Text caret set', { objectId, caret, len: fullText.length });
57
+ } catch (_) {}
58
+ }, 0);
59
+ }
60
+ } catch (_) {}
61
+ }
62
+
63
+ export function createTextEditorFinalize(controller, {
64
+ textarea,
65
+ wrapper,
66
+ view,
67
+ viewRes,
68
+ position,
69
+ fontSize,
70
+ objectId,
71
+ isNewCreation,
72
+ initialContent = '',
73
+ }) {
74
+ return (commit) => {
75
+ if (controller.textEditor?._removeDomListeners) {
76
+ controller.textEditor._removeDomListeners();
77
+ controller.textEditor._removeDomListeners = null;
78
+ }
79
+
80
+ const value = textarea.value.trim();
81
+ const commitValue = commit && value.length > 0;
82
+
83
+ const currentObjectType = controller.textEditor.objectType;
84
+
85
+ if (objectId && (commitValue || !isNewCreation)) {
86
+ showStaticTextAfterEditing(controller, objectId);
87
+ }
88
+
89
+ if (objectId && (currentObjectType === 'text' || currentObjectType === 'simple-text')) {
90
+ try {
91
+ const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
92
+ const scaleX = worldLayerRef?.scale?.x || 1;
93
+ const viewResLocal = (controller.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
94
+ const wPx = Math.max(1, wrapper.offsetWidth);
95
+ const hPx = Math.max(1, wrapper.offsetHeight);
96
+ const newW = Math.max(1, Math.round(wPx * viewResLocal / scaleX));
97
+ const newH = Math.max(1, Math.round(hPx * viewResLocal / scaleX));
98
+ const sizeReq = { objectId, size: null };
99
+ controller.eventBus.emit(Events.Tool.GetObjectSize, sizeReq);
100
+ const oldSize = sizeReq.size || { width: newW, height: newH };
101
+ const posReq = { objectId, position: null };
102
+ controller.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
103
+ const oldPos = posReq.position || { x: position.x, y: position.y };
104
+ const newSize = { width: newW, height: newH };
105
+ controller.eventBus.emit(Events.Tool.ResizeUpdate, { object: objectId, size: newSize, position: oldPos });
106
+ controller.eventBus.emit(Events.Tool.ResizeEnd, { object: objectId, oldSize, newSize, oldPosition: oldPos, newPosition: oldPos });
107
+ } catch (err) {
108
+ console.warn('⚠️ Не удалось применить размеры после редактирования текста:', err);
109
+ }
110
+ }
111
+
112
+ cleanupActiveTextEditor(controller, wrapper);
113
+ if (currentObjectType === 'note') {
114
+ controller.eventBus.emit(Events.UI.NoteEditEnd, { objectId: objectId || null });
115
+ showNotePixiText(controller, objectId);
116
+ } else {
117
+ controller.eventBus.emit(Events.UI.TextEditEnd, { objectId: objectId || null });
118
+ }
119
+
120
+ updateGlobalTextEditorHandlesLayer();
121
+
122
+ if (!commitValue) {
123
+ if (isNewCreation && objectId) {
124
+ controller.eventBus.emit(Events.Tool.ObjectsDelete, { objects: [objectId] });
125
+ }
126
+ return;
127
+ }
128
+
129
+ if (objectId == null) {
130
+ const objectType = currentObjectType || 'text';
131
+ const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
132
+ const scaleX = worldLayerRef?.scale?.x || 1;
133
+ const wPx = Math.max(1, wrapper.offsetWidth);
134
+ const hPx = Math.max(1, wrapper.offsetHeight);
135
+ const wWorld = Math.max(1, Math.round(wPx * viewRes / scaleX));
136
+ const hWorld = Math.max(1, Math.round(hPx * viewRes / scaleX));
137
+ controller.eventBus.emit(Events.UI.ToolbarAction, {
138
+ type: objectType,
139
+ id: objectType,
140
+ position: { x: position.x, y: position.y },
141
+ properties: { content: value, fontSize, width: wWorld, height: hWorld },
142
+ });
143
+ } else {
144
+ if (isNewCreation) {
145
+ controller.eventBus.emit(Events.Tool.UpdateObjectContent, { objectId, content: value });
146
+ controller.eventBus.emit(Events.Object.StateChanged, {
147
+ objectId,
148
+ updates: { properties: { content: value } },
149
+ });
150
+ } else {
151
+ const oldContent = typeof initialContent === 'string' ? initialContent : '';
152
+ controller.eventBus.emit(Events.Object.ContentChange, {
153
+ objectId,
154
+ oldContent,
155
+ newContent: value,
156
+ });
157
+ }
158
+ }
159
+ };
160
+ }
161
+
162
+ export function bindTextEditorInteractions(controller, {
163
+ textarea,
164
+ isNewCreation,
165
+ isNote,
166
+ autoSize,
167
+ updateNoteEditor,
168
+ finalize,
169
+ }) {
170
+ const blurHandler = () => {
171
+ const value = (textarea.value || '').trim();
172
+ if (isNewCreation && value.length === 0) {
173
+ finalize(false);
174
+ return;
175
+ }
176
+ finalize(true);
177
+ };
178
+
179
+ const keydownHandler = (e) => {
180
+ if (e.key === 'Enter' && !e.shiftKey) {
181
+ e.preventDefault();
182
+ finalize(true);
183
+ } else if (e.key === 'Escape') {
184
+ e.preventDefault();
185
+ finalize(false);
186
+ }
187
+ };
188
+
189
+ const inputHandler = !isNote
190
+ ? autoSize
191
+ : () => { try { if (updateNoteEditor) updateNoteEditor(); } catch (_) {} };
192
+
193
+ textarea.addEventListener('blur', blurHandler);
194
+ textarea.addEventListener('keydown', keydownHandler);
195
+ textarea.addEventListener('input', inputHandler);
196
+
197
+ const removeDomListeners = () => {
198
+ textarea.removeEventListener('blur', blurHandler);
199
+ textarea.removeEventListener('keydown', keydownHandler);
200
+ textarea.removeEventListener('input', inputHandler);
201
+ };
202
+
203
+ if (controller.textEditor) {
204
+ controller.textEditor._removeDomListeners = removeDomListeners;
205
+ }
206
+ }
207
+
208
+ export function closeTextEditorFromState(controller, commit) {
209
+ const textarea = controller.textEditor.textarea;
210
+ if (!textarea) return;
211
+ const value = textarea.value.trim();
212
+ const commitValue = commit && value.length > 0;
213
+ const objectType = controller.textEditor.objectType || 'text';
214
+ const objectId = controller.textEditor.objectId;
215
+ const position = controller.textEditor.position;
216
+ const properties = controller.textEditor.properties;
217
+ const isNewCreation = controller.textEditor.isNewCreation;
218
+ const initialContent = controller.textEditor.initialContent ?? '';
219
+
220
+ if (objectId) {
221
+ if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
222
+ const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
223
+ if (el) {
224
+ controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
225
+ alignStaticTextToEditorCssPosition({
226
+ controller,
227
+ objectId,
228
+ worldLayerRef: controller.textEditor.world || (controller.app?.stage),
229
+ view: controller.app?.view,
230
+ cssLeft: controller.textEditor._cssLeftPx,
231
+ cssTop: controller.textEditor._cssTopPx,
232
+ });
233
+ } else {
234
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем ShowObjectText`);
235
+ }
236
+ } else {
237
+ controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
238
+ }
239
+ }
240
+
241
+ textarea.remove();
242
+ controller.textEditor = { active: false, objectId: null, textarea: null, world: null, objectType: 'text' };
243
+ if (!commitValue) return;
244
+ if (objectId == null) {
245
+ controller.eventBus.emit(Events.UI.ToolbarAction, {
246
+ type: objectType,
247
+ id: objectType,
248
+ position: { x: position.x, y: position.y },
249
+ properties: { content: value, fontSize: properties.fontSize },
250
+ });
251
+ } else {
252
+ if (isNewCreation) {
253
+ controller.eventBus.emit(Events.Tool.UpdateObjectContent, { objectId, content: value });
254
+ controller.eventBus.emit(Events.Object.StateChanged, {
255
+ objectId,
256
+ updates: { properties: { content: value } },
257
+ });
258
+ } else {
259
+ controller.eventBus.emit(Events.Object.ContentChange, {
260
+ objectId,
261
+ oldContent: initialContent,
262
+ newContent: value,
263
+ });
264
+ }
265
+ }
266
+ }
@@ -0,0 +1,90 @@
1
+ import { Events } from '../../../core/events/Events.js';
2
+ import { unregisterEditorListeners } from './InlineEditorListenersRegistry.js';
3
+
4
+ export function hideGlobalTextEditorHandlesLayer() {
5
+ try {
6
+ if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
7
+ window.moodboardHtmlHandlesLayer.hide();
8
+ }
9
+ } catch (_) {}
10
+ }
11
+
12
+ export function updateGlobalTextEditorHandlesLayer() {
13
+ try {
14
+ if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer) {
15
+ window.moodboardHtmlHandlesLayer.update();
16
+ }
17
+ } catch (_) {}
18
+ }
19
+
20
+ export function hideStaticTextDuringEditing(controller, objectId) {
21
+ if (!objectId) return;
22
+
23
+ if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
24
+ const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
25
+ if (el) {
26
+ controller.eventBus.emit(Events.Tool.HideObjectText, { objectId });
27
+ } else {
28
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем HideObjectText`);
29
+ }
30
+ } else {
31
+ controller.eventBus.emit(Events.Tool.HideObjectText, { objectId });
32
+ }
33
+ }
34
+
35
+ export function showStaticTextAfterEditing(controller, objectId) {
36
+ if (!objectId) return;
37
+
38
+ if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
39
+ const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
40
+ if (el) {
41
+ controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
42
+ } else {
43
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем ShowObjectText`);
44
+ }
45
+ } else {
46
+ controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
47
+ }
48
+ }
49
+
50
+ export function hideNotePixiText(controller, objectId) {
51
+ try {
52
+ const pixiReq = { objectId, pixiObject: null };
53
+ controller.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
54
+ const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
55
+ if (inst && typeof inst.hideText === 'function') {
56
+ inst.hideText();
57
+ }
58
+ } catch (_) {}
59
+ }
60
+
61
+ export function showNotePixiText(controller, objectId) {
62
+ try {
63
+ const pixiReq = { objectId, pixiObject: null };
64
+ controller.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
65
+ const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
66
+ if (inst && typeof inst.showText === 'function') {
67
+ inst.showText();
68
+ }
69
+ } catch (_) {}
70
+ }
71
+
72
+ export function cleanupActiveTextEditor(controller, wrapper) {
73
+ try {
74
+ if (controller.textEditor && Array.isArray(controller.textEditor._listeners)) {
75
+ unregisterEditorListeners(controller.eventBus, controller.textEditor._listeners);
76
+ }
77
+ } catch (_) {}
78
+
79
+ wrapper.remove();
80
+ controller.textEditor = {
81
+ active: false,
82
+ objectId: null,
83
+ textarea: null,
84
+ wrapper: null,
85
+ world: null,
86
+ position: null,
87
+ properties: null,
88
+ objectType: 'text',
89
+ };
90
+ }
@@ -0,0 +1,158 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+ import { toScreenWithContainerOffset } from './InlineEditorPositioningService.js';
4
+
5
+ export function createTextEditorToScreen(controller, view) {
6
+ return (wx, wy) => toScreenWithContainerOffset(
7
+ controller.textEditor.world || (controller.app?.stage),
8
+ view,
9
+ wx,
10
+ wy
11
+ );
12
+ }
13
+
14
+ export function positionRegularTextEditor({
15
+ create,
16
+ objectId,
17
+ screenPos,
18
+ textarea,
19
+ wrapper,
20
+ }) {
21
+ let padTop = 0;
22
+ let padLeft = 0;
23
+ let lineHeightPx = 0;
24
+ try {
25
+ if (typeof window !== 'undefined' && window.getComputedStyle) {
26
+ const cs = window.getComputedStyle(textarea);
27
+ const pt = parseFloat(cs.paddingTop);
28
+ const pl = parseFloat(cs.paddingLeft);
29
+ const lh = parseFloat(cs.lineHeight);
30
+ if (isFinite(pt)) padTop = pt;
31
+ if (isFinite(pl)) padLeft = pl;
32
+ if (isFinite(lh)) lineHeightPx = lh;
33
+ }
34
+ } catch (_) {}
35
+
36
+ if (!isFinite(lineHeightPx) || lineHeightPx <= 0) {
37
+ try {
38
+ const rect = textarea.getBoundingClientRect && textarea.getBoundingClientRect();
39
+ if (rect && isFinite(rect.height)) lineHeightPx = rect.height;
40
+ } catch (_) {}
41
+ }
42
+
43
+ let baseLeftPx = screenPos.x;
44
+ let baseTopPx = screenPos.y;
45
+ try {
46
+ if (!create && objectId && typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
47
+ const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
48
+ if (el) {
49
+ const cssLeft = parseFloat(el.style.left || 'NaN');
50
+ const cssTop = parseFloat(el.style.top || 'NaN');
51
+ if (isFinite(cssLeft)) baseLeftPx = cssLeft;
52
+ if (isFinite(cssTop)) baseTopPx = cssTop;
53
+ }
54
+ }
55
+ } catch (_) {}
56
+
57
+ const leftPx = Math.round(baseLeftPx - padLeft);
58
+ const topPx = create
59
+ ? Math.round(baseTopPx - padTop - (lineHeightPx / 2))
60
+ : Math.round(baseTopPx - padTop);
61
+
62
+ wrapper.style.left = `${leftPx}px`;
63
+ wrapper.style.top = `${topPx}px`;
64
+
65
+ return {
66
+ leftPx,
67
+ topPx,
68
+ padTop,
69
+ padLeft,
70
+ lineHeightPx,
71
+ baseLeftPx,
72
+ baseTopPx,
73
+ };
74
+ }
75
+
76
+ export function syncCreatedTextEditorWorldPosition({
77
+ controller,
78
+ create,
79
+ objectId,
80
+ position,
81
+ leftPx,
82
+ topPx,
83
+ padTop,
84
+ }) {
85
+ try {
86
+ if (create && objectId) {
87
+ const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
88
+ const viewEl = controller.app?.view;
89
+ if (worldLayerRef && viewEl && viewEl.parentElement) {
90
+ const containerRect = viewEl.parentElement.getBoundingClientRect();
91
+ const viewRect = viewEl.getBoundingClientRect();
92
+ const offsetLeft = viewRect.left - containerRect.left;
93
+ const offsetTop = viewRect.top - containerRect.top;
94
+
95
+ const yCssStaticTop = Math.round(topPx + padTop);
96
+ const screenX = Math.round(leftPx - offsetLeft);
97
+ const screenY = Math.round(yCssStaticTop - offsetTop);
98
+ const globalPoint = new PIXI.Point(screenX, screenY);
99
+ const worldPoint = worldLayerRef.toLocal
100
+ ? worldLayerRef.toLocal(globalPoint)
101
+ : { x: position.x, y: position.y };
102
+ const newWorldPos = {
103
+ x: Math.round(worldPoint.x),
104
+ y: Math.round(worldPoint.y),
105
+ };
106
+
107
+ controller.eventBus.emit(Events.Object.StateChanged, {
108
+ objectId,
109
+ updates: { position: newWorldPos },
110
+ });
111
+
112
+ console.log('🧭 Text position sync', {
113
+ objectId,
114
+ newWorldPos,
115
+ leftPx,
116
+ topPx,
117
+ yCssStaticTop,
118
+ padTop,
119
+ offsetLeft,
120
+ offsetTop,
121
+ });
122
+ }
123
+ }
124
+ } catch (_) {}
125
+ }
126
+
127
+ export function alignStaticTextToEditorCssPosition({
128
+ controller,
129
+ objectId,
130
+ worldLayerRef,
131
+ view,
132
+ cssLeft,
133
+ cssTop,
134
+ }) {
135
+ try {
136
+ if (view && view.parentElement && isFinite(cssLeft) && isFinite(cssTop) && worldLayerRef) {
137
+ setTimeout(() => {
138
+ try {
139
+ const containerRect = view.parentElement.getBoundingClientRect();
140
+ const viewRect = view.getBoundingClientRect();
141
+ const offsetLeft = viewRect.left - containerRect.left;
142
+ const offsetTop = viewRect.top - containerRect.top;
143
+ const screenX = cssLeft - offsetLeft;
144
+ const screenY = cssTop - offsetTop;
145
+ const desiredWorld = worldLayerRef.toLocal(new PIXI.Point(screenX, screenY));
146
+ const newPos = { x: Math.round(desiredWorld.x), y: Math.round(desiredWorld.y) };
147
+
148
+ controller.eventBus.emit(Events.Object.StateChanged, {
149
+ objectId,
150
+ updates: { position: newPos },
151
+ });
152
+
153
+ console.log('🧭 Text post-show align', { objectId, cssLeft, cssTop, newPos });
154
+ } catch (_) {}
155
+ }, 0);
156
+ }
157
+ } catch (_) {}
158
+ }