@sequent-org/moodboard 1.4.51 → 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.
- package/package.json +1 -1
- package/src/core/bootstrap/CoreInitializer.js +3 -0
- package/src/core/flows/ObjectLifecycleFlow.js +4 -2
- package/src/objects/FrameObject.js +171 -32
- package/src/services/FrameService.js +55 -9
- package/src/services/ImageAttachmentService.js +260 -0
- package/src/tools/object-tools/placement/GhostController.js +2 -2
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +9 -1
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +13 -4
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +15 -10
- package/src/ui/FramePropertiesPanel.js +150 -25
- package/src/ui/ImagePropertiesPanel.js +8 -0
- package/src/ui/styles/panels.css +43 -3
- package/src/ui/styles/workspace.css +1 -1
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { BoardService } from '../../services/BoardService.js';
|
|
|
4
4
|
import { ZoomPanController } from '../../services/ZoomPanController.js';
|
|
5
5
|
import { ZOrderManager } from '../../services/ZOrderManager.js';
|
|
6
6
|
import { FrameService } from '../../services/FrameService.js';
|
|
7
|
+
import { ImageAttachmentService } from '../../services/ImageAttachmentService.js';
|
|
7
8
|
import { Events } from '../events/Events.js';
|
|
8
9
|
|
|
9
10
|
export async function initializeCore(core) {
|
|
@@ -20,6 +21,8 @@ export async function initializeCore(core) {
|
|
|
20
21
|
core.zOrder.attach();
|
|
21
22
|
core.frameService = new FrameService(core.eventBus, core.pixi, core.state);
|
|
22
23
|
core.frameService.attach();
|
|
24
|
+
core.imageAttachmentService = new ImageAttachmentService(core.eventBus, core.pixi, core.state);
|
|
25
|
+
core.imageAttachmentService.attach();
|
|
23
26
|
|
|
24
27
|
setupViewportResize(core);
|
|
25
28
|
|
|
@@ -169,12 +169,14 @@ function tryCreateConnectorStyleCommand(core, object, objectId, updates) {
|
|
|
169
169
|
const { style, start, end } = updates.properties;
|
|
170
170
|
|
|
171
171
|
// Проверяем, что в updates.properties только style/start/end (и нет посторонних ключей)
|
|
172
|
-
const allowedKeys = new Set(['style', 'start', 'end', 'locked']);
|
|
172
|
+
const allowedKeys = new Set(['style', 'start', 'end', 'locked', 'lockMode', 'lockedByFrame']);
|
|
173
173
|
const hasOtherKeys = Object.keys(updates.properties).some(k => !allowedKeys.has(k));
|
|
174
174
|
if (hasOtherKeys) return false;
|
|
175
175
|
// Должно быть хотя бы одно из: style, start, end
|
|
176
176
|
if (!style && start === undefined && end === undefined
|
|
177
|
-
&& updates.properties.locked === undefined
|
|
177
|
+
&& updates.properties.locked === undefined
|
|
178
|
+
&& updates.properties.lockMode === undefined
|
|
179
|
+
&& updates.properties.lockedByFrame === undefined) return false;
|
|
178
180
|
|
|
179
181
|
const commandUpdates = {};
|
|
180
182
|
if (style !== undefined) commandUpdates.style = style;
|
|
@@ -17,10 +17,10 @@ export class FrameObject {
|
|
|
17
17
|
this.height = this.objectData.height || 100;
|
|
18
18
|
// Берем стили рамки из CSS-переменных, с дефолтом
|
|
19
19
|
const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
|
|
20
|
-
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '
|
|
20
|
+
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '5') : 5;
|
|
21
21
|
const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
|
|
22
22
|
const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
|
|
23
|
-
this.borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth :
|
|
23
|
+
this.borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 5;
|
|
24
24
|
// Используем backgroundColor из данных объекта, если есть, иначе белый
|
|
25
25
|
this.fillColor = this.objectData.backgroundColor || this.objectData.properties?.backgroundColor || 0xFFFFFF;
|
|
26
26
|
// Режим заливки: solid | solid-bordered | outline
|
|
@@ -50,6 +50,10 @@ export class FrameObject {
|
|
|
50
50
|
// Под-контейнер: масштаб компенсирует зум, поэтому заголовок всегда одного размера на экране
|
|
51
51
|
this.titleLayer = new PIXI.Container();
|
|
52
52
|
this.titleLayer.eventMode = 'none'; // не перехватывать указатель
|
|
53
|
+
const _frameObjectId = this.objectData.id || '';
|
|
54
|
+
if (_frameObjectId) {
|
|
55
|
+
this.titleLayer.name = `mb-frame-title-${_frameObjectId}`;
|
|
56
|
+
}
|
|
53
57
|
|
|
54
58
|
this.titleBg = new PIXI.Graphics();
|
|
55
59
|
this.titleLayer.addChild(this.titleBg);
|
|
@@ -60,7 +64,7 @@ export class FrameObject {
|
|
|
60
64
|
fill: 0x333333,
|
|
61
65
|
fontWeight: '500'
|
|
62
66
|
});
|
|
63
|
-
this.titleText.anchor.set(0, 0);
|
|
67
|
+
this.titleText.anchor.set(0, 0.5);
|
|
64
68
|
this.titleLayer.addChild(this.titleText);
|
|
65
69
|
|
|
66
70
|
this.container.addChild(this.titleLayer);
|
|
@@ -141,7 +145,10 @@ export class FrameObject {
|
|
|
141
145
|
|
|
142
146
|
_onSelectionAdd(data) {
|
|
143
147
|
const myId = this.objectData?.id ?? this.container?._mb?.objectId;
|
|
144
|
-
if (data?.object
|
|
148
|
+
if (data?.object !== myId) return;
|
|
149
|
+
// В outline цветная рамка — основной визуал фрейма, не дублирует синюю рамку выделения.
|
|
150
|
+
if (this.bgMode === 'outline') return;
|
|
151
|
+
this.setBorderVisible(false);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
_onSelectionRemove(data) {
|
|
@@ -276,37 +283,29 @@ export class FrameObject {
|
|
|
276
283
|
const fillColor = typeof color === 'number' ? color : 0xFFFFFF;
|
|
277
284
|
|
|
278
285
|
if (bgMode === 'solid') {
|
|
279
|
-
// Сплошная заливка
|
|
280
|
-
if (showStroke) {
|
|
281
|
-
try {
|
|
282
|
-
g.lineStyle({ width: this.borderWidth, color: this.strokeColor, alpha: 1, alignment: 1 });
|
|
283
|
-
} catch (e) {
|
|
284
|
-
g.lineStyle(this.borderWidth, this.strokeColor, 1);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
286
|
+
// Сплошная заливка без собственной рамки (рамка выделения — отдельно)
|
|
287
287
|
g.beginFill(fillColor, 1);
|
|
288
288
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
289
289
|
g.endFill();
|
|
290
290
|
} else if (bgMode === 'solid-bordered') {
|
|
291
|
-
//
|
|
291
|
+
// Фон = выбранный цвет; рамка = S=100,V=50 от того же тона палитры
|
|
292
|
+
const borderColor = this._pickBorderColor(fillColor);
|
|
292
293
|
if (showStroke) {
|
|
293
294
|
try {
|
|
294
|
-
g.lineStyle({ width: this.borderWidth
|
|
295
|
+
g.lineStyle({ width: this.borderWidth, color: borderColor, alpha: 1, alignment: 0 });
|
|
295
296
|
} catch (e) {
|
|
296
|
-
g.lineStyle(this.borderWidth
|
|
297
|
+
g.lineStyle(this.borderWidth, borderColor, 1);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
g.beginFill(
|
|
300
|
+
g.beginFill(fillColor, 1);
|
|
300
301
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
301
302
|
g.endFill();
|
|
302
303
|
} else {
|
|
303
|
-
// outline: прозрачный фон, рамка цвета выбранного цвета
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
g.lineStyle(this.borderWidth, fillColor, 1);
|
|
309
|
-
}
|
|
304
|
+
// outline: прозрачный фон, рамка цвета выбранного цвета (всегда видна, в т.ч. при фокусе)
|
|
305
|
+
try {
|
|
306
|
+
g.lineStyle({ width: this.borderWidth, color: fillColor, alpha: 1, alignment: 1 });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
g.lineStyle(this.borderWidth, fillColor, 1);
|
|
310
309
|
}
|
|
311
310
|
g.beginFill(0x000000, 0);
|
|
312
311
|
g.drawRoundedRect(0, 0, Math.max(0, width), Math.max(0, height), this.cornerRadius);
|
|
@@ -352,7 +351,7 @@ export class FrameObject {
|
|
|
352
351
|
|
|
353
352
|
// Высота подложки в базовых пикселях: baseFontSize + 4px сверху + 4px снизу
|
|
354
353
|
const labelBaseH = this.baseFontSize + 8;
|
|
355
|
-
const gap =
|
|
354
|
+
const gap = 5; // зазор между нижним краем подписи и верхней границей фрейма
|
|
356
355
|
|
|
357
356
|
// Позиционируем над фреймом (y=0 — верхний край фрейма в локальных координатах контейнера)
|
|
358
357
|
this.titleLayer.x = 0;
|
|
@@ -372,6 +371,143 @@ export class FrameObject {
|
|
|
372
371
|
this._redrawTitleBg();
|
|
373
372
|
}
|
|
374
373
|
|
|
374
|
+
/**
|
|
375
|
+
* YIQ-контраст: возвращает 0x000000 или 0xFFFFFF — читаемый цвет на заданном фоне
|
|
376
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
377
|
+
* @returns {number}
|
|
378
|
+
*/
|
|
379
|
+
_pickContrastColor(hexInt) {
|
|
380
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
381
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
382
|
+
const b = hexInt & 0xFF;
|
|
383
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
384
|
+
const darkness = (1 - yiq / 255) * 100;
|
|
385
|
+
return darkness >= 35 ? 0xFFFFFF : 0x000000;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Цвет рамки в режиме solid-bordered.
|
|
390
|
+
* Крайне правый по горизонтали (S=100) и по середине вертикали (V=50)
|
|
391
|
+
* в палитре того же тона, что и фон.
|
|
392
|
+
* Белый фон: крайне левый по горизонтали (S=0), середина по вертикали (V=50) → нейтральный серый.
|
|
393
|
+
* Прочие ахроматические цвета (серый, чёрный — s=0): чёрный.
|
|
394
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
395
|
+
* @returns {number}
|
|
396
|
+
*/
|
|
397
|
+
_pickBorderColor(hexInt) {
|
|
398
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
399
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
400
|
+
const b = hexInt & 0xFF;
|
|
401
|
+
if (r === 255 && g === 255 && b === 255) {
|
|
402
|
+
return 0x808080;
|
|
403
|
+
}
|
|
404
|
+
const { h, s } = this._rgbToHsv(r, g, b);
|
|
405
|
+
if (s === 0) {
|
|
406
|
+
return 0x000000;
|
|
407
|
+
}
|
|
408
|
+
return this._hsvToHex(h, 100, 50);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Цвет текста заголовка фрейма.
|
|
413
|
+
* Для почти-белых тонированных фонов (яркость < 5 по шкале 0=белый…100=чёрный,
|
|
414
|
+
* но не чистый белый) берёт тон из той же палитры с макс. насыщенностью (S=100, V=50).
|
|
415
|
+
* Иначе — обычный YIQ-контраст (чёрный/белый).
|
|
416
|
+
* @param {number} hexInt Цвет фона (PIXI hex)
|
|
417
|
+
* @returns {number}
|
|
418
|
+
*/
|
|
419
|
+
_pickTitleTextColor(hexInt) {
|
|
420
|
+
const r = (hexInt >> 16) & 0xFF;
|
|
421
|
+
const g = (hexInt >> 8) & 0xFF;
|
|
422
|
+
const b = hexInt & 0xFF;
|
|
423
|
+
|
|
424
|
+
// Чистый белый фон — исключение: чёрный текст
|
|
425
|
+
if (r === 255 && g === 255 && b === 255) {
|
|
426
|
+
return 0x000000;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Яркость по шкале 0 (белый) → 100 (чёрный) через HSL-lightness
|
|
430
|
+
const darkness = 100 - ((Math.max(r, g, b) + Math.min(r, g, b)) / 2 / 255) * 100;
|
|
431
|
+
if (darkness >= 5) {
|
|
432
|
+
return this._pickContrastColor(hexInt);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Почти-белый тонированный фон: та же палитра, правый край по центру (S=100, V=50)
|
|
436
|
+
const { h } = this._rgbToHsv(r, g, b);
|
|
437
|
+
return this._hsvToHex(h, 100, 50);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {number} r 0..255
|
|
442
|
+
* @param {number} g 0..255
|
|
443
|
+
* @param {number} b 0..255
|
|
444
|
+
* @returns {{ h: number, s: number, v: number }} h 0..360, s/v 0..100
|
|
445
|
+
*/
|
|
446
|
+
_rgbToHsv(r, g, b) {
|
|
447
|
+
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
448
|
+
const max = Math.max(rn, gn, bn);
|
|
449
|
+
const min = Math.min(rn, gn, bn);
|
|
450
|
+
const d = max - min;
|
|
451
|
+
let h = 0;
|
|
452
|
+
if (d !== 0) {
|
|
453
|
+
if (max === rn) {
|
|
454
|
+
h = ((gn - bn) / d) % 6;
|
|
455
|
+
} else if (max === gn) {
|
|
456
|
+
h = (bn - rn) / d + 2;
|
|
457
|
+
} else {
|
|
458
|
+
h = (rn - gn) / d + 4;
|
|
459
|
+
}
|
|
460
|
+
h *= 60;
|
|
461
|
+
if (h < 0) {
|
|
462
|
+
h += 360;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const s = max === 0 ? 0 : (d / max) * 100;
|
|
466
|
+
return { h, s, v: max * 100 };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {number} h 0..360
|
|
471
|
+
* @param {number} s 0..100
|
|
472
|
+
* @param {number} v 0..100
|
|
473
|
+
* @returns {number} PIXI hex
|
|
474
|
+
*/
|
|
475
|
+
_hsvToHex(h, s, v) {
|
|
476
|
+
const sn = s / 100, vn = v / 100;
|
|
477
|
+
const c = vn * sn;
|
|
478
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
479
|
+
const m = vn - c;
|
|
480
|
+
let rp = 0, gp = 0, bp = 0;
|
|
481
|
+
if (h < 60) {
|
|
482
|
+
rp = c; gp = x;
|
|
483
|
+
} else if (h < 120) {
|
|
484
|
+
rp = x; gp = c;
|
|
485
|
+
} else if (h < 180) {
|
|
486
|
+
gp = c; bp = x;
|
|
487
|
+
} else if (h < 240) {
|
|
488
|
+
gp = x; bp = c;
|
|
489
|
+
} else if (h < 300) {
|
|
490
|
+
rp = x; bp = c;
|
|
491
|
+
} else {
|
|
492
|
+
rp = c; bp = x;
|
|
493
|
+
}
|
|
494
|
+
const r = Math.round((rp + m) * 255);
|
|
495
|
+
const g = Math.round((gp + m) * 255);
|
|
496
|
+
const b = Math.round((bp + m) * 255);
|
|
497
|
+
return (r << 16) | (g << 8) | b;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Возвращает цвета плашки заголовка в формате CSS-строк для HTML-инпута
|
|
502
|
+
* @returns {{ bgCss: string, textCss: string }}
|
|
503
|
+
*/
|
|
504
|
+
getTitleColors() {
|
|
505
|
+
const bg = this.fillColor;
|
|
506
|
+
const text = this._pickTitleTextColor(bg);
|
|
507
|
+
const toHex = (n) => '#' + n.toString(16).padStart(6, '0');
|
|
508
|
+
return { bgCss: toHex(bg), textCss: toHex(text) };
|
|
509
|
+
}
|
|
510
|
+
|
|
375
511
|
/**
|
|
376
512
|
* Нарисовать скруглённую подложку под текущую ширину текста
|
|
377
513
|
*/
|
|
@@ -381,6 +517,13 @@ export class FrameObject {
|
|
|
381
517
|
const padH = 8; // горизонтальный отступ с каждой стороны
|
|
382
518
|
const padV = 4; // вертикальный отступ с каждой стороны
|
|
383
519
|
|
|
520
|
+
// Фон плашки = цвет фрейма (fill или border в зависимости от bgMode — всегда this.fillColor)
|
|
521
|
+
const bgColor = typeof this.fillColor === 'number' ? this.fillColor : 0xFFFFFF;
|
|
522
|
+
const textColor = this._pickTitleTextColor(bgColor);
|
|
523
|
+
|
|
524
|
+
// Обновляем цвет текста под контраст фона
|
|
525
|
+
this.titleText.style.fill = textColor;
|
|
526
|
+
|
|
384
527
|
// Измеряем текст в базовых единицах
|
|
385
528
|
const style = new PIXI.TextStyle({
|
|
386
529
|
fontFamily: this.titleText.style.fontFamily,
|
|
@@ -394,18 +537,14 @@ export class FrameObject {
|
|
|
394
537
|
|
|
395
538
|
const g = this.titleBg;
|
|
396
539
|
g.clear();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch (_) {
|
|
400
|
-
g.lineStyle(1, this.strokeColor, 1);
|
|
401
|
-
}
|
|
402
|
-
g.beginFill(0xFFFFFF, 1);
|
|
540
|
+
g.lineStyle(0);
|
|
541
|
+
g.beginFill(bgColor, 1);
|
|
403
542
|
g.drawRoundedRect(0, 0, bgW, bgH, 6);
|
|
404
543
|
g.endFill();
|
|
405
544
|
|
|
406
|
-
// Текст внутри подложки
|
|
545
|
+
// Текст внутри подложки — по вертикали по центру (anchor.y = 0.5)
|
|
407
546
|
this.titleText.x = padH;
|
|
408
|
-
this.titleText.y =
|
|
547
|
+
this.titleText.y = Math.round(bgH / 2);
|
|
409
548
|
}
|
|
410
549
|
|
|
411
550
|
/**
|
|
@@ -86,14 +86,40 @@ export class FrameService {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
const lockedChanged = data.updates.properties.locked !== undefined;
|
|
90
|
+
const lockModeChanged = data.updates.properties.lockMode !== undefined;
|
|
91
|
+
if (lockedChanged || lockModeChanged) {
|
|
92
|
+
const obj = this.state.state.objects.find(o => o.id === data.objectId);
|
|
93
|
+
if (obj && obj.type === 'frame') {
|
|
94
|
+
const newLocked = lockedChanged
|
|
95
|
+
? !!data.updates.properties.locked
|
|
96
|
+
: !!(obj.properties && obj.properties.locked);
|
|
97
|
+
const newLockMode = lockModeChanged
|
|
98
|
+
? data.updates.properties.lockMode
|
|
99
|
+
: (obj.properties && obj.properties.lockMode) || 'frame';
|
|
100
|
+
const children = this._getFrameChildren(obj.id);
|
|
101
|
+
for (const childId of children) {
|
|
102
|
+
const childObj = this.state.state.objects.find(o => o.id === childId);
|
|
103
|
+
if (!childObj) continue;
|
|
104
|
+
childObj.properties = childObj.properties || {};
|
|
105
|
+
if (newLocked && newLockMode === 'frame-and-content') {
|
|
106
|
+
childObj.properties.locked = true;
|
|
107
|
+
childObj.properties.lockedByFrame = true;
|
|
108
|
+
} else if (childObj.properties.lockedByFrame) {
|
|
109
|
+
childObj.properties.locked = false;
|
|
110
|
+
delete childObj.properties.lockedByFrame;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.state.markDirty();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
89
116
|
};
|
|
90
117
|
this.eventBus.on(Events.Object.StateChanged, this._onStateChanged);
|
|
91
118
|
|
|
92
|
-
this._onDragStart = (data) => {
|
|
119
|
+
this. _onDragStart = (data) => {
|
|
93
120
|
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
94
121
|
if (moved && moved.type === 'frame') {
|
|
95
|
-
|
|
96
|
-
this.pixi.setFrameFill(moved.id, moved.width, moved.height, 0xFAFAFA);
|
|
122
|
+
this._frameDragOriginalFill = moved.backgroundColor ?? moved.properties?.backgroundColor ?? 0xFFFFFF;
|
|
97
123
|
// Cнимок стартовых позиций по центру PIXI
|
|
98
124
|
const fp = this.pixi.objects.get(moved.id);
|
|
99
125
|
this._frameDragFrameStart = { x: fp?.x || 0, y: fp?.y || 0 };
|
|
@@ -157,18 +183,25 @@ export class FrameService {
|
|
|
157
183
|
const movedObj = this.state.state.objects.find(o => o.id === data.object);
|
|
158
184
|
if (!movedObj) return;
|
|
159
185
|
if (movedObj.type === 'frame') {
|
|
160
|
-
this.
|
|
186
|
+
const restoreFill = this._frameDragOriginalFill ?? movedObj.backgroundColor ?? movedObj.properties?.backgroundColor ?? 0xFFFFFF;
|
|
187
|
+
this.pixi.setFrameFill(movedObj.id, movedObj.width, movedObj.height, restoreFill);
|
|
161
188
|
}
|
|
162
189
|
this._recomputeFrameAttachment(movedObj.id);
|
|
163
190
|
this._forceFramesBelow();
|
|
164
191
|
this._frameDragFrameStart = null;
|
|
165
192
|
this._frameDragChildStart = null;
|
|
193
|
+
this._frameDragOriginalFill = null;
|
|
166
194
|
if (this._frameHoverId) {
|
|
167
195
|
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
168
196
|
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
169
|
-
if (prev)
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
if (prev) {
|
|
198
|
+
const hoverOriginal = this._frameHoverOriginalFill?.get(this._frameHoverId)
|
|
199
|
+
?? prev.backgroundColor ?? prev.properties?.backgroundColor ?? 0xFFFFFF;
|
|
200
|
+
this.pixi.setFrameFill(prev.id, prev.width, prev.height, hoverOriginal);
|
|
201
|
+
}
|
|
202
|
+
this._frameHoverId = null;
|
|
203
|
+
this._frameHoverOriginalFill = null;
|
|
204
|
+
}
|
|
172
205
|
};
|
|
173
206
|
this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
|
|
174
207
|
}
|
|
@@ -192,7 +225,9 @@ export class FrameService {
|
|
|
192
225
|
this._onDragEnd = null;
|
|
193
226
|
this._frameDragFrameStart = null;
|
|
194
227
|
this._frameDragChildStart = null;
|
|
228
|
+
this._frameDragOriginalFill = null;
|
|
195
229
|
this._frameHoverId = null;
|
|
230
|
+
this._frameHoverOriginalFill = null;
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
_getFrameChildren(frameId) {
|
|
@@ -234,11 +269,22 @@ export class FrameService {
|
|
|
234
269
|
if (hoverId !== this._frameHoverId) {
|
|
235
270
|
if (this._frameHoverId) {
|
|
236
271
|
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
237
|
-
if (prev)
|
|
272
|
+
if (prev) {
|
|
273
|
+
const origFill = this._frameHoverOriginalFill?.get(this._frameHoverId)
|
|
274
|
+
?? prev.backgroundColor ?? prev.properties?.backgroundColor ?? 0xFFFFFF;
|
|
275
|
+
this.pixi.setFrameFill(prev.id, prev.width, prev.height, origFill);
|
|
276
|
+
}
|
|
277
|
+
this._frameHoverOriginalFill?.delete(this._frameHoverId);
|
|
238
278
|
}
|
|
239
279
|
if (hoverId) {
|
|
240
280
|
const cur = frames.find(fr => fr.id === hoverId);
|
|
241
|
-
if (cur)
|
|
281
|
+
if (cur) {
|
|
282
|
+
if (!this._frameHoverOriginalFill) this._frameHoverOriginalFill = new Map();
|
|
283
|
+
if (!this._frameHoverOriginalFill.has(hoverId)) {
|
|
284
|
+
this._frameHoverOriginalFill.set(hoverId, cur.backgroundColor ?? cur.properties?.backgroundColor ?? 0xFFFFFF);
|
|
285
|
+
}
|
|
286
|
+
this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xFAFAFA);
|
|
287
|
+
}
|
|
242
288
|
}
|
|
243
289
|
this._frameHoverId = hoverId || null;
|
|
244
290
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
const IMAGE_TYPES = new Set(['image', 'revit-screenshot-img', 'model3d-screenshot-img']);
|
|
4
|
+
|
|
5
|
+
function isImageType(type) { return IMAGE_TYPES.has(type); }
|
|
6
|
+
function isDrawingType(type) { return type === 'drawing'; }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Проверяет, что прямоугольник inner целиком вмещается в outer.
|
|
10
|
+
* Координаты — мировые (top-left + size).
|
|
11
|
+
*/
|
|
12
|
+
function isFullyContained(inner, outer) {
|
|
13
|
+
return inner.x >= outer.x &&
|
|
14
|
+
inner.y >= outer.y &&
|
|
15
|
+
(inner.x + inner.w) <= (outer.x + outer.w) &&
|
|
16
|
+
(inner.y + inner.h) <= (outer.y + outer.h);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getObjRect(obj) {
|
|
20
|
+
return {
|
|
21
|
+
x: obj.position?.x ?? 0,
|
|
22
|
+
y: obj.position?.y ?? 0,
|
|
23
|
+
w: obj.width ?? 0,
|
|
24
|
+
h: obj.height ?? 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Управляет привязкой drawing-объектов к изображениям.
|
|
30
|
+
*
|
|
31
|
+
* Правила:
|
|
32
|
+
* - Привязка происходит только если drawing целиком внутри изображения.
|
|
33
|
+
* - Фрейм первичен: если у drawing уже есть properties.frameId — imageId не ставим.
|
|
34
|
+
* - Привязанный drawing перемещается вместе с изображением при drag.
|
|
35
|
+
* - При resize изображения пересчитываем, какие drawing остаются внутри.
|
|
36
|
+
*/
|
|
37
|
+
export class ImageAttachmentService {
|
|
38
|
+
constructor(eventBus, pixi, state) {
|
|
39
|
+
this.eventBus = eventBus;
|
|
40
|
+
this.pixi = pixi;
|
|
41
|
+
this.state = state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Вспомогательные ──────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Находит id изображения, полностью вмещающего drawing.
|
|
48
|
+
* Если несколько — берём с наибольшим zIndex PIXI (верхнее).
|
|
49
|
+
* Если у drawing есть frameId — возвращает null (фрейм первичен).
|
|
50
|
+
*/
|
|
51
|
+
_findHostImage(drawingObj) {
|
|
52
|
+
if (drawingObj.properties?.frameId) return null;
|
|
53
|
+
|
|
54
|
+
const dr = getObjRect(drawingObj);
|
|
55
|
+
const images = (this.state.state.objects || []).filter(o => isImageType(o.type));
|
|
56
|
+
const candidates = images.filter(img => isFullyContained(dr, getObjRect(img)));
|
|
57
|
+
if (!candidates.length) return null;
|
|
58
|
+
|
|
59
|
+
candidates.sort((a, b) => {
|
|
60
|
+
const pa = this.pixi.objects.get(a.id);
|
|
61
|
+
const pb = this.pixi.objects.get(b.id);
|
|
62
|
+
return (pb?.zIndex ?? 0) - (pa?.zIndex ?? 0);
|
|
63
|
+
});
|
|
64
|
+
return candidates[0].id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_getImageChildren(imageId) {
|
|
68
|
+
return (this.state.state.objects || [])
|
|
69
|
+
.filter(o => o.properties?.imageId === imageId)
|
|
70
|
+
.map(o => o.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Логика пересчёта ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** Пересчитывает imageId для одного drawing по текущим bounds. */
|
|
76
|
+
_recomputeImageAttachment(objectId) {
|
|
77
|
+
const obj = (this.state.state.objects || []).find(o => o.id === objectId);
|
|
78
|
+
if (!obj || !isDrawingType(obj.type)) return;
|
|
79
|
+
|
|
80
|
+
const newImageId = this._findHostImage(obj);
|
|
81
|
+
const prevImageId = obj.properties?.imageId ?? null;
|
|
82
|
+
if (newImageId === prevImageId) return;
|
|
83
|
+
|
|
84
|
+
obj.properties = obj.properties || {};
|
|
85
|
+
if (newImageId) {
|
|
86
|
+
obj.properties.imageId = newImageId;
|
|
87
|
+
} else {
|
|
88
|
+
delete obj.properties.imageId;
|
|
89
|
+
}
|
|
90
|
+
this.state.markDirty();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** При создании изображения прикрепляет все drawing, оказавшиеся внутри. */
|
|
94
|
+
_attachDrawingsToNewImage(imageId) {
|
|
95
|
+
const imageObj = (this.state.state.objects || []).find(o => o.id === imageId);
|
|
96
|
+
if (!imageObj) return;
|
|
97
|
+
const ir = getObjRect(imageObj);
|
|
98
|
+
let changed = false;
|
|
99
|
+
|
|
100
|
+
for (const obj of this.state.state.objects || []) {
|
|
101
|
+
if (!isDrawingType(obj.type)) continue;
|
|
102
|
+
if (obj.properties?.frameId) continue;
|
|
103
|
+
if (obj.properties?.imageId) continue;
|
|
104
|
+
if (isFullyContained(getObjRect(obj), ir)) {
|
|
105
|
+
obj.properties = obj.properties || {};
|
|
106
|
+
obj.properties.imageId = imageId;
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (changed) this.state.markDirty();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Пересчитывает все drawing относительно конкретного изображения.
|
|
115
|
+
* Вызывается после resize или drag изображения.
|
|
116
|
+
*/
|
|
117
|
+
_recomputeAllForImage(imageId) {
|
|
118
|
+
const imageObj = (this.state.state.objects || []).find(o => o.id === imageId);
|
|
119
|
+
if (!imageObj) return;
|
|
120
|
+
const ir = getObjRect(imageObj);
|
|
121
|
+
let changed = false;
|
|
122
|
+
|
|
123
|
+
for (const obj of this.state.state.objects || []) {
|
|
124
|
+
if (!isDrawingType(obj.type)) continue;
|
|
125
|
+
if (obj.properties?.frameId) continue;
|
|
126
|
+
|
|
127
|
+
const prevImageId = obj.properties?.imageId ?? null;
|
|
128
|
+
const inside = isFullyContained(getObjRect(obj), ir);
|
|
129
|
+
const attached = prevImageId === imageId;
|
|
130
|
+
|
|
131
|
+
if (inside && !attached && !prevImageId) {
|
|
132
|
+
obj.properties = obj.properties || {};
|
|
133
|
+
obj.properties.imageId = imageId;
|
|
134
|
+
changed = true;
|
|
135
|
+
} else if (!inside && attached) {
|
|
136
|
+
delete obj.properties.imageId;
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (changed) this.state.markDirty();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
attach() {
|
|
146
|
+
if (this._attached) return;
|
|
147
|
+
this._attached = true;
|
|
148
|
+
|
|
149
|
+
// Создан объект: изображение → прикрепляем вложенные drawing;
|
|
150
|
+
// drawing → проверяем попадание внутрь изображения.
|
|
151
|
+
this._onObjectCreated = ({ objectId, objectData }) => {
|
|
152
|
+
try {
|
|
153
|
+
if (!objectData) return;
|
|
154
|
+
if (isImageType(objectData.type)) {
|
|
155
|
+
this._attachDrawingsToNewImage(objectId);
|
|
156
|
+
} else if (isDrawingType(objectData.type)) {
|
|
157
|
+
this._recomputeImageAttachment(objectId);
|
|
158
|
+
}
|
|
159
|
+
} catch (_) { /* no-op */ }
|
|
160
|
+
};
|
|
161
|
+
this.eventBus.on(Events.Object.Created, this._onObjectCreated);
|
|
162
|
+
|
|
163
|
+
// DragStart изображения — сохраняем начальные позиции (PIXI-центры) детей.
|
|
164
|
+
this._onDragStart = (data) => {
|
|
165
|
+
const moved = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
166
|
+
if (!moved || !isImageType(moved.type)) return;
|
|
167
|
+
|
|
168
|
+
const p = this.pixi.objects.get(moved.id);
|
|
169
|
+
this._imgDragImgStart = { x: p?.x ?? 0, y: p?.y ?? 0 };
|
|
170
|
+
this._imgDragChildStart = new Map();
|
|
171
|
+
for (const childId of this._getImageChildren(moved.id)) {
|
|
172
|
+
const cp = this.pixi.objects.get(childId);
|
|
173
|
+
if (cp) this._imgDragChildStart.set(childId, { x: cp.x, y: cp.y });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
this.eventBus.on(Events.Tool.DragStart, this._onDragStart);
|
|
177
|
+
|
|
178
|
+
// DragUpdate изображения — тащим привязанные drawing.
|
|
179
|
+
this._onDragUpdate = (data) => {
|
|
180
|
+
const moved = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
181
|
+
if (!moved || !isImageType(moved.type)) return;
|
|
182
|
+
|
|
183
|
+
const p = this.pixi.objects.get(moved.id);
|
|
184
|
+
const start = this._imgDragImgStart ?? { x: p?.x ?? 0, y: p?.y ?? 0 };
|
|
185
|
+
const dx = (p?.x ?? 0) - start.x;
|
|
186
|
+
const dy = (p?.y ?? 0) - start.y;
|
|
187
|
+
|
|
188
|
+
for (const childId of this._getImageChildren(moved.id)) {
|
|
189
|
+
let startPos = this._imgDragChildStart?.get(childId);
|
|
190
|
+
const cp = this.pixi.objects.get(childId);
|
|
191
|
+
if (!startPos) {
|
|
192
|
+
if (cp) {
|
|
193
|
+
startPos = { x: cp.x - dx, y: cp.y - dy };
|
|
194
|
+
this._imgDragChildStart ??= new Map();
|
|
195
|
+
this._imgDragChildStart.set(childId, startPos);
|
|
196
|
+
} else {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const nx = startPos.x + dx;
|
|
201
|
+
const ny = startPos.y + dy;
|
|
202
|
+
if (cp) { cp.x = nx; cp.y = ny; }
|
|
203
|
+
const stObj = (this.state.state.objects || []).find(o => o.id === childId);
|
|
204
|
+
if (stObj) {
|
|
205
|
+
const hw = cp ? (cp.width ?? 0) / 2 : 0;
|
|
206
|
+
const hh = cp ? (cp.height ?? 0) / 2 : 0;
|
|
207
|
+
stObj.position.x = nx - hw;
|
|
208
|
+
stObj.position.y = ny - hh;
|
|
209
|
+
this.eventBus.emit(Events.Object.TransformUpdated, {
|
|
210
|
+
objectId: childId,
|
|
211
|
+
type: 'position',
|
|
212
|
+
position: { x: stObj.position.x, y: stObj.position.y },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
this.eventBus.on(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
218
|
+
|
|
219
|
+
// DragEnd: для drawing — пересчитываем imageId;
|
|
220
|
+
// для изображения — финализируем drag-state и пересчитываем вложенных.
|
|
221
|
+
this._onDragEnd = (data) => {
|
|
222
|
+
const movedObj = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
223
|
+
if (!movedObj) return;
|
|
224
|
+
|
|
225
|
+
if (isDrawingType(movedObj.type)) {
|
|
226
|
+
this._recomputeImageAttachment(movedObj.id);
|
|
227
|
+
} else if (isImageType(movedObj.type)) {
|
|
228
|
+
this._imgDragImgStart = null;
|
|
229
|
+
this._imgDragChildStart = null;
|
|
230
|
+
this._recomputeAllForImage(movedObj.id);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
this.eventBus.on(Events.Tool.DragEnd, this._onDragEnd);
|
|
234
|
+
|
|
235
|
+
// ResizeEnd изображения — пересчитываем, какие drawing остаются внутри.
|
|
236
|
+
this._onResizeEnd = (data) => {
|
|
237
|
+
const obj = (this.state.state.objects || []).find(o => o.id === data.object);
|
|
238
|
+
if (!obj || !isImageType(obj.type)) return;
|
|
239
|
+
this._recomputeAllForImage(obj.id);
|
|
240
|
+
};
|
|
241
|
+
this.eventBus.on(Events.Tool.ResizeEnd, this._onResizeEnd);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
detach() {
|
|
245
|
+
if (!this._attached) return;
|
|
246
|
+
this._attached = false;
|
|
247
|
+
if (this._onObjectCreated) this.eventBus.off(Events.Object.Created, this._onObjectCreated);
|
|
248
|
+
if (this._onDragStart) this.eventBus.off(Events.Tool.DragStart, this._onDragStart);
|
|
249
|
+
if (this._onDragUpdate) this.eventBus.off(Events.Tool.DragUpdate, this._onDragUpdate);
|
|
250
|
+
if (this._onDragEnd) this.eventBus.off(Events.Tool.DragEnd, this._onDragEnd);
|
|
251
|
+
if (this._onResizeEnd) this.eventBus.off(Events.Tool.ResizeEnd, this._onResizeEnd);
|
|
252
|
+
this._onObjectCreated = null;
|
|
253
|
+
this._onDragStart = null;
|
|
254
|
+
this._onDragUpdate = null;
|
|
255
|
+
this._onDragEnd = null;
|
|
256
|
+
this._onResizeEnd = null;
|
|
257
|
+
this._imgDragImgStart = null;
|
|
258
|
+
this._imgDragChildStart = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -378,10 +378,10 @@ export class GhostController {
|
|
|
378
378
|
const title = host.pending.properties?.title || 'Новый';
|
|
379
379
|
|
|
380
380
|
const rootStyles = (typeof window !== 'undefined') ? getComputedStyle(document.documentElement) : null;
|
|
381
|
-
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '
|
|
381
|
+
const cssBorderWidth = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-border-width') || '5') : 5;
|
|
382
382
|
const cssCornerRadius = rootStyles ? parseFloat(rootStyles.getPropertyValue('--frame-corner-radius') || '6') : 6;
|
|
383
383
|
const cssBorderColor = rootStyles ? rootStyles.getPropertyValue('--frame-border-color').trim() : '';
|
|
384
|
-
const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth :
|
|
384
|
+
const borderWidth = Number.isFinite(cssBorderWidth) ? cssBorderWidth : 5;
|
|
385
385
|
const cornerRadius = Number.isFinite(cssCornerRadius) ? cssCornerRadius : 6;
|
|
386
386
|
let strokeColor;
|
|
387
387
|
if (cssBorderColor && cssBorderColor.startsWith('#')) {
|
|
@@ -43,7 +43,15 @@ export class PlacementPayloadFactory {
|
|
|
43
43
|
type: 'frame',
|
|
44
44
|
id: 'frame',
|
|
45
45
|
position: { x, y },
|
|
46
|
-
properties: {
|
|
46
|
+
properties: {
|
|
47
|
+
width: Math.round(w),
|
|
48
|
+
height: Math.round(h),
|
|
49
|
+
title: 'Произвольный',
|
|
50
|
+
lockedAspect: false,
|
|
51
|
+
isArbitrary: true,
|
|
52
|
+
bgMode: 'solid',
|
|
53
|
+
backgroundColor: 0xFFFFFF,
|
|
54
|
+
}
|
|
47
55
|
});
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -50,16 +50,25 @@ export function openFrameTitleEditor(object, _create = false) {
|
|
|
50
50
|
? toScreenWithContainerOffset(titleLayer, view, 0, 0)
|
|
51
51
|
: { x: 0, y: 0 };
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// Габариты редактора = titleBg в экранных пикселях (с учётом зум-компенсации)
|
|
54
54
|
let inputWidth = 150;
|
|
55
|
+
let inputHeight = 22;
|
|
55
56
|
if (titleLayer && frameInstance.titleBg) {
|
|
56
57
|
const bgRight = toScreenWithContainerOffset(titleLayer, view, frameInstance.titleBg.width || 150, 0);
|
|
57
|
-
|
|
58
|
+
const bgBottom = toScreenWithContainerOffset(titleLayer, view, 0, frameInstance.titleBg.height || 22);
|
|
59
|
+
inputWidth = Math.max(1, Math.round(bgRight.x - screenPos.x));
|
|
60
|
+
inputHeight = Math.max(1, Math.round(bgBottom.y - screenPos.y));
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
const
|
|
61
|
-
|
|
63
|
+
const titleColors = typeof frameInstance.getTitleColors === 'function'
|
|
64
|
+
? frameInstance.getTitleColors()
|
|
65
|
+
: { bgCss: '#ffffff', textCss: '#333333' };
|
|
66
|
+
|
|
67
|
+
const wrapper = createFrameTitleEditorWrapper(titleColors.bgCss);
|
|
68
|
+
wrapper.id = `mb-frame-title-editor-${objectId}`;
|
|
69
|
+
const input = createFrameTitleEditorInput(currentTitle, titleColors.textCss);
|
|
62
70
|
wrapper.style.width = `${inputWidth}px`;
|
|
71
|
+
wrapper.style.height = `${inputHeight}px`;
|
|
63
72
|
|
|
64
73
|
wrapper.appendChild(input);
|
|
65
74
|
view.parentElement.appendChild(wrapper);
|
|
@@ -30,38 +30,43 @@ export function createFileNameEditorWrapper() {
|
|
|
30
30
|
return wrapper;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function createFrameTitleEditorWrapper() {
|
|
33
|
+
export function createFrameTitleEditorWrapper(bgColor = '#ffffff') {
|
|
34
34
|
const wrapper = document.createElement('div');
|
|
35
35
|
wrapper.className = 'moodboard-frame-title-editor';
|
|
36
36
|
wrapper.style.cssText = `
|
|
37
37
|
position: absolute;
|
|
38
38
|
z-index: 1000;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
background: ${bgColor};
|
|
43
|
+
border: none;
|
|
41
44
|
border-radius: 6px;
|
|
42
|
-
padding:
|
|
43
|
-
|
|
44
|
-
min-width: 100px;
|
|
45
|
-
max-width: 320px;
|
|
45
|
+
padding: 4px 8px;
|
|
46
|
+
overflow: hidden;
|
|
46
47
|
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
47
48
|
`;
|
|
48
49
|
return wrapper;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export function createFrameTitleEditorInput(title) {
|
|
52
|
+
export function createFrameTitleEditorInput(title, textColor = '#333333') {
|
|
52
53
|
const input = document.createElement('input');
|
|
53
54
|
input.type = 'text';
|
|
54
55
|
input.value = title;
|
|
55
56
|
input.style.cssText = `
|
|
57
|
+
box-sizing: border-box;
|
|
56
58
|
border: none;
|
|
57
59
|
outline: none;
|
|
58
60
|
background: transparent;
|
|
59
61
|
font-family: Inter, system-ui, -apple-system, Arial, sans-serif;
|
|
60
62
|
font-size: 14px;
|
|
61
63
|
font-weight: 500;
|
|
64
|
+
line-height: 14px;
|
|
65
|
+
height: 14px;
|
|
62
66
|
width: 100%;
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0;
|
|
69
|
+
color: ${textColor};
|
|
65
70
|
`;
|
|
66
71
|
return input;
|
|
67
72
|
}
|
|
@@ -42,19 +42,19 @@ function hexToHsv(hex) {
|
|
|
42
42
|
const FRAME_COLORS = [
|
|
43
43
|
{ name: 'Белый', hex: '#FFFFFF' },
|
|
44
44
|
{ name: 'Светло-серый', hex: '#EBEBEB' },
|
|
45
|
-
{ name: 'Серый', hex: '#
|
|
46
|
-
{ name: 'Светло-розовый', hex: '#
|
|
47
|
-
{ name: 'Розовый', hex: '#
|
|
48
|
-
{ name: 'Персиковый', hex: '#
|
|
49
|
-
{ name: 'Оранжевый', hex: '#
|
|
50
|
-
{ name: 'Светло-жёлтый',
|
|
51
|
-
{ name: 'Жёлтый', hex: '#
|
|
52
|
-
{ name: 'Светло-зелёный', hex: '#
|
|
53
|
-
{ name: 'Зелёный', hex: '#
|
|
54
|
-
{ name: 'Небесный', hex: '#
|
|
55
|
-
{ name: 'Голубой', hex: '#
|
|
56
|
-
{ name: 'Лавандовый', hex: '#
|
|
57
|
-
{ name: 'Фиолетовый', hex: '#
|
|
45
|
+
{ name: 'Серый', hex: '#E4E4E4' },
|
|
46
|
+
{ name: 'Светло-розовый', hex: '#FFEFEF' },
|
|
47
|
+
{ name: 'Розовый', hex: '#FDD8D8' },
|
|
48
|
+
{ name: 'Персиковый', hex: '#FFEDD5' },
|
|
49
|
+
{ name: 'Оранжевый', hex: '#FFD3A4' },
|
|
50
|
+
{ name: 'Светло-жёлтый', hex: '#FFFBE0' },
|
|
51
|
+
{ name: 'Жёлтый', hex: '#FCF3AF' },
|
|
52
|
+
{ name: 'Светло-зелёный', hex: '#E9F9EE' },
|
|
53
|
+
{ name: 'Зелёный', hex: '#CCEBD7' },
|
|
54
|
+
{ name: 'Небесный', hex: '#EDF6FF' },
|
|
55
|
+
{ name: 'Голубой', hex: '#CEE7FE' },
|
|
56
|
+
{ name: 'Лавандовый', hex: '#F5F2FF' },
|
|
57
|
+
{ name: 'Фиолетовый', hex: '#E4DEFC' },
|
|
58
58
|
];
|
|
59
59
|
|
|
60
60
|
const ICONS = {
|
|
@@ -75,6 +75,7 @@ const ICONS = {
|
|
|
75
75
|
bgBordered: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><rect x="3" y="3" width="16" height="16" rx="2.5" fill="#D1D5DB"/><rect x="3" y="3" width="16" height="16" rx="2.5" stroke="#374151" stroke-width="1.5"/></svg>',
|
|
76
76
|
bgOutline: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><rect x="3" y="3" width="16" height="16" rx="2.5" stroke="#374151" stroke-width="1.5"/></svg>',
|
|
77
77
|
eyedropper: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 20.5l4-4"/><path d="M6.5 17.5L17 7a2.5 2.5 0 0 0 0-3.5l-2-2a2.5 2.5 0 0 0-3.5 0L3 10l4 4z"/></svg>',
|
|
78
|
+
check: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
const RATIO_LABELS = {
|
|
@@ -241,8 +242,7 @@ export class FramePropertiesPanel {
|
|
|
241
242
|
|
|
242
243
|
const divider = this._makeDivider();
|
|
243
244
|
|
|
244
|
-
|
|
245
|
-
this._btn_lock.addEventListener('click', (e) => { e.stopPropagation(); this._toggleLocked(); });
|
|
245
|
+
const lockControl = this._makeLockControl();
|
|
246
246
|
|
|
247
247
|
const more = this._makeMoreButton();
|
|
248
248
|
|
|
@@ -251,7 +251,7 @@ export class FramePropertiesPanel {
|
|
|
251
251
|
panel.appendChild(rename);
|
|
252
252
|
panel.appendChild(eye);
|
|
253
253
|
panel.appendChild(divider);
|
|
254
|
-
panel.appendChild(
|
|
254
|
+
panel.appendChild(lockControl);
|
|
255
255
|
panel.appendChild(more);
|
|
256
256
|
|
|
257
257
|
this._lockableEls = [ratio, color, rename, eye, divider];
|
|
@@ -271,11 +271,81 @@ export class FramePropertiesPanel {
|
|
|
271
271
|
const btn = document.createElement('button');
|
|
272
272
|
btn.className = 'ipp-btn';
|
|
273
273
|
btn.title = title;
|
|
274
|
-
if (key) {
|
|
274
|
+
if (key) {
|
|
275
|
+
btn.id = 'fpp-btn-' + key;
|
|
276
|
+
btn.dataset.id = 'fpp-btn-' + key;
|
|
277
|
+
}
|
|
275
278
|
btn.innerHTML = iconHtml;
|
|
276
279
|
return btn;
|
|
277
280
|
}
|
|
278
281
|
|
|
282
|
+
_makeLockControl() {
|
|
283
|
+
const wrapper = document.createElement('div');
|
|
284
|
+
wrapper.className = 'ipp-btn-split-wrapper fpp-lock-wrap';
|
|
285
|
+
|
|
286
|
+
const mainBtn = document.createElement('button');
|
|
287
|
+
mainBtn.className = 'ipp-btn-split-main';
|
|
288
|
+
mainBtn.id = 'fpp-btn-lock';
|
|
289
|
+
mainBtn.dataset.id = 'fpp-btn-lock';
|
|
290
|
+
mainBtn.title = 'Заблокировать';
|
|
291
|
+
mainBtn.innerHTML = ICONS.unlock;
|
|
292
|
+
mainBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleLocked(); });
|
|
293
|
+
this._lockSplitMain = mainBtn;
|
|
294
|
+
|
|
295
|
+
const expandBtn = document.createElement('button');
|
|
296
|
+
expandBtn.className = 'ipp-btn-split-expand fpp-lock-chevron';
|
|
297
|
+
expandBtn.title = 'Режим блокировки';
|
|
298
|
+
expandBtn.innerHTML = ICONS.expand;
|
|
299
|
+
expandBtn.addEventListener('click', (e) => {
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
const isOpen = this._lockDropdown.classList.contains('is-open');
|
|
302
|
+
this._closeMenus();
|
|
303
|
+
if (!isOpen) {
|
|
304
|
+
this._lockDropdown.classList.add('is-open');
|
|
305
|
+
expandBtn.classList.add('is-expanded');
|
|
306
|
+
this._attachOutside();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
this._lockExpandBtn = expandBtn;
|
|
310
|
+
|
|
311
|
+
const dropdown = document.createElement('div');
|
|
312
|
+
dropdown.className = 'fpp-lock-dropdown';
|
|
313
|
+
this._lockDropdown = dropdown;
|
|
314
|
+
|
|
315
|
+
const lockModeItems = [
|
|
316
|
+
{ id: 'frame', label: 'Только фрейм' },
|
|
317
|
+
{ id: 'frame-and-content', label: 'Фрейм и содержимое' },
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
lockModeItems.forEach((item) => {
|
|
321
|
+
const btn = document.createElement('button');
|
|
322
|
+
btn.className = 'ipp-dropdown-item';
|
|
323
|
+
btn.id = 'fpp-lock-mode-' + item.id;
|
|
324
|
+
btn.dataset.lockMode = item.id;
|
|
325
|
+
|
|
326
|
+
const checkSpan = document.createElement('span');
|
|
327
|
+
checkSpan.className = 'fpp-lock-mode-check';
|
|
328
|
+
checkSpan.innerHTML = ICONS.check;
|
|
329
|
+
btn.appendChild(checkSpan);
|
|
330
|
+
|
|
331
|
+
const labelSpan = document.createElement('span');
|
|
332
|
+
labelSpan.textContent = item.label;
|
|
333
|
+
btn.appendChild(labelSpan);
|
|
334
|
+
|
|
335
|
+
btn.addEventListener('click', (e) => {
|
|
336
|
+
e.stopPropagation();
|
|
337
|
+
this._closeMenus();
|
|
338
|
+
this._applyLockMode(item.id);
|
|
339
|
+
});
|
|
340
|
+
dropdown.appendChild(btn);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
wrapper.appendChild(mainBtn);
|
|
344
|
+
wrapper.appendChild(expandBtn);
|
|
345
|
+
wrapper.appendChild(dropdown);
|
|
346
|
+
return wrapper;
|
|
347
|
+
}
|
|
348
|
+
|
|
279
349
|
_makeDivider() {
|
|
280
350
|
const div = document.createElement('div');
|
|
281
351
|
div.className = 'ipp-divider';
|
|
@@ -288,6 +358,7 @@ export class FramePropertiesPanel {
|
|
|
288
358
|
|
|
289
359
|
const btn = document.createElement('button');
|
|
290
360
|
btn.className = 'fpp-ratio-btn';
|
|
361
|
+
btn.id = 'fpp-ratio-btn';
|
|
291
362
|
btn.dataset.id = 'fpp-ratio-btn';
|
|
292
363
|
|
|
293
364
|
const textBox = document.createElement('span');
|
|
@@ -324,6 +395,7 @@ export class FramePropertiesPanel {
|
|
|
324
395
|
items.forEach((item) => {
|
|
325
396
|
const el = document.createElement('button');
|
|
326
397
|
el.className = 'ipp-dropdown-item';
|
|
398
|
+
el.id = 'fpp-ratio-item-' + item.id;
|
|
327
399
|
const ic = document.createElement('span');
|
|
328
400
|
ic.className = 'ipp-dropdown-icon';
|
|
329
401
|
ic.innerHTML = item.icon;
|
|
@@ -362,6 +434,7 @@ export class FramePropertiesPanel {
|
|
|
362
434
|
|
|
363
435
|
const colorButton = document.createElement('button');
|
|
364
436
|
colorButton.className = 'fpp-color-button';
|
|
437
|
+
colorButton.id = 'fpp-color-btn';
|
|
365
438
|
colorButton.addEventListener('click', (e) => {
|
|
366
439
|
e.stopPropagation();
|
|
367
440
|
this._toggleColorPopup(colorButton);
|
|
@@ -394,6 +467,7 @@ export class FramePropertiesPanel {
|
|
|
394
467
|
BG_TYPES.forEach(({ id, title, icon }) => {
|
|
395
468
|
const btn = document.createElement('button');
|
|
396
469
|
btn.className = 'fpp-bg-type-btn';
|
|
470
|
+
btn.id = 'fpp-bg-type-btn-' + id;
|
|
397
471
|
btn.title = title;
|
|
398
472
|
btn.dataset.bgMode = id;
|
|
399
473
|
btn.innerHTML = ICONS[icon];
|
|
@@ -425,6 +499,7 @@ export class FramePropertiesPanel {
|
|
|
425
499
|
// "None" swatch — transparent
|
|
426
500
|
const noneSwatch = document.createElement('button');
|
|
427
501
|
noneSwatch.className = 'fpp-color-swatch fpp-color-swatch--none';
|
|
502
|
+
noneSwatch.id = 'fpp-swatch-none';
|
|
428
503
|
noneSwatch.title = 'Прозрачный';
|
|
429
504
|
noneSwatch.dataset.colorHex = 'none';
|
|
430
505
|
noneSwatch.innerHTML = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><line x1="3" y1="17" x2="17" y2="3" stroke="#EF4444" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
@@ -440,6 +515,7 @@ export class FramePropertiesPanel {
|
|
|
440
515
|
FRAME_COLORS.forEach((color) => {
|
|
441
516
|
const btn = document.createElement('button');
|
|
442
517
|
btn.className = 'fpp-color-swatch';
|
|
518
|
+
btn.id = 'fpp-swatch-' + color.hex.replace('#', '').toLowerCase();
|
|
443
519
|
btn.title = color.name;
|
|
444
520
|
btn.dataset.colorHex = color.hex.toUpperCase();
|
|
445
521
|
btn.style.setProperty('--swatch-color', color.hex);
|
|
@@ -470,6 +546,7 @@ export class FramePropertiesPanel {
|
|
|
470
546
|
|
|
471
547
|
const addBtn = document.createElement('button');
|
|
472
548
|
addBtn.className = 'fpp-add-color-btn';
|
|
549
|
+
addBtn.id = 'fpp-add-color-btn';
|
|
473
550
|
addBtn.title = 'Добавить цвет';
|
|
474
551
|
addBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><line x1="7" y1="1" x2="7" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
475
552
|
addBtn.addEventListener('click', (e) => {
|
|
@@ -562,9 +639,18 @@ export class FramePropertiesPanel {
|
|
|
562
639
|
this.colorButton.classList.remove('fpp-color-button--none');
|
|
563
640
|
}
|
|
564
641
|
|
|
642
|
+
const targetBgMode = this._getCurrentBgMode();
|
|
643
|
+
|
|
644
|
+
this._bgTypeBtns.forEach((btn) => {
|
|
645
|
+
btn.classList.toggle('is-active', btn.dataset.bgMode === targetBgMode);
|
|
646
|
+
});
|
|
647
|
+
|
|
565
648
|
this.eventBus.emit(Events.Object.StateChanged, {
|
|
566
649
|
objectId: this.currentId,
|
|
567
|
-
updates: {
|
|
650
|
+
updates: {
|
|
651
|
+
backgroundColor: pixi,
|
|
652
|
+
properties: { bgMode: targetBgMode }
|
|
653
|
+
},
|
|
568
654
|
});
|
|
569
655
|
}
|
|
570
656
|
|
|
@@ -574,9 +660,10 @@ export class FramePropertiesPanel {
|
|
|
574
660
|
}
|
|
575
661
|
|
|
576
662
|
_setColorSectionDisabled(disabled) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
663
|
+
// Disabled logic removed as per requirements (all colour buttons remain active)
|
|
664
|
+
// if (this._colorSection) {
|
|
665
|
+
// this._colorSection.classList.toggle('is-disabled', disabled);
|
|
666
|
+
// }
|
|
580
667
|
}
|
|
581
668
|
|
|
582
669
|
_markNoneSelected() {
|
|
@@ -633,6 +720,7 @@ export class FramePropertiesPanel {
|
|
|
633
720
|
canvas.width = 212;
|
|
634
721
|
canvas.height = 148;
|
|
635
722
|
canvas.className = 'fpp-color-picker__canvas';
|
|
723
|
+
canvas.id = 'fpp-hsv-canvas';
|
|
636
724
|
this._hsvCanvas = canvas;
|
|
637
725
|
|
|
638
726
|
const cursor = document.createElement('div');
|
|
@@ -661,6 +749,7 @@ export class FramePropertiesPanel {
|
|
|
661
749
|
hueSlider.max = '360';
|
|
662
750
|
hueSlider.value = '0';
|
|
663
751
|
hueSlider.className = 'fpp-color-picker__hue-slider';
|
|
752
|
+
hueSlider.id = 'fpp-hue-slider';
|
|
664
753
|
this._hueSlider = hueSlider;
|
|
665
754
|
|
|
666
755
|
hueSlider.addEventListener('input', (e) => {
|
|
@@ -679,6 +768,7 @@ export class FramePropertiesPanel {
|
|
|
679
768
|
|
|
680
769
|
const eyedropper = document.createElement('button');
|
|
681
770
|
eyedropper.className = 'fpp-color-picker__eyedropper';
|
|
771
|
+
eyedropper.id = 'fpp-eyedropper-btn';
|
|
682
772
|
eyedropper.title = 'Пипетка';
|
|
683
773
|
eyedropper.innerHTML = ICONS.eyedropper;
|
|
684
774
|
eyedropper.addEventListener('click', (e) => {
|
|
@@ -694,6 +784,7 @@ export class FramePropertiesPanel {
|
|
|
694
784
|
const hexInput = document.createElement('input');
|
|
695
785
|
hexInput.type = 'text';
|
|
696
786
|
hexInput.className = 'fpp-color-picker__hex-input';
|
|
787
|
+
hexInput.id = 'fpp-hex-input';
|
|
697
788
|
hexInput.value = '#ffffff';
|
|
698
789
|
hexInput.maxLength = 7;
|
|
699
790
|
hexInput.spellcheck = false;
|
|
@@ -724,6 +815,7 @@ export class FramePropertiesPanel {
|
|
|
724
815
|
// Apply button
|
|
725
816
|
const applyBtn = document.createElement('button');
|
|
726
817
|
applyBtn.className = 'fpp-color-picker__apply';
|
|
818
|
+
applyBtn.id = 'fpp-picker-apply-btn';
|
|
727
819
|
applyBtn.textContent = 'Добавить';
|
|
728
820
|
applyBtn.addEventListener('click', (e) => {
|
|
729
821
|
e.stopPropagation();
|
|
@@ -867,6 +959,7 @@ export class FramePropertiesPanel {
|
|
|
867
959
|
const mainBtn = document.createElement('button');
|
|
868
960
|
mainBtn.className = 'ipp-btn';
|
|
869
961
|
mainBtn.title = 'Ещё';
|
|
962
|
+
mainBtn.id = 'fpp-btn-more';
|
|
870
963
|
mainBtn.dataset.id = 'fpp-btn-more';
|
|
871
964
|
mainBtn.innerHTML = ICONS.more;
|
|
872
965
|
|
|
@@ -911,6 +1004,7 @@ export class FramePropertiesPanel {
|
|
|
911
1004
|
|
|
912
1005
|
const btn = document.createElement('button');
|
|
913
1006
|
btn.className = 'ipp-dropdown-item';
|
|
1007
|
+
btn.id = 'fpp-more-' + item.id;
|
|
914
1008
|
btn.dataset.id = 'fpp-more-' + item.id;
|
|
915
1009
|
|
|
916
1010
|
if (item.icon) {
|
|
@@ -1052,6 +1146,8 @@ export class FramePropertiesPanel {
|
|
|
1052
1146
|
if (this._moreDropdown) { this._moreDropdown.classList.remove('is-open'); }
|
|
1053
1147
|
if (this._moreMainBtn) { this._moreMainBtn.classList.remove('is-active'); }
|
|
1054
1148
|
if (this._infoPopover) { this._infoPopover.classList.remove('is-open'); }
|
|
1149
|
+
if (this._lockDropdown) { this._lockDropdown.classList.remove('is-open'); }
|
|
1150
|
+
if (this._lockExpandBtn) { this._lockExpandBtn.classList.remove('is-expanded'); }
|
|
1055
1151
|
this._detachOutside();
|
|
1056
1152
|
}
|
|
1057
1153
|
|
|
@@ -1098,23 +1194,52 @@ export class FramePropertiesPanel {
|
|
|
1098
1194
|
_toggleLocked() {
|
|
1099
1195
|
if (!this.currentId) { return; }
|
|
1100
1196
|
const newLocked = !this._isLocked();
|
|
1197
|
+
const updates = { locked: newLocked };
|
|
1198
|
+
if (newLocked) {
|
|
1199
|
+
const data = this._getObjectData();
|
|
1200
|
+
updates.lockMode = (data && data.properties && data.properties.lockMode) || 'frame';
|
|
1201
|
+
}
|
|
1101
1202
|
this.eventBus.emit(Events.Object.StateChanged, {
|
|
1102
1203
|
objectId: this.currentId,
|
|
1103
|
-
updates: { properties:
|
|
1204
|
+
updates: { properties: updates },
|
|
1205
|
+
});
|
|
1206
|
+
this._updateLockUI();
|
|
1207
|
+
this.reposition();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
_applyLockMode(mode) {
|
|
1211
|
+
if (!this.currentId) { return; }
|
|
1212
|
+
const locked = this._isLocked();
|
|
1213
|
+
const updates = { lockMode: mode };
|
|
1214
|
+
if (!locked) { updates.locked = true; }
|
|
1215
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
1216
|
+
objectId: this.currentId,
|
|
1217
|
+
updates: { properties: updates },
|
|
1104
1218
|
});
|
|
1105
1219
|
this._updateLockUI();
|
|
1106
1220
|
this.reposition();
|
|
1107
1221
|
}
|
|
1108
1222
|
|
|
1109
1223
|
_updateLockUI() {
|
|
1110
|
-
if (!this.
|
|
1224
|
+
if (!this._lockSplitMain) { return; }
|
|
1111
1225
|
const locked = this._isLocked();
|
|
1112
1226
|
|
|
1113
1227
|
const data = this._getObjectData();
|
|
1114
1228
|
const hidden = !!(data && data.properties && data.properties.hidden);
|
|
1229
|
+
const lockMode = (data && data.properties && data.properties.lockMode) || 'frame';
|
|
1230
|
+
|
|
1231
|
+
this._lockSplitMain.innerHTML = locked ? ICONS.lock : ICONS.unlock;
|
|
1232
|
+
this._lockSplitMain.title = locked ? 'Разблокировать' : 'Заблокировать';
|
|
1115
1233
|
|
|
1116
|
-
this.
|
|
1117
|
-
|
|
1234
|
+
if (this._lockExpandBtn) {
|
|
1235
|
+
this._lockExpandBtn.style.display = locked ? 'none' : '';
|
|
1236
|
+
}
|
|
1237
|
+
if (this._lockDropdown) {
|
|
1238
|
+
if (locked) { this._lockDropdown.classList.remove('is-open'); }
|
|
1239
|
+
this._lockDropdown.querySelectorAll('[data-lock-mode]').forEach((btn) => {
|
|
1240
|
+
btn.classList.toggle('is-active', btn.dataset.lockMode === lockMode);
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1118
1243
|
|
|
1119
1244
|
if (this._eyeBtn) {
|
|
1120
1245
|
this._eyeBtn.dataset.hidden = hidden ? '1' : '0';
|
|
@@ -1050,6 +1050,14 @@ export class ImagePropertiesPanel {
|
|
|
1050
1050
|
|
|
1051
1051
|
if (item.id === 'copy') {
|
|
1052
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 });
|
|
1053
1061
|
} else if (item.id === 'duplicate') {
|
|
1054
1062
|
this._duplicateImage();
|
|
1055
1063
|
} else if (item.id === 'lock') {
|
package/src/ui/styles/panels.css
CHANGED
|
@@ -429,12 +429,12 @@
|
|
|
429
429
|
}
|
|
430
430
|
.fpp-color-swatch--none svg { pointer-events: none; }
|
|
431
431
|
|
|
432
|
-
/* Disabled colour section when transparent is selected */
|
|
433
|
-
.fpp-section--color.is-disabled .fpp-color-swatch:not(.fpp-color-swatch--none) {
|
|
432
|
+
/* Disabled colour section when transparent is selected (removed) */
|
|
433
|
+
/* .fpp-section--color.is-disabled .fpp-color-swatch:not(.fpp-color-swatch--none) {
|
|
434
434
|
opacity: 0.32;
|
|
435
435
|
pointer-events: none;
|
|
436
436
|
cursor: default;
|
|
437
|
-
}
|
|
437
|
+
} */
|
|
438
438
|
|
|
439
439
|
/* Custom colours row */
|
|
440
440
|
.fpp-custom-row {
|
|
@@ -1326,3 +1326,43 @@
|
|
|
1326
1326
|
color: #6B7280;
|
|
1327
1327
|
}
|
|
1328
1328
|
|
|
1329
|
+
/* Lock Mode Dropdown */
|
|
1330
|
+
.fpp-lock-wrap {
|
|
1331
|
+
position: relative;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.fpp-lock-dropdown {
|
|
1335
|
+
position: absolute;
|
|
1336
|
+
top: calc(100% + 6px);
|
|
1337
|
+
left: 50%;
|
|
1338
|
+
transform: translateX(-50%);
|
|
1339
|
+
background: #ffffff;
|
|
1340
|
+
border: 1px solid #E5E7EB;
|
|
1341
|
+
border-radius: 8px;
|
|
1342
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
|
|
1343
|
+
padding: 6px;
|
|
1344
|
+
display: none;
|
|
1345
|
+
flex-direction: column;
|
|
1346
|
+
min-width: 190px;
|
|
1347
|
+
z-index: 1001;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
.fpp-lock-dropdown.is-open {
|
|
1351
|
+
display: flex;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
.fpp-lock-mode-check {
|
|
1355
|
+
width: 14px;
|
|
1356
|
+
height: 14px;
|
|
1357
|
+
display: inline-flex;
|
|
1358
|
+
align-items: center;
|
|
1359
|
+
justify-content: center;
|
|
1360
|
+
color: #374151;
|
|
1361
|
+
flex-shrink: 0;
|
|
1362
|
+
opacity: 0;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.ipp-dropdown-item.is-active .fpp-lock-mode-check {
|
|
1366
|
+
opacity: 1;
|
|
1367
|
+
}
|
|
1368
|
+
|