@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
package/package.json
CHANGED
|
@@ -87,7 +87,13 @@ export class PasteObjectCommand extends BaseCommand {
|
|
|
87
87
|
|
|
88
88
|
// Создаем PIXI объект
|
|
89
89
|
this.coreMoodboard.pixi.createObject(this.newObjectData);
|
|
90
|
-
|
|
90
|
+
|
|
91
|
+
// Уведомляем слои (HtmlTextLayer и др.), которые ждут { objectId, objectData }
|
|
92
|
+
this.coreMoodboard.eventBus.emit(Events.Object.Created, {
|
|
93
|
+
objectId: this.newObjectId,
|
|
94
|
+
objectData: this.newObjectData,
|
|
95
|
+
});
|
|
96
|
+
|
|
91
97
|
this.emit(Events.Object.Pasted, {
|
|
92
98
|
originalId: originalData.id,
|
|
93
99
|
newId: this.newObjectId,
|
|
@@ -47,6 +47,22 @@ export class UpdateContentCommand extends BaseCommand {
|
|
|
47
47
|
if (!object.properties) {
|
|
48
48
|
object.properties = {};
|
|
49
49
|
}
|
|
50
|
+
// Пересчитываем диапазоны ссылок и подсветки при изменении контента
|
|
51
|
+
const oldContent = object.properties.content ?? '';
|
|
52
|
+
if (Array.isArray(object.properties.links) && object.properties.links.length > 0) {
|
|
53
|
+
object.properties.links = _adjustLinks(
|
|
54
|
+
object.properties.links,
|
|
55
|
+
oldContent,
|
|
56
|
+
content
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(object.properties.highlights) && object.properties.highlights.length > 0) {
|
|
60
|
+
object.properties.highlights = _adjustHighlights(
|
|
61
|
+
object.properties.highlights,
|
|
62
|
+
oldContent,
|
|
63
|
+
content
|
|
64
|
+
);
|
|
65
|
+
}
|
|
50
66
|
object.properties.content = content;
|
|
51
67
|
|
|
52
68
|
const isMindmap = object.type === 'mindmap';
|
|
@@ -93,3 +109,82 @@ export class UpdateContentCommand extends BaseCommand {
|
|
|
93
109
|
});
|
|
94
110
|
}
|
|
95
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Пересчитывает диапазоны ссылок после изменения контента.
|
|
115
|
+
* Алгоритм: находим наибольший общий префикс и суффикс, определяем
|
|
116
|
+
* изменённый диапазон; ссылки внутри него удаляем, за ним — сдвигаем.
|
|
117
|
+
* @param {Array<{start:number,end:number,url:string}>} links
|
|
118
|
+
* @param {string} oldText
|
|
119
|
+
* @param {string} newText
|
|
120
|
+
* @returns {Array<{start:number,end:number,url:string}>}
|
|
121
|
+
*/
|
|
122
|
+
function _adjustLinks(links, oldText, newText) {
|
|
123
|
+
if (!links || links.length === 0) return links;
|
|
124
|
+
|
|
125
|
+
let prefixLen = 0;
|
|
126
|
+
const minLen = Math.min(oldText.length, newText.length);
|
|
127
|
+
while (prefixLen < minLen && oldText[prefixLen] === newText[prefixLen]) prefixLen++;
|
|
128
|
+
|
|
129
|
+
let oldSuffix = 0;
|
|
130
|
+
while (
|
|
131
|
+
oldSuffix < oldText.length - prefixLen &&
|
|
132
|
+
oldSuffix < newText.length - prefixLen &&
|
|
133
|
+
oldText[oldText.length - 1 - oldSuffix] === newText[newText.length - 1 - oldSuffix]
|
|
134
|
+
) oldSuffix++;
|
|
135
|
+
|
|
136
|
+
const oldChangeEnd = oldText.length - oldSuffix; // конец изменённого диапазона в старом тексте
|
|
137
|
+
const newChangeEnd = newText.length - oldSuffix; // конец изменённого диапазона в новом тексте
|
|
138
|
+
const delta = newChangeEnd - oldChangeEnd; // сдвиг для символов после изменения
|
|
139
|
+
|
|
140
|
+
return links.reduce((acc, link) => {
|
|
141
|
+
const { start, end, url } = link;
|
|
142
|
+
if (end <= prefixLen) {
|
|
143
|
+
// Ссылка полностью в неизменённом префиксе — оставляем
|
|
144
|
+
acc.push({ start, end, url });
|
|
145
|
+
} else if (start >= oldChangeEnd) {
|
|
146
|
+
// Ссылка полностью после изменения — сдвигаем
|
|
147
|
+
acc.push({ start: start + delta, end: end + delta, url });
|
|
148
|
+
}
|
|
149
|
+
// Ссылка перекрывает изменённый диапазон — удаляем (стала некорректной)
|
|
150
|
+
return acc;
|
|
151
|
+
}, []);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Пересчитывает диапазоны подсветки после изменения контента.
|
|
156
|
+
* Алгоритм идентичен _adjustLinks: общий префикс/суффикс, сдвиг или удаление.
|
|
157
|
+
* @param {Array<{start:number,end:number,color:string}>} highlights
|
|
158
|
+
* @param {string} oldText
|
|
159
|
+
* @param {string} newText
|
|
160
|
+
* @returns {Array<{start:number,end:number,color:string}>}
|
|
161
|
+
*/
|
|
162
|
+
function _adjustHighlights(highlights, oldText, newText) {
|
|
163
|
+
if (!highlights || highlights.length === 0) return highlights;
|
|
164
|
+
|
|
165
|
+
let prefixLen = 0;
|
|
166
|
+
const minLen = Math.min(oldText.length, newText.length);
|
|
167
|
+
while (prefixLen < minLen && oldText[prefixLen] === newText[prefixLen]) prefixLen++;
|
|
168
|
+
|
|
169
|
+
let oldSuffix = 0;
|
|
170
|
+
while (
|
|
171
|
+
oldSuffix < oldText.length - prefixLen &&
|
|
172
|
+
oldSuffix < newText.length - prefixLen &&
|
|
173
|
+
oldText[oldText.length - 1 - oldSuffix] === newText[newText.length - 1 - oldSuffix]
|
|
174
|
+
) oldSuffix++;
|
|
175
|
+
|
|
176
|
+
const oldChangeEnd = oldText.length - oldSuffix;
|
|
177
|
+
const newChangeEnd = newText.length - oldSuffix;
|
|
178
|
+
const delta = newChangeEnd - oldChangeEnd;
|
|
179
|
+
|
|
180
|
+
return highlights.reduce((acc, hi) => {
|
|
181
|
+
const { start, end, color } = hi;
|
|
182
|
+
if (end <= prefixLen) {
|
|
183
|
+
acc.push({ start, end, color });
|
|
184
|
+
} else if (start >= oldChangeEnd) {
|
|
185
|
+
acc.push({ start: start + delta, end: end + delta, color });
|
|
186
|
+
}
|
|
187
|
+
// Подсветка перекрывает изменённый диапазон — удаляем
|
|
188
|
+
return acc;
|
|
189
|
+
}, []);
|
|
190
|
+
}
|
|
@@ -309,7 +309,16 @@ export function setupObjectLifecycleFlow(core) {
|
|
|
309
309
|
const objects = core.state.getObjects();
|
|
310
310
|
const object = objects.find(obj => obj.id === data.objectId);
|
|
311
311
|
if (object) {
|
|
312
|
-
|
|
312
|
+
let w = object.width;
|
|
313
|
+
let h = object.height;
|
|
314
|
+
if (typeof w !== 'number' || typeof h !== 'number') {
|
|
315
|
+
const pixiObj = core.pixi.objects.get(data.objectId);
|
|
316
|
+
if (pixiObj) {
|
|
317
|
+
if (typeof w !== 'number') w = pixiObj.width;
|
|
318
|
+
if (typeof h !== 'number') h = pixiObj.height;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
data.size = { width: w, height: h };
|
|
313
322
|
}
|
|
314
323
|
});
|
|
315
324
|
|
package/src/initNoBundler.js
CHANGED
|
@@ -203,9 +203,9 @@ export function injectCriticalStyles() {
|
|
|
203
203
|
min-height: 20px;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
.current-color-button, .
|
|
207
|
-
width:
|
|
208
|
-
height:
|
|
206
|
+
.current-color-button, .fpp-color-button {
|
|
207
|
+
width: 24px !important;
|
|
208
|
+
height: 24px !important;
|
|
209
209
|
border: 1px solid #ddd;
|
|
210
210
|
border-radius: 50%;
|
|
211
211
|
cursor: pointer;
|
|
@@ -214,6 +214,18 @@ export function injectCriticalStyles() {
|
|
|
214
214
|
display: block;
|
|
215
215
|
box-sizing: border-box;
|
|
216
216
|
}
|
|
217
|
+
|
|
218
|
+
.current-bgcolor-button {
|
|
219
|
+
width: 20px !important;
|
|
220
|
+
height: 20px !important;
|
|
221
|
+
border: none;
|
|
222
|
+
border-radius: 50%;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
margin: 0;
|
|
225
|
+
padding: 0;
|
|
226
|
+
display: block;
|
|
227
|
+
box-sizing: border-box;
|
|
228
|
+
}
|
|
217
229
|
|
|
218
230
|
/* Скрыть до полной загрузки стилей */
|
|
219
231
|
.moodboard-toolbar__popup {
|
|
@@ -358,7 +370,7 @@ export function forceInjectPanelStyles() {
|
|
|
358
370
|
.font-select { min-width: 110px !important; }
|
|
359
371
|
.font-size-select { min-width: 32px !important; }
|
|
360
372
|
|
|
361
|
-
.current-color-button, .
|
|
373
|
+
.current-color-button, .fpp-color-button {
|
|
362
374
|
width: 28px !important;
|
|
363
375
|
height: 28px !important;
|
|
364
376
|
border: 1px solid #ddd !important;
|
|
@@ -369,6 +381,18 @@ export function forceInjectPanelStyles() {
|
|
|
369
381
|
display: block !important;
|
|
370
382
|
box-sizing: border-box !important;
|
|
371
383
|
}
|
|
384
|
+
|
|
385
|
+
.current-bgcolor-button {
|
|
386
|
+
width: 20px !important;
|
|
387
|
+
height: 20px !important;
|
|
388
|
+
border: none !important;
|
|
389
|
+
border-radius: 50% !important;
|
|
390
|
+
cursor: pointer !important;
|
|
391
|
+
margin: 0 !important;
|
|
392
|
+
padding: 0 !important;
|
|
393
|
+
display: block !important;
|
|
394
|
+
box-sizing: border-box !important;
|
|
395
|
+
}
|
|
372
396
|
`;
|
|
373
397
|
|
|
374
398
|
const style = document.createElement('style');
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Единый источник истины для геометрии текстового бокса.
|
|
3
|
+
*
|
|
4
|
+
* Один и тот же объект рисуется двумя независимыми механизмами: статический слой
|
|
5
|
+
* `HtmlTextLayer` (.mb-text) и inline-редактор (textarea + backdrop). Чтобы рамка
|
|
6
|
+
* выделения, межстрочный интервал и паддинги не расходились между режимами и не
|
|
7
|
+
* разъезжались после очередной правки, все метрики бокса считаются здесь.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Соотношение line-height по базовому (немасштабированному) размеру шрифта.
|
|
11
|
+
// Берётся от базового размера, а не от отрисованного, поэтому пропорция строк
|
|
12
|
+
// не зависит от зума и не страдает от округления.
|
|
13
|
+
const LINE_HEIGHT_BREAKPOINTS = [
|
|
14
|
+
[12, 1.40],
|
|
15
|
+
[18, 1.34],
|
|
16
|
+
[36, 1.26],
|
|
17
|
+
[48, 1.24],
|
|
18
|
+
[72, 1.22],
|
|
19
|
+
[96, 1.20],
|
|
20
|
+
];
|
|
21
|
+
const LINE_HEIGHT_FALLBACK_RATIO = 1.18;
|
|
22
|
+
|
|
23
|
+
// Нижний запас под хвосты глифов (например, «з», «у»), общий для обоих режимов.
|
|
24
|
+
export const TEXT_BOX_BOTTOM_PAD_PX = 2;
|
|
25
|
+
|
|
26
|
+
// Вертикальный паддинг inline-редактора (textarea/backdrop). Дублируется в CSS
|
|
27
|
+
// (.moodboard-text-input / .moodboard-text-backdrop) и учитывается при сведении
|
|
28
|
+
// высоты поля к мировой высоте объекта.
|
|
29
|
+
export const TEXT_EDITOR_VERTICAL_PAD_PX = 1;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} baseFontSizePx — базовый размер шрифта без учёта зума
|
|
33
|
+
* @param {object|undefined} properties — допускается явный properties.lineHeight
|
|
34
|
+
* @returns {number} коэффициент (не пиксели)
|
|
35
|
+
*/
|
|
36
|
+
export function resolveLineHeightRatio(baseFontSizePx, properties) {
|
|
37
|
+
if (properties && typeof properties.lineHeight === 'number') {
|
|
38
|
+
return properties.lineHeight;
|
|
39
|
+
}
|
|
40
|
+
const fs = baseFontSizePx;
|
|
41
|
+
for (const [maxFs, ratio] of LINE_HEIGHT_BREAKPOINTS) {
|
|
42
|
+
if (fs <= maxFs) {
|
|
43
|
+
return ratio;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return LINE_HEIGHT_FALLBACK_RATIO;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Межстрочный интервал в пикселях (для inline-редактора, где line-height задаётся в px).
|
|
51
|
+
* @param {number} fontSizePx
|
|
52
|
+
* @param {object|undefined} properties
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function computeLineHeightPx(fontSizePx, properties) {
|
|
56
|
+
return Math.round(fontSizePx * resolveLineHeightRatio(fontSizePx, properties));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Правый запас, чтобы рамка не прилипала к тексту справа. Одинаков для статического
|
|
61
|
+
* слоя и для авторазмера редактора — иначе ширина рамки в режимах различается.
|
|
62
|
+
* @param {number} fontSizePx
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function computeTextRightPadPx(fontSizePx) {
|
|
66
|
+
return Math.ceil(fontSizePx * 0.7) + 6;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Применяет ВСЕ текстовые параметры на DOM-элемент в одинаковом формате.
|
|
71
|
+
*
|
|
72
|
+
* Используется как для статического .mb-text, так и для textarea/backdrop
|
|
73
|
+
* в inline-редакторе, гарантируя идентичный рендеринг глифов в обоих режимах.
|
|
74
|
+
*
|
|
75
|
+
* Ключевое: `line-height` всегда задаётся как **безразмерный коэффициент** (e.g. `"1.34"`),
|
|
76
|
+
* а не в пикселях. Безразмерный коэффициент масштабируется браузером вместе с font-size
|
|
77
|
+
* и не страдает от округления — строки в редакторе и статике совпадают точно.
|
|
78
|
+
*
|
|
79
|
+
* Цвет (`color`) умышленно вынесен за пределы функции: textarea рендерит текст
|
|
80
|
+
* прозрачным (видимый текст рисует backdrop), backdrop и .mb-text — реальным цветом.
|
|
81
|
+
*
|
|
82
|
+
* @param {HTMLElement} el
|
|
83
|
+
* @param {{ fontSizePx: number, baseFontSizePx: number, fontFamily: string, properties?: object }} params
|
|
84
|
+
*/
|
|
85
|
+
export function applyTextStyles(el, { fontSizePx, baseFontSizePx, fontFamily, properties = {} }) {
|
|
86
|
+
const ratio = resolveLineHeightRatio(baseFontSizePx, properties);
|
|
87
|
+
el.style.fontSize = `${fontSizePx}px`;
|
|
88
|
+
el.style.lineHeight = `${ratio}`;
|
|
89
|
+
el.style.fontFamily = fontFamily || '';
|
|
90
|
+
el.style.fontWeight = properties.bold ? 'bold' : '';
|
|
91
|
+
el.style.fontStyle = properties.italic ? 'italic' : '';
|
|
92
|
+
const dec = [properties.underline && 'underline', properties.strikethrough && 'line-through']
|
|
93
|
+
.filter(Boolean).join(' ');
|
|
94
|
+
el.style.textDecoration = dec || '';
|
|
95
|
+
el.style.textAlign = properties.textAlign || '';
|
|
96
|
+
el.style.letterSpacing = '0px';
|
|
97
|
+
el.style.fontKerning = 'normal';
|
|
98
|
+
el.style.textRendering = 'optimizeLegibility';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Применяет параметры размера поля для inline-редактора (textarea/backdrop).
|
|
103
|
+
* Отдельно от applyTextStyles, чтобы не дублировать логику в статическом слое.
|
|
104
|
+
*
|
|
105
|
+
* @param {HTMLElement} el
|
|
106
|
+
* @param {number} lineHeightPx — вычисленный lineHeight в px (для initialHeight)
|
|
107
|
+
*/
|
|
108
|
+
export function applyEditorSizing(el, lineHeightPx) {
|
|
109
|
+
const initialHeightPx = Math.max(1, lineHeightPx);
|
|
110
|
+
el.style.minHeight = `${initialHeightPx}px`;
|
|
111
|
+
el.style.height = `${initialHeightPx}px`;
|
|
112
|
+
el.setAttribute('rows', '1');
|
|
113
|
+
el.style.overflowY = 'hidden';
|
|
114
|
+
el.style.whiteSpace = 'pre';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Inline-значение `padding` статического .mb-text по текущему режиму.
|
|
119
|
+
* Обычный текст → '0' (рамка плотно по глифам, как в редакторе);
|
|
120
|
+
* список/markdown → '' (берётся CSS `.mb-text { padding: 0.3em }`).
|
|
121
|
+
*
|
|
122
|
+
* Применяется детерминированно при каждом проходе: если возвращать паддинг только
|
|
123
|
+
* при смене режима, обычный текст после режима списка сохраняет CSS-отступ 0.3em
|
|
124
|
+
* сверху, и статическая рамка оказывается выше глифов, чем поле ввода.
|
|
125
|
+
* @param {{ isMarkdown?: boolean, useList?: boolean }} mode
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function resolveStaticTextPadding({ isMarkdown = false, useList = false } = {}) {
|
|
129
|
+
if (useList || isMarkdown) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
return '0';
|
|
133
|
+
}
|
|
@@ -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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Events } from '../../../core/events/Events.js';
|
|
2
2
|
|
|
3
3
|
const DRAG_START_THRESHOLD_PX = 4;
|
|
4
|
-
const TEXT_EDITOR_STYLE_KEYS = ['fontFamily', 'fontSize', 'color', 'backgroundColor', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType', 'listChecked'];
|
|
4
|
+
const TEXT_EDITOR_STYLE_KEYS = ['fontFamily', 'fontSize', 'color', 'highlightColor', 'backgroundColor', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType', 'listChecked'];
|
|
5
5
|
|
|
6
6
|
function pickTextEditorProperties(properties = {}) {
|
|
7
7
|
const picked = {};
|
|
@@ -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
|
}
|