@sequent-org/moodboard 1.4.64 → 1.4.66
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/HistoryManager.js +9 -3
- package/src/core/index.js +6 -0
- package/src/services/text/TextBoxMetrics.js +10 -0
- package/src/tools/object-tools/selection/TextEditorSyncService.js +36 -8
- package/src/tools/object-tools/selection/TextInlineEditorController.js +6 -2
- package/src/ui/HtmlTextLayer.js +63 -14
- package/src/ui/connectors/ConnectionAnchorsLayer.js +5 -18
- package/src/ui/styles/panels.css +13 -2
package/package.json
CHANGED
|
@@ -41,7 +41,9 @@ export class HistoryManager {
|
|
|
41
41
|
lastCommand.mergeWith(command);
|
|
42
42
|
this._executeCommandSafely(lastCommand);
|
|
43
43
|
this.eventBus.emit('history:changed', {
|
|
44
|
-
historySize: this.history.length
|
|
44
|
+
historySize: this.history.length,
|
|
45
|
+
canUndo: this.currentIndex >= 0,
|
|
46
|
+
canRedo: this.currentIndex < this.history.length - 1,
|
|
45
47
|
});
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
@@ -67,7 +69,9 @@ export class HistoryManager {
|
|
|
67
69
|
// Уведомляем об изменении истории
|
|
68
70
|
this.eventBus.emit(Events.History.Changed, {
|
|
69
71
|
historySize: this.history.length,
|
|
70
|
-
currentCommand: command.toString()
|
|
72
|
+
currentCommand: command.toString(),
|
|
73
|
+
canUndo: this.currentIndex >= 0,
|
|
74
|
+
canRedo: this.currentIndex < this.history.length - 1,
|
|
71
75
|
});
|
|
72
76
|
|
|
73
77
|
|
|
@@ -107,7 +111,9 @@ export class HistoryManager {
|
|
|
107
111
|
this.currentIndex = -1;
|
|
108
112
|
|
|
109
113
|
this.eventBus.emit(Events.History.Changed, {
|
|
110
|
-
historySize: 0
|
|
114
|
+
historySize: 0,
|
|
115
|
+
canUndo: false,
|
|
116
|
+
canRedo: false,
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
|
package/src/core/index.js
CHANGED
|
@@ -230,6 +230,12 @@ export class CoreMoodBoard {
|
|
|
230
230
|
historySize: data.historySize,
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
|
+
if (data && ('canUndo' in data || 'canRedo' in data)) {
|
|
234
|
+
this.eventBus.emit(Events.UI.UpdateHistoryButtons, {
|
|
235
|
+
canUndo: !!data.canUndo,
|
|
236
|
+
canRedo: !!data.canRedo,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
233
239
|
});
|
|
234
240
|
}
|
|
235
241
|
|
|
@@ -111,6 +111,16 @@ try {
|
|
|
111
111
|
const _vCenterCache = new Map();
|
|
112
112
|
const _VCENTER_CACHE_LIMIT = 512;
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Сбрасывает кеш дельт вертикального центрирования. Нужен после подгрузки веб-шрифтов:
|
|
116
|
+
* до swap measureText считает по fallback-шрифту (например Arial вместо Caveat) и даёт
|
|
117
|
+
* другую дельту. Ключ кеша не включает состояние загрузки шрифта, поэтому без сброса
|
|
118
|
+
* fallback-значение «залипает» и одиночная строка остаётся смещённой вниз.
|
|
119
|
+
*/
|
|
120
|
+
export function clearSingleLineCenterDeltaCache() {
|
|
121
|
+
_vCenterCache.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
/**
|
|
115
125
|
* Дельта высоты (px) для вертикального центрирования видимых глифов в однострочном
|
|
116
126
|
* текстовом боксе.
|
|
@@ -4,6 +4,8 @@ import { registerEditorListeners } from './InlineEditorListenersRegistry.js';
|
|
|
4
4
|
import {
|
|
5
5
|
computeTextRightPadPx,
|
|
6
6
|
resolveLineHeightRatio,
|
|
7
|
+
computeSingleLineCenterDelta,
|
|
8
|
+
TEXT_BOX_BOTTOM_PAD_PX,
|
|
7
9
|
} from '../../../services/text/TextBoxMetrics.js';
|
|
8
10
|
|
|
9
11
|
// Максимальное число визуальных строк в записке. По достижении лимита ввод
|
|
@@ -186,14 +188,36 @@ export function createRegularTextAutoSize({
|
|
|
186
188
|
// число визуальных строк = число переводов строки.
|
|
187
189
|
let lhRatio = parseFloat(textarea.style.lineHeight);
|
|
188
190
|
if (!isFinite(lhRatio) || lhRatio <= 0 || lhRatio > 4) lhRatio = 1.2;
|
|
189
|
-
const cs = (typeof window !== 'undefined' && window.getComputedStyle)
|
|
190
|
-
? window.getComputedStyle(textarea)
|
|
191
|
-
: null;
|
|
192
|
-
const padV = cs
|
|
193
|
-
? ((parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0))
|
|
194
|
-
: 0;
|
|
195
191
|
const lineCount = value.length ? (value.split('\n').length) : 1;
|
|
196
|
-
|
|
192
|
+
// Вертикальная геометрия должна совпадать со статическим .mb-text
|
|
193
|
+
// (HtmlTextLayer.updateOne): высота = line-box + extra, где extra для одиночной
|
|
194
|
+
// строки — дельта центрирования глифов, иначе нижний запас TEXT_BOX_BOTTOM_PAD_PX.
|
|
195
|
+
// Паддинги textarea/backdrop обнуляем: статический слой кладёт весь запас снизу
|
|
196
|
+
// (padding 0), поэтому прежний 1px сверху уводил текст в редакторе ниже, чем после
|
|
197
|
+
// блюра, и менял высоту рамки. Backdrop делит кеш дельты со статикой (один ключ),
|
|
198
|
+
// поэтому высота поля совпадает со статикой пиксель в пиксель.
|
|
199
|
+
textarea.style.paddingTop = '0px';
|
|
200
|
+
textarea.style.paddingBottom = '0px';
|
|
201
|
+
if (backdrop) {
|
|
202
|
+
backdrop.style.paddingTop = '0px';
|
|
203
|
+
backdrop.style.paddingBottom = '0px';
|
|
204
|
+
}
|
|
205
|
+
let naturalH;
|
|
206
|
+
if (lineCount === 1 && backdrop) {
|
|
207
|
+
// Одиночную строку считаем ровно как статический .mb-text
|
|
208
|
+
// (HtmlTextLayer.updateOne): height auto → scrollHeight → + центр-дельта.
|
|
209
|
+
// Детерминированная формула round(fontPx*lhRatio) расходилась со scrollHeight
|
|
210
|
+
// на 1px при дробном devicePixelRatio (масштаб Windows 110/125/150%), из-за чего
|
|
211
|
+
// высота рамки ввода отличалась от статики. Общий scrollHeight + общий кеш дельты
|
|
212
|
+
// гарантируют совпадение при любом dpr.
|
|
213
|
+
backdrop.style.height = 'auto';
|
|
214
|
+
const sh = backdrop.scrollHeight;
|
|
215
|
+
const d = computeSingleLineCenterDelta(backdrop);
|
|
216
|
+
const extra = (d === null) ? TEXT_BOX_BOTTOM_PAD_PX : d;
|
|
217
|
+
naturalH = Math.max(1, Math.round(sh + extra));
|
|
218
|
+
} else {
|
|
219
|
+
naturalH = Math.max(1, lineCount * Math.round(fontPx * lhRatio) + TEXT_BOX_BOTTOM_PAD_PX);
|
|
220
|
+
}
|
|
197
221
|
const targetH = Math.round(Math.max(bounds.minH, naturalH));
|
|
198
222
|
textarea.style.height = `${targetH}px`;
|
|
199
223
|
wrapper.style.height = `${targetH}px`;
|
|
@@ -366,7 +390,11 @@ export function createRegularTextEditorUpdater(controller, {
|
|
|
366
390
|
} catch (_) {}
|
|
367
391
|
|
|
368
392
|
const leftPx = Math.round(baseLeft - padLeft);
|
|
369
|
-
|
|
393
|
+
// Текст в редакторе выровнен по верху (padding 0, как статический .mb-text),
|
|
394
|
+
// поэтому верх обёртки = верх объекта + статический верхний отступ. Прежний
|
|
395
|
+
// «+1» компенсировал убранный теперь 1px верхнего паддинга textarea и опускал
|
|
396
|
+
// строку ниже статики.
|
|
397
|
+
const topPx = Math.round(baseTop + staticPadTop - padTop);
|
|
370
398
|
wrapper.style.left = `${leftPx}px`;
|
|
371
399
|
wrapper.style.top = `${topPx}px`;
|
|
372
400
|
controller.textEditor._cssLeftPx = leftPx;
|
|
@@ -510,8 +510,12 @@ export function openTextEditor(object, create = false) {
|
|
|
510
510
|
? ({ widthPx, heightPx }) => {
|
|
511
511
|
try {
|
|
512
512
|
const scaleX = (worldLayerRef?.scale?.x) || 1;
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
// Мировой размер resolution-независим: CSS = world × scale (как toGlobal),
|
|
514
|
+
// без множителя renderer.resolution. Прежний × viewRes раздувал obj.height
|
|
515
|
+
// при браузерном зуме/HiDPI (res ≠ 1), и рамка выделения во время
|
|
516
|
+
// редактирования была в res раз выше текста.
|
|
517
|
+
const widthWorld = Math.max(1, Math.ceil(widthPx / scaleX));
|
|
518
|
+
const heightWorld = Math.max(1, Math.round(heightPx / scaleX));
|
|
515
519
|
const posReq = { objectId, position: null };
|
|
516
520
|
this.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
|
|
517
521
|
this.eventBus.emit(Events.Tool.ResizeUpdate, {
|
package/src/ui/HtmlTextLayer.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
computeTextRightPadPx,
|
|
11
11
|
resolveStaticTextPadding,
|
|
12
12
|
computeSingleLineCenterDelta,
|
|
13
|
+
clearSingleLineCenterDeltaCache,
|
|
13
14
|
TEXT_BOX_BOTTOM_PAD_PX,
|
|
14
15
|
} from '../services/text/TextBoxMetrics.js';
|
|
15
16
|
|
|
@@ -364,12 +365,23 @@ export class HtmlTextLayer {
|
|
|
364
365
|
// Отслеживаем выделение, чтобы не показывать hover у выделенных объектов
|
|
365
366
|
this._onSelectionAdd = (data) => {
|
|
366
367
|
const id = data?.object ?? data?.objectId ?? data?.id ?? data;
|
|
367
|
-
if (id)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
368
|
+
if (!id) return;
|
|
369
|
+
const sid = String(id);
|
|
370
|
+
this._selectedIds.add(sid);
|
|
371
|
+
if (this._hoveredTextId === sid) this._hoveredTextId = null;
|
|
372
|
+
// Рамку выделения слой ручек строит по getBoundingClientRect .mb-text,
|
|
373
|
+
// который включает CSS-transform hover-lift (translateY/scale). Если выделение
|
|
374
|
+
// происходит на приподнятом hover-тексте, рамка снимается со смещённого/
|
|
375
|
+
// увеличенного бокса, а после анимированного отката hover текст возвращается —
|
|
376
|
+
// рамка же остаётся смещённой (её никто не пересчитывает). Поэтому сбрасываем
|
|
377
|
+
// hover МГНОВЕННО (без твина) и форсируем пересчёт рамки по осевшему боксу.
|
|
378
|
+
const state = this._hoverStates.get(sid);
|
|
379
|
+
if (state && (Math.abs(state.ty) > 0.001 || Math.abs(state.sc - 1) > 0.001)) {
|
|
380
|
+
gsap.killTweensOf(state);
|
|
381
|
+
state.ty = 0;
|
|
382
|
+
state.sc = 1;
|
|
383
|
+
this._applyHoverTransform(sid);
|
|
384
|
+
this.eventBus.emit(Events.Object.TransformUpdated, { objectId: id });
|
|
373
385
|
}
|
|
374
386
|
};
|
|
375
387
|
this._onSelectionRemove = (data) => {
|
|
@@ -386,9 +398,22 @@ export class HtmlTextLayer {
|
|
|
386
398
|
this.updateAll();
|
|
387
399
|
|
|
388
400
|
// После загрузки веб-шрифтов переизмеряем все боксы: до swap шрифта scrollHeight
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
401
|
+
// и метрики глифов фиксируются по fallback-шрифту, после — по реальным глифам.
|
|
402
|
+
// Кеш вертикального центрирования сбрасываем ПЕРЕД updateAll, иначе одиночная
|
|
403
|
+
// строка остаётся смещённой вниз: дельта, посчитанная на fallback-шрифте,
|
|
404
|
+
// залипает (ключ кеша не зависит от состояния загрузки шрифта).
|
|
405
|
+
if (typeof document !== 'undefined' && document.fonts) {
|
|
406
|
+
const refitAfterFonts = () => {
|
|
407
|
+
clearSingleLineCenterDeltaCache();
|
|
408
|
+
this.updateAll();
|
|
409
|
+
};
|
|
410
|
+
if (document.fonts.ready) {
|
|
411
|
+
document.fonts.ready.then(refitAfterFonts).catch(() => {});
|
|
412
|
+
}
|
|
413
|
+
if (typeof document.fonts.addEventListener === 'function') {
|
|
414
|
+
this._onFontsLoadingDone = refitAfterFonts;
|
|
415
|
+
document.fonts.addEventListener('loadingdone', this._onFontsLoadingDone);
|
|
416
|
+
}
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
// Хелпер: при каждом обновлении ручек — обновляем HTML блок
|
|
@@ -399,6 +424,13 @@ export class HtmlTextLayer {
|
|
|
399
424
|
}
|
|
400
425
|
|
|
401
426
|
destroy() {
|
|
427
|
+
if (this._onFontsLoadingDone
|
|
428
|
+
&& typeof document !== 'undefined'
|
|
429
|
+
&& document.fonts
|
|
430
|
+
&& typeof document.fonts.removeEventListener === 'function') {
|
|
431
|
+
document.fonts.removeEventListener('loadingdone', this._onFontsLoadingDone);
|
|
432
|
+
this._onFontsLoadingDone = null;
|
|
433
|
+
}
|
|
402
434
|
if (this.layer) this.layer.remove();
|
|
403
435
|
this.layer = null;
|
|
404
436
|
this.idToEl.clear();
|
|
@@ -918,20 +950,37 @@ export class HtmlTextLayer {
|
|
|
918
950
|
// а форма (квадрат остаётся квадратом) не подгоняется под высоту текста.
|
|
919
951
|
const obj = (this.core.state.state.objects || []).find(o => o.id === objectId);
|
|
920
952
|
if (obj?.type === 'shape') return;
|
|
921
|
-
// Измеряем фактическую высоту HTML-текста
|
|
953
|
+
// Измеряем фактическую высоту HTML-текста ровно так же, как updateOne
|
|
954
|
+
// (scrollHeight + extra): иначе _autoFitTextHeight фиксирует высоту без
|
|
955
|
+
// центр-дельты, шлёт ResizeUpdate с завышенным боксом, рамка выделения
|
|
956
|
+
// снимается по нему, а следующий updateOne досаживает финальную (меньшую)
|
|
957
|
+
// высоту уже без обновления рамки — рамка остаётся выше текста.
|
|
922
958
|
el.style.height = 'auto';
|
|
923
|
-
const
|
|
959
|
+
const props = obj?.properties || {};
|
|
960
|
+
const content = obj?.content || props.content;
|
|
961
|
+
const useList = !!(props.listType && props.listType !== 'none');
|
|
962
|
+
const isMarkdown = !useList && resolveMarkdown(obj?.properties, content);
|
|
963
|
+
const centerDelta = (!isMarkdown && !useList)
|
|
964
|
+
? computeSingleLineCenterDelta(el)
|
|
965
|
+
: null;
|
|
966
|
+
const extra = (centerDelta === null) ? TEXT_BOX_BOTTOM_PAD_PX : centerDelta;
|
|
967
|
+
const measured = Math.max(1, Math.round(el.scrollHeight + extra));
|
|
924
968
|
|
|
925
969
|
const world = this.core.pixi.worldLayer || this.core.pixi.app.stage;
|
|
926
970
|
const s = world?.scale?.x || 1;
|
|
927
|
-
|
|
928
|
-
|
|
971
|
+
// Мировой размер обязан быть resolution-независимым: позиции и ширина текста
|
|
972
|
+
// конвертируются через toGlobal как CSS = world × scale, без множителя
|
|
973
|
+
// renderer.resolution. Раньше высота писалась как measured × res / s, поэтому
|
|
974
|
+
// при браузерном зуме/HiDPI (res ≠ 1) obj.height оказывался в res раз больше
|
|
975
|
+
// ширины, и рамка выделения (строится по obj.height через toGlobal, без res)
|
|
976
|
+
// получалась в res раз выше реального текста — буквы прижимались к верху рамки.
|
|
977
|
+
const worldH_auto = measured / s;
|
|
929
978
|
|
|
930
979
|
const currentWorldH = obj?.height || 0;
|
|
931
980
|
// Высота объекта всегда подгоняется под контент (и вверх, и вниз): без сжатия
|
|
932
981
|
// единожды завышенная высота залипала и давала пустой зазор под текстом.
|
|
933
982
|
const finalWorldH = worldH_auto;
|
|
934
|
-
const finalCssH = Math.round(
|
|
983
|
+
const finalCssH = Math.round(finalWorldH * s);
|
|
935
984
|
|
|
936
985
|
el.style.height = `${finalCssH}px`;
|
|
937
986
|
|
|
@@ -124,23 +124,6 @@ export class ConnectionAnchorsLayer {
|
|
|
124
124
|
this._eventsAttached = false;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
_getSingleSelectionWorldBounds(id) {
|
|
128
|
-
const positionData = { objectId: id, position: null };
|
|
129
|
-
const sizeData = { objectId: id, size: null };
|
|
130
|
-
this.eventBus.emit(Events.Tool.GetObjectPosition, positionData);
|
|
131
|
-
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
132
|
-
|
|
133
|
-
if (positionData.position && sizeData.size) {
|
|
134
|
-
return {
|
|
135
|
-
x: positionData.position.x,
|
|
136
|
-
y: positionData.position.y,
|
|
137
|
-
width: sizeData.size.width,
|
|
138
|
-
height: sizeData.size.height,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
127
|
update() {
|
|
145
128
|
if (!this.layer) return;
|
|
146
129
|
if (this._commentPopoverOpen) return;
|
|
@@ -174,7 +157,11 @@ export class ConnectionAnchorsLayer {
|
|
|
174
157
|
return;
|
|
175
158
|
}
|
|
176
159
|
|
|
177
|
-
|
|
160
|
+
// Границы считаем тем же сервисом, что и рамка выделения
|
|
161
|
+
// (HandlesPositioningService.getSingleSelectionWorldBounds): для текста это
|
|
162
|
+
// реальный DOM-бокс .mb-text, а не state-размер. Иначе якоря смещены
|
|
163
|
+
// относительно рамки, т.к. у текста DOM-бокс ≠ сохранённый width/height.
|
|
164
|
+
const worldBounds = this.positioningService.getSingleSelectionWorldBounds(id, req.pixiObject);
|
|
178
165
|
if (!worldBounds) return;
|
|
179
166
|
|
|
180
167
|
const cssRect = this.positioningService.worldBoundsToCssRect(worldBounds);
|
package/src/ui/styles/panels.css
CHANGED
|
@@ -189,6 +189,7 @@
|
|
|
189
189
|
.moodboard-minimap-canvas { display: block; width: 280px; height: 180px; border-radius: 8px; }
|
|
190
190
|
|
|
191
191
|
.text-properties-panel {
|
|
192
|
+
box-sizing: border-box;
|
|
192
193
|
position: absolute;
|
|
193
194
|
pointer-events: auto;
|
|
194
195
|
display: flex;
|
|
@@ -634,7 +635,7 @@
|
|
|
634
635
|
box-sizing: border-box;
|
|
635
636
|
min-width: 70px;
|
|
636
637
|
width: 70px;
|
|
637
|
-
height:
|
|
638
|
+
height: 34px;
|
|
638
639
|
padding: 4px 0px;
|
|
639
640
|
font-size: 13px;
|
|
640
641
|
background-color: #fff;
|
|
@@ -723,7 +724,7 @@
|
|
|
723
724
|
border: none;
|
|
724
725
|
border-radius: 4px;
|
|
725
726
|
background-color: #fff;
|
|
726
|
-
height:
|
|
727
|
+
height: 34px;
|
|
727
728
|
box-sizing: border-box;
|
|
728
729
|
}
|
|
729
730
|
|
|
@@ -739,6 +740,16 @@
|
|
|
739
740
|
padding: 0 4px 0 8px;
|
|
740
741
|
font-size: 13px;
|
|
741
742
|
cursor: pointer;
|
|
743
|
+
/* Панель — светлая поверхность. Хост может форсить color-scheme: dark
|
|
744
|
+
(как futurello), из-за чего нативный select и его опции рендерятся белым
|
|
745
|
+
текстом на белом фоне. Фиксируем светлую схему и явные цвета. */
|
|
746
|
+
color-scheme: light;
|
|
747
|
+
color: #111827;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.text-properties-panel .font-size-select option {
|
|
751
|
+
color: #111827;
|
|
752
|
+
background-color: #fff;
|
|
742
753
|
}
|
|
743
754
|
|
|
744
755
|
.text-properties-panel .font-size-steppers {
|