@sequent-org/moodboard 1.4.56 → 1.4.58
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 +1 -1
- package/src/core/commands/PasteObjectCommand.js +7 -1
- package/src/core/commands/UpdateContentCommand.js +95 -0
- package/src/core/flows/ObjectLifecycleFlow.js +10 -1
- package/src/initNoBundler.js +28 -4
- package/src/services/text/TextBoxMetrics.js +133 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +98 -7
- package/src/tools/object-tools/selection/SelectInputRouter.js +56 -3
- package/src/tools/object-tools/selection/TextEditorCaretService.js +113 -0
- package/src/tools/object-tools/selection/TextEditorDomFactory.js +37 -21
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +111 -12
- package/src/tools/object-tools/selection/TextEditorPositioningService.js +19 -1
- package/src/tools/object-tools/selection/TextEditorSyncService.js +2 -1
- package/src/tools/object-tools/selection/TextInlineEditorController.js +70 -45
- package/src/tools/object-tools/selection/TransformInteractionController.js +7 -1
- package/src/ui/FramePropertiesPanel.js +7 -0
- package/src/ui/HtmlTextLayer.js +174 -64
- package/src/ui/ImagePropertiesPanel.js +7 -0
- package/src/ui/TextPropertiesPanel.js +305 -2
- package/src/ui/handles/HandlesDomRenderer.js +2 -0
- package/src/ui/handles/HandlesInteractionController.js +40 -0
- package/src/ui/styles/panels.css +240 -19
- package/src/ui/styles/workspace.css +94 -7
- package/src/ui/text-properties/TextFormatControls.js +132 -21
- package/src/ui/text-properties/TextLinkControl.js +255 -0
- package/src/ui/text-properties/TextLockMoreControls.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +45 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +52 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +263 -26
- package/src/ui/text-properties/TextPropertiesPanelState.js +6 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const propertiesToCopy = [
|
|
2
|
+
'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY',
|
|
3
|
+
'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle',
|
|
4
|
+
'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
|
|
5
|
+
'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily',
|
|
6
|
+
'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing',
|
|
7
|
+
'tabSize', 'MozTabSize', 'whiteSpace', 'wordBreak', 'wordWrap'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
let mirrorDiv = null;
|
|
11
|
+
|
|
12
|
+
export function getCaretCoordinates(element, position) {
|
|
13
|
+
if (!mirrorDiv) {
|
|
14
|
+
mirrorDiv = document.createElement('div');
|
|
15
|
+
document.body.appendChild(mirrorDiv);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const style = mirrorDiv.style;
|
|
19
|
+
const computed = window.getComputedStyle(element);
|
|
20
|
+
|
|
21
|
+
style.whiteSpace = 'pre-wrap';
|
|
22
|
+
if (element.nodeName !== 'INPUT') {
|
|
23
|
+
style.wordWrap = 'break-word';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
style.position = 'absolute';
|
|
27
|
+
style.top = '-9999px';
|
|
28
|
+
style.left = '-9999px';
|
|
29
|
+
style.visibility = 'hidden';
|
|
30
|
+
|
|
31
|
+
propertiesToCopy.forEach(prop => {
|
|
32
|
+
style[prop] = computed[prop];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (window.mozInnerScreenX != null) {
|
|
36
|
+
if (element.scrollHeight > parseInt(computed.height)) {
|
|
37
|
+
style.overflowY = 'scroll';
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
style.overflow = 'hidden';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mirrorDiv.textContent = element.value.substring(0, position);
|
|
44
|
+
|
|
45
|
+
if (element.nodeName === 'INPUT') {
|
|
46
|
+
mirrorDiv.textContent = mirrorDiv.textContent.replace(/\s/g, '\u00a0');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const span = document.createElement('span');
|
|
50
|
+
span.textContent = element.value.substring(position) || '.';
|
|
51
|
+
mirrorDiv.appendChild(span);
|
|
52
|
+
|
|
53
|
+
const coordinates = {
|
|
54
|
+
top: span.offsetTop + parseInt(computed['borderTopWidth'] || 0),
|
|
55
|
+
left: span.offsetLeft + parseInt(computed['borderLeftWidth'] || 0),
|
|
56
|
+
height: parseInt(computed['lineHeight']) || span.offsetHeight
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return coordinates;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function updateCustomCaret(textarea, caretEl) {
|
|
63
|
+
if (!textarea || !caretEl) return;
|
|
64
|
+
|
|
65
|
+
if (document.activeElement !== textarea) {
|
|
66
|
+
caretEl.style.display = 'none';
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (textarea.selectionStart !== textarea.selectionEnd) {
|
|
71
|
+
caretEl.style.display = 'none';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
caretEl.style.display = 'block';
|
|
76
|
+
|
|
77
|
+
const pos = textarea.selectionStart;
|
|
78
|
+
const coords = getCaretCoordinates(textarea, pos);
|
|
79
|
+
|
|
80
|
+
// Adjust for scroll
|
|
81
|
+
const top = coords.top - textarea.scrollTop;
|
|
82
|
+
const left = coords.left - textarea.scrollLeft;
|
|
83
|
+
|
|
84
|
+
// Calculate width based on font size to match stroke thickness
|
|
85
|
+
const computed = window.getComputedStyle(textarea);
|
|
86
|
+
const fontSize = parseFloat(computed.fontSize) || 16;
|
|
87
|
+
// Caveat has thicker strokes, Arial is standard. A good heuristic is ~0.06 to 0.08 of font size, min 2px.
|
|
88
|
+
const caretWidth = Math.max(2, Math.round(fontSize * 0.07));
|
|
89
|
+
|
|
90
|
+
// Цвет текста в textarea теперь прозрачный (видимый текст рисует backdrop-слой),
|
|
91
|
+
// поэтому цвет каретки берём из backdrop, иначе она станет невидимой.
|
|
92
|
+
let caretColor = computed.color;
|
|
93
|
+
if (!caretColor || caretColor === 'transparent' || caretColor === 'rgba(0, 0, 0, 0)') {
|
|
94
|
+
const backdrop = caretEl.parentElement
|
|
95
|
+
? caretEl.parentElement.querySelector('.moodboard-text-backdrop')
|
|
96
|
+
: null;
|
|
97
|
+
const backdropColor = backdrop ? window.getComputedStyle(backdrop).color : '';
|
|
98
|
+
caretColor = (backdropColor && backdropColor !== 'transparent' && backdropColor !== 'rgba(0, 0, 0, 0)')
|
|
99
|
+
? backdropColor
|
|
100
|
+
: '#111';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
caretEl.style.top = `${top - 2}px`;
|
|
104
|
+
caretEl.style.left = `${left - Math.floor(caretWidth / 2)}px`;
|
|
105
|
+
caretEl.style.height = `${coords.height}px`;
|
|
106
|
+
caretEl.style.width = `${caretWidth}px`;
|
|
107
|
+
caretEl.style.backgroundColor = caretColor;
|
|
108
|
+
|
|
109
|
+
// Reset animation to keep it visible while typing
|
|
110
|
+
caretEl.style.animation = 'none';
|
|
111
|
+
void caretEl.offsetWidth; // trigger reflow
|
|
112
|
+
caretEl.style.animation = 'mb-caret-blink 1s step-end infinite';
|
|
113
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { applyEditorSizing, applyTextStyles, computeLineHeightPx } from '../../../services/text/TextBoxMetrics.js';
|
|
2
|
+
|
|
1
3
|
export function createTextEditorWrapper() {
|
|
2
4
|
const wrapper = document.createElement('div');
|
|
3
5
|
wrapper.className = 'moodboard-text-editor';
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
const backdrop = document.createElement('div');
|
|
8
|
+
backdrop.className = 'moodboard-text-backdrop';
|
|
9
|
+
wrapper.appendChild(backdrop);
|
|
10
|
+
|
|
11
|
+
const caret = document.createElement('div');
|
|
12
|
+
caret.className = 'mb-custom-caret';
|
|
13
|
+
wrapper.appendChild(caret);
|
|
14
|
+
|
|
15
|
+
return { wrapper, caret, backdrop };
|
|
5
16
|
}
|
|
6
17
|
|
|
7
18
|
export function createTextEditorTextarea(content) {
|
|
@@ -13,27 +24,32 @@ export function createTextEditorTextarea(content) {
|
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export function computeTextEditorLineHeightPx(fontSizePx) {
|
|
16
|
-
|
|
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);
|
|
27
|
+
return computeLineHeightPx(fontSizePx);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Применяет все текстовые параметры на textarea/backdrop через общий applyTextStyles,
|
|
32
|
+
* и дополнительно выставляет sizing-параметры поля ввода (height, rows, overflow, white-space).
|
|
33
|
+
*
|
|
34
|
+
* fontSizePx — отображаемый размер в px (с учётом зума);
|
|
35
|
+
* baseFontSizePx — базовый без зума (для выбора line-height ratio).
|
|
36
|
+
* Эти два значения разделены, чтобы line-height ratio вычислялся по тому же baseFontSizePx,
|
|
37
|
+
* что и в HtmlTextLayer — строки совпадают при любом уровне зума.
|
|
38
|
+
*/
|
|
39
|
+
export function applyInitialTextEditorTextareaStyles(textarea, {
|
|
40
|
+
effectiveFontPx,
|
|
41
|
+
baseFontSizePx,
|
|
42
|
+
fontFamily,
|
|
43
|
+
properties,
|
|
44
|
+
lineHeightPx,
|
|
45
|
+
}) {
|
|
46
|
+
applyTextStyles(textarea, {
|
|
47
|
+
fontSizePx: effectiveFontPx,
|
|
48
|
+
baseFontSizePx: baseFontSizePx || effectiveFontPx,
|
|
49
|
+
fontFamily: fontFamily || textarea.style.fontFamily || '',
|
|
50
|
+
properties: properties || {},
|
|
51
|
+
});
|
|
52
|
+
applyEditorSizing(textarea, lineHeightPx);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export function measureTextEditorPlaceholderWidth(textarea, placeholder = 'Напишите что-нибудь') {
|
|
@@ -57,7 +73,7 @@ export function attachTextEditorPlaceholderStyle(textarea, { effectiveFontPx, is
|
|
|
57
73
|
|
|
58
74
|
const styleEl = document.createElement('style');
|
|
59
75
|
const placeholderOpacity = isNote ? '0.4' : '0.6';
|
|
60
|
-
styleEl.textContent = `.${uid}::placeholder{font-size:${effectiveFontPx}px;opacity:${placeholderOpacity};line-height:${computeTextEditorLineHeightPx(effectiveFontPx)}px;white-space:nowrap;}`;
|
|
76
|
+
styleEl.textContent = `.${uid}::placeholder{font-size:${effectiveFontPx}px;opacity:${placeholderOpacity};line-height:${computeTextEditorLineHeightPx(effectiveFontPx)}px;white-space:nowrap;color:#111;-webkit-text-fill-color:#111;}`;
|
|
61
77
|
document.head.appendChild(styleEl);
|
|
62
78
|
|
|
63
79
|
return styleEl;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Events } from '../../../core/events/Events.js';
|
|
2
2
|
import { alignStaticTextToEditorCssPosition } from './TextEditorPositioningService.js';
|
|
3
|
+
import { updateCustomCaret } from './TextEditorCaretService.js';
|
|
4
|
+
import { buildHtmlWithRanges } from '../../../ui/HtmlTextLayer.js';
|
|
3
5
|
import {
|
|
4
6
|
cleanupActiveTextEditor,
|
|
5
7
|
showNotePixiText,
|
|
@@ -92,7 +94,9 @@ export function createTextEditorFinalize(controller, {
|
|
|
92
94
|
const scaleX = worldLayerRef?.scale?.x || 1;
|
|
93
95
|
const viewResLocal = (controller.app?.renderer?.resolution) || (view.width && view.clientWidth ? (view.width / view.clientWidth) : 1);
|
|
94
96
|
const wPx = Math.max(1, wrapper.offsetWidth);
|
|
95
|
-
const
|
|
97
|
+
const _taStyle = typeof window !== 'undefined' ? window.getComputedStyle(textarea) : null;
|
|
98
|
+
const _taPaddingV = _taStyle ? (parseFloat(_taStyle.paddingTop) || 0) + (parseFloat(_taStyle.paddingBottom) || 0) : 0;
|
|
99
|
+
const hPx = Math.max(1, wrapper.offsetHeight - _taPaddingV);
|
|
96
100
|
const newW = Math.max(1, Math.round(wPx * viewResLocal / scaleX));
|
|
97
101
|
const newH = Math.max(1, Math.round(hPx * viewResLocal / scaleX));
|
|
98
102
|
const sizeReq = { objectId, size: null };
|
|
@@ -135,14 +139,22 @@ export function createTextEditorFinalize(controller, {
|
|
|
135
139
|
const worldLayerRef = controller.textEditor.world || (controller.app?.stage);
|
|
136
140
|
const scaleX = worldLayerRef?.scale?.x || 1;
|
|
137
141
|
const wPx = Math.max(1, wrapper.offsetWidth);
|
|
138
|
-
const
|
|
142
|
+
const _taStyle2 = typeof window !== 'undefined' ? window.getComputedStyle(textarea) : null;
|
|
143
|
+
const _taPaddingV2 = _taStyle2 ? (parseFloat(_taStyle2.paddingTop) || 0) + (parseFloat(_taStyle2.paddingBottom) || 0) : 0;
|
|
144
|
+
const hPx = Math.max(1, wrapper.offsetHeight - _taPaddingV2);
|
|
139
145
|
const wWorld = Math.max(1, Math.round(wPx * viewRes / scaleX));
|
|
140
146
|
const hWorld = Math.max(1, Math.round(hPx * viewRes / scaleX));
|
|
141
147
|
controller.eventBus.emit(Events.UI.ToolbarAction, {
|
|
142
148
|
type: objectType,
|
|
143
149
|
id: objectType,
|
|
144
150
|
position: { x: position.x, y: position.y },
|
|
145
|
-
properties: {
|
|
151
|
+
properties: {
|
|
152
|
+
content: value,
|
|
153
|
+
fontSize,
|
|
154
|
+
width: wWorld,
|
|
155
|
+
height: hWorld,
|
|
156
|
+
highlightColor: properties.highlightColor
|
|
157
|
+
},
|
|
146
158
|
});
|
|
147
159
|
} else {
|
|
148
160
|
if (isNewCreation) {
|
|
@@ -180,6 +192,20 @@ function _isInsideToolbarUI(target) {
|
|
|
180
192
|
return !!(target && typeof target.closest === 'function' && target.closest(UI_BLOCK_SELECTOR));
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Возвращает true, если элемент находится внутри панели свойств текста (форматирование).
|
|
197
|
+
*/
|
|
198
|
+
function _isInsideTextPropertiesPanel(target) {
|
|
199
|
+
return !!(target && typeof target.closest === 'function' && target.closest('.text-properties-layer'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Нативный color-input открывает системную палитру по mousedown; preventDefault его ломает.
|
|
204
|
+
*/
|
|
205
|
+
function _isNativeColorInput(target) {
|
|
206
|
+
return !!(target && target.tagName === 'INPUT' && target.type === 'color');
|
|
207
|
+
}
|
|
208
|
+
|
|
183
209
|
export function bindTextEditorInteractions(controller, {
|
|
184
210
|
textarea,
|
|
185
211
|
isNewCreation,
|
|
@@ -219,6 +245,15 @@ export function bindTextEditorInteractions(controller, {
|
|
|
219
245
|
if (e.target === textarea) return;
|
|
220
246
|
if (!_isInsideToolbarUI(e.target)) return;
|
|
221
247
|
|
|
248
|
+
// Клик по панели свойств текста (например, выбор цвета подсветки выделенного
|
|
249
|
+
// фрагмента): сохраняем фокус и выделение в textarea, но НЕ глушим сам click —
|
|
250
|
+
// он должен дойти до кнопки/дропдауна. Иначе blur закроет редактор, и выделение
|
|
251
|
+
// пропадёт ещё до выбора цвета.
|
|
252
|
+
if (_isInsideTextPropertiesPanel(e.target) && !_isNativeColorInput(e.target)) {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
222
257
|
const value = (textarea.value || '').trim();
|
|
223
258
|
if (!isNewCreation || value.length > 0) return;
|
|
224
259
|
|
|
@@ -292,14 +327,74 @@ export function bindTextEditorInteractions(controller, {
|
|
|
292
327
|
inputHandler = autoSize;
|
|
293
328
|
}
|
|
294
329
|
|
|
330
|
+
const syncBackdrop = () => {
|
|
331
|
+
if (!controller.textEditor) return;
|
|
332
|
+
const wrapper = controller.textEditor.wrapper;
|
|
333
|
+
const objectId = controller.textEditor.objectId;
|
|
334
|
+
const editorProps = controller.textEditor.properties || {};
|
|
335
|
+
|
|
336
|
+
const backdrop = wrapper ? wrapper.querySelector('.moodboard-text-backdrop') : null;
|
|
337
|
+
if (!backdrop) return;
|
|
338
|
+
|
|
339
|
+
// Актуальные highlights/links берём из _mb.properties объекта (обновляются
|
|
340
|
+
// через Events.Object.StateChanged), с fallback на свойства редактора.
|
|
341
|
+
let currentHighlights = editorProps.highlights || null;
|
|
342
|
+
let currentLinks = editorProps.links || null;
|
|
343
|
+
if (objectId) {
|
|
344
|
+
try {
|
|
345
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
346
|
+
controller.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
347
|
+
const props = pixiReq.pixiObject && pixiReq.pixiObject._mb
|
|
348
|
+
? pixiReq.pixiObject._mb.properties
|
|
349
|
+
: null;
|
|
350
|
+
if (props) {
|
|
351
|
+
if (Array.isArray(props.highlights)) currentHighlights = props.highlights;
|
|
352
|
+
if (Array.isArray(props.links)) currentLinks = props.links;
|
|
353
|
+
}
|
|
354
|
+
} catch (_) {}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const content = textarea.value;
|
|
358
|
+
backdrop.innerHTML = buildHtmlWithRanges(content, currentLinks, currentHighlights);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const caretUpdateHandler = () => {
|
|
362
|
+
if (controller.textEditor && controller.textEditor.caret) {
|
|
363
|
+
updateCustomCaret(textarea, controller.textEditor.caret);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
295
367
|
textarea.addEventListener('blur', blurHandler);
|
|
296
368
|
textarea.addEventListener('keydown', keydownHandler);
|
|
297
|
-
textarea.addEventListener('input',
|
|
369
|
+
textarea.addEventListener('input', (e) => {
|
|
370
|
+
inputHandler(e);
|
|
371
|
+
syncBackdrop();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
textarea.addEventListener('input', caretUpdateHandler);
|
|
375
|
+
textarea.addEventListener('keydown', () => setTimeout(caretUpdateHandler, 0));
|
|
376
|
+
textarea.addEventListener('keyup', caretUpdateHandler);
|
|
377
|
+
textarea.addEventListener('click', caretUpdateHandler);
|
|
378
|
+
textarea.addEventListener('focus', caretUpdateHandler);
|
|
379
|
+
document.addEventListener('selectionchange', caretUpdateHandler);
|
|
380
|
+
|
|
381
|
+
// Initial caret and backdrop update
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
caretUpdateHandler();
|
|
384
|
+
syncBackdrop();
|
|
385
|
+
}, 0);
|
|
298
386
|
|
|
299
387
|
const removeDomListeners = () => {
|
|
300
388
|
textarea.removeEventListener('blur', blurHandler);
|
|
301
389
|
textarea.removeEventListener('keydown', keydownHandler);
|
|
302
390
|
textarea.removeEventListener('input', inputHandler);
|
|
391
|
+
|
|
392
|
+
textarea.removeEventListener('input', caretUpdateHandler);
|
|
393
|
+
textarea.removeEventListener('keyup', caretUpdateHandler);
|
|
394
|
+
textarea.removeEventListener('click', caretUpdateHandler);
|
|
395
|
+
textarea.removeEventListener('focus', caretUpdateHandler);
|
|
396
|
+
document.removeEventListener('selectionchange', caretUpdateHandler);
|
|
397
|
+
|
|
303
398
|
document.removeEventListener('mousedown', mousedownCaptureHandler, true);
|
|
304
399
|
if (_pendingClickBlocker) {
|
|
305
400
|
document.removeEventListener('click', _pendingClickBlocker, true);
|
|
@@ -377,14 +472,18 @@ export function closeTextEditorFromState(controller, commit) {
|
|
|
377
472
|
}
|
|
378
473
|
return;
|
|
379
474
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
475
|
+
if (objectId == null) {
|
|
476
|
+
controller.eventBus.emit(Events.UI.ToolbarAction, {
|
|
477
|
+
type: objectType,
|
|
478
|
+
id: objectType,
|
|
479
|
+
position: { x: position.x, y: position.y },
|
|
480
|
+
properties: {
|
|
481
|
+
content: value,
|
|
482
|
+
fontSize: properties.fontSize,
|
|
483
|
+
highlightColor: properties.highlightColor
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
} else {
|
|
388
487
|
if (isNewCreation) {
|
|
389
488
|
const oldContent = typeof initialContent === 'string' ? initialContent : '';
|
|
390
489
|
controller.eventBus.emit(Events.Object.ContentChange, {
|
|
@@ -42,6 +42,12 @@ export function positionRegularTextEditor({
|
|
|
42
42
|
|
|
43
43
|
let baseLeftPx = screenPos.x;
|
|
44
44
|
let baseTopPx = screenPos.y;
|
|
45
|
+
// Computed padding-top of the static .mb-text element (e.g. 0.3em for list/markdown types).
|
|
46
|
+
// The static element top edge ≠ text glyph top: glyphs start paddingTop pixels lower.
|
|
47
|
+
// The editor wrapper must be shifted down by the same amount so text doesn't jump.
|
|
48
|
+
let staticPadTop = 0;
|
|
49
|
+
let baseWidthPx = null;
|
|
50
|
+
let baseHeightPx = null;
|
|
45
51
|
try {
|
|
46
52
|
if (!create && objectId && typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
47
53
|
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
@@ -50,6 +56,16 @@ export function positionRegularTextEditor({
|
|
|
50
56
|
const cssTop = parseFloat(el.style.top || 'NaN');
|
|
51
57
|
if (isFinite(cssLeft)) baseLeftPx = cssLeft;
|
|
52
58
|
if (isFinite(cssTop)) baseTopPx = cssTop;
|
|
59
|
+
const rect = el.getBoundingClientRect?.();
|
|
60
|
+
if (rect && isFinite(rect.width) && rect.width > 0) baseWidthPx = rect.width;
|
|
61
|
+
if (rect && isFinite(rect.height) && rect.height > 0) baseHeightPx = rect.height;
|
|
62
|
+
if (typeof window.getComputedStyle === 'function') {
|
|
63
|
+
try {
|
|
64
|
+
const cs = window.getComputedStyle(el);
|
|
65
|
+
const pt = parseFloat(cs.paddingTop);
|
|
66
|
+
if (isFinite(pt) && pt > 0) staticPadTop = pt;
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
}
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
} catch (_) {}
|
|
@@ -57,7 +73,7 @@ export function positionRegularTextEditor({
|
|
|
57
73
|
const leftPx = Math.round(baseLeftPx - padLeft);
|
|
58
74
|
const topPx = create
|
|
59
75
|
? Math.round(baseTopPx - padTop - (lineHeightPx / 2))
|
|
60
|
-
: Math.round(baseTopPx - padTop);
|
|
76
|
+
: Math.round(baseTopPx + staticPadTop - padTop + 1);
|
|
61
77
|
|
|
62
78
|
wrapper.style.left = `${leftPx}px`;
|
|
63
79
|
wrapper.style.top = `${topPx}px`;
|
|
@@ -70,6 +86,8 @@ export function positionRegularTextEditor({
|
|
|
70
86
|
lineHeightPx,
|
|
71
87
|
baseLeftPx,
|
|
72
88
|
baseTopPx,
|
|
89
|
+
baseWidthPx,
|
|
90
|
+
baseHeightPx,
|
|
73
91
|
};
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Events } from '../../../core/events/Events.js';
|
|
2
2
|
import { registerEditorListeners } from './InlineEditorListenersRegistry.js';
|
|
3
|
+
import { computeTextRightPadPx } from '../../../services/text/TextBoxMetrics.js';
|
|
3
4
|
|
|
4
5
|
export function createRegularTextAutoSize({
|
|
5
6
|
textarea,
|
|
@@ -13,7 +14,7 @@ export function createRegularTextAutoSize({
|
|
|
13
14
|
textarea.style.height = 'auto';
|
|
14
15
|
|
|
15
16
|
const fontPx = parseFloat(textarea.style.fontSize) || 16;
|
|
16
|
-
const rightPad =
|
|
17
|
+
const rightPad = computeTextRightPadPx(fontPx);
|
|
17
18
|
const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + rightPad));
|
|
18
19
|
const targetW = Math.round(Math.max(minWBound, naturalW));
|
|
19
20
|
textarea.style.width = `${targetW}px`;
|