@sequent-org/moodboard 1.4.57 → 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/UpdateContentCommand.js +48 -2
- 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 +55 -2
- 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 +86 -1
- 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 +68 -43
- package/src/tools/object-tools/selection/TransformInteractionController.js +7 -1
- package/src/ui/HtmlTextLayer.js +113 -70
- package/src/ui/TextPropertiesPanel.js +154 -7
- package/src/ui/handles/HandlesDomRenderer.js +2 -0
- package/src/ui/handles/HandlesInteractionController.js +40 -0
- package/src/ui/styles/workspace.css +64 -1
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +8 -0
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +29 -0
|
@@ -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,
|
|
@@ -190,6 +192,20 @@ function _isInsideToolbarUI(target) {
|
|
|
190
192
|
return !!(target && typeof target.closest === 'function' && target.closest(UI_BLOCK_SELECTOR));
|
|
191
193
|
}
|
|
192
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
|
+
|
|
193
209
|
export function bindTextEditorInteractions(controller, {
|
|
194
210
|
textarea,
|
|
195
211
|
isNewCreation,
|
|
@@ -229,6 +245,15 @@ export function bindTextEditorInteractions(controller, {
|
|
|
229
245
|
if (e.target === textarea) return;
|
|
230
246
|
if (!_isInsideToolbarUI(e.target)) return;
|
|
231
247
|
|
|
248
|
+
// Клик по панели свойств текста (например, выбор цвета подсветки выделенного
|
|
249
|
+
// фрагмента): сохраняем фокус и выделение в textarea, но НЕ глушим сам click —
|
|
250
|
+
// он должен дойти до кнопки/дропдауна. Иначе blur закроет редактор, и выделение
|
|
251
|
+
// пропадёт ещё до выбора цвета.
|
|
252
|
+
if (_isInsideTextPropertiesPanel(e.target) && !_isNativeColorInput(e.target)) {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
232
257
|
const value = (textarea.value || '').trim();
|
|
233
258
|
if (!isNewCreation || value.length > 0) return;
|
|
234
259
|
|
|
@@ -302,14 +327,74 @@ export function bindTextEditorInteractions(controller, {
|
|
|
302
327
|
inputHandler = autoSize;
|
|
303
328
|
}
|
|
304
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
|
+
|
|
305
367
|
textarea.addEventListener('blur', blurHandler);
|
|
306
368
|
textarea.addEventListener('keydown', keydownHandler);
|
|
307
|
-
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);
|
|
308
386
|
|
|
309
387
|
const removeDomListeners = () => {
|
|
310
388
|
textarea.removeEventListener('blur', blurHandler);
|
|
311
389
|
textarea.removeEventListener('keydown', keydownHandler);
|
|
312
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
|
+
|
|
313
398
|
document.removeEventListener('mousedown', mousedownCaptureHandler, true);
|
|
314
399
|
if (_pendingClickBlocker) {
|
|
315
400
|
document.removeEventListener('click', _pendingClickBlocker, true);
|
|
@@ -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`;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createTextEditorFinalize,
|
|
12
12
|
} from './TextEditorInteractionController.js';
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
updateGlobalTextEditorHandlesLayer,
|
|
15
15
|
hideNotePixiText,
|
|
16
16
|
hideStaticTextDuringEditing,
|
|
17
17
|
} from './TextEditorLifecycleRegistry.js';
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
} from './TextEditorDomFactory.js';
|
|
26
26
|
import { setupNoteInlineEditor } from './NoteInlineEditorController.js';
|
|
27
27
|
import { createRegularTextAutoSize } from './TextEditorSyncService.js';
|
|
28
|
+
import { TEXT_BOX_BOTTOM_PAD_PX } from '../../../services/text/TextBoxMetrics.js';
|
|
29
|
+
import { buildHtmlWithRanges } from '../../../ui/HtmlTextLayer.js';
|
|
28
30
|
|
|
29
31
|
export function openTextEditor(object, create = false) {
|
|
30
32
|
|
|
@@ -100,8 +102,6 @@ export function openTextEditor(object, create = false) {
|
|
|
100
102
|
} else {
|
|
101
103
|
this.eventBus.emit(Events.UI.TextEditStart, { objectId: objectId || null });
|
|
102
104
|
}
|
|
103
|
-
// Прячем глобальные HTML-ручки на время редактирования, чтобы не было второй рамки
|
|
104
|
-
hideGlobalTextEditorHandlesLayer();
|
|
105
105
|
// Подавляем пересоздание ручек при паразитных ResizeUpdate (тач double-tap):
|
|
106
106
|
// host.update() внутри HandlesEventBridge вызывается при каждом ResizeUpdate,
|
|
107
107
|
// _handlesSuppressed=true гарантирует что showBounds не создаст ручки поверх textarea.
|
|
@@ -110,6 +110,8 @@ export function openTextEditor(object, create = false) {
|
|
|
110
110
|
window.moodboardHtmlHandlesLayer._handlesSuppressed = true;
|
|
111
111
|
}
|
|
112
112
|
} catch (_) {}
|
|
113
|
+
// Обновляем глобальные HTML-ручки на время редактирования, чтобы осталась только рамка без точек
|
|
114
|
+
updateGlobalTextEditorHandlesLayer();
|
|
113
115
|
|
|
114
116
|
const app = this.app;
|
|
115
117
|
const world = app?.stage?.getChildByName && app.stage.getChildByName('worldLayer');
|
|
@@ -147,19 +149,19 @@ export function openTextEditor(object, create = false) {
|
|
|
147
149
|
}
|
|
148
150
|
// Используем только HTML-ручки во время редактирования текста
|
|
149
151
|
// Обертка для рамки + textarea + ручек
|
|
150
|
-
const wrapper = createTextEditorWrapper();
|
|
152
|
+
const { wrapper, caret } = createTextEditorWrapper();
|
|
151
153
|
|
|
152
154
|
// Базовые стили вынесены в CSS (.moodboard-text-editor)
|
|
153
155
|
|
|
154
156
|
const textarea = createTextEditorTextarea(content || '');
|
|
155
|
-
|
|
156
|
-
textarea.style.fontFamily = 'Caveat, Arial, cursive';
|
|
157
|
+
const backdrop = wrapper.querySelector('.moodboard-text-backdrop');
|
|
157
158
|
|
|
158
|
-
//
|
|
159
|
+
// Собираем текстовые параметры из статического .mb-text (для существующих объектов)
|
|
160
|
+
// или из properties. Единый источник гарантирует идентичный рендер в обоих режимах.
|
|
161
|
+
let resolvedFontFamily = properties.fontFamily || 'Caveat, Arial, cursive';
|
|
162
|
+
let resolvedColor = properties.color || '#111';
|
|
159
163
|
let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
|
|
160
|
-
|
|
161
|
-
lhInitial = Math.round(effectiveFontPx * properties.lineHeight);
|
|
162
|
-
}
|
|
164
|
+
|
|
163
165
|
try {
|
|
164
166
|
if (objectId) {
|
|
165
167
|
if (objectType === 'note') {
|
|
@@ -171,54 +173,54 @@ export function openTextEditor(object, create = false) {
|
|
|
171
173
|
const scaleY = Math.max(0.0001, Math.hypot(wt.c || 0, wt.d || 0));
|
|
172
174
|
const baseLH = parseFloat(inst.textField.style?.lineHeight || (fontSize * 1.2)) || (fontSize * 1.2);
|
|
173
175
|
lhInitial = Math.max(1, Math.round(baseLH * (scaleY / viewResEarly)));
|
|
176
|
+
const ff = (inst.textField.style && inst.textField.style.fontFamily)
|
|
177
|
+
|| (pixiReq.pixiObject._mb.properties && pixiReq.pixiObject._mb.properties.fontFamily)
|
|
178
|
+
|| null;
|
|
179
|
+
if (ff) resolvedFontFamily = ff;
|
|
174
180
|
}
|
|
175
181
|
} else if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
176
182
|
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
177
|
-
if (el) {
|
|
183
|
+
if (el && typeof window.getComputedStyle === 'function') {
|
|
178
184
|
const cs = window.getComputedStyle(el);
|
|
185
|
+
const f = parseFloat(cs.fontSize);
|
|
186
|
+
if (isFinite(f) && f > 0) effectiveFontPx = Math.round(f);
|
|
179
187
|
const lh = parseFloat(cs.lineHeight);
|
|
180
188
|
if (isFinite(lh) && lh > 0) lhInitial = Math.round(lh);
|
|
189
|
+
if (cs.fontFamily) resolvedFontFamily = cs.fontFamily;
|
|
190
|
+
if (cs.color) resolvedColor = cs.color;
|
|
181
191
|
}
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
194
|
} catch (_) {}
|
|
185
195
|
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
if (objectId) {
|
|
190
|
-
if (objectType === 'note') {
|
|
191
|
-
// Для записки читаем из PIXI-инстанса NoteObject
|
|
192
|
-
const pixiReq = { objectId, pixiObject: null };
|
|
193
|
-
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
194
|
-
const inst = pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.instance;
|
|
195
|
-
const ff = (inst && inst.textField && inst.textField.style && inst.textField.style.fontFamily)
|
|
196
|
-
|| (pixiReq.pixiObject && pixiReq.pixiObject._mb && pixiReq.pixiObject._mb.properties && pixiReq.pixiObject._mb.properties.fontFamily)
|
|
197
|
-
|| null;
|
|
198
|
-
if (ff) textarea.style.fontFamily = ff;
|
|
199
|
-
} else if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
|
|
200
|
-
// Для обычного текста читаем из HTML-элемента
|
|
201
|
-
const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
|
|
202
|
-
if (el) {
|
|
203
|
-
const cs = window.getComputedStyle(el);
|
|
204
|
-
const ff = cs && cs.fontFamily ? cs.fontFamily : null;
|
|
205
|
-
if (ff) textarea.style.fontFamily = ff;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
} catch (_) {}
|
|
196
|
+
// Применяем все текстовые параметры через единый applyTextStyles.
|
|
197
|
+
// textarea рендерит текст прозрачным (backdrop отвечает за видимые глифы),
|
|
198
|
+
// поэтому после applyTextStyles явно переопределяем color.
|
|
210
199
|
applyInitialTextEditorTextareaStyles(textarea, {
|
|
211
200
|
effectiveFontPx,
|
|
201
|
+
baseFontSizePx: fontSize,
|
|
202
|
+
fontFamily: resolvedFontFamily,
|
|
203
|
+
properties,
|
|
212
204
|
lineHeightPx: lhInitial,
|
|
213
205
|
});
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
textarea.style.color = 'transparent';
|
|
207
|
+
|
|
208
|
+
if (backdrop) {
|
|
209
|
+
applyInitialTextEditorTextareaStyles(backdrop, {
|
|
210
|
+
effectiveFontPx,
|
|
211
|
+
baseFontSizePx: fontSize,
|
|
212
|
+
fontFamily: resolvedFontFamily,
|
|
213
|
+
properties,
|
|
214
|
+
lineHeightPx: lhInitial,
|
|
215
|
+
});
|
|
216
|
+
backdrop.style.color = resolvedColor;
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
// Shape: текст по
|
|
219
|
+
// Shape: текст по центру — переопределяем textAlign после applyTextStyles
|
|
219
220
|
if (isShape) {
|
|
220
221
|
textarea.style.textAlign = 'center';
|
|
221
222
|
textarea.placeholder = '';
|
|
223
|
+
if (backdrop) backdrop.style.textAlign = 'center';
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
wrapper.appendChild(textarea);
|
|
@@ -310,6 +312,7 @@ export function openTextEditor(object, create = false) {
|
|
|
310
312
|
|
|
311
313
|
// Для записок позиционируем редактор внутри записки
|
|
312
314
|
let updateNoteEditor = null;
|
|
315
|
+
let regularEditorVisualBox = null;
|
|
313
316
|
if (objectType === 'note') {
|
|
314
317
|
const noteSetup = setupNoteInlineEditor(this, {
|
|
315
318
|
objectId,
|
|
@@ -356,6 +359,16 @@ export function openTextEditor(object, create = false) {
|
|
|
356
359
|
textarea.style.width = `${shapeCssW}px`;
|
|
357
360
|
textarea.style.height = `${shapeCssH}px`;
|
|
358
361
|
textarea.style.boxSizing = 'border-box';
|
|
362
|
+
|
|
363
|
+
if (backdrop) {
|
|
364
|
+
backdrop.style.width = `${shapeCssW}px`;
|
|
365
|
+
backdrop.style.height = `${shapeCssH}px`;
|
|
366
|
+
backdrop.style.boxSizing = 'border-box';
|
|
367
|
+
backdrop.style.display = 'block';
|
|
368
|
+
backdrop.style.paddingTop = `${paddingTopShape}px`;
|
|
369
|
+
backdrop.style.paddingBottom = '0px';
|
|
370
|
+
}
|
|
371
|
+
|
|
359
372
|
// display:block убирает baseline-смещение inline-block textarea внутри wrapper'а
|
|
360
373
|
// (wrapper наследует line-height ~24px от body, из-за чего на сильном отдалении
|
|
361
374
|
// textarea съезжает вниз от фигуры на фиксированные ~11px независимо от зума).
|
|
@@ -376,6 +389,8 @@ export function openTextEditor(object, create = false) {
|
|
|
376
389
|
lineHeightPx,
|
|
377
390
|
baseLeftPx,
|
|
378
391
|
baseTopPx,
|
|
392
|
+
baseWidthPx,
|
|
393
|
+
baseHeightPx,
|
|
379
394
|
} = positionRegularTextEditor({
|
|
380
395
|
create,
|
|
381
396
|
objectId,
|
|
@@ -383,6 +398,12 @@ export function openTextEditor(object, create = false) {
|
|
|
383
398
|
textarea,
|
|
384
399
|
wrapper,
|
|
385
400
|
});
|
|
401
|
+
if (!create && baseWidthPx && baseHeightPx) {
|
|
402
|
+
regularEditorVisualBox = {
|
|
403
|
+
width: Math.max(1, Math.round(baseWidthPx)),
|
|
404
|
+
height: Math.max(1, Math.round(baseHeightPx)),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
386
407
|
// Сохраняем CSS-позицию редактора для точной синхронизации при закрытии
|
|
387
408
|
this.textEditor._cssLeftPx = leftPx;
|
|
388
409
|
this.textEditor._cssTopPx = topPx;
|
|
@@ -420,14 +441,16 @@ export function openTextEditor(object, create = false) {
|
|
|
420
441
|
// Синхронизируем стартовый размер шрифта textarea с текущим зумом (как HtmlTextLayer)
|
|
421
442
|
// Используем ранее вычисленный effectiveFontPx (до вставки в DOM), если он есть в замыкании
|
|
422
443
|
textarea.style.fontSize = `${effectiveFontPx}px`;
|
|
423
|
-
const initialWpx =
|
|
424
|
-
|
|
444
|
+
const initialWpx = regularEditorVisualBox?.width
|
|
445
|
+
|| (initialSize ? Math.max(1, (initialSize.width || 0) * s / viewRes) : null);
|
|
446
|
+
const initialHpx = regularEditorVisualBox?.height
|
|
447
|
+
|| (initialSize ? Math.max(1, (initialSize.height || 0) * s / viewRes) : null);
|
|
425
448
|
|
|
426
449
|
// Определяем минимальные границы для всех типов объектов
|
|
427
450
|
let minWBound = initialWpx || 120; // базово близко к призраку
|
|
428
451
|
let minHBound = effectiveFontPx; // базовая высота
|
|
429
452
|
// Уменьшаем визуальный нижний запас, который браузеры добавляют к textarea
|
|
430
|
-
const BASELINE_FIX =
|
|
453
|
+
const BASELINE_FIX = TEXT_BOX_BOTTOM_PAD_PX; // px
|
|
431
454
|
if (!isNote) {
|
|
432
455
|
minHBound = Math.max(1, effectiveFontPx - BASELINE_FIX);
|
|
433
456
|
}
|
|
@@ -484,8 +507,9 @@ export function openTextEditor(object, create = false) {
|
|
|
484
507
|
onSizeChange: syncRegularTextSizeToObject,
|
|
485
508
|
});
|
|
486
509
|
|
|
487
|
-
//
|
|
488
|
-
|
|
510
|
+
// Для существующего текста стартовый размер уже взят из видимого DOM-бокса:
|
|
511
|
+
// пересчёт нужен только после фактического ввода, иначе рамка прыгает при входе в редактор.
|
|
512
|
+
if (!isNote && !isShape && create) {
|
|
489
513
|
autoSize();
|
|
490
514
|
}
|
|
491
515
|
textarea.focus();
|
|
@@ -500,6 +524,7 @@ export function openTextEditor(object, create = false) {
|
|
|
500
524
|
objectId,
|
|
501
525
|
textarea,
|
|
502
526
|
wrapper,
|
|
527
|
+
caret,
|
|
503
528
|
world: this.textEditor.world,
|
|
504
529
|
position,
|
|
505
530
|
properties: { fontSize, highlightColor: properties.highlightColor },
|
|
@@ -2,6 +2,11 @@ import { Events } from '../../../core/events/Events.js';
|
|
|
2
2
|
import { tryStartAltCloneDuringDrag, resetCloneStateAfterDragEnd } from './CloneFlowController.js';
|
|
3
3
|
|
|
4
4
|
export function handleObjectSelect(objectId, event) {
|
|
5
|
+
// Запоминаем ДО clearSelection: объект был единственным выделенным?
|
|
6
|
+
const wasAlreadySingleSelected = !this.isMultiSelect &&
|
|
7
|
+
this.selection.size() === 1 &&
|
|
8
|
+
this.selection.has(objectId);
|
|
9
|
+
|
|
5
10
|
if (!this.isMultiSelect) {
|
|
6
11
|
this.clearSelection();
|
|
7
12
|
}
|
|
@@ -19,7 +24,8 @@ export function handleObjectSelect(objectId, event) {
|
|
|
19
24
|
if (this.selection.size() > 1) {
|
|
20
25
|
this._pendingDrag = { isGroup: true, objectId: null, downX: event.x, downY: event.y, event };
|
|
21
26
|
} else {
|
|
22
|
-
|
|
27
|
+
// editCandidate=true если это повторный клик по уже единственному выделенному объекту
|
|
28
|
+
this._pendingDrag = { isGroup: false, objectId, downX: event.x, downY: event.y, event, editCandidate: wasAlreadySingleSelected };
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
31
|
}
|