@sequent-org/moodboard 1.4.22 → 1.4.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.22",
3
+ "version": "1.4.24",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -3,6 +3,9 @@ import { Events } from '../../../core/events/Events.js';
3
3
  export function onMouseDown(event) {
4
4
  // Если активен текстовый редактор, закрываем его при клике вне
5
5
  if (this.textEditor.active) {
6
+ // Помечаем, что закрытие инициировано кликом вне редактора.
7
+ // Это используется в blur-handler, чтобы избежать двойного finalize в одном цикле.
8
+ this.textEditor._closingByOutside = true;
6
9
  const activeEditorType = this.textEditor.objectType;
7
10
  if (this.textEditor.objectType === 'file') {
8
11
  this._closeFileNameEditor(true);
@@ -172,12 +172,21 @@ export function bindTextEditorInteractions(controller, {
172
172
  finalize,
173
173
  }) {
174
174
  const blurHandler = () => {
175
- const value = (textarea.value || '').trim();
176
- if (isNewCreation && value.length === 0) {
177
- finalize(false);
178
- return;
179
- }
180
- finalize(true);
175
+ const editorObjectId = controller?.textEditor?.objectId || null;
176
+ setTimeout(() => {
177
+ // Если редактор уже закрыт/переключен другим обработчиком (outside click),
178
+ // blur не должен дублировать finalize.
179
+ if (!controller?.textEditor?.active) return;
180
+ if ((controller?.textEditor?.objectId || null) !== editorObjectId) return;
181
+ if (controller?.textEditor?._closingByOutside) return;
182
+
183
+ const value = (textarea.value || '').trim();
184
+ if (isNewCreation && value.length === 0) {
185
+ finalize(false);
186
+ return;
187
+ }
188
+ finalize(true);
189
+ }, 0);
181
190
  };
182
191
 
183
192
  const keydownHandler = (e) => {
@@ -220,6 +229,7 @@ export function closeTextEditorFromState(controller, commit) {
220
229
  const properties = controller.textEditor.properties;
221
230
  const isNewCreation = controller.textEditor.isNewCreation;
222
231
  const initialContent = controller.textEditor.initialContent ?? '';
232
+ const shouldDeleteEmptyNewCreation = !commitValue && !!isNewCreation && !!objectId;
223
233
 
224
234
  if (objectId) {
225
235
  if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
@@ -244,7 +254,12 @@ export function closeTextEditorFromState(controller, commit) {
244
254
 
245
255
  textarea.remove();
246
256
  controller.textEditor = { active: false, objectId: null, textarea: null, world: null, objectType: 'text' };
247
- if (!commitValue) return;
257
+ if (!commitValue) {
258
+ if (shouldDeleteEmptyNewCreation) {
259
+ controller.eventBus.emit(Events.Tool.ObjectsDelete, { objects: [objectId] });
260
+ }
261
+ return;
262
+ }
248
263
  if (objectId == null) {
249
264
  controller.eventBus.emit(Events.UI.ToolbarAction, {
250
265
  type: objectType,
@@ -6,32 +6,28 @@ export function createRegularTextAutoSize({
6
6
  wrapper,
7
7
  minWBound,
8
8
  minHBound,
9
- effectiveFontPx,
10
- computeLineHeightPx,
9
+ onSizeChange,
11
10
  }) {
12
11
  const MAX_AUTO_WIDTH = 360;
13
- const BASELINE_FIX = 2;
14
12
 
15
13
  return () => {
16
14
  textarea.style.width = 'auto';
17
15
  textarea.style.height = 'auto';
18
16
 
19
- const naturalW = textarea.scrollWidth + 1;
20
- const targetW = Math.min(MAX_AUTO_WIDTH, Math.max(minWBound, naturalW));
17
+ const naturalW = Math.max(1, Math.ceil(textarea.scrollWidth + 1));
18
+ const targetW = Math.round(Math.min(MAX_AUTO_WIDTH, Math.max(minWBound, naturalW)));
21
19
  textarea.style.width = `${targetW}px`;
22
20
  wrapper.style.width = `${targetW}px`;
23
21
 
24
22
  textarea.style.height = 'auto';
25
- const adjust = BASELINE_FIX;
26
- const computed = (typeof window !== 'undefined') ? window.getComputedStyle(textarea) : null;
27
- const lineH = (computed ? parseFloat(computed.lineHeight) : computeLineHeightPx(effectiveFontPx)) + 10;
28
- const rawH = textarea.scrollHeight;
29
- const lines = lineH > 0 ? Math.max(1, Math.round(rawH / lineH)) : 1;
30
- const targetH = lines <= 1
31
- ? Math.max(minHBound, Math.max(1, lineH - BASELINE_FIX))
32
- : Math.max(minHBound, Math.max(1, rawH - adjust));
23
+ const naturalH = Math.max(1, Math.ceil(textarea.scrollHeight));
24
+ const targetH = Math.round(Math.max(minHBound, naturalH));
33
25
  textarea.style.height = `${targetH}px`;
34
26
  wrapper.style.height = `${targetH}px`;
27
+
28
+ if (typeof onSizeChange === 'function') {
29
+ onSizeChange({ widthPx: targetW, heightPx: targetH });
30
+ }
35
31
  };
36
32
  }
37
33
 
@@ -58,8 +58,22 @@ export function openTextEditor(object, create = false) {
58
58
  return;
59
59
  }
60
60
 
61
- // Закрываем предыдущий редактор, если он открыт
62
- if (this.textEditor.active) this._closeTextEditor(true);
61
+ // Закрываем предыдущий редактор, если он открыт.
62
+ // Защита от повторного открытия того же объекта в один цикл событий:
63
+ // не пересоздаём textarea/обёртку, если уже редактируем этот объект.
64
+ if (this.textEditor.active) {
65
+ const sameEditorObject = !!(
66
+ objectId &&
67
+ this.textEditor.objectId &&
68
+ this.textEditor.objectId === objectId &&
69
+ this.textEditor.objectType === objectType
70
+ );
71
+ if (sameEditorObject && this.textEditor.textarea) {
72
+ try { this.textEditor.textarea.focus(); } catch (_) {}
73
+ return;
74
+ }
75
+ this._closeTextEditor(true);
76
+ }
63
77
 
64
78
  // Если это редактирование существующего объекта, получаем его данные
65
79
  if (!create && objectId) {
@@ -129,6 +143,8 @@ export function openTextEditor(object, create = false) {
129
143
  // Базовые стили вынесены в CSS (.moodboard-text-editor)
130
144
 
131
145
  const textarea = createTextEditorTextarea(content || '');
146
+ // Без доступного статичного HTML-элемента (часто при create) не оставляем Caveat как fallback.
147
+ textarea.style.fontFamily = isNote ? 'Caveat, Arial, cursive' : 'Roboto, Arial, sans-serif';
132
148
 
133
149
  // Вычисляем межстрочный интервал; подгоняем к реальным значениям HTML-отображения
134
150
  let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
@@ -366,7 +382,7 @@ export function openTextEditor(object, create = false) {
366
382
  }
367
383
 
368
384
  // Для записок размеры уже установлены выше, пропускаем эту логику
369
- if (!isNote) {
385
+ if (!isNote && !create) {
370
386
  if (initialWpx) {
371
387
  textarea.style.width = `${initialWpx}px`;
372
388
  wrapper.style.width = `${initialWpx}px`;
@@ -377,13 +393,29 @@ export function openTextEditor(object, create = false) {
377
393
  }
378
394
  }
379
395
  // Автоподгон
396
+ const syncRegularTextSizeToObject = !isNote && objectId
397
+ ? ({ widthPx, heightPx }) => {
398
+ try {
399
+ const scaleX = (worldLayerRef?.scale?.x) || 1;
400
+ const widthWorld = Math.max(1, Math.round(widthPx * viewRes / scaleX));
401
+ const heightWorld = Math.max(1, Math.round(heightPx * viewRes / scaleX));
402
+ const posReq = { objectId, position: null };
403
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posReq);
404
+ this.eventBus.emit(Events.Tool.ResizeUpdate, {
405
+ object: objectId,
406
+ size: { width: widthWorld, height: heightWorld },
407
+ position: posReq.position || { x: position.x, y: position.y },
408
+ });
409
+ } catch (_) {}
410
+ }
411
+ : null;
412
+
380
413
  const autoSize = createRegularTextAutoSize({
381
414
  textarea,
382
415
  wrapper,
383
416
  minWBound,
384
417
  minHBound,
385
- effectiveFontPx,
386
- computeLineHeightPx: computeTextEditorLineHeightPx,
418
+ onSizeChange: syncRegularTextSizeToObject,
387
419
  });
388
420
 
389
421
  // Вызываем autoSize только для обычного текста