@sequent-org/moodboard 1.4.49 → 1.4.52

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.
@@ -27,6 +27,7 @@ export class HtmlHandlesLayer {
27
27
  this.handles = {};
28
28
  this._drag = null;
29
29
  this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
30
+ this._cropMode = false; // полное подавление во время crop (ни рамка, ни ручки)
30
31
  this._groupRotationPreview = null;
31
32
 
32
33
  // Ссылки на обработчики, чтобы корректно отписаться при destroy()
@@ -96,6 +97,7 @@ export class HtmlHandlesLayer {
96
97
  }
97
98
 
98
99
  update() {
100
+ if (this._cropMode) return;
99
101
  // Дополнительная защита: если слой или core уже уничтожены,
100
102
  // выходим, чтобы не получить ошибок при resize/смене DPR
101
103
  if (!this.core || !this.core.pixi || !this.core.pixi.app || !this.layer) return;
@@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js';
2
2
  import { Events } from '../core/events/Events.js';
3
3
  import { EditFileNameCommand } from '../core/commands/EditFileNameCommand.js';
4
4
  import { applyRoundedMask } from '../utils/applyRoundedMask.js';
5
+ import { CropController } from '../services/CropController.js';
5
6
 
6
7
  const ICONS = {
7
8
  download: `<svg width="24" height="24" fill="none" viewBox="0 0 24 24" type="download"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 14.75V16.25C4.75 17.9069 6.09315 19.25 7.75 19.25H16.25C17.9069 19.25 19.25 17.9069 19.25 16.25V14.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 14.25L12 4.75"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.75 10.75L12 14.25L15.25 10.75"></path></svg>`,
@@ -38,6 +39,9 @@ export class ImagePropertiesPanel {
38
39
  this.currentId = null;
39
40
  this._handlers = null;
40
41
 
42
+ this._cropController = new CropController({ core, eventBus, container });
43
+ this._cropController._onActivate = (active) => this._onCropActivate(active);
44
+
41
45
  this._attachEvents();
42
46
  this._createPanel();
43
47
  }
@@ -125,6 +129,9 @@ export class ImagePropertiesPanel {
125
129
  }
126
130
 
127
131
  showFor(objectId) {
132
+ if (this.currentId !== objectId) {
133
+ this._cropController?.cancel();
134
+ }
128
135
  this.currentId = objectId;
129
136
  if (this.panel) {
130
137
  this.panel.style.display = 'flex';
@@ -148,13 +155,12 @@ export class ImagePropertiesPanel {
148
155
  }
149
156
 
150
157
  _toggleLocked() {
151
- const obj = this._getCurrentObject();
152
- if (!obj) return;
153
- if (!obj.properties) obj.properties = {};
154
- obj.properties.locked = !obj.properties.locked;
155
- if (typeof this.core?.state?.markDirty === 'function') {
156
- this.core.state.markDirty();
157
- }
158
+ if (!this.currentId) return;
159
+ const newLocked = !this._isLocked();
160
+ this.eventBus.emit(Events.Object.StateChanged, {
161
+ objectId: this.currentId,
162
+ updates: { properties: { locked: newLocked } },
163
+ });
158
164
  this._updateLockUI();
159
165
  this.reposition();
160
166
  }
@@ -163,7 +169,7 @@ export class ImagePropertiesPanel {
163
169
  if (!this._btn_lock) return;
164
170
  const locked = this._isLocked();
165
171
 
166
- this._btn_lock.innerHTML = locked ? ICONS.unlock : ICONS.lock;
172
+ this._btn_lock.innerHTML = locked ? ICONS.lock : ICONS.unlock;
167
173
  this._btn_lock.title = locked ? 'Разблокировать' : 'Заблокировать';
168
174
 
169
175
  if (Array.isArray(this._lockableEls)) {
@@ -178,6 +184,7 @@ export class ImagePropertiesPanel {
178
184
  }
179
185
 
180
186
  hide() {
187
+ this._cropController?.cancel();
181
188
  this.currentId = null;
182
189
  if (this.panel) {
183
190
  this.panel.style.display = 'none';
@@ -185,6 +192,36 @@ export class ImagePropertiesPanel {
185
192
  this._closeBorderRadiusPopover();
186
193
  }
187
194
 
195
+ /**
196
+ * Вызывается CropController при входе/выходе из режима crop.
197
+ * Прячет тулбар и ручки выделения на время crop; восстанавливает по завершении.
198
+ */
199
+ _onCropActivate(active) {
200
+ if (!this.panel) return;
201
+
202
+ const handlesLayer = typeof window !== 'undefined' ? window.moodboardHtmlHandlesLayer : null;
203
+ const anchorsLayer = typeof window !== 'undefined' ? window.moodboardConnectionAnchorsLayer : null;
204
+
205
+ if (active) {
206
+ this.panel.style.display = 'none';
207
+ if (handlesLayer) {
208
+ handlesLayer._cropMode = true;
209
+ handlesLayer.hide();
210
+ }
211
+ anchorsLayer?.update();
212
+ } else {
213
+ // Восстанавливаем тулбар только если объект ещё выделен
214
+ if (this.currentId) {
215
+ this.panel.style.display = 'flex';
216
+ }
217
+ if (handlesLayer) {
218
+ handlesLayer._cropMode = false;
219
+ handlesLayer.update();
220
+ }
221
+ anchorsLayer?.update();
222
+ }
223
+ }
224
+
188
225
  _closeBorderRadiusPopover() {
189
226
  if (this._borderRadiusPopover) {
190
227
  this._borderRadiusPopover.classList.remove('is-open');
@@ -316,12 +353,6 @@ export class ImagePropertiesPanel {
316
353
  mainBtn.className = 'ipp-btn-split-main';
317
354
  mainBtn.title = 'Обрезать';
318
355
  mainBtn.innerHTML = ICONS.crop;
319
- mainBtn.addEventListener('click', (e) => {
320
- e.preventDefault();
321
- e.stopPropagation();
322
- // Логика применения текущего кропа
323
- });
324
-
325
356
  const expandBtn = document.createElement('button');
326
357
  expandBtn.className = 'ipp-btn-split-expand';
327
358
  expandBtn.title = 'Выбрать пропорции';
@@ -330,15 +361,36 @@ export class ImagePropertiesPanel {
330
361
  const dropdown = document.createElement('div');
331
362
  dropdown.className = 'ipp-crop-dropdown';
332
363
 
364
+ const toggleCropDropdown = (e) => {
365
+ e.preventDefault();
366
+ e.stopPropagation();
367
+ const isOpen = dropdown.classList.contains('is-open');
368
+
369
+ document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open, .ipp-border-radius-popover.is-open, .ipp-border-style-popover.is-open').forEach(el => {
370
+ el.classList.remove('is-open');
371
+ });
372
+ document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
373
+ el.classList.remove('is-expanded', 'is-active');
374
+ });
375
+
376
+ if (!isOpen) {
377
+ dropdown.classList.add('is-open');
378
+ expandBtn.classList.add('is-expanded');
379
+ }
380
+ };
381
+
382
+ mainBtn.addEventListener('click', toggleCropDropdown);
383
+ expandBtn.addEventListener('click', toggleCropDropdown);
384
+
333
385
  const items = [
334
- { id: 'custom', label: 'Custom', icon: ICONS.ddCustom },
335
- { id: 'original', label: 'Original', icon: ICONS.ddOriginal },
386
+ { id: 'custom', label: 'Произвольный', icon: ICONS.ddCustom },
387
+ { id: 'original', label: 'Оригинал', icon: ICONS.ddOriginal },
336
388
  { divider: true },
337
- { id: 'circle', label: 'Circle', icon: ICONS.ddCircle },
338
- { id: 'square', label: 'Square', icon: ICONS.ddSquare },
339
- { id: 'portrait', label: 'Portrait', icon: ICONS.ddPortrait, ratio: '3:4' },
340
- { id: 'landscape', label: 'Landscape', icon: ICONS.ddLandscape, ratio: '4:3' },
341
- { id: 'wide', label: 'Wide', icon: ICONS.ddWide, ratio: '16:9' },
389
+ { id: 'circle', label: 'Круг', icon: ICONS.ddCircle },
390
+ { id: 'square', label: 'Квадрат', icon: ICONS.ddSquare },
391
+ { id: 'portrait', label: 'Портрет', icon: ICONS.ddPortrait, ratio: '3:4' },
392
+ { id: 'landscape', label: 'Пейзаж', icon: ICONS.ddLandscape, ratio: '4:3' },
393
+ { id: 'wide', label: 'Широкий', icon: ICONS.ddWide, ratio: '16:9' },
342
394
  ];
343
395
 
344
396
  items.forEach(item => {
@@ -372,33 +424,16 @@ export class ImagePropertiesPanel {
372
424
  btn.addEventListener('click', (e) => {
373
425
  e.preventDefault();
374
426
  e.stopPropagation();
375
- // Логика выбора пропорции
376
427
  dropdown.classList.remove('is-open');
377
428
  expandBtn.classList.remove('is-expanded');
429
+ if (this.currentId) {
430
+ this._cropController.start(this.currentId, item.id);
431
+ }
378
432
  });
379
433
 
380
434
  dropdown.appendChild(btn);
381
435
  });
382
436
 
383
- expandBtn.addEventListener('click', (e) => {
384
- e.preventDefault();
385
- e.stopPropagation();
386
- const isOpen = dropdown.classList.contains('is-open');
387
-
388
- // Закрываем все другие дропдауны, если есть (на будущее)
389
- document.querySelectorAll('.ipp-crop-dropdown.is-open, .ipp-more-dropdown.is-open').forEach(el => {
390
- el.classList.remove('is-open');
391
- });
392
- document.querySelectorAll('.ipp-btn-split-expand.is-expanded, .ipp-btn.is-active').forEach(el => {
393
- el.classList.remove('is-expanded', 'is-active');
394
- });
395
-
396
- if (!isOpen) {
397
- dropdown.classList.add('is-open');
398
- expandBtn.classList.add('is-expanded');
399
- }
400
- });
401
-
402
437
  wrapper.appendChild(mainBtn);
403
438
  wrapper.appendChild(expandBtn);
404
439
  wrapper.appendChild(dropdown);
@@ -1015,6 +1050,14 @@ export class ImagePropertiesPanel {
1015
1050
 
1016
1051
  if (item.id === 'copy') {
1017
1052
  this.eventBus.emit(Events.Keyboard.Copy);
1053
+ } else if (item.id === 'bring-front') {
1054
+ if (this.currentId) this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: this.currentId });
1055
+ } else if (item.id === 'bring-forward') {
1056
+ if (this.currentId) this.eventBus.emit(Events.UI.LayerBringForward, { objectId: this.currentId });
1057
+ } else if (item.id === 'send-backward') {
1058
+ if (this.currentId) this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: this.currentId });
1059
+ } else if (item.id === 'send-back') {
1060
+ if (this.currentId) this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: this.currentId });
1018
1061
  } else if (item.id === 'duplicate') {
1019
1062
  this._duplicateImage();
1020
1063
  } else if (item.id === 'lock') {
@@ -1442,6 +1485,7 @@ export class ImagePropertiesPanel {
1442
1485
  }
1443
1486
 
1444
1487
  destroy() {
1488
+ this._cropController?.destroy();
1445
1489
  if (!this.eventBus || !this._handlers) return;
1446
1490
 
1447
1491
  this.eventBus.off(Events.Tool.SelectionAdd, this._handlers.onSelectionAdd);
@@ -144,6 +144,10 @@ export class ConnectionAnchorsLayer {
144
144
  update() {
145
145
  if (!this.layer) return;
146
146
  if (this._commentPopoverOpen) return;
147
+ if (typeof window !== 'undefined' && window.moodboardHtmlHandlesLayer?._cropMode) {
148
+ this.layer.innerHTML = '';
149
+ return;
150
+ }
147
151
  this.layer.innerHTML = '';
148
152
 
149
153
  const selection = Array.from(this.core?.selectTool?.selectedObjects || []);
@@ -1157,6 +1157,7 @@ export class HandlesDomRenderer {
1157
1157
  let isFrameTarget = false;
1158
1158
  let isMindmapTarget = false;
1159
1159
  let isMindmapOnlyGroupTarget = false;
1160
+ let isLockedTarget = false;
1160
1161
  let isRevitScreenshotTarget = false;
1161
1162
  let revitViewPayload = null;
1162
1163
  let isModel3dScreenshotTarget = false;
@@ -1167,11 +1168,12 @@ export class HandlesDomRenderer {
1167
1168
  const hiddenIncomingSide = { value: null };
1168
1169
  if (id !== '__group__') {
1169
1170
  const req = { objectId: id, pixiObject: null };
1170
- this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
1171
+ this.host.eventBus.emit('tool:get:object:pixi', req);
1171
1172
  const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
1172
1173
  isFileTarget = mbType === 'file';
1173
1174
  isFrameTarget = mbType === 'frame';
1174
1175
  isMindmapTarget = mbType === 'mindmap';
1176
+ isLockedTarget = !!req.pixiObject?._mb?.properties?.locked;
1175
1177
  isRevitScreenshotTarget = mbType === 'revit-screenshot-img';
1176
1178
  revitViewPayload = req.pixiObject?._mb?.properties?.view || null;
1177
1179
  isModel3dScreenshotTarget = mbType === 'model3d-screenshot-img';
@@ -1199,9 +1201,14 @@ export class HandlesDomRenderer {
1199
1201
  if (selectionIds.length > 0) {
1200
1202
  const byId = new Map((this.host.core?.state?.state?.objects || []).map((obj) => [obj?.id, obj]));
1201
1203
  isMindmapOnlyGroupTarget = selectionIds.every((selectedId) => byId.get(selectedId)?.type === 'mindmap');
1204
+ isLockedTarget = selectionIds.some(selId => {
1205
+ const req = { objectId: selId, pixiObject: null };
1206
+ this.host.eventBus.emit('tool:get:object:pixi', req);
1207
+ return !!req.pixiObject?._mb?.properties?.locked;
1208
+ });
1202
1209
  }
1203
1210
  }
1204
- const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget;
1211
+ const isNonResizableTarget = isFileTarget || isMindmapTarget || isMindmapOnlyGroupTarget || isLockedTarget;
1205
1212
 
1206
1213
  const left = Math.round(cssRect.left);
1207
1214
  const top = Math.round(cssRect.top);
@@ -114,6 +114,7 @@ export class HandlesEventBridge {
114
114
  [Events.UI.ZoomPercent, () => this.host.update()],
115
115
  [Events.Tool.PanUpdate, () => this.host.update()],
116
116
  [Events.Viewport.Changed, () => this.host.update()],
117
+ [Events.Object.TransformUpdated, () => this.host.update()],
117
118
  [Events.History.Changed, (data) => {
118
119
  if (data?.lastUndone || data?.lastRedone) {
119
120
  this.host._endGroupRotationPreview();