@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
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
applyTextAppearanceToDom,
|
|
13
13
|
buildBackgroundColorUpdate,
|
|
14
|
-
buildHighlightColorUpdate,
|
|
15
14
|
buildFontFamilyUpdate,
|
|
16
15
|
buildFontSizeUpdate,
|
|
17
16
|
buildMarkdownUpdate,
|
|
@@ -62,7 +61,7 @@ export class TextPropertiesPanel {
|
|
|
62
61
|
position: 'absolute',
|
|
63
62
|
inset: '0',
|
|
64
63
|
pointerEvents: 'none',
|
|
65
|
-
zIndex:
|
|
64
|
+
zIndex: 10050,
|
|
66
65
|
});
|
|
67
66
|
this.container.appendChild(this.layer);
|
|
68
67
|
|
|
@@ -122,6 +121,8 @@ export class TextPropertiesPanel {
|
|
|
122
121
|
|
|
123
122
|
if (this.panel) {
|
|
124
123
|
this.panel.style.display = 'none';
|
|
124
|
+
this.panel.querySelectorAll('.tpp-more-dropdown.is-open').forEach((el) => el.classList.remove('is-open'));
|
|
125
|
+
this.panel.querySelectorAll('.ipp-btn.is-active').forEach((el) => el.classList.remove('is-active'));
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
this._hideColorDropdown();
|
|
@@ -213,11 +214,78 @@ export class TextPropertiesPanel {
|
|
|
213
214
|
this._updateTextAppearance(this.currentId, { fontSize });
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
_applyFormatToSelection(formatType, value) {
|
|
218
|
+
const activeEditor = document.querySelector('.moodboard-text-input');
|
|
219
|
+
if (!activeEditor || activeEditor.selectionStart === activeEditor.selectionEnd) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const start = activeEditor.selectionStart;
|
|
224
|
+
const end = activeEditor.selectionEnd;
|
|
225
|
+
const text = activeEditor.value;
|
|
226
|
+
const selected = text.substring(start, end);
|
|
227
|
+
const before = text.substring(0, start);
|
|
228
|
+
const after = text.substring(end);
|
|
229
|
+
|
|
230
|
+
let newSelected = selected;
|
|
231
|
+
|
|
232
|
+
const toggleWrap = (str, prefix, suffix = prefix) => {
|
|
233
|
+
if (str.startsWith(prefix) && str.endsWith(suffix)) {
|
|
234
|
+
return str.substring(prefix.length, str.length - suffix.length);
|
|
235
|
+
}
|
|
236
|
+
return `${prefix}${str}${suffix}`;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
switch (formatType) {
|
|
240
|
+
case 'color': {
|
|
241
|
+
const cleanSelected = selected.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, '');
|
|
242
|
+
newSelected = `<span style="color: ${value}">${cleanSelected}</span>`;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'bold':
|
|
246
|
+
newSelected = toggleWrap(selected, '**');
|
|
247
|
+
break;
|
|
248
|
+
case 'italic':
|
|
249
|
+
newSelected = toggleWrap(selected, '*');
|
|
250
|
+
break;
|
|
251
|
+
case 'underline':
|
|
252
|
+
newSelected = toggleWrap(selected, '<u>', '</u>');
|
|
253
|
+
break;
|
|
254
|
+
case 'strikethrough':
|
|
255
|
+
newSelected = toggleWrap(selected, '~~');
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
activeEditor.value = before + newSelected + after;
|
|
260
|
+
|
|
261
|
+
// Restore selection
|
|
262
|
+
activeEditor.selectionStart = start;
|
|
263
|
+
activeEditor.selectionEnd = start + newSelected.length;
|
|
264
|
+
|
|
265
|
+
// Force markdown mode so HTML/MD tags are rendered
|
|
266
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
267
|
+
objectId: this.currentId,
|
|
268
|
+
updates: buildMarkdownUpdate(true),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Update the object's content
|
|
272
|
+
this.eventBus.emit(Events.Tool.UpdateObjectContent, {
|
|
273
|
+
objectId: this.currentId,
|
|
274
|
+
content: activeEditor.value
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
216
280
|
_changeTextColor(color) {
|
|
217
281
|
if (!this.currentId) {
|
|
218
282
|
return;
|
|
219
283
|
}
|
|
220
284
|
|
|
285
|
+
if (this._applyFormatToSelection('color', color)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
221
289
|
this.eventBus.emit(Events.Object.StateChanged, {
|
|
222
290
|
objectId: this.currentId,
|
|
223
291
|
updates: buildTextColorUpdate(color),
|
|
@@ -244,12 +312,39 @@ export class TextPropertiesPanel {
|
|
|
244
312
|
return;
|
|
245
313
|
}
|
|
246
314
|
|
|
247
|
-
this.eventBus
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
315
|
+
const props = getObjectProperties(this.eventBus, this.currentId);
|
|
316
|
+
|
|
317
|
+
// Получаем актуальный контент (из живого редактора, если он есть)
|
|
318
|
+
let contentLength = 0;
|
|
319
|
+
const editor = document.querySelector('.moodboard-text-input');
|
|
320
|
+
if (editor) {
|
|
321
|
+
contentLength = editor.value.length;
|
|
322
|
+
} else {
|
|
323
|
+
contentLength = (props?.content ?? '').length;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const sel = this._savedTextSelection;
|
|
327
|
+
// С выделением — диапазон выделения; без выделения — весь текст (как у ссылок),
|
|
328
|
+
// box-фон всего поля не используется.
|
|
329
|
+
const start = (sel && sel.end > sel.start) ? sel.start : 0;
|
|
330
|
+
const end = (sel && sel.end > sel.start) ? sel.end : contentLength;
|
|
251
331
|
|
|
252
|
-
this.
|
|
332
|
+
this._addHighlight(highlightColor, start, end, this.currentId);
|
|
333
|
+
this._savedTextSelection = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Снимок выделения активного текстового редактора. Вызывается на mousedown по
|
|
338
|
+
* UI подсветки ДО того, как фокус уйдёт из textarea (особенно перед нативным
|
|
339
|
+
* color-input, который гарантированно сбрасывает выделение).
|
|
340
|
+
*/
|
|
341
|
+
_snapshotTextSelection() {
|
|
342
|
+
const editor = document.querySelector('.moodboard-text-input');
|
|
343
|
+
if (editor && typeof editor.selectionStart === 'number' && typeof editor.selectionEnd === 'number') {
|
|
344
|
+
this._savedTextSelection = { start: editor.selectionStart, end: editor.selectionEnd };
|
|
345
|
+
} else {
|
|
346
|
+
this._savedTextSelection = null;
|
|
347
|
+
}
|
|
253
348
|
}
|
|
254
349
|
|
|
255
350
|
_toggleFormat(prop) {
|
|
@@ -257,6 +352,10 @@ export class TextPropertiesPanel {
|
|
|
257
352
|
return;
|
|
258
353
|
}
|
|
259
354
|
|
|
355
|
+
if (this._applyFormatToSelection(prop, null)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
260
359
|
const properties = getObjectProperties(this.eventBus, this.currentId);
|
|
261
360
|
const newValue = !(properties ? Boolean(properties[prop]) : false);
|
|
262
361
|
|
|
@@ -360,6 +459,54 @@ export class TextPropertiesPanel {
|
|
|
360
459
|
this._updateTextAppearance(targetId, { links: newLinks });
|
|
361
460
|
}
|
|
362
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Добавляет/убирает цветовой диапазон подсветки в properties.highlights.
|
|
464
|
+
* Не вставляет теги в текст — textarea остаётся чистой.
|
|
465
|
+
* @param {string} color — CSS-цвет или 'transparent' (удалить подсветку в диапазоне)
|
|
466
|
+
* @param {number} start — начало диапазона (selectionStart)
|
|
467
|
+
* @param {number} end — конец диапазона (selectionEnd)
|
|
468
|
+
* @param {string} [objectId] — если не задан, используется this.currentId
|
|
469
|
+
*/
|
|
470
|
+
_addHighlight(color, start, end, objectId) {
|
|
471
|
+
const targetId = objectId || this.currentId;
|
|
472
|
+
if (!targetId) return;
|
|
473
|
+
const props = getObjectProperties(this.eventBus, targetId);
|
|
474
|
+
|
|
475
|
+
let contentLength = 0;
|
|
476
|
+
const editor = document.querySelector('.moodboard-text-input');
|
|
477
|
+
if (editor) {
|
|
478
|
+
contentLength = editor.value.length;
|
|
479
|
+
} else {
|
|
480
|
+
contentLength = (props?.content ?? '').length;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const safeEnd = Math.min(end, contentLength);
|
|
484
|
+
const safeStart = Math.min(start, safeEnd);
|
|
485
|
+
if (safeStart >= safeEnd) return;
|
|
486
|
+
|
|
487
|
+
const oldHighlights = Array.isArray(props?.highlights) ? props.highlights : [];
|
|
488
|
+
const filtered = oldHighlights.filter(h => h.end <= safeStart || h.start >= safeEnd);
|
|
489
|
+
const newHighlights = color === 'transparent'
|
|
490
|
+
? filtered
|
|
491
|
+
: [...filtered, { start: safeStart, end: safeEnd, color }]
|
|
492
|
+
.sort((a, b) => a.start - b.start);
|
|
493
|
+
|
|
494
|
+
// Если редактор открыт, синхронизируем его текущее значение в state вместе с
|
|
495
|
+
// подсветкой. Иначе при коммите UpdateContentCommand._applyContent вычисляет
|
|
496
|
+
// _adjustHighlights(oldText = stale initialContent, newText = editor.value) и
|
|
497
|
+
// сдвигает/удаляет только что добавленные диапазоны.
|
|
498
|
+
const propertiesUpdate = { highlights: newHighlights };
|
|
499
|
+
if (editor) {
|
|
500
|
+
propertiesUpdate.content = editor.value;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
504
|
+
objectId: targetId,
|
|
505
|
+
updates: { properties: propertiesUpdate },
|
|
506
|
+
});
|
|
507
|
+
this._updateTextAppearance(targetId, { highlights: newHighlights });
|
|
508
|
+
}
|
|
509
|
+
|
|
363
510
|
_updateTextAppearance(objectId, properties) {
|
|
364
511
|
applyTextAppearanceToDom(objectId, properties);
|
|
365
512
|
syncPixiTextProperties(this.eventBus, objectId, properties);
|
|
@@ -972,6 +972,43 @@ export class ChatWindow {
|
|
|
972
972
|
});
|
|
973
973
|
}
|
|
974
974
|
|
|
975
|
+
_placeGeneratedVideoOnBoard(result, skeletonWorld = null) {
|
|
976
|
+
if (!this._boardCore?.eventBus) return;
|
|
977
|
+
const src = result?.videoUrl;
|
|
978
|
+
if (!src) {
|
|
979
|
+
if (this._refs?.errorBlock) {
|
|
980
|
+
this._refs.errorBlock.textContent = 'Видео сгенерировано, но ссылка отсутствует.';
|
|
981
|
+
this._refs.errorBlock.classList.add('is-visible');
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const world = this._boardCore.pixi?.worldLayer || this._boardCore.pixi?.app?.stage;
|
|
987
|
+
const s = world?.scale?.x || 1;
|
|
988
|
+
|
|
989
|
+
// Приоритет — мировая точка скелетона: видео приземляется туда, где была заглушка,
|
|
990
|
+
// даже если пользователь панорамировал/зумил во время генерации.
|
|
991
|
+
let x;
|
|
992
|
+
let y;
|
|
993
|
+
if (skeletonWorld) {
|
|
994
|
+
x = Math.round(skeletonWorld.x * s + (world?.x || 0));
|
|
995
|
+
y = Math.round(skeletonWorld.y * s + (world?.y || 0));
|
|
996
|
+
} else {
|
|
997
|
+
const composerRect = this._refs?.composer?.getBoundingClientRect?.();
|
|
998
|
+
x = composerRect ? Math.round(composerRect.left + composerRect.width / 2) : 400;
|
|
999
|
+
y = composerRect ? Math.round(composerRect.top - 200) : 200;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
this._boardCore.eventBus.emit(Events.UI.PasteVideoAt, {
|
|
1003
|
+
x,
|
|
1004
|
+
y,
|
|
1005
|
+
src,
|
|
1006
|
+
mimeType: result?.mimeType || 'video/mp4',
|
|
1007
|
+
poster: null,
|
|
1008
|
+
skipUpload: true
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
975
1012
|
_clearBoardSelection() {
|
|
976
1013
|
if (typeof this._boardCore?.selectTool?.clearSelection === 'function') {
|
|
977
1014
|
this._boardCore.selectTool.clearSelection();
|
|
@@ -1227,15 +1264,9 @@ export class ChatWindow {
|
|
|
1227
1264
|
}
|
|
1228
1265
|
if (state.status === 'done') {
|
|
1229
1266
|
this._composer?.setStreaming(false);
|
|
1267
|
+
const skeletonWorld = this._videoSkeletonWorld;
|
|
1230
1268
|
this._clearVideoSkeleton();
|
|
1231
|
-
|
|
1232
|
-
// ожидает data URL картинки — видео-URL несовместим. Размещение на доске
|
|
1233
|
-
// выходит за рамки текущей фазы (нет объекта типа video).
|
|
1234
|
-
console.info('[ChatWindow] Видео готово. Размещение на доске не поддерживается: нет объекта типа video.', state.result?.videoUrl);
|
|
1235
|
-
if (this._refs?.errorBlock) {
|
|
1236
|
-
this._refs.errorBlock.textContent = 'Видео сгенерировано. Размещение на доске появится в следующей фазе.';
|
|
1237
|
-
this._refs.errorBlock.classList.add('is-visible');
|
|
1238
|
-
}
|
|
1269
|
+
this._placeGeneratedVideoOnBoard(state.result, skeletonWorld);
|
|
1239
1270
|
}
|
|
1240
1271
|
if (state.status === 'error') {
|
|
1241
1272
|
this._composer?.setStreaming(false);
|
|
@@ -136,7 +136,9 @@ export class CommentThreadPopover {
|
|
|
136
136
|
position: 'absolute',
|
|
137
137
|
inset: '0',
|
|
138
138
|
pointerEvents: 'none',
|
|
139
|
-
|
|
139
|
+
// Выше панелей свойств (text-properties-layer = 10050, *PropertiesPanel = 10000),
|
|
140
|
+
// иначе открытый поверх объекта попап комментария оказывается под тулбаром свойств.
|
|
141
|
+
zIndex: 10060,
|
|
140
142
|
});
|
|
141
143
|
this.container.appendChild(this.layer);
|
|
142
144
|
|
|
@@ -1218,6 +1218,8 @@ export class HandlesDomRenderer {
|
|
|
1218
1218
|
this.host.layer.innerHTML = '';
|
|
1219
1219
|
const box = document.createElement('div');
|
|
1220
1220
|
box.className = 'mb-handles-box';
|
|
1221
|
+
box.id = `mb-handles-box-${id}`;
|
|
1222
|
+
box.dataset.objectId = id;
|
|
1221
1223
|
|
|
1222
1224
|
let rotation = options.rotation ?? 0;
|
|
1223
1225
|
if (id !== '__group__') {
|
|
@@ -500,7 +500,17 @@ export class HandlesInteractionController {
|
|
|
500
500
|
isNoteTarget = (mbType === 'note');
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
let _edgeHasMoved = false;
|
|
504
|
+
const EDGE_DRAG_THRESHOLD = 4;
|
|
505
|
+
|
|
503
506
|
const onMove = (ev) => {
|
|
507
|
+
if (!_edgeHasMoved) {
|
|
508
|
+
const dx = ev.clientX - startMouse.x;
|
|
509
|
+
const dy = ev.clientY - startMouse.y;
|
|
510
|
+
if (Math.hypot(dx, dy) > EDGE_DRAG_THRESHOLD) {
|
|
511
|
+
_edgeHasMoved = true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
504
514
|
const maintainAspectRatio = !!ev.shiftKey;
|
|
505
515
|
if (isGroup && maintainAspectRatio !== previousMaintainAspectRatio) {
|
|
506
516
|
startCSS = this._readBoxCss(box);
|
|
@@ -657,6 +667,36 @@ export class HandlesInteractionController {
|
|
|
657
667
|
const onUp = () => {
|
|
658
668
|
document.removeEventListener('pointermove', onMove);
|
|
659
669
|
document.removeEventListener('pointerup', onUp);
|
|
670
|
+
|
|
671
|
+
// Клик по рамке без перетаскивания на текстовом объекте → редактирование
|
|
672
|
+
if (!_edgeHasMoved && isTextTarget && !isGroup) {
|
|
673
|
+
const posData = { objectId: id, position: null };
|
|
674
|
+
this.host.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
675
|
+
const _pixiReq = { objectId: id, pixiObject: null };
|
|
676
|
+
this.host.eventBus.emit(Events.Tool.GetObjectPixi, _pixiReq);
|
|
677
|
+
const _textProps = _pixiReq.pixiObject?._mb?.properties || {};
|
|
678
|
+
this.host.eventBus.emit(Events.Tool.ObjectEdit, {
|
|
679
|
+
id,
|
|
680
|
+
type: 'text',
|
|
681
|
+
position: posData.position,
|
|
682
|
+
properties: {
|
|
683
|
+
..._textProps,
|
|
684
|
+
content: _textProps.content || '',
|
|
685
|
+
},
|
|
686
|
+
caretClick: { clientX: e.clientX, clientY: e.clientY },
|
|
687
|
+
create: false,
|
|
688
|
+
});
|
|
689
|
+
// Сбрасываем состояние resize без создания команды в истории
|
|
690
|
+
this.host.eventBus.emit(Events.Tool.ResizeEnd, {
|
|
691
|
+
object: id,
|
|
692
|
+
oldSize: { width: startWorld.width, height: startWorld.height },
|
|
693
|
+
newSize: { width: startWorld.width, height: startWorld.height },
|
|
694
|
+
oldPosition: { x: startWorld.x, y: startWorld.y },
|
|
695
|
+
newPosition: { x: startWorld.x, y: startWorld.y },
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
660
700
|
const endCSS = {
|
|
661
701
|
left: parseFloat(box.style.left),
|
|
662
702
|
top: parseFloat(box.style.top),
|
|
@@ -665,14 +665,45 @@
|
|
|
665
665
|
z-index: 10000;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
.mb-custom-caret {
|
|
669
|
+
position: absolute;
|
|
670
|
+
background-color: #111;
|
|
671
|
+
width: 2px;
|
|
672
|
+
pointer-events: none;
|
|
673
|
+
z-index: 10001;
|
|
674
|
+
animation: mb-caret-blink 1s step-end infinite;
|
|
675
|
+
display: none;
|
|
676
|
+
border-radius: 1px;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@keyframes mb-caret-blink {
|
|
680
|
+
0%, 100% { opacity: 1; }
|
|
681
|
+
50% { opacity: 0; }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Панель свойств текста и её дропдауны — выше inline-редактора */
|
|
685
|
+
.text-properties-layer {
|
|
686
|
+
z-index: 10050;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.moodboard-text-input::selection {
|
|
690
|
+
background: rgba(33, 150, 243, 0.28);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.moodboard-text-input::-moz-selection {
|
|
694
|
+
background: rgba(33, 150, 243, 0.28);
|
|
695
|
+
}
|
|
696
|
+
|
|
668
697
|
.moodboard-text-input {
|
|
698
|
+
caret-color: transparent;
|
|
669
699
|
position: relative;
|
|
700
|
+
z-index: 2;
|
|
670
701
|
left: 0;
|
|
671
702
|
top: 0;
|
|
672
703
|
border: none;
|
|
673
704
|
padding: 0;
|
|
674
705
|
font-family: Caveat, Arial, cursive;
|
|
675
|
-
color:
|
|
706
|
+
color: transparent;
|
|
676
707
|
background: transparent;
|
|
677
708
|
outline: none;
|
|
678
709
|
resize: none;
|
|
@@ -685,7 +716,34 @@
|
|
|
685
716
|
padding-bottom: 0; /* без нижнего отступа */
|
|
686
717
|
letter-spacing: 0px;
|
|
687
718
|
font-kerning: normal;
|
|
719
|
+
text-rendering: optimizeLegibility;
|
|
720
|
+
margin: 0;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.moodboard-text-backdrop {
|
|
724
|
+
position: absolute;
|
|
725
|
+
z-index: 1;
|
|
726
|
+
left: 0;
|
|
727
|
+
top: 0;
|
|
728
|
+
width: 100%;
|
|
729
|
+
height: 100%;
|
|
730
|
+
border: none;
|
|
731
|
+
padding: 0;
|
|
732
|
+
font-family: Caveat, Arial, cursive;
|
|
733
|
+
color: #111;
|
|
734
|
+
background: transparent;
|
|
735
|
+
box-sizing: content-box;
|
|
736
|
+
overflow: hidden;
|
|
737
|
+
-webkit-font-smoothing: antialiased;
|
|
738
|
+
-moz-osx-font-smoothing: grayscale;
|
|
739
|
+
white-space: pre-wrap;
|
|
740
|
+
word-break: break-word;
|
|
741
|
+
padding-bottom: 0;
|
|
742
|
+
letter-spacing: 0px;
|
|
743
|
+
font-kerning: normal;
|
|
744
|
+
text-rendering: optimizeLegibility;
|
|
688
745
|
margin: 0;
|
|
746
|
+
pointer-events: none;
|
|
689
747
|
}
|
|
690
748
|
|
|
691
749
|
.text-properties-panel .font-select,
|
|
@@ -1576,6 +1634,11 @@
|
|
|
1576
1634
|
padding-bottom: 1px;
|
|
1577
1635
|
}
|
|
1578
1636
|
|
|
1637
|
+
.moodboard-text-backdrop {
|
|
1638
|
+
padding-top: 1px;
|
|
1639
|
+
padding-bottom: 1px;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1579
1642
|
.moodboard-text-input:focus {
|
|
1580
1643
|
outline: none;
|
|
1581
1644
|
}
|
|
@@ -111,6 +111,14 @@ export function bindTextPropertiesPanelControls(panel) {
|
|
|
111
111
|
document.addEventListener('click', panel._onColorDocumentClick);
|
|
112
112
|
|
|
113
113
|
if (panel.currentHighlightButton) {
|
|
114
|
+
// Снимок выделения textarea до ухода фокуса (capture-фаза, до click/blur).
|
|
115
|
+
// Покрывает и кнопку, и пресеты, и нативный color-input внутри контейнера.
|
|
116
|
+
if (panel._highlightSelectorContainer) {
|
|
117
|
+
panel._highlightSelectorContainer.addEventListener('mousedown', () => {
|
|
118
|
+
panel._snapshotTextSelection();
|
|
119
|
+
}, true);
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
panel.currentHighlightButton.addEventListener('click', (event) => {
|
|
115
123
|
event.stopPropagation();
|
|
116
124
|
panel._toggleHighlightDropdown();
|
|
@@ -170,6 +170,35 @@ export function buildPropertyUpdate(key, value) {
|
|
|
170
170
|
|
|
171
171
|
export function applyTextAppearanceToDom(objectId, properties) {
|
|
172
172
|
const htmlElement = document.querySelector(`[data-id="${objectId}"]`);
|
|
173
|
+
|
|
174
|
+
// Также обновляем активный редактор, если он открыт
|
|
175
|
+
const activeEditor = document.querySelector('.moodboard-text-editor');
|
|
176
|
+
if (activeEditor) {
|
|
177
|
+
const textarea = activeEditor.querySelector('.moodboard-text-input');
|
|
178
|
+
const backdrop = activeEditor.querySelector('.moodboard-text-backdrop');
|
|
179
|
+
|
|
180
|
+
if (textarea) {
|
|
181
|
+
if (properties.fontFamily) {
|
|
182
|
+
textarea.style.fontFamily = properties.fontFamily;
|
|
183
|
+
if (backdrop) backdrop.style.fontFamily = properties.fontFamily;
|
|
184
|
+
}
|
|
185
|
+
if (properties.fontSize) {
|
|
186
|
+
textarea.style.fontSize = `${properties.fontSize}px`;
|
|
187
|
+
if (backdrop) backdrop.style.fontSize = `${properties.fontSize}px`;
|
|
188
|
+
}
|
|
189
|
+
if (properties.textAlign !== undefined) {
|
|
190
|
+
textarea.style.textAlign = properties.textAlign;
|
|
191
|
+
if (backdrop) backdrop.style.textAlign = properties.textAlign;
|
|
192
|
+
}
|
|
193
|
+
if (properties.color && backdrop) {
|
|
194
|
+
backdrop.style.color = properties.color;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Эмулируем событие input для перерисовки backdrop
|
|
198
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
173
202
|
if (!htmlElement) {
|
|
174
203
|
return;
|
|
175
204
|
}
|