@sequent-org/moodboard 1.3.4 → 1.4.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 (64) hide show
  1. package/package.json +6 -1
  2. package/src/assets/icons/mindmap.svg +3 -0
  3. package/src/core/SaveManager.js +44 -15
  4. package/src/core/commands/MindmapStatePatchCommand.js +85 -0
  5. package/src/core/commands/UpdateContentCommand.js +47 -4
  6. package/src/core/flows/LayerAndViewportFlow.js +87 -14
  7. package/src/core/flows/ObjectLifecycleFlow.js +7 -2
  8. package/src/core/flows/SaveFlow.js +10 -7
  9. package/src/core/flows/TransformFlow.js +2 -2
  10. package/src/core/index.js +81 -11
  11. package/src/core/rendering/ObjectRenderer.js +7 -2
  12. package/src/grid/BaseGrid.js +65 -0
  13. package/src/grid/CrossGrid.js +89 -24
  14. package/src/grid/CrossGridZoomPhases.js +167 -0
  15. package/src/grid/DotGrid.js +117 -34
  16. package/src/grid/DotGridZoomPhases.js +214 -16
  17. package/src/grid/GridDiagnostics.js +80 -0
  18. package/src/grid/GridFactory.js +13 -11
  19. package/src/grid/LineGrid.js +176 -37
  20. package/src/grid/LineGridZoomPhases.js +163 -0
  21. package/src/grid/ScreenGridPhaseMachine.js +51 -0
  22. package/src/mindmap/MindmapCompoundContract.js +235 -0
  23. package/src/moodboard/ActionHandler.js +1 -0
  24. package/src/moodboard/DataManager.js +57 -0
  25. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
  26. package/src/moodboard/integration/MoodBoardEventBindings.js +26 -1
  27. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
  28. package/src/objects/MindmapObject.js +76 -0
  29. package/src/objects/ObjectFactory.js +3 -1
  30. package/src/services/BoardService.js +127 -31
  31. package/src/services/GridSnapResolver.js +60 -0
  32. package/src/services/MiroZoomLevels.js +39 -0
  33. package/src/services/SettingsApplier.js +0 -4
  34. package/src/services/ZoomPanController.js +51 -32
  35. package/src/tools/object-tools/PlacementTool.js +12 -3
  36. package/src/tools/object-tools/SelectTool.js +11 -1
  37. package/src/tools/object-tools/placement/GhostController.js +100 -1
  38. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
  39. package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
  40. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
  41. package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
  42. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +716 -0
  43. package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
  44. package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
  45. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
  46. package/src/ui/ContextMenu.js +6 -6
  47. package/src/ui/DotGridDebugPanel.js +253 -0
  48. package/src/ui/HtmlTextLayer.js +1 -1
  49. package/src/ui/TextPropertiesPanel.js +2 -2
  50. package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
  51. package/src/ui/handles/HandlesDomRenderer.js +1486 -15
  52. package/src/ui/handles/HandlesEventBridge.js +49 -5
  53. package/src/ui/handles/HandlesInteractionController.js +4 -4
  54. package/src/ui/mindmap/MindmapConnectionLayer.js +239 -0
  55. package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
  56. package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
  57. package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
  58. package/src/ui/styles/toolbar.css +1 -0
  59. package/src/ui/styles/workspace.css +100 -0
  60. package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
  61. package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
  62. package/src/ui/toolbar/ToolbarRenderer.js +1 -0
  63. package/src/ui/toolbar/ToolbarStateController.js +1 -0
  64. package/src/utils/iconLoader.js +10 -4
