@sequent-org/moodboard 1.4.55 → 1.4.57

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.
@@ -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,
@@ -10,6 +11,7 @@ import {
10
11
  import {
11
12
  applyTextAppearanceToDom,
12
13
  buildBackgroundColorUpdate,
14
+ buildHighlightColorUpdate,
13
15
  buildFontFamilyUpdate,
14
16
  buildFontSizeUpdate,
15
17
  buildMarkdownUpdate,
@@ -26,10 +28,13 @@ import {
26
28
  createTextPropertiesPanelRenderer,
27
29
  hideBgColorDropdown,
28
30
  hideColorDropdown,
31
+ hideHighlightDropdown,
29
32
  toggleBgColorDropdown,
30
33
  toggleColorDropdown,
34
+ toggleHighlightDropdown,
31
35
  updateCurrentBgColorButton,
32
36
  updateCurrentColorButton,
37
+ updateCurrentHighlightButton,
33
38
  } from './text-properties/TextPropertiesPanelRenderer.js';
34
39
  import {
35
40
  clearTextPropertiesPanelState,
@@ -57,7 +62,7 @@ export class TextPropertiesPanel {
57
62
  position: 'absolute',
58
63
  inset: '0',
59
64
  pointerEvents: 'none',
60
- zIndex: 20,
65
+ zIndex: 10000,
61
66
  });
62
67
  this.container.appendChild(this.layer);
63
68
 
@@ -107,8 +112,9 @@ export class TextPropertiesPanel {
107
112
  }
108
113
 
109
114
  this.panel.style.display = 'flex';
110
- this.reposition();
111
115
  this._updateControlsFromObject();
116
+ this._updateLockUI();
117
+ this.reposition();
112
118
  }
113
119
 
114
120
  hide() {
@@ -119,6 +125,7 @@ export class TextPropertiesPanel {
119
125
  }
120
126
 
121
127
  this._hideColorDropdown();
128
+ this._hideHighlightDropdown();
122
129
  this._hideBgColorDropdown();
123
130
  if (this._docMouseDownAttached) {
124
131
  document.removeEventListener('mousedown', this._onDocMouseDown, true);
@@ -144,6 +151,24 @@ export class TextPropertiesPanel {
144
151
  updateCurrentColorButton(this, color);
145
152
  }
146
153
 
154
+ _toggleHighlightDropdown() {
155
+ toggleHighlightDropdown(this);
156
+ }
157
+
158
+ _hideHighlightDropdown() {
159
+ hideHighlightDropdown(this);
160
+ }
161
+
162
+ _selectHighlightColor(color) {
163
+ this._changeHighlightColor(color);
164
+ this._updateCurrentHighlightButton(color);
165
+ this._hideHighlightDropdown();
166
+ }
167
+
168
+ _updateCurrentHighlightButton(color) {
169
+ updateCurrentHighlightButton(this, color);
170
+ }
171
+
147
172
  _toggleBgColorDropdown() {
148
173
  toggleBgColorDropdown(this);
149
174
  }
@@ -214,6 +239,19 @@ export class TextPropertiesPanel {
214
239
  this._updateTextAppearance(this.currentId, { backgroundColor });
215
240
  }
216
241
 
242
+ _changeHighlightColor(highlightColor) {
243
+ if (!this.currentId) {
244
+ return;
245
+ }
246
+
247
+ this.eventBus.emit(Events.Object.StateChanged, {
248
+ objectId: this.currentId,
249
+ updates: buildHighlightColorUpdate(highlightColor),
250
+ });
251
+
252
+ this._updateTextAppearance(this.currentId, { highlightColor });
253
+ }
254
+
217
255
  _toggleFormat(prop) {
218
256
  if (!this.currentId) {
219
257
  return;
@@ -288,6 +326,40 @@ export class TextPropertiesPanel {
288
326
  this._updateTextAppearance(this.currentId, { markdown });
289
327
  }
290
328
 
329
+ /**
330
+ * Добавляет web-ссылку к диапазону текста (plain-режим).
331
+ * При включённом MD-режиме ничего не делает (кнопка должна быть disabled).
332
+ * @param {string} url — нормализованный URL
333
+ * @param {number} start — индекс начала диапазона (включительно)
334
+ * @param {number} end — индекс конца диапазона (не включительно)
335
+ * @param {string} [objectId] — id объекта; если не задан, используется this.currentId.
336
+ * Нужен потому, что во время ввода URL фокус уходит в input и выделение объекта
337
+ * может сброситься (this.currentId → null).
338
+ */
339
+ _addLink(url, start, end, objectId) {
340
+ const targetId = objectId || this.currentId;
341
+ if (!targetId || !url) return;
342
+ const props = getObjectProperties(this.eventBus, targetId);
343
+ if (props?.markdown === true) return;
344
+
345
+ const content = props?.content ?? '';
346
+ const safeEnd = Math.min(end, content.length);
347
+ const safeStart = Math.min(start, safeEnd);
348
+ if (safeStart >= safeEnd) return;
349
+
350
+ const oldLinks = Array.isArray(props?.links) ? props.links : [];
351
+ // Удаляем ссылки, пересекающиеся с новым диапазоном
352
+ const filtered = oldLinks.filter(l => l.end <= safeStart || l.start >= safeEnd);
353
+ const newLinks = [...filtered, { start: safeStart, end: safeEnd, url }]
354
+ .sort((a, b) => a.start - b.start);
355
+
356
+ this.eventBus.emit(Events.Object.StateChanged, {
357
+ objectId: targetId,
358
+ updates: { properties: { links: newLinks } },
359
+ });
360
+ this._updateTextAppearance(targetId, { links: newLinks });
361
+ }
362
+
291
363
  _updateTextAppearance(objectId, properties) {
292
364
  applyTextAppearanceToDom(objectId, properties);
293
365
  syncPixiTextProperties(this.eventBus, objectId, properties);
@@ -310,9 +382,11 @@ export class TextPropertiesPanel {
310
382
  this.fontSelect.value = values.fontFamily;
311
383
  this.fontSizeSelect.value = values.fontSize;
312
384
  this._updateCurrentColorButton(values.color);
385
+ this._updateCurrentHighlightButton(values.highlightColor);
313
386
  this._updateCurrentBgColorButton(values.backgroundColor);
314
387
  if (this.markdownToggle) {
315
388
  this.markdownToggle.checked = values.markdown;
389
+ updateLinkButtonState(this, values.markdown);
316
390
  }
317
391
 
318
392
  if (this.boldBtn) this.boldBtn.classList.toggle('is-active', values.bold);
@@ -349,7 +423,11 @@ export class TextPropertiesPanel {
349
423
  const panelY = screenY - this.panel.offsetHeight - 20;
350
424
 
351
425
  const containerRect = this.container.getBoundingClientRect();
352
- const finalX = Math.max(10, Math.min(panelX, containerRect.width - this.panel.offsetWidth - 10));
426
+ const toolbarEl = document.querySelector('.moodboard-toolbar');
427
+ const toolbarRight = toolbarEl
428
+ ? toolbarEl.getBoundingClientRect().right - containerRect.left + 10
429
+ : 10;
430
+ const finalX = Math.max(toolbarRight, Math.min(panelX, containerRect.width - this.panel.offsetWidth - 10));
353
431
  const finalY = Math.max(10, panelY);
354
432
 
355
433
  this.panel.style.left = `${Math.round(finalX)}px`;
@@ -379,4 +457,86 @@ export class TextPropertiesPanel {
379
457
  this.container.getBoundingClientRect();
380
458
  this.hide();
381
459
  }
460
+
461
+ _isLocked() {
462
+ if (!this.currentId) return false;
463
+ const objects = this.core?.state?.getObjects ? this.core.state.getObjects() : [];
464
+ const obj = objects.find((o) => o.id === this.currentId);
465
+ return !!(obj?.properties?.locked);
466
+ }
467
+
468
+ _toggleLocked() {
469
+ if (!this.currentId) return;
470
+ const newLocked = !this._isLocked();
471
+ this.eventBus.emit(Events.Object.StateChanged, {
472
+ objectId: this.currentId,
473
+ updates: { properties: { locked: newLocked } },
474
+ });
475
+ this._updateLockUI();
476
+ this.reposition();
477
+ }
478
+
479
+ _updateLockUI() {
480
+ if (!this._tppBtnLock) return;
481
+ const locked = this._isLocked();
482
+
483
+ this._tppBtnLock.innerHTML = locked ? this._tppLockIcon : this._tppUnlockIcon;
484
+ this._tppBtnLock.title = locked ? 'Разблокировать' : 'Заблокировать';
485
+
486
+ if (Array.isArray(this._lockableEls)) {
487
+ this._lockableEls.forEach((el) => {
488
+ if (el) el.style.display = locked ? 'none' : '';
489
+ });
490
+ }
491
+
492
+ if (this._moreLockLabel) {
493
+ this._moreLockLabel.textContent = locked ? 'Разблокировать' : 'Заблокировать';
494
+ }
495
+
496
+ if (this.panel) {
497
+ this.panel.classList.toggle('is-locked', locked);
498
+ }
499
+ }
500
+
501
+ _duplicateText() {
502
+ if (!this.currentId) return;
503
+
504
+ const posData = { objectId: this.currentId, position: null };
505
+ const sizeData = { objectId: this.currentId, size: null };
506
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
507
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
508
+
509
+ if (!posData.position || !sizeData.size) return;
510
+
511
+ let w = sizeData.size.width;
512
+ if (typeof w !== 'number' || isNaN(w)) {
513
+ const pixiObj = this.core?.pixi?.objects?.get(this.currentId);
514
+ w = pixiObj ? pixiObj.width : 160;
515
+ }
516
+
517
+ const originalId = this.currentId;
518
+ const newPos = {
519
+ x: posData.position.x + (w || 160) + 14,
520
+ y: posData.position.y,
521
+ };
522
+
523
+ const onReady = (data) => {
524
+ if (!data || data.originalId !== originalId) return;
525
+ this.eventBus.off(Events.Tool.DuplicateReady, onReady);
526
+ this._selectObject(data.newId);
527
+ };
528
+ this.eventBus.on(Events.Tool.DuplicateReady, onReady);
529
+
530
+ this.eventBus.emit(Events.Tool.DuplicateRequest, { originalId, position: newPos });
531
+ }
532
+
533
+ _selectObject(objectId) {
534
+ if (!objectId) return;
535
+ const selectTool = this.core?.selectTool;
536
+ if (!selectTool || typeof selectTool.setSelection !== 'function') return;
537
+ selectTool.setSelection([objectId]);
538
+ if (typeof selectTool.updateResizeHandles === 'function') {
539
+ selectTool.updateResizeHandles();
540
+ }
541
+ }
382
542
  }