@sequent-org/moodboard 1.4.57 → 1.4.59
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/ApiClient.js +41 -2
- package/src/core/PixiEngine.js +10 -1
- package/src/core/commands/UpdateContentCommand.js +48 -2
- package/src/core/events/Events.js +1 -0
- package/src/core/flows/ClipboardFlow.js +48 -0
- package/src/objects/ObjectFactory.js +2 -0
- package/src/objects/VideoObject.js +171 -0
- 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/chat/ChatWindow.js +39 -8
- package/src/ui/comments/CommentThreadPopover.js +3 -1
- 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 * as PIXI from 'pixi.js';
|
|
2
2
|
import { Events } from '../../../core/events/Events.js';
|
|
3
|
+
import { updateCustomCaret } from './TextEditorCaretService.js';
|
|
4
|
+
import { buildHtmlWithRanges } from '../../../ui/HtmlTextLayer.js';
|
|
3
5
|
import {
|
|
4
6
|
createTextEditorTextarea,
|
|
5
7
|
createTextEditorWrapper,
|
|
@@ -196,10 +198,14 @@ export function openMindmapEditor(object, create = false) {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
const wrapper = createTextEditorWrapper();
|
|
201
|
+
const { wrapper, caret, backdrop } = createTextEditorWrapper();
|
|
200
202
|
const textarea = createTextEditorTextarea(properties.content || '');
|
|
201
203
|
wrapper.classList.add('moodboard-text-editor--mindmap-debug');
|
|
202
204
|
textarea.classList.add('moodboard-text-input--mindmap-debug');
|
|
205
|
+
|
|
206
|
+
if (backdrop) {
|
|
207
|
+
backdrop.classList.add('moodboard-text-backdrop--mindmap-debug');
|
|
208
|
+
}
|
|
203
209
|
textarea.placeholder = 'Напишите что-нибудь';
|
|
204
210
|
textarea.style.fontFamily = properties.fontFamily || 'Roboto, Arial, sans-serif';
|
|
205
211
|
textarea.style.fontWeight = '400';
|
|
@@ -261,15 +267,33 @@ export function openMindmapEditor(object, create = false) {
|
|
|
261
267
|
|
|
262
268
|
if (typeof window.getComputedStyle === 'function') {
|
|
263
269
|
const staticStyle = window.getComputedStyle(staticTextEl);
|
|
264
|
-
if (staticStyle?.fontFamily)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (staticStyle?.
|
|
269
|
-
|
|
270
|
+
if (staticStyle?.fontFamily) {
|
|
271
|
+
textarea.style.fontFamily = staticStyle.fontFamily;
|
|
272
|
+
if (backdrop) backdrop.style.fontFamily = staticStyle.fontFamily;
|
|
273
|
+
}
|
|
274
|
+
if (staticStyle?.fontSize) {
|
|
275
|
+
textarea.style.fontSize = staticStyle.fontSize;
|
|
276
|
+
if (backdrop) backdrop.style.fontSize = staticStyle.fontSize;
|
|
277
|
+
}
|
|
278
|
+
if (staticStyle?.lineHeight) {
|
|
279
|
+
textarea.style.lineHeight = staticStyle.lineHeight;
|
|
280
|
+
if (backdrop) backdrop.style.lineHeight = staticStyle.lineHeight;
|
|
281
|
+
}
|
|
282
|
+
if (staticStyle?.color) {
|
|
283
|
+
if (backdrop) backdrop.style.color = staticStyle.color;
|
|
284
|
+
}
|
|
285
|
+
if (staticStyle?.paddingLeft) {
|
|
286
|
+
textarea.style.paddingLeft = staticStyle.paddingLeft;
|
|
287
|
+
if (backdrop) backdrop.style.paddingLeft = staticStyle.paddingLeft;
|
|
288
|
+
}
|
|
289
|
+
if (staticStyle?.paddingRight) {
|
|
290
|
+
textarea.style.paddingRight = staticStyle.paddingRight;
|
|
291
|
+
if (backdrop) backdrop.style.paddingRight = staticStyle.paddingRight;
|
|
292
|
+
}
|
|
270
293
|
}
|
|
271
294
|
} else if (properties.fontSize) {
|
|
272
295
|
textarea.style.fontSize = `${properties.fontSize}px`;
|
|
296
|
+
if (backdrop) backdrop.style.fontSize = `${properties.fontSize}px`;
|
|
273
297
|
}
|
|
274
298
|
|
|
275
299
|
wrapper.style.left = `${targetLeft}px`;
|
|
@@ -285,6 +309,14 @@ export function openMindmapEditor(object, create = false) {
|
|
|
285
309
|
textarea.style.width = '100%';
|
|
286
310
|
textarea.style.height = 'auto';
|
|
287
311
|
textarea.style.minHeight = '0';
|
|
312
|
+
|
|
313
|
+
if (backdrop) {
|
|
314
|
+
backdrop.style.width = '100%';
|
|
315
|
+
backdrop.style.height = '100%';
|
|
316
|
+
backdrop.style.display = 'flex';
|
|
317
|
+
backdrop.style.alignItems = 'center';
|
|
318
|
+
backdrop.style.justifyContent = 'center';
|
|
319
|
+
}
|
|
288
320
|
|
|
289
321
|
const initialCssWidth = targetWidth;
|
|
290
322
|
const initialCssHeight = targetHeight;
|
|
@@ -734,14 +766,67 @@ export function openMindmapEditor(object, create = false) {
|
|
|
734
766
|
}
|
|
735
767
|
};
|
|
736
768
|
|
|
769
|
+
const caretUpdateHandler = () => {
|
|
770
|
+
if (this.textEditor && this.textEditor.caret) {
|
|
771
|
+
updateCustomCaret(textarea, this.textEditor.caret);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const syncBackdrop = () => {
|
|
776
|
+
if (!backdrop) return;
|
|
777
|
+
let currentHighlights = properties.highlights || null;
|
|
778
|
+
let currentLinks = properties.links || null;
|
|
779
|
+
if (objectId) {
|
|
780
|
+
try {
|
|
781
|
+
const pixiReq = { objectId, pixiObject: null };
|
|
782
|
+
this.eventBus.emit(Events.Tool.GetObjectPixi, pixiReq);
|
|
783
|
+
const props = pixiReq.pixiObject && pixiReq.pixiObject._mb
|
|
784
|
+
? pixiReq.pixiObject._mb.properties
|
|
785
|
+
: null;
|
|
786
|
+
if (props) {
|
|
787
|
+
if (Array.isArray(props.highlights)) currentHighlights = props.highlights;
|
|
788
|
+
if (Array.isArray(props.links)) currentLinks = props.links;
|
|
789
|
+
}
|
|
790
|
+
} catch (_) {}
|
|
791
|
+
}
|
|
792
|
+
const content = textarea.value;
|
|
793
|
+
backdrop.innerHTML = buildHtmlWithRanges(content, currentLinks, currentHighlights);
|
|
794
|
+
};
|
|
795
|
+
|
|
737
796
|
textarea.addEventListener('blur', onBlur);
|
|
738
797
|
textarea.addEventListener('keydown', onKeyDown);
|
|
798
|
+
|
|
799
|
+
textarea.addEventListener('input', (e) => {
|
|
800
|
+
caretUpdateHandler(e);
|
|
801
|
+
syncBackdrop();
|
|
802
|
+
});
|
|
803
|
+
textarea.addEventListener('keydown', () => setTimeout(caretUpdateHandler, 0));
|
|
804
|
+
textarea.addEventListener('keyup', caretUpdateHandler);
|
|
805
|
+
textarea.addEventListener('click', caretUpdateHandler);
|
|
806
|
+
textarea.addEventListener('focus', caretUpdateHandler);
|
|
807
|
+
document.addEventListener('selectionchange', caretUpdateHandler);
|
|
808
|
+
|
|
809
|
+
setTimeout(() => {
|
|
810
|
+
caretUpdateHandler();
|
|
811
|
+
syncBackdrop();
|
|
812
|
+
}, 0);
|
|
813
|
+
|
|
814
|
+
const removeDomListeners = () => {
|
|
815
|
+
textarea.removeEventListener('blur', onBlur);
|
|
816
|
+
textarea.removeEventListener('keydown', onKeyDown);
|
|
817
|
+
textarea.removeEventListener('input', caretUpdateHandler);
|
|
818
|
+
textarea.removeEventListener('keyup', caretUpdateHandler);
|
|
819
|
+
textarea.removeEventListener('click', caretUpdateHandler);
|
|
820
|
+
textarea.removeEventListener('focus', caretUpdateHandler);
|
|
821
|
+
document.removeEventListener('selectionchange', caretUpdateHandler);
|
|
822
|
+
};
|
|
739
823
|
|
|
740
824
|
this.textEditor = {
|
|
741
825
|
active: true,
|
|
742
826
|
objectId,
|
|
743
827
|
textarea,
|
|
744
828
|
wrapper,
|
|
829
|
+
caret,
|
|
745
830
|
world,
|
|
746
831
|
position,
|
|
747
832
|
properties: { ...properties },
|
|
@@ -749,6 +834,7 @@ export function openMindmapEditor(object, create = false) {
|
|
|
749
834
|
isResizing: false,
|
|
750
835
|
_finalize: finalize,
|
|
751
836
|
_listeners: editorListeners,
|
|
837
|
+
_removeDomListeners: removeDomListeners,
|
|
752
838
|
};
|
|
753
839
|
|
|
754
840
|
textarea.focus();
|
|
@@ -762,6 +848,11 @@ export function openMindmapEditor(object, create = false) {
|
|
|
762
848
|
|
|
763
849
|
export function closeMindmapEditor(commit) {
|
|
764
850
|
if (!this.textEditor?.active || this.textEditor.objectType !== 'mindmap') return;
|
|
851
|
+
|
|
852
|
+
if (typeof this.textEditor._removeDomListeners === 'function') {
|
|
853
|
+
try { this.textEditor._removeDomListeners(); } catch (_) {}
|
|
854
|
+
}
|
|
855
|
+
|
|
765
856
|
const finalize = this.textEditor._finalize;
|
|
766
857
|
if (typeof finalize === 'function') {
|
|
767
858
|
finalize(commit);
|
|
@@ -130,8 +130,8 @@ export function onMouseDown(event) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
// Откладываем drag до превышения порога
|
|
134
|
-
this._pendingDrag = { isGroup: false, objectId: selId, downX: event.x, downY: event.y, event };
|
|
133
|
+
// Откладываем drag до превышения порога смещения; editCandidate — уже выделен
|
|
134
|
+
this._pendingDrag = { isGroup: false, objectId: selId, downX: event.x, downY: event.y, event, editCandidate: true };
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -194,7 +194,37 @@ export function onMouseMove(event) {
|
|
|
194
194
|
|
|
195
195
|
export function onMouseUp(event) {
|
|
196
196
|
if (this._pendingDrag) {
|
|
197
|
+
const pending = this._pendingDrag;
|
|
197
198
|
this._pendingDrag = null;
|
|
199
|
+
|
|
200
|
+
// Повторный клик по уже выделенному тексту (без перехода в drag) —
|
|
201
|
+
// вход в режим редактирования, как при двойном клике.
|
|
202
|
+
if (pending.editCandidate && !pending.isGroup && pending.objectId) {
|
|
203
|
+
const req = { objectId: pending.objectId, pixiObject: null };
|
|
204
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
205
|
+
const pix = req.pixiObject;
|
|
206
|
+
const isText = !!(pix && pix._mb && pix._mb.type === 'text');
|
|
207
|
+
if (isText) {
|
|
208
|
+
const posData = { objectId: pending.objectId, position: null };
|
|
209
|
+
this.emit(Events.Tool.GetObjectPosition, posData);
|
|
210
|
+
const textProperties = pix._mb?.properties || {};
|
|
211
|
+
const textContent = textProperties.content || '';
|
|
212
|
+
this.emit(Events.Tool.ObjectEdit, {
|
|
213
|
+
id: pending.objectId,
|
|
214
|
+
type: 'text',
|
|
215
|
+
position: posData.position,
|
|
216
|
+
properties: {
|
|
217
|
+
...pickTextEditorProperties(textProperties),
|
|
218
|
+
content: textContent,
|
|
219
|
+
},
|
|
220
|
+
caretClick: {
|
|
221
|
+
clientX: event?.originalEvent?.clientX,
|
|
222
|
+
clientY: event?.originalEvent?.clientY,
|
|
223
|
+
},
|
|
224
|
+
create: false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
198
228
|
return;
|
|
199
229
|
}
|
|
200
230
|
if (this.isResizing || this.isGroupResizing) {
|
|
@@ -332,6 +362,29 @@ export function onContextMenu(event) {
|
|
|
332
362
|
} else if (this.selection.size() > 1) {
|
|
333
363
|
context = 'group';
|
|
334
364
|
}
|
|
365
|
+
|
|
366
|
+
// Для одиночного текстового объекта — открываем tpp-more-dropdown вместо moodboard-contextmenu
|
|
367
|
+
if (context === 'object' && targetId) {
|
|
368
|
+
const req = { objectId: targetId, pixiObject: null };
|
|
369
|
+
this.emit(Events.Tool.GetObjectPixi, req);
|
|
370
|
+
const pix = req.pixiObject;
|
|
371
|
+
if (pix && pix._mb && pix._mb.type === 'text') {
|
|
372
|
+
// Если объект ещё не выделен — выделяем его, чтобы панель свойств текста появилась в DOM
|
|
373
|
+
if (!this.selection.has(targetId)) {
|
|
374
|
+
this.setSelection([targetId]);
|
|
375
|
+
}
|
|
376
|
+
// Открываем дропдаун «Ещё» из TextPropertiesPanel в следующем кадре
|
|
377
|
+
// (панель рендерится асинхронно после выделения)
|
|
378
|
+
requestAnimationFrame(() => {
|
|
379
|
+
const moreBtn = document.getElementById('tpp-btn-more');
|
|
380
|
+
if (moreBtn) {
|
|
381
|
+
moreBtn.click();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
335
388
|
// Сообщаем ядру/UI, что нужно показать контекстное меню (пока без пунктов)
|
|
336
389
|
this.emit(Events.Tool.ContextMenuShow, { x: event.x, y: event.y, context, targetId });
|
|
337
390
|
}
|
|
@@ -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,
|
|
@@ -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`;
|