@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
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
bindTextPropertiesPanelControls,
|
|
4
4
|
unbindTextPropertiesPanelControls,
|
|
5
5
|
} from './text-properties/TextPropertiesPanelBindings.js';
|
|
6
|
+
import { updateLinkButtonState } from './text-properties/TextLinkControl.js';
|
|
6
7
|
import {
|
|
7
8
|
attachTextPropertiesPanelEventBridge,
|
|
8
9
|
detachTextPropertiesPanelEventBridge,
|
|
@@ -26,10 +27,13 @@ import {
|
|
|
26
27
|
createTextPropertiesPanelRenderer,
|
|
27
28
|
hideBgColorDropdown,
|
|
28
29
|
hideColorDropdown,
|
|
30
|
+
hideHighlightDropdown,
|
|
29
31
|
toggleBgColorDropdown,
|
|
30
32
|
toggleColorDropdown,
|
|
33
|
+
toggleHighlightDropdown,
|
|
31
34
|
updateCurrentBgColorButton,
|
|
32
35
|
updateCurrentColorButton,
|
|
36
|
+
updateCurrentHighlightButton,
|
|
33
37
|
} from './text-properties/TextPropertiesPanelRenderer.js';
|
|
34
38
|
import {
|
|
35
39
|
clearTextPropertiesPanelState,
|
|
@@ -57,7 +61,7 @@ export class TextPropertiesPanel {
|
|
|
57
61
|
position: 'absolute',
|
|
58
62
|
inset: '0',
|
|
59
63
|
pointerEvents: 'none',
|
|
60
|
-
zIndex:
|
|
64
|
+
zIndex: 10050,
|
|
61
65
|
});
|
|
62
66
|
this.container.appendChild(this.layer);
|
|
63
67
|
|
|
@@ -107,8 +111,9 @@ export class TextPropertiesPanel {
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
this.panel.style.display = 'flex';
|
|
110
|
-
this.reposition();
|
|
111
114
|
this._updateControlsFromObject();
|
|
115
|
+
this._updateLockUI();
|
|
116
|
+
this.reposition();
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
hide() {
|
|
@@ -116,9 +121,12 @@ export class TextPropertiesPanel {
|
|
|
116
121
|
|
|
117
122
|
if (this.panel) {
|
|
118
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'));
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
this._hideColorDropdown();
|
|
129
|
+
this._hideHighlightDropdown();
|
|
122
130
|
this._hideBgColorDropdown();
|
|
123
131
|
if (this._docMouseDownAttached) {
|
|
124
132
|
document.removeEventListener('mousedown', this._onDocMouseDown, true);
|
|
@@ -144,6 +152,24 @@ export class TextPropertiesPanel {
|
|
|
144
152
|
updateCurrentColorButton(this, color);
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
_toggleHighlightDropdown() {
|
|
156
|
+
toggleHighlightDropdown(this);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_hideHighlightDropdown() {
|
|
160
|
+
hideHighlightDropdown(this);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_selectHighlightColor(color) {
|
|
164
|
+
this._changeHighlightColor(color);
|
|
165
|
+
this._updateCurrentHighlightButton(color);
|
|
166
|
+
this._hideHighlightDropdown();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_updateCurrentHighlightButton(color) {
|
|
170
|
+
updateCurrentHighlightButton(this, color);
|
|
171
|
+
}
|
|
172
|
+
|
|
147
173
|
_toggleBgColorDropdown() {
|
|
148
174
|
toggleBgColorDropdown(this);
|
|
149
175
|
}
|
|
@@ -188,11 +214,78 @@ export class TextPropertiesPanel {
|
|
|
188
214
|
this._updateTextAppearance(this.currentId, { fontSize });
|
|
189
215
|
}
|
|
190
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
|
+
|
|
191
280
|
_changeTextColor(color) {
|
|
192
281
|
if (!this.currentId) {
|
|
193
282
|
return;
|
|
194
283
|
}
|
|
195
284
|
|
|
285
|
+
if (this._applyFormatToSelection('color', color)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
196
289
|
this.eventBus.emit(Events.Object.StateChanged, {
|
|
197
290
|
objectId: this.currentId,
|
|
198
291
|
updates: buildTextColorUpdate(color),
|
|
@@ -214,11 +307,55 @@ export class TextPropertiesPanel {
|
|
|
214
307
|
this._updateTextAppearance(this.currentId, { backgroundColor });
|
|
215
308
|
}
|
|
216
309
|
|
|
310
|
+
_changeHighlightColor(highlightColor) {
|
|
311
|
+
if (!this.currentId) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
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;
|
|
331
|
+
|
|
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
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
217
350
|
_toggleFormat(prop) {
|
|
218
351
|
if (!this.currentId) {
|
|
219
352
|
return;
|
|
220
353
|
}
|
|
221
354
|
|
|
355
|
+
if (this._applyFormatToSelection(prop, null)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
222
359
|
const properties = getObjectProperties(this.eventBus, this.currentId);
|
|
223
360
|
const newValue = !(properties ? Boolean(properties[prop]) : false);
|
|
224
361
|
|
|
@@ -288,6 +425,88 @@ export class TextPropertiesPanel {
|
|
|
288
425
|
this._updateTextAppearance(this.currentId, { markdown });
|
|
289
426
|
}
|
|
290
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Добавляет web-ссылку к диапазону текста (plain-режим).
|
|
430
|
+
* При включённом MD-режиме ничего не делает (кнопка должна быть disabled).
|
|
431
|
+
* @param {string} url — нормализованный URL
|
|
432
|
+
* @param {number} start — индекс начала диапазона (включительно)
|
|
433
|
+
* @param {number} end — индекс конца диапазона (не включительно)
|
|
434
|
+
* @param {string} [objectId] — id объекта; если не задан, используется this.currentId.
|
|
435
|
+
* Нужен потому, что во время ввода URL фокус уходит в input и выделение объекта
|
|
436
|
+
* может сброситься (this.currentId → null).
|
|
437
|
+
*/
|
|
438
|
+
_addLink(url, start, end, objectId) {
|
|
439
|
+
const targetId = objectId || this.currentId;
|
|
440
|
+
if (!targetId || !url) return;
|
|
441
|
+
const props = getObjectProperties(this.eventBus, targetId);
|
|
442
|
+
if (props?.markdown === true) return;
|
|
443
|
+
|
|
444
|
+
const content = props?.content ?? '';
|
|
445
|
+
const safeEnd = Math.min(end, content.length);
|
|
446
|
+
const safeStart = Math.min(start, safeEnd);
|
|
447
|
+
if (safeStart >= safeEnd) return;
|
|
448
|
+
|
|
449
|
+
const oldLinks = Array.isArray(props?.links) ? props.links : [];
|
|
450
|
+
// Удаляем ссылки, пересекающиеся с новым диапазоном
|
|
451
|
+
const filtered = oldLinks.filter(l => l.end <= safeStart || l.start >= safeEnd);
|
|
452
|
+
const newLinks = [...filtered, { start: safeStart, end: safeEnd, url }]
|
|
453
|
+
.sort((a, b) => a.start - b.start);
|
|
454
|
+
|
|
455
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
456
|
+
objectId: targetId,
|
|
457
|
+
updates: { properties: { links: newLinks } },
|
|
458
|
+
});
|
|
459
|
+
this._updateTextAppearance(targetId, { links: newLinks });
|
|
460
|
+
}
|
|
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
|
+
|
|
291
510
|
_updateTextAppearance(objectId, properties) {
|
|
292
511
|
applyTextAppearanceToDom(objectId, properties);
|
|
293
512
|
syncPixiTextProperties(this.eventBus, objectId, properties);
|
|
@@ -310,9 +529,11 @@ export class TextPropertiesPanel {
|
|
|
310
529
|
this.fontSelect.value = values.fontFamily;
|
|
311
530
|
this.fontSizeSelect.value = values.fontSize;
|
|
312
531
|
this._updateCurrentColorButton(values.color);
|
|
532
|
+
this._updateCurrentHighlightButton(values.highlightColor);
|
|
313
533
|
this._updateCurrentBgColorButton(values.backgroundColor);
|
|
314
534
|
if (this.markdownToggle) {
|
|
315
535
|
this.markdownToggle.checked = values.markdown;
|
|
536
|
+
updateLinkButtonState(this, values.markdown);
|
|
316
537
|
}
|
|
317
538
|
|
|
318
539
|
if (this.boldBtn) this.boldBtn.classList.toggle('is-active', values.bold);
|
|
@@ -383,4 +604,86 @@ export class TextPropertiesPanel {
|
|
|
383
604
|
this.container.getBoundingClientRect();
|
|
384
605
|
this.hide();
|
|
385
606
|
}
|
|
607
|
+
|
|
608
|
+
_isLocked() {
|
|
609
|
+
if (!this.currentId) return false;
|
|
610
|
+
const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
|
|
611
|
+
const obj = objects.find((o) => o.id === this.currentId);
|
|
612
|
+
return !!(obj?.properties?.locked);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
_toggleLocked() {
|
|
616
|
+
if (!this.currentId) return;
|
|
617
|
+
const newLocked = !this._isLocked();
|
|
618
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
619
|
+
objectId: this.currentId,
|
|
620
|
+
updates: { properties: { locked: newLocked } },
|
|
621
|
+
});
|
|
622
|
+
this._updateLockUI();
|
|
623
|
+
this.reposition();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
_updateLockUI() {
|
|
627
|
+
if (!this._tppBtnLock) return;
|
|
628
|
+
const locked = this._isLocked();
|
|
629
|
+
|
|
630
|
+
this._tppBtnLock.innerHTML = locked ? this._tppLockIcon : this._tppUnlockIcon;
|
|
631
|
+
this._tppBtnLock.title = locked ? 'Разблокировать' : 'Заблокировать';
|
|
632
|
+
|
|
633
|
+
if (Array.isArray(this._lockableEls)) {
|
|
634
|
+
this._lockableEls.forEach((el) => {
|
|
635
|
+
if (el) el.style.display = locked ? 'none' : '';
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (this._moreLockLabel) {
|
|
640
|
+
this._moreLockLabel.textContent = locked ? 'Разблокировать' : 'Заблокировать';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (this.panel) {
|
|
644
|
+
this.panel.classList.toggle('is-locked', locked);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_duplicateText() {
|
|
649
|
+
if (!this.currentId) return;
|
|
650
|
+
|
|
651
|
+
const posData = { objectId: this.currentId, position: null };
|
|
652
|
+
const sizeData = { objectId: this.currentId, size: null };
|
|
653
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
654
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
655
|
+
|
|
656
|
+
if (!posData.position || !sizeData.size) return;
|
|
657
|
+
|
|
658
|
+
let w = sizeData.size.width;
|
|
659
|
+
if (typeof w !== 'number' || isNaN(w)) {
|
|
660
|
+
const pixiObj = this.core?.pixi?.objects?.get(this.currentId);
|
|
661
|
+
w = pixiObj ? pixiObj.width : 160;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const originalId = this.currentId;
|
|
665
|
+
const newPos = {
|
|
666
|
+
x: posData.position.x + (w || 160) + 14,
|
|
667
|
+
y: posData.position.y,
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const onReady = (data) => {
|
|
671
|
+
if (!data || data.originalId !== originalId) return;
|
|
672
|
+
this.eventBus.off(Events.Tool.DuplicateReady, onReady);
|
|
673
|
+
this._selectObject(data.newId);
|
|
674
|
+
};
|
|
675
|
+
this.eventBus.on(Events.Tool.DuplicateReady, onReady);
|
|
676
|
+
|
|
677
|
+
this.eventBus.emit(Events.Tool.DuplicateRequest, { originalId, position: newPos });
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
_selectObject(objectId) {
|
|
681
|
+
if (!objectId) return;
|
|
682
|
+
const selectTool = this.core?.selectTool;
|
|
683
|
+
if (!selectTool || typeof selectTool.setSelection !== 'function') return;
|
|
684
|
+
selectTool.setSelection([objectId]);
|
|
685
|
+
if (typeof selectTool.updateResizeHandles === 'function') {
|
|
686
|
+
selectTool.updateResizeHandles();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
386
689
|
}
|
|
@@ -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),
|