@@ -0,0 +1,716 @@
1
+ import * as PIXI from 'pixi.js';
2
+ import { Events } from '../../../core/events/Events.js';
3
+ import {
4
+ createTextEditorTextarea,
5
+ createTextEditorWrapper,
6
+ } from './TextEditorDomFactory.js';
7
+ import {
8
+ registerEditorListeners,
9
+ unregisterEditorListeners,
10
+ } from './InlineEditorListenersRegistry.js';
11
+ import {
12
+ hideStaticTextDuringEditing,
13
+ showStaticTextAfterEditing,
14
+ updateGlobalTextEditorHandlesLayer,
15
+ } from './TextEditorLifecycleRegistry.js';
16
+ import { MINDMAP_LAYOUT } from '../../../ui/mindmap/MindmapLayoutConfig.js';
17
+
18
+ function applyMindmapCaretFromClick({ create, objectId, object, textarea }) {
19
+ try {
20
+ const click = (object && object.caretClick) ? object.caretClick : null;
21
+ if (typeof window === 'undefined') return;
22
+ setTimeout(() => {
23
+ try {
24
+ const fullText = (typeof textarea.value === 'string') ? textarea.value : '';
25
+ if (create || fullText.length === 0) {
26
+ textarea.selectionStart = textarea.selectionEnd = 0;
27
+ if (typeof textarea.scrollTop === 'number') textarea.scrollTop = 0;
28
+ return;
29
+ }
30
+
31
+ if (!objectId || !click) {
32
+ textarea.selectionStart = textarea.selectionEnd = fullText.length;
33
+ return;
34
+ }
35
+
36
+ const contentEl = window.moodboardMindmapHtmlTextLayer?.idToContentEl?.get?.(objectId) || null;
37
+ const textNode = contentEl?.firstChild || null;
38
+ if (!contentEl || !textNode) {
39
+ textarea.selectionStart = textarea.selectionEnd = fullText.length;
40
+ return;
41
+ }
42
+
43
+ const len = textNode.textContent?.length || 0;
44
+ if (len === 0) {
45
+ textarea.selectionStart = textarea.selectionEnd = 0;
46
+ return;
47
+ }
48
+
49
+ const doc = contentEl.ownerDocument || document;
50
+ let bestIdx = 0;
51
+ let bestDist = Infinity;
52
+ for (let i = 0; i <= len; i++) {
53
+ const range = doc.createRange();
54
+ range.setStart(textNode, i);
55
+ range.setEnd(textNode, i);
56
+ const rects = range.getClientRects();
57
+ const rect = rects && rects.length > 0 ? rects[0] : range.getBoundingClientRect();
58
+ if (rect && isFinite(rect.left) && isFinite(rect.top)) {
59
+ if (click.clientX >= rect.left && click.clientX <= rect.right &&
60
+ click.clientY >= rect.top && click.clientY <= rect.bottom) {
61
+ bestIdx = i;
62
+ bestDist = 0;
63
+ break;
64
+ }
65
+ const cx = Math.max(rect.left, Math.min(click.clientX, rect.right));
66
+ const cy = Math.max(rect.top, Math.min(click.clientY, rect.bottom));
67
+ const dx = click.clientX - cx;
68
+ const dy = click.clientY - cy;
69
+ const d2 = dx * dx + dy * dy;
70
+ if (d2 < bestDist) {
71
+ bestDist = d2;
72
+ bestIdx = i;
73
+ }
74
+ }
75
+ }
76
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
77
+ const caret = clamp(bestIdx, 0, fullText.length);
78
+ textarea.selectionStart = textarea.selectionEnd = caret;
79
+ if (typeof textarea.scrollTop === 'number') textarea.scrollTop = 0;
80
+ } catch (_) {}
81
+ }, 0);
82
+ } catch (_) {}
83
+ }
84
+
85
+ function measureMindmapTextWidthPx(textarea, measureEl, valueOverride = null) {
86
+ if (!textarea || !measureEl) return 0;
87
+ const rawValue = (typeof valueOverride === 'string') ? valueOverride : textarea.value;
88
+ const text = String(rawValue || '');
89
+ if (text.length === 0) return 0;
90
+
91
+ const style = (typeof window !== 'undefined' && typeof window.getComputedStyle === 'function')
92
+ ? window.getComputedStyle(textarea)
93
+ : null;
94
+ if (style) {
95
+ measureEl.style.fontFamily = style.fontFamily || '';
96
+ measureEl.style.fontSize = style.fontSize || '';
97
+ measureEl.style.fontWeight = style.fontWeight || '';
98
+ measureEl.style.fontStyle = style.fontStyle || '';
99
+ measureEl.style.letterSpacing = style.letterSpacing || 'normal';
100
+ }
101
+
102
+ const lines = text.split('\n');
103
+ let maxWidth = 0;
104
+ for (const rawLine of lines) {
105
+ const line = rawLine.length > 0 ? rawLine : ' ';
106
+ measureEl.textContent = line;
107
+ const rect = measureEl.getBoundingClientRect();
108
+ if (Number.isFinite(rect.width)) {
109
+ maxWidth = Math.max(maxWidth, rect.width);
110
+ }
111
+ }
112
+ return Math.max(0, Math.ceil(maxWidth));
113
+ }
114
+
115
+ function normalizeMindmapLineLength(value, maxLineChars = MINDMAP_LAYOUT.maxLineChars) {
116
+ const text = (typeof value === 'string')
117
+ ? value.replace(/\r/g, '').replace(/\n/g, '')
118
+ : '';
119
+ const chunks = [];
120
+ if (text.length === 0) return '';
121
+ for (let i = 0; i < text.length; i += maxLineChars) {
122
+ chunks.push(text.slice(i, i + maxLineChars));
123
+ }
124
+ return chunks.join('\n');
125
+ }
126
+
127
+ function normalizeMindmapValueAndCaret(value, caretPos, maxLineChars = MINDMAP_LAYOUT.maxLineChars) {
128
+ const safeValue = (typeof value === 'string') ? value : '';
129
+ const safeCaret = Number.isFinite(caretPos) ? Math.max(0, Math.min(safeValue.length, caretPos)) : safeValue.length;
130
+ const normalizedValue = normalizeMindmapLineLength(safeValue, maxLineChars);
131
+ const normalizedCaret = normalizeMindmapLineLength(safeValue.slice(0, safeCaret), maxLineChars).length;
132
+ return { normalizedValue, normalizedCaret };
133
+ }
134
+
135
+ /**
136
+ * Изолированный входной контроллер редактирования mindmap.
137
+ * Не зависит от TextInlineEditorController.
138
+ */
139
+ export function openMindmapEditor(object, create = false) {
140
+ let objectId;
141
+ let position;
142
+ let properties;
143
+
144
+ if (create) {
145
+ const objData = object.object || object;
146
+ objectId = objData.id || null;
147
+ position = objData.position || null;
148
+ properties = objData.properties || {};
149
+ } else {
150
+ objectId = object.id || null;
151
+ position = object.position || null;
152
+ properties = object.properties || {};
153
+ }
154
+
155
+ if (this.textEditor.active) {
156
+ if (this.textEditor.objectType === 'file') {
157
+ this._closeFileNameEditor(true);
158
+ } else if (this.textEditor.objectType === 'mindmap') {
159
+ this._closeMindmapEditor(true);
160
+ } else {
161
+ this._closeTextEditor(true);
162
+ }
163
+ }
164
+
165
+ if (!position && objectId) {
166
+ const posData = { objectId, position: null };
167
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
168
+ position = posData.position;
169
+ }
170
+ if (!position) return;
171
+
172
+ const view = this.app?.view;
173
+ const world = this.app?.stage?.getChildByName?.('worldLayer');
174
+ if (!view || !world || !view.parentElement) return;
175
+
176
+ this.eventBus.emit(Events.UI.TextEditStart, { objectId: objectId || null });
177
+ if (objectId && typeof this.setSelection === 'function') {
178
+ this.setSelection([objectId]);
179
+ }
180
+ updateGlobalTextEditorHandlesLayer();
181
+
182
+ let objectWidth = properties.width || MINDMAP_LAYOUT.width;
183
+ let objectHeight = properties.height || MINDMAP_LAYOUT.height;
184
+ const maxLineChars = Math.max(1, Math.round(properties.maxLineChars || MINDMAP_LAYOUT.maxLineChars));
185
+ if (objectId) {
186
+ const sizeData = { objectId, size: null };
187
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
188
+ if (sizeData.size) {
189
+ objectWidth = sizeData.size.width;
190
+ objectHeight = sizeData.size.height;
191
+ }
192
+ }
193
+
194
+ const wrapper = createTextEditorWrapper();
195
+ const textarea = createTextEditorTextarea(properties.content || '');
196
+ wrapper.classList.add('moodboard-text-editor--mindmap-debug');
197
+ textarea.classList.add('moodboard-text-input--mindmap-debug');
198
+ textarea.placeholder = 'Напишите что-нибудь';
199
+ textarea.style.fontFamily = properties.fontFamily || 'Roboto, Arial, sans-serif';
200
+ textarea.style.fontWeight = '400';
201
+ textarea.style.textAlign = 'left';
202
+ textarea.style.resize = 'none';
203
+ textarea.style.overflow = 'hidden';
204
+ textarea.style.whiteSpace = 'pre';
205
+ textarea.style.wordBreak = 'normal';
206
+ textarea.style.overflowWrap = 'normal';
207
+ textarea.style.paddingTop = '0px';
208
+ textarea.style.paddingBottom = '0px';
209
+ textarea.setAttribute('rows', '1');
210
+ textarea.setAttribute('wrap', 'off');
211
+
212
+ const measureEl = (typeof document !== 'undefined')
213
+ ? document.createElement('span')
214
+ : null;
215
+ if (measureEl) {
216
+ measureEl.style.position = 'fixed';
217
+ measureEl.style.left = '-99999px';
218
+ measureEl.style.top = '-99999px';
219
+ measureEl.style.visibility = 'hidden';
220
+ measureEl.style.pointerEvents = 'none';
221
+ measureEl.style.whiteSpace = 'pre';
222
+ measureEl.style.margin = '0';
223
+ measureEl.style.padding = '0';
224
+ document.body.appendChild(measureEl);
225
+ }
226
+
227
+ const containerRect = view.parentElement.getBoundingClientRect();
228
+ const viewRect = view.getBoundingClientRect();
229
+ const offsetLeft = viewRect.left - containerRect.left;
230
+ const offsetTop = viewRect.top - containerRect.top;
231
+
232
+ const tl = world.toGlobal(new PIXI.Point(position.x, position.y));
233
+ const br = world.toGlobal(new PIXI.Point(position.x + objectWidth, position.y + objectHeight));
234
+ const left = offsetLeft + tl.x;
235
+ const top = offsetTop + tl.y;
236
+ const width = Math.max(1, br.x - tl.x);
237
+ const height = Math.max(1, br.y - tl.y);
238
+
239
+ let targetLeft = Math.round(left);
240
+ let targetTop = Math.round(top);
241
+ let targetWidth = Math.max(1, Math.round(width));
242
+ let targetHeight = Math.max(1, Math.round(height));
243
+
244
+ const staticTextEl = (typeof window !== 'undefined')
245
+ ? window.moodboardMindmapHtmlTextLayer?.idToEl?.get?.(objectId)
246
+ : null;
247
+ if (staticTextEl) {
248
+ const cssLeft = parseFloat(staticTextEl.style.left || 'NaN');
249
+ const cssTop = parseFloat(staticTextEl.style.top || 'NaN');
250
+ const cssWidth = parseFloat(staticTextEl.style.width || 'NaN');
251
+ const cssHeight = parseFloat(staticTextEl.style.height || 'NaN');
252
+ if (isFinite(cssLeft)) targetLeft = Math.round(cssLeft);
253
+ if (isFinite(cssTop)) targetTop = Math.round(cssTop);
254
+ if (isFinite(cssWidth) && cssWidth > 0) targetWidth = Math.max(1, Math.round(cssWidth));
255
+ if (isFinite(cssHeight) && cssHeight > 0) targetHeight = Math.max(1, Math.round(cssHeight));
256
+
257
+ if (typeof window.getComputedStyle === 'function') {
258
+ const staticStyle = window.getComputedStyle(staticTextEl);
259
+ if (staticStyle?.fontFamily) textarea.style.fontFamily = staticStyle.fontFamily;
260
+ if (staticStyle?.fontSize) textarea.style.fontSize = staticStyle.fontSize;
261
+ if (staticStyle?.lineHeight) textarea.style.lineHeight = staticStyle.lineHeight;
262
+ if (staticStyle?.color) textarea.style.color = staticStyle.color;
263
+ if (staticStyle?.paddingLeft) textarea.style.paddingLeft = staticStyle.paddingLeft;
264
+ if (staticStyle?.paddingRight) textarea.style.paddingRight = staticStyle.paddingRight;
265
+ }
266
+ } else if (properties.fontSize) {
267
+ textarea.style.fontSize = `${properties.fontSize}px`;
268
+ }
269
+
270
+ wrapper.style.left = `${targetLeft}px`;
271
+ wrapper.style.top = `${targetTop}px`;
272
+ wrapper.style.width = `${targetWidth}px`;
273
+ wrapper.style.height = `${targetHeight}px`;
274
+ wrapper.style.borderRadius = '999px';
275
+ wrapper.style.padding = '0';
276
+ wrapper.style.display = 'flex';
277
+ wrapper.style.alignItems = 'center';
278
+ wrapper.style.justifyContent = 'center';
279
+
280
+ textarea.style.width = '100%';
281
+ textarea.style.height = 'auto';
282
+ textarea.style.minHeight = '0';
283
+
284
+ const initialCssWidth = targetWidth;
285
+ const initialCssHeight = targetHeight;
286
+ const initialWorldWidth = objectWidth;
287
+ const initialWorldHeight = objectHeight;
288
+ const stableBaseWorldWidth = Math.max(
289
+ 1,
290
+ Math.round(
291
+ (typeof properties.capsuleBaseWidth === 'number' && properties.capsuleBaseWidth > 0)
292
+ ? properties.capsuleBaseWidth
293
+ : ((typeof objectWidth === 'number' && objectWidth > 0)
294
+ ? objectWidth
295
+ : ((typeof properties.width === 'number' && properties.width > 0) ? properties.width : MINDMAP_LAYOUT.width))
296
+ )
297
+ );
298
+ const stableBaseWorldHeight = Math.max(
299
+ 1,
300
+ Math.round(
301
+ (typeof properties.capsuleBaseHeight === 'number' && properties.capsuleBaseHeight > 0)
302
+ ? properties.capsuleBaseHeight
303
+ : ((typeof objectHeight === 'number' && objectHeight > 0)
304
+ ? objectHeight
305
+ : ((typeof properties.height === 'number' && properties.height > 0) ? properties.height : MINDMAP_LAYOUT.height))
306
+ )
307
+ );
308
+ const resizeSession = {
309
+ started: false,
310
+ oldSize: null,
311
+ newSize: null,
312
+ oldPosition: null,
313
+ newPosition: null,
314
+ };
315
+
316
+ const getSingleLineTextareaHeight = () => {
317
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return 1;
318
+ const style = window.getComputedStyle(textarea);
319
+ const lineHeight = parseFloat(style.lineHeight || '0');
320
+ const paddingTop = parseFloat(style.paddingTop || '0');
321
+ const paddingBottom = parseFloat(style.paddingBottom || '0');
322
+ const base = lineHeight + paddingTop + paddingBottom;
323
+ return Math.max(1, Math.ceil(Number.isFinite(base) ? base : 1));
324
+ };
325
+
326
+ let baselineLineInset = null;
327
+
328
+ const alignTextareaLineTop = () => {
329
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return;
330
+ const wrapperRect = wrapper.getBoundingClientRect();
331
+ const textareaRect = textarea.getBoundingClientRect();
332
+ const textareaStyle = window.getComputedStyle(textarea);
333
+ const lineHeight = parseFloat(textareaStyle.lineHeight || '0');
334
+ const paddingTop = parseFloat(textareaStyle.paddingTop || '0');
335
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0 || !Number.isFinite(paddingTop)) return;
336
+ const currentLineTopY = textareaRect.top + paddingTop;
337
+ if (!Number.isFinite(currentLineTopY)) return;
338
+ if (!Number.isFinite(baselineLineInset)) {
339
+ baselineLineInset = currentLineTopY - wrapperRect.top;
340
+ }
341
+ const desiredLineTopY = wrapperRect.top + baselineLineInset;
342
+ const deltaY = desiredLineTopY - currentLineTopY;
343
+ textarea.style.transform = `translateY(${deltaY}px)`;
344
+ };
345
+
346
+ const syncTextareaHeight = () => {
347
+ textarea.style.height = 'auto';
348
+ const singleLineHeight = getSingleLineTextareaHeight();
349
+ const scrollHeight = Math.max(1, Math.ceil(textarea.scrollHeight));
350
+ const nextHeight = Math.max(singleLineHeight, scrollHeight);
351
+ textarea.style.height = `${nextHeight}px`;
352
+ textarea.style.marginTop = '0px';
353
+ textarea.style.transform = 'translateY(0px)';
354
+ alignTextareaLineTop();
355
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
356
+ window.requestAnimationFrame(() => {
357
+ alignTextareaLineTop();
358
+ });
359
+ }
360
+ };
361
+
362
+ const getEditorHorizontalPaddingPx = () => {
363
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
364
+ return { left: 0, right: 0 };
365
+ }
366
+ const style = window.getComputedStyle(textarea);
367
+ const left = parseFloat(style.paddingLeft || '0');
368
+ const right = parseFloat(style.paddingRight || '0');
369
+ return {
370
+ left: Number.isFinite(left) ? left : 0,
371
+ right: Number.isFinite(right) ? right : 0,
372
+ };
373
+ };
374
+
375
+ const getCssToWorldScale = () => {
376
+ const viewRes = (this.app?.renderer?.resolution)
377
+ || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
378
+ const worldScale = world?.scale?.x || 1;
379
+ if (!Number.isFinite(viewRes) || !Number.isFinite(worldScale) || worldScale === 0) {
380
+ return 1;
381
+ }
382
+ return viewRes / worldScale;
383
+ };
384
+
385
+ const getWorldToCssScale = () => {
386
+ const cssToWorld = getCssToWorldScale();
387
+ if (!Number.isFinite(cssToWorld) || cssToWorld === 0) return 1;
388
+ return 1 / cssToWorld;
389
+ };
390
+
391
+ const getEditorLineCount = () => {
392
+ const value = String(textarea.value || '');
393
+ return Math.max(1, value.split('\n').length);
394
+ };
395
+
396
+ const getEditorLineHeightPx = () => {
397
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return 1;
398
+ const style = window.getComputedStyle(textarea);
399
+ const lineHeight = parseFloat(style.lineHeight || '0');
400
+ return Math.max(1, Math.ceil(Number.isFinite(lineHeight) ? lineHeight : 1));
401
+ };
402
+
403
+ const shouldAnchorRightEdge = String(properties?.mindmap?.side || '') === 'left';
404
+
405
+ const syncMindmapSize = () => {
406
+ if (!objectId) return;
407
+
408
+ const value = String(textarea.value || '');
409
+ const hasText = value.length > 0;
410
+ const textWidth = hasText ? measureMindmapTextWidthPx(textarea, measureEl) : 0;
411
+ const padding = getEditorHorizontalPaddingPx();
412
+ const placeholderWidth = measureMindmapTextWidthPx(textarea, measureEl, textarea.placeholder || '');
413
+ const baseCssWidth = Math.max(1, Math.round(stableBaseWorldWidth * getWorldToCssScale()));
414
+ const placeholderCssWidth = Math.max(1, Math.ceil(placeholderWidth + padding.left + padding.right));
415
+ const nextCssWidth = hasText
416
+ ? Math.max(1, Math.ceil(textWidth + padding.left + padding.right))
417
+ : Math.max(baseCssWidth, placeholderCssWidth);
418
+ const lineCount = getEditorLineCount();
419
+ const lineHeightPx = getEditorLineHeightPx();
420
+ const baseCssHeight = Math.max(1, Math.round(stableBaseWorldHeight * getWorldToCssScale()));
421
+ const nextCssHeight = Math.max(1, Math.ceil(baseCssHeight + (Math.max(1, lineCount) - 1) * lineHeightPx));
422
+
423
+ const currentCssWidth = Math.max(1, Math.round(parseFloat(wrapper.style.width || `${initialCssWidth}`)));
424
+ const currentCssHeight = Math.max(1, Math.round(parseFloat(wrapper.style.height || `${initialCssHeight}`)));
425
+ const widthChangedCss = nextCssWidth !== currentCssWidth;
426
+ const heightChangedCss = nextCssHeight !== currentCssHeight;
427
+ if (!widthChangedCss && !heightChangedCss) return;
428
+
429
+ const currentCssLeft = Math.round(parseFloat(wrapper.style.left || '0'));
430
+ const nextCssLeft = shouldAnchorRightEdge
431
+ ? (currentCssLeft + (currentCssWidth - nextCssWidth))
432
+ : currentCssLeft;
433
+
434
+ if (widthChangedCss) {
435
+ wrapper.style.width = `${nextCssWidth}px`;
436
+ if (shouldAnchorRightEdge) {
437
+ wrapper.style.left = `${nextCssLeft}px`;
438
+ }
439
+ }
440
+ if (heightChangedCss) wrapper.style.height = `${nextCssHeight}px`;
441
+
442
+ const cssToWorld = getCssToWorldScale();
443
+ const nextWorldWidth = Math.max(1, Math.round(nextCssWidth * cssToWorld));
444
+ const nextWorldHeight = Math.max(1, Math.round(nextCssHeight * cssToWorld));
445
+
446
+ const sizeData = { objectId, size: null };
447
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
448
+ const currentSize = sizeData.size || { width: initialWorldWidth, height: objectHeight };
449
+ const currentWorldWidth = Math.max(1, Math.round(currentSize.width || initialWorldWidth));
450
+ const currentWorldHeight = Math.max(1, Math.round(currentSize.height || initialWorldHeight));
451
+ const widthChangedWorld = nextWorldWidth !== currentWorldWidth;
452
+ const heightChangedWorld = nextWorldHeight !== currentWorldHeight;
453
+ if (!widthChangedWorld && !heightChangedWorld) return;
454
+
455
+ const posData = { objectId, position: null };
456
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
457
+ const currentPosition = posData.position || position;
458
+
459
+ if (!resizeSession.started) {
460
+ resizeSession.started = true;
461
+ resizeSession.oldSize = { width: currentWorldWidth, height: currentWorldHeight };
462
+ resizeSession.oldPosition = { x: currentPosition.x, y: currentPosition.y };
463
+ }
464
+
465
+ const nextWorldPositionX = shouldAnchorRightEdge
466
+ ? Math.round(currentPosition.x + (currentWorldWidth - nextWorldWidth))
467
+ : Math.round(currentPosition.x);
468
+ resizeSession.newSize = { width: nextWorldWidth, height: nextWorldHeight };
469
+ resizeSession.newPosition = { x: nextWorldPositionX, y: Math.round(currentPosition.y) };
470
+
471
+ this.eventBus.emit(Events.Tool.ResizeUpdate, {
472
+ object: objectId,
473
+ size: resizeSession.newSize,
474
+ position: resizeSession.newPosition,
475
+ });
476
+ };
477
+
478
+ const onInput = () => {
479
+ const { normalizedValue, normalizedCaret } = normalizeMindmapValueAndCaret(
480
+ textarea.value,
481
+ textarea.selectionStart,
482
+ maxLineChars
483
+ );
484
+ if (normalizedValue !== textarea.value) {
485
+ textarea.value = normalizedValue;
486
+ textarea.selectionStart = textarea.selectionEnd = normalizedCaret;
487
+ }
488
+ syncMindmapSize();
489
+ syncTextareaHeight();
490
+ };
491
+
492
+ textarea.addEventListener('input', onInput);
493
+ wrapper.appendChild(textarea);
494
+ view.parentElement.appendChild(wrapper);
495
+ syncTextareaHeight();
496
+
497
+ hideStaticTextDuringEditing(this, objectId);
498
+
499
+ const syncEditorBoundsToObject = () => {
500
+ if (!objectId || !wrapper || !view || !view.parentElement || !world) return;
501
+ const staticEl = (typeof window !== 'undefined')
502
+ ? window.moodboardMindmapHtmlTextLayer?.idToEl?.get?.(objectId)
503
+ : null;
504
+ if (staticEl) {
505
+ const cssLeft = parseFloat(staticEl.style.left || 'NaN');
506
+ const cssTop = parseFloat(staticEl.style.top || 'NaN');
507
+ const cssWidth = parseFloat(staticEl.style.width || 'NaN');
508
+ const cssHeight = parseFloat(staticEl.style.height || 'NaN');
509
+ if (Number.isFinite(cssLeft)) wrapper.style.left = `${Math.round(cssLeft)}px`;
510
+ if (Number.isFinite(cssTop)) wrapper.style.top = `${Math.round(cssTop)}px`;
511
+ if (Number.isFinite(cssWidth) && cssWidth > 0) wrapper.style.width = `${Math.max(1, Math.round(cssWidth))}px`;
512
+ if (Number.isFinite(cssHeight) && cssHeight > 0) wrapper.style.height = `${Math.max(1, Math.round(cssHeight))}px`;
513
+ syncTextareaHeight();
514
+ return;
515
+ }
516
+
517
+ const posData = { objectId, position: null };
518
+ const sizeData = { objectId, size: null };
519
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
520
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
521
+ const pos = posData.position || position;
522
+ const size = sizeData.size || { width: objectWidth, height: objectHeight };
523
+
524
+ const containerRectNow = view.parentElement.getBoundingClientRect();
525
+ const viewRectNow = view.getBoundingClientRect();
526
+ const offsetLeftNow = viewRectNow.left - containerRectNow.left;
527
+ const offsetTopNow = viewRectNow.top - containerRectNow.top;
528
+ const tlNow = world.toGlobal(new PIXI.Point(pos.x, pos.y));
529
+ const brNow = world.toGlobal(new PIXI.Point(pos.x + size.width, pos.y + size.height));
530
+ wrapper.style.left = `${Math.round(offsetLeftNow + tlNow.x)}px`;
531
+ wrapper.style.top = `${Math.round(offsetTopNow + tlNow.y)}px`;
532
+ wrapper.style.width = `${Math.max(1, Math.round(brNow.x - tlNow.x))}px`;
533
+ wrapper.style.height = `${Math.max(1, Math.round(brNow.y - tlNow.y))}px`;
534
+ syncTextareaHeight();
535
+ };
536
+
537
+ const onObjectSync = (data) => {
538
+ const changedId = data?.objectId || data?.object || data;
539
+ if (changedId !== objectId) return;
540
+ syncEditorBoundsToObject();
541
+ };
542
+ const onGroupSync = (data) => {
543
+ const ids = Array.isArray(data?.objects) ? data.objects : [];
544
+ if (!ids.includes(objectId)) return;
545
+ syncEditorBoundsToObject();
546
+ };
547
+ const editorListeners = registerEditorListeners(this.eventBus, [
548
+ [Events.Object.TransformUpdated, onObjectSync],
549
+ [Events.Tool.DragUpdate, onObjectSync],
550
+ [Events.Tool.ResizeUpdate, onObjectSync],
551
+ [Events.Tool.RotateUpdate, onObjectSync],
552
+ [Events.Tool.GroupDragUpdate, onGroupSync],
553
+ [Events.Tool.GroupResizeUpdate, onGroupSync],
554
+ [Events.Tool.GroupRotateUpdate, onGroupSync],
555
+ [Events.UI.ZoomPercent, () => syncEditorBoundsToObject()],
556
+ [Events.Tool.PanUpdate, () => syncEditorBoundsToObject()],
557
+ ]);
558
+
559
+ const initialContent = String(properties.content || '');
560
+ let finalized = false;
561
+ const finalize = (commit) => {
562
+ if (finalized) return;
563
+ finalized = true;
564
+
565
+ unregisterEditorListeners(this.eventBus, editorListeners);
566
+
567
+ textarea.removeEventListener('blur', onBlur);
568
+ textarea.removeEventListener('keydown', onKeyDown);
569
+ textarea.removeEventListener('input', onInput);
570
+
571
+ const value = normalizeMindmapLineLength(textarea.value, maxLineChars).trim();
572
+ const commitValue = commit;
573
+
574
+ if (objectId && resizeSession.started && resizeSession.oldSize && resizeSession.newSize) {
575
+ const widthChanged = resizeSession.oldSize.width !== resizeSession.newSize.width;
576
+ const heightChanged = resizeSession.oldSize.height !== resizeSession.newSize.height;
577
+ if (widthChanged || heightChanged) {
578
+ this.eventBus.emit(Events.Tool.ResizeEnd, {
579
+ object: objectId,
580
+ oldSize: resizeSession.oldSize,
581
+ newSize: resizeSession.newSize,
582
+ oldPosition: resizeSession.oldPosition,
583
+ newPosition: resizeSession.newPosition,
584
+ });
585
+ }
586
+ }
587
+
588
+ if (objectId) {
589
+ showStaticTextAfterEditing(this, objectId);
590
+ }
591
+
592
+ wrapper.remove();
593
+ if (measureEl && typeof measureEl.remove === 'function') {
594
+ measureEl.remove();
595
+ }
596
+ this.textEditor = {
597
+ active: false,
598
+ objectId: null,
599
+ textarea: null,
600
+ wrapper: null,
601
+ world: null,
602
+ position: null,
603
+ properties: null,
604
+ objectType: 'text',
605
+ };
606
+
607
+ this.eventBus.emit(Events.UI.TextEditEnd, { objectId: objectId || null });
608
+ updateGlobalTextEditorHandlesLayer();
609
+
610
+ if (!commitValue) return;
611
+
612
+ if (objectId) {
613
+ const resolveCurrentObjectBox = () => {
614
+ const sizeData = { objectId, size: null };
615
+ const posData = { objectId, position: null };
616
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
617
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
618
+ return {
619
+ size: {
620
+ width: Math.max(1, Math.round(sizeData?.size?.width || objectWidth || initialWorldWidth || MINDMAP_LAYOUT.width)),
621
+ height: Math.max(1, Math.round(sizeData?.size?.height || objectHeight || initialWorldHeight || MINDMAP_LAYOUT.height)),
622
+ },
623
+ position: {
624
+ x: Math.round(posData?.position?.x || position?.x || 0),
625
+ y: Math.round(posData?.position?.y || position?.y || 0),
626
+ },
627
+ };
628
+ };
629
+ const currentBox = resolveCurrentObjectBox();
630
+ const oldBox = (resizeSession.started && resizeSession.oldSize)
631
+ ? {
632
+ size: {
633
+ width: Math.max(1, Math.round(resizeSession.oldSize.width)),
634
+ height: Math.max(1, Math.round(resizeSession.oldSize.height)),
635
+ },
636
+ position: {
637
+ x: Math.round(resizeSession.oldPosition?.x || currentBox.position.x),
638
+ y: Math.round(resizeSession.oldPosition?.y || currentBox.position.y),
639
+ },
640
+ }
641
+ : currentBox;
642
+ const newBox = (resizeSession.started && resizeSession.newSize)
643
+ ? {
644
+ size: {
645
+ width: Math.max(1, Math.round(resizeSession.newSize.width)),
646
+ height: Math.max(1, Math.round(resizeSession.newSize.height)),
647
+ },
648
+ position: {
649
+ x: Math.round(resizeSession.newPosition?.x || currentBox.position.x),
650
+ y: Math.round(resizeSession.newPosition?.y || currentBox.position.y),
651
+ },
652
+ }
653
+ : currentBox;
654
+ this.eventBus.emit(Events.Object.ContentChange, {
655
+ objectId,
656
+ oldContent: initialContent,
657
+ newContent: value,
658
+ oldSize: oldBox.size,
659
+ oldPosition: oldBox.position,
660
+ newSize: newBox.size,
661
+ newPosition: newBox.position,
662
+ });
663
+ } else {
664
+ this.eventBus.emit(Events.UI.ToolbarAction, {
665
+ type: 'mindmap',
666
+ id: 'mindmap',
667
+ position: { x: position.x, y: position.y },
668
+ properties: { ...properties, content: value },
669
+ });
670
+ }
671
+ };
672
+
673
+ const onBlur = () => finalize(true);
674
+ const onKeyDown = (event) => {
675
+ if (event.key === 'Enter' && !event.shiftKey) {
676
+ event.preventDefault();
677
+ finalize(true);
678
+ } else if (event.key === 'Escape') {
679
+ event.preventDefault();
680
+ finalize(false);
681
+ }
682
+ };
683
+
684
+ textarea.addEventListener('blur', onBlur);
685
+ textarea.addEventListener('keydown', onKeyDown);
686
+
687
+ this.textEditor = {
688
+ active: true,
689
+ objectId,
690
+ textarea,
691
+ wrapper,
692
+ world,
693
+ position,
694
+ properties: { ...properties },
695
+ objectType: 'mindmap',
696
+ isResizing: false,
697
+ _finalize: finalize,
698
+ _listeners: editorListeners,
699
+ };
700
+
701
+ textarea.focus();
702
+ applyMindmapCaretFromClick({
703
+ create,
704
+ objectId,
705
+ object,
706
+ textarea,
707
+ });
708
+ }
709
+
710
+ export function closeMindmapEditor(commit) {
711
+ if (!this.textEditor?.active || this.textEditor.objectType !== 'mindmap') return;
712
+ const finalize = this.textEditor._finalize;
713
+ if (typeof finalize === 'function') {
714
+ finalize(commit);
715
+ }
716
+ }