@sequent-org/moodboard 1.4.49 → 1.4.51
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/commands/ApplyCropCommand.js +90 -0
- package/src/core/commands/UpdateFramePropertiesCommand.js +9 -2
- package/src/core/flows/ObjectLifecycleFlow.js +25 -6
- package/src/objects/FrameObject.js +90 -8
- package/src/objects/ImageObject.js +23 -7
- package/src/services/CropController.js +242 -0
- package/src/services/FrameService.js +32 -0
- package/src/tools/manager/ToolManagerLifecycle.js +4 -0
- package/src/tools/object-tools/DrawingTool.js +1 -0
- package/src/tools/object-tools/selection/TransformInteractionController.js +50 -0
- package/src/ui/CropOverlay.js +654 -0
- package/src/ui/FramePropertiesPanel.js +1061 -407
- package/src/ui/HtmlHandlesLayer.js +2 -0
- package/src/ui/ImagePropertiesPanel.js +77 -41
- package/src/ui/connectors/ConnectionAnchorsLayer.js +4 -0
- package/src/ui/handles/HandlesDomRenderer.js +9 -2
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/styles/panels.css +362 -27
- package/src/ui/styles/workspace.css +21 -1
- package/src/utils/applyCrop.js +92 -0
|
@@ -1,9 +1,99 @@
|
|
|
1
1
|
import { Events } from '../core/events/Events.js';
|
|
2
2
|
import { UpdateFrameTypeCommand } from '../core/commands/UpdateFrameTypeCommand.js';
|
|
3
3
|
|
|
4
|
+
// --- colour helpers ---
|
|
5
|
+
|
|
6
|
+
function hexToPixi(hex) {
|
|
7
|
+
return parseInt(hex.replace('#', ''), 16);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pixiToHex(pixi) {
|
|
11
|
+
return '#' + pixi.toString(16).padStart(6, '0').toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hsvToHex(h, s, v) {
|
|
15
|
+
const f = (n) => {
|
|
16
|
+
const k = (n + h / 60) % 6;
|
|
17
|
+
const val = v - v * s * Math.max(0, Math.min(k, 4 - k, 1));
|
|
18
|
+
return Math.round(val * 255).toString(16).padStart(2, '0');
|
|
19
|
+
};
|
|
20
|
+
return '#' + f(5) + f(3) + f(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hexToHsv(hex) {
|
|
24
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
25
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
26
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
27
|
+
const max = Math.max(r, g, b);
|
|
28
|
+
const min = Math.min(r, g, b);
|
|
29
|
+
const d = max - min;
|
|
30
|
+
let h = 0;
|
|
31
|
+
if (d !== 0) {
|
|
32
|
+
if (max === r) h = ((g - b) / d) % 6;
|
|
33
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
34
|
+
else h = (r - g) / d + 4;
|
|
35
|
+
h = Math.round(h * 60);
|
|
36
|
+
if (h < 0) h += 360;
|
|
37
|
+
}
|
|
38
|
+
return { h, s: max === 0 ? 0 : d / max, v: max };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 15 preset colours (none/transparent is separate)
|
|
42
|
+
const FRAME_COLORS = [
|
|
43
|
+
{ name: 'Белый', hex: '#FFFFFF' },
|
|
44
|
+
{ name: 'Светло-серый', hex: '#EBEBEB' },
|
|
45
|
+
{ name: 'Серый', hex: '#ABABAB' },
|
|
46
|
+
{ name: 'Светло-розовый', hex: '#FFDDE0' },
|
|
47
|
+
{ name: 'Розовый', hex: '#FFB3BB' },
|
|
48
|
+
{ name: 'Персиковый', hex: '#FFE5CC' },
|
|
49
|
+
{ name: 'Оранжевый', hex: '#FFB066' },
|
|
50
|
+
{ name: 'Светло-жёлтый', hex: '#FFFACC' },
|
|
51
|
+
{ name: 'Жёлтый', hex: '#FFE566' },
|
|
52
|
+
{ name: 'Светло-зелёный', hex: '#D4F5D4' },
|
|
53
|
+
{ name: 'Зелёный', hex: '#88D888' },
|
|
54
|
+
{ name: 'Небесный', hex: '#D4EEFF' },
|
|
55
|
+
{ name: 'Голубой', hex: '#80BFFF' },
|
|
56
|
+
{ name: 'Лавандовый', hex: '#E8D4FF' },
|
|
57
|
+
{ name: 'Фиолетовый', hex: '#C499FF' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const ICONS = {
|
|
61
|
+
rename: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" type="input-field"><path d="M15.75 7.75H17.25C18.3546 7.75 19.25 8.64543 19.25 9.75V14.25C19.25 15.3546 18.3546 16.25 17.25 16.25H15.75M8.25 16.25H6.75C5.64543 16.25 4.75 15.3546 4.75 14.25V9.75C4.75 8.64543 5.64543 7.75 6.75 7.75H8.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10.75 4.75H13.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10.75 19.25H13.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 5V19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>',
|
|
62
|
+
eye: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
63
|
+
eyeOff: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18"/><path d="M10.6 10.7a2 2 0 0 0 2.7 2.8"/><path d="M9.4 5.2A9.6 9.6 0 0 1 12 5c6.5 0 10 7 10 7a13 13 0 0 1-2.2 2.9M6.2 6.2A13 13 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 3.5-.6"/></svg>',
|
|
64
|
+
lock: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.75 11.75C5.75 11.2 6.2 10.75 6.75 10.75H17.25C17.8 10.75 18.25 11.2 18.25 11.75V17.25C18.25 18.35 17.35 19.25 16.25 19.25H7.75C6.65 19.25 5.75 18.35 5.75 17.25V11.75Z"/><path d="M7.75 10.5V10.34C7.75 8.78 7.66 7.04 8.75 5.92C9.37 5.29 10.37 4.75 12 4.75C13.63 4.75 14.63 5.29 15.25 5.92C16.34 7.04 16.25 8.78 16.25 10.34V10.5"/><path d="M12 14.25V15.75"/></svg>',
|
|
65
|
+
unlock: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.75 11.75C5.75 11.2 6.2 10.75 6.75 10.75H17.25C17.8 10.75 18.25 11.2 18.25 11.75V17.25C18.25 18.35 17.35 19.25 16.25 19.25H7.75C6.65 19.25 5.75 18.35 5.75 17.25V11.75Z"/><path d="M7.75 10.5V9.84C7.75 8.61 7.7 7.3 8.42 6.31C9 5.52 10.06 4.75 12 4.75C14 4.75 15.25 6.25 15.25 6.25"/><path d="M12 14.25V15.75"/></svg>',
|
|
66
|
+
more: '<svg width="22" height="22" viewBox="0 0 22 22" fill="currentColor"><path d="M16.22 12.38a1.38 1.38 0 1 0 0-2.76 1.38 1.38 0 0 0 0 2.76Z"/><path d="M11 12.38a1.38 1.38 0 1 0 0-2.76 1.38 1.38 0 0 0 0 2.76Z"/><path d="M5.78 12.38a1.38 1.38 0 1 0 0-2.76 1.38 1.38 0 0 0 0 2.76Z"/></svg>',
|
|
67
|
+
expand: '<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke-width="1.2" type="chevron-down" size="24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M15.25 10.75L12 14.25L8.75 10.75"></path></svg>',
|
|
68
|
+
rCustom: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="4 4"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>',
|
|
69
|
+
rWide: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="1"/></svg>',
|
|
70
|
+
r43: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="1"/></svg>',
|
|
71
|
+
rSquare: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="1"/></svg>',
|
|
72
|
+
rA4: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="1"/></svg>',
|
|
73
|
+
comment: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>',
|
|
74
|
+
bgSolid: '<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="#374151"/></svg>',
|
|
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
|
+
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
|
+
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
|
+
};
|
|
79
|
+
|
|
80
|
+
const RATIO_LABELS = {
|
|
81
|
+
custom: 'Произвольное',
|
|
82
|
+
'16x9': '16:9',
|
|
83
|
+
'4x3': '4:3',
|
|
84
|
+
'1x1': '1:1',
|
|
85
|
+
a4: 'A4',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const BG_TYPES = [
|
|
89
|
+
{ id: 'solid', title: 'Сплошной фон', icon: 'bgSolid' },
|
|
90
|
+
{ id: 'solid-bordered', title: 'Фон с рамкой', icon: 'bgBordered' },
|
|
91
|
+
{ id: 'outline', title: 'Только рамка', icon: 'bgOutline' },
|
|
92
|
+
];
|
|
93
|
+
|
|
4
94
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
95
|
+
* Тулбар над выделенным фреймом.
|
|
96
|
+
* Состав: Ratio (пропорции), цвет фона, переименование, видимость, блокировка, меню.
|
|
7
97
|
*/
|
|
8
98
|
export class FramePropertiesPanel {
|
|
9
99
|
constructor(eventBus, container, core = null) {
|
|
@@ -12,7 +102,15 @@ export class FramePropertiesPanel {
|
|
|
12
102
|
this.core = core;
|
|
13
103
|
this.panel = null;
|
|
14
104
|
this.currentId = null;
|
|
15
|
-
|
|
105
|
+
|
|
106
|
+
this._sessionCustomColors = [];
|
|
107
|
+
this._colorPopup = null;
|
|
108
|
+
this._colorPickerModal = null;
|
|
109
|
+
this._pickerDragging = false;
|
|
110
|
+
this._pickerHue = 0;
|
|
111
|
+
this._pickerSat = 0;
|
|
112
|
+
this._pickerVal = 1;
|
|
113
|
+
|
|
16
114
|
this._attachEvents();
|
|
17
115
|
this._createPanel();
|
|
18
116
|
}
|
|
@@ -23,7 +121,8 @@ export class FramePropertiesPanel {
|
|
|
23
121
|
this._handlers.onSelectionRemove = () => this.updateFromSelection();
|
|
24
122
|
this._handlers.onSelectionClear = () => this.hide();
|
|
25
123
|
this._handlers.onDeleted = (objectId) => {
|
|
26
|
-
|
|
124
|
+
const id = objectId?.objectId || objectId;
|
|
125
|
+
if (this.currentId && id === this.currentId) { this.hide(); }
|
|
27
126
|
};
|
|
28
127
|
this._handlers.onDragStart = () => this.hide();
|
|
29
128
|
this._handlers.onDragUpdate = () => this._repositionThrottled();
|
|
@@ -31,20 +130,14 @@ export class FramePropertiesPanel {
|
|
|
31
130
|
this._handlers.onGroupDragUpdate = () => this._repositionThrottled();
|
|
32
131
|
this._handlers.onGroupDragStart = () => this.hide();
|
|
33
132
|
this._handlers.onGroupDragEnd = () => this.updateFromSelection();
|
|
133
|
+
this._handlers.onResizeStart = () => this.hide();
|
|
34
134
|
this._handlers.onResizeUpdate = () => this._repositionThrottled();
|
|
135
|
+
this._handlers.onResizeEnd = () => this.updateFromSelection();
|
|
35
136
|
this._handlers.onRotateUpdate = () => this._repositionThrottled();
|
|
36
|
-
this._handlers.onZoomPercent = () => {
|
|
37
|
-
|
|
38
|
-
};
|
|
39
|
-
this._handlers.
|
|
40
|
-
if (this.currentId) this._repositionThrottled();
|
|
41
|
-
};
|
|
42
|
-
this._handlers.onViewportChanged = () => {
|
|
43
|
-
if (this.currentId) this._repositionThrottled();
|
|
44
|
-
};
|
|
45
|
-
this._handlers.onActivated = ({ tool }) => {
|
|
46
|
-
if (tool !== 'select') this.hide();
|
|
47
|
-
};
|
|
137
|
+
this._handlers.onZoomPercent = () => { if (this.currentId) { this._repositionThrottled(); } };
|
|
138
|
+
this._handlers.onPanUpdate = () => { if (this.currentId) { this._repositionThrottled(); } };
|
|
139
|
+
this._handlers.onViewportChanged = () => { if (this.currentId) { this._repositionThrottled(); } };
|
|
140
|
+
this._handlers.onActivated = ({ tool }) => { if (tool !== 'select') { this.hide(); } };
|
|
48
141
|
this._handlers.onStateChanged = (data) => {
|
|
49
142
|
const { objectId } = data;
|
|
50
143
|
if (this.currentId && objectId === this.currentId && this.panel && this.panel.style.display !== 'none') {
|
|
@@ -74,7 +167,9 @@ export class FramePropertiesPanel {
|
|
|
74
167
|
this.eventBus.on(Events.Tool.GroupDragUpdate, this._handlers.onGroupDragUpdate);
|
|
75
168
|
this.eventBus.on(Events.Tool.GroupDragStart, this._handlers.onGroupDragStart);
|
|
76
169
|
this.eventBus.on(Events.Tool.GroupDragEnd, this._handlers.onGroupDragEnd);
|
|
170
|
+
this.eventBus.on(Events.Tool.ResizeStart, this._handlers.onResizeStart);
|
|
77
171
|
this.eventBus.on(Events.Tool.ResizeUpdate, this._handlers.onResizeUpdate);
|
|
172
|
+
this.eventBus.on(Events.Tool.ResizeEnd, this._handlers.onResizeEnd);
|
|
78
173
|
this.eventBus.on(Events.Tool.RotateUpdate, this._handlers.onRotateUpdate);
|
|
79
174
|
this.eventBus.on(Events.UI.ZoomPercent, this._handlers.onZoomPercent);
|
|
80
175
|
this.eventBus.on(Events.Tool.PanUpdate, this._handlers.onPanUpdate);
|
|
@@ -86,25 +181,15 @@ export class FramePropertiesPanel {
|
|
|
86
181
|
}
|
|
87
182
|
|
|
88
183
|
updateFromSelection() {
|
|
89
|
-
// Показываем только для одиночного выделения фрейма
|
|
90
184
|
const ids = this.core?.selectTool ? Array.from(this.core.selectTool.selectedObjects || []) : [];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.hide();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
185
|
+
if (!ids || ids.length !== 1) { this.hide(); return; }
|
|
186
|
+
|
|
97
187
|
const id = ids[0];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (this.currentId === id && this.panel && this.panel.style.display !== 'none') {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
188
|
+
if (this.currentId === id && this.panel && this.panel.style.display !== 'none') { return; }
|
|
189
|
+
|
|
104
190
|
const pixi = this.core?.pixi?.objects?.get ? this.core.pixi.objects.get(id) : null;
|
|
105
191
|
const isFrame = !!(pixi && pixi._mb && pixi._mb.type === 'frame');
|
|
106
|
-
|
|
107
|
-
|
|
192
|
+
|
|
108
193
|
if (isFrame) {
|
|
109
194
|
this.showFor(id);
|
|
110
195
|
} else {
|
|
@@ -118,499 +203,1067 @@ export class FramePropertiesPanel {
|
|
|
118
203
|
this.panel.style.display = 'flex';
|
|
119
204
|
this.reposition();
|
|
120
205
|
}
|
|
121
|
-
|
|
122
|
-
// Обновляем контролы в соответствии с текущими свойствами объекта
|
|
123
206
|
this._updateControlsFromObject();
|
|
124
207
|
this._syncTypeFromObject();
|
|
208
|
+
this._updateLockUI();
|
|
125
209
|
}
|
|
126
210
|
|
|
127
211
|
hide() {
|
|
128
212
|
this.currentId = null;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
213
|
+
this._closeMenus();
|
|
214
|
+
if (this.panel) { this.panel.style.display = 'none'; }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_getObjectData() {
|
|
218
|
+
if (!this.currentId || !this.core?.getObjectData) { return null; }
|
|
219
|
+
return this.core.getObjectData(this.currentId);
|
|
132
220
|
}
|
|
133
221
|
|
|
134
222
|
_createPanel() {
|
|
135
223
|
const panel = document.createElement('div');
|
|
136
224
|
panel.className = 'frame-properties-panel';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
225
|
+
panel.id = 'frame-properties-panel';
|
|
226
|
+
panel.style.position = 'absolute';
|
|
227
|
+
panel.style.display = 'none';
|
|
228
|
+
panel.style.zIndex = '10000';
|
|
142
229
|
|
|
143
|
-
|
|
144
|
-
this.
|
|
230
|
+
this._boundDocumentClickHandler = this._documentClickHandler.bind(this);
|
|
231
|
+
this._boundPickerMouseMove = this._pickerMouseMove.bind(this);
|
|
232
|
+
this._boundPickerMouseUp = this._pickerMouseUp.bind(this);
|
|
145
233
|
|
|
146
|
-
|
|
147
|
-
|
|
234
|
+
const ratio = this._makeRatioControl();
|
|
235
|
+
const color = this._makeColorControl();
|
|
236
|
+
const rename = this._makeButton(ICONS.rename, 'Переименовать', 'rename');
|
|
237
|
+
rename.addEventListener('click', (e) => { e.stopPropagation(); this._openRename(); });
|
|
238
|
+
const eye = this._makeButton(ICONS.eye, 'Скрыть / показать', 'eye');
|
|
239
|
+
eye.addEventListener('click', (e) => { e.stopPropagation(); this._toggleVisibility(eye); });
|
|
240
|
+
this._eyeBtn = eye;
|
|
241
|
+
|
|
242
|
+
const divider = this._makeDivider();
|
|
243
|
+
|
|
244
|
+
this._btn_lock = this._makeButton(ICONS.unlock, 'Заблокировать', 'lock');
|
|
245
|
+
this._btn_lock.addEventListener('click', (e) => { e.stopPropagation(); this._toggleLocked(); });
|
|
246
|
+
|
|
247
|
+
const more = this._makeMoreButton();
|
|
248
|
+
|
|
249
|
+
panel.appendChild(ratio);
|
|
250
|
+
panel.appendChild(color);
|
|
251
|
+
panel.appendChild(rename);
|
|
252
|
+
panel.appendChild(eye);
|
|
253
|
+
panel.appendChild(divider);
|
|
254
|
+
panel.appendChild(this._btn_lock);
|
|
255
|
+
panel.appendChild(more);
|
|
256
|
+
|
|
257
|
+
this._lockableEls = [ratio, color, rename, eye, divider];
|
|
258
|
+
this._leftEls = [ratio, color, rename];
|
|
259
|
+
|
|
260
|
+
const colorPopup = this._makeColorPopup();
|
|
261
|
+
panel.appendChild(colorPopup);
|
|
262
|
+
|
|
263
|
+
const colorPicker = this._makeColorPickerModal();
|
|
264
|
+
panel.appendChild(colorPicker);
|
|
148
265
|
|
|
149
266
|
this.panel = panel;
|
|
150
267
|
this.container.appendChild(panel);
|
|
151
|
-
this._boundDocumentClickHandler = this._documentClickHandler.bind(this);
|
|
152
268
|
}
|
|
153
269
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
270
|
+
_makeButton(iconHtml, title, key) {
|
|
271
|
+
const btn = document.createElement('button');
|
|
272
|
+
btn.className = 'ipp-btn';
|
|
273
|
+
btn.title = title;
|
|
274
|
+
if (key) { btn.dataset.id = 'fpp-btn-' + key; }
|
|
275
|
+
btn.innerHTML = iconHtml;
|
|
276
|
+
return btn;
|
|
157
277
|
}
|
|
158
278
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
279
|
+
_makeDivider() {
|
|
280
|
+
const div = document.createElement('div');
|
|
281
|
+
div.className = 'ipp-divider';
|
|
282
|
+
return div;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_makeRatioControl() {
|
|
286
|
+
const wrap = document.createElement('div');
|
|
287
|
+
wrap.className = 'fpp-ratio';
|
|
288
|
+
|
|
289
|
+
const btn = document.createElement('button');
|
|
290
|
+
btn.className = 'fpp-ratio-btn';
|
|
291
|
+
btn.dataset.id = 'fpp-ratio-btn';
|
|
292
|
+
|
|
293
|
+
const textBox = document.createElement('span');
|
|
294
|
+
textBox.className = 'fpp-ratio-text';
|
|
295
|
+
const caption = document.createElement('span');
|
|
296
|
+
caption.className = 'fpp-ratio-caption';
|
|
297
|
+
caption.textContent = 'Соотношение сторон';
|
|
298
|
+
const value = document.createElement('span');
|
|
299
|
+
value.className = 'fpp-ratio-value';
|
|
300
|
+
value.textContent = RATIO_LABELS.custom;
|
|
301
|
+
textBox.appendChild(caption);
|
|
302
|
+
textBox.appendChild(value);
|
|
303
|
+
this._ratioValueEl = value;
|
|
304
|
+
this._ratioBtn = btn;
|
|
305
|
+
|
|
306
|
+
const chevron = document.createElement('span');
|
|
307
|
+
chevron.innerHTML = ICONS.expand;
|
|
308
|
+
|
|
309
|
+
btn.appendChild(textBox);
|
|
310
|
+
btn.appendChild(chevron);
|
|
311
|
+
|
|
312
|
+
const dropdown = document.createElement('div');
|
|
313
|
+
dropdown.className = 'fpp-ratio-dropdown';
|
|
314
|
+
this._ratioDropdown = dropdown;
|
|
315
|
+
|
|
316
|
+
const items = [
|
|
317
|
+
{ id: 'custom', label: RATIO_LABELS.custom, icon: ICONS.rCustom },
|
|
318
|
+
{ id: '16x9', label: '16:9', icon: ICONS.rWide },
|
|
319
|
+
{ id: '4x3', label: '4:3', icon: ICONS.r43 },
|
|
320
|
+
{ id: '1x1', label: '1:1', icon: ICONS.rSquare },
|
|
321
|
+
{ id: 'a4', label: 'A4', icon: ICONS.rA4 },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
items.forEach((item) => {
|
|
325
|
+
const el = document.createElement('button');
|
|
326
|
+
el.className = 'ipp-dropdown-item';
|
|
327
|
+
const ic = document.createElement('span');
|
|
328
|
+
ic.className = 'ipp-dropdown-icon';
|
|
329
|
+
ic.innerHTML = item.icon;
|
|
330
|
+
const lbl = document.createElement('span');
|
|
331
|
+
lbl.textContent = item.label;
|
|
332
|
+
el.appendChild(ic);
|
|
333
|
+
el.appendChild(lbl);
|
|
334
|
+
el.addEventListener('click', (e) => {
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
this._closeMenus();
|
|
337
|
+
this._applyFrameType(item.id);
|
|
338
|
+
});
|
|
339
|
+
dropdown.appendChild(el);
|
|
167
340
|
});
|
|
168
|
-
|
|
341
|
+
|
|
342
|
+
btn.addEventListener('click', (e) => {
|
|
343
|
+
e.stopPropagation();
|
|
344
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
345
|
+
this._closeMenus();
|
|
346
|
+
if (!isOpen) {
|
|
347
|
+
dropdown.classList.add('is-open');
|
|
348
|
+
btn.classList.add('is-active');
|
|
349
|
+
this._attachOutside();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
wrap.appendChild(btn);
|
|
354
|
+
wrap.appendChild(dropdown);
|
|
355
|
+
return wrap;
|
|
169
356
|
}
|
|
170
357
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
358
|
+
_makeColorControl() {
|
|
359
|
+
const wrap = document.createElement('div');
|
|
360
|
+
wrap.className = 'fpp-color-wrap';
|
|
361
|
+
wrap.title = 'Цвет фона';
|
|
175
362
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
363
|
+
const colorButton = document.createElement('button');
|
|
364
|
+
colorButton.className = 'fpp-color-button';
|
|
365
|
+
colorButton.addEventListener('click', (e) => {
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
this._toggleColorPopup(colorButton);
|
|
368
|
+
});
|
|
182
369
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const sizeData = { objectId: this.currentId, size: null };
|
|
188
|
-
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
370
|
+
this.colorButton = colorButton;
|
|
371
|
+
wrap.appendChild(colorButton);
|
|
372
|
+
return wrap;
|
|
373
|
+
}
|
|
189
374
|
|
|
190
|
-
|
|
191
|
-
|
|
375
|
+
// ─── colour popup ──────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
_makeColorPopup() {
|
|
378
|
+
const popup = document.createElement('div');
|
|
379
|
+
popup.className = 'fpp-color-popup';
|
|
380
|
+
this._colorPopup = popup;
|
|
381
|
+
|
|
382
|
+
// Background type section
|
|
383
|
+
const typeSection = document.createElement('div');
|
|
384
|
+
typeSection.className = 'fpp-section';
|
|
385
|
+
|
|
386
|
+
const typeLabel = document.createElement('div');
|
|
387
|
+
typeLabel.className = 'fpp-section-label';
|
|
388
|
+
typeLabel.textContent = 'Background type';
|
|
389
|
+
|
|
390
|
+
const typeRow = document.createElement('div');
|
|
391
|
+
typeRow.className = 'fpp-bg-type-row';
|
|
392
|
+
|
|
393
|
+
this._bgTypeBtns = [];
|
|
394
|
+
BG_TYPES.forEach(({ id, title, icon }) => {
|
|
395
|
+
const btn = document.createElement('button');
|
|
396
|
+
btn.className = 'fpp-bg-type-btn';
|
|
397
|
+
btn.title = title;
|
|
398
|
+
btn.dataset.bgMode = id;
|
|
399
|
+
btn.innerHTML = ICONS[icon];
|
|
400
|
+
btn.addEventListener('click', (e) => {
|
|
401
|
+
e.stopPropagation();
|
|
402
|
+
this._selectBgMode(id);
|
|
403
|
+
});
|
|
404
|
+
typeRow.appendChild(btn);
|
|
405
|
+
this._bgTypeBtns.push(btn);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
typeSection.appendChild(typeLabel);
|
|
409
|
+
typeSection.appendChild(typeRow);
|
|
410
|
+
popup.appendChild(typeSection);
|
|
411
|
+
|
|
412
|
+
// Colour section
|
|
413
|
+
const colorSection = document.createElement('div');
|
|
414
|
+
colorSection.className = 'fpp-section fpp-section--color';
|
|
415
|
+
this._colorSection = colorSection;
|
|
416
|
+
|
|
417
|
+
const colorLabel = document.createElement('div');
|
|
418
|
+
colorLabel.className = 'fpp-section-label';
|
|
419
|
+
colorLabel.textContent = 'Color';
|
|
420
|
+
|
|
421
|
+
const grid = document.createElement('div');
|
|
422
|
+
grid.className = 'fpp-color-grid';
|
|
423
|
+
this._colorSwatches = [];
|
|
424
|
+
|
|
425
|
+
// "None" swatch — transparent
|
|
426
|
+
const noneSwatch = document.createElement('button');
|
|
427
|
+
noneSwatch.className = 'fpp-color-swatch fpp-color-swatch--none';
|
|
428
|
+
noneSwatch.title = 'Прозрачный';
|
|
429
|
+
noneSwatch.dataset.colorHex = 'none';
|
|
430
|
+
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>';
|
|
431
|
+
noneSwatch.addEventListener('click', (e) => {
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
this._applyTransparent();
|
|
434
|
+
});
|
|
435
|
+
grid.appendChild(noneSwatch);
|
|
436
|
+
this._noneSwatch = noneSwatch;
|
|
437
|
+
this._colorSwatches.push(noneSwatch);
|
|
438
|
+
|
|
439
|
+
// Preset swatches
|
|
440
|
+
FRAME_COLORS.forEach((color) => {
|
|
441
|
+
const btn = document.createElement('button');
|
|
442
|
+
btn.className = 'fpp-color-swatch';
|
|
443
|
+
btn.title = color.name;
|
|
444
|
+
btn.dataset.colorHex = color.hex.toUpperCase();
|
|
445
|
+
btn.style.setProperty('--swatch-color', color.hex);
|
|
446
|
+
btn.addEventListener('click', (e) => {
|
|
447
|
+
e.stopPropagation();
|
|
448
|
+
this._selectColor(color.hex);
|
|
449
|
+
});
|
|
450
|
+
grid.appendChild(btn);
|
|
451
|
+
this._colorSwatches.push(btn);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
colorSection.appendChild(colorLabel);
|
|
455
|
+
colorSection.appendChild(grid);
|
|
456
|
+
popup.appendChild(colorSection);
|
|
457
|
+
|
|
458
|
+
// Custom colours section
|
|
459
|
+
const customSection = document.createElement('div');
|
|
460
|
+
customSection.className = 'fpp-section fpp-section--custom';
|
|
461
|
+
this._customSection = customSection;
|
|
462
|
+
|
|
463
|
+
const customLabel = document.createElement('div');
|
|
464
|
+
customLabel.className = 'fpp-section-label';
|
|
465
|
+
customLabel.textContent = 'Custom colors';
|
|
466
|
+
|
|
467
|
+
const customRow = document.createElement('div');
|
|
468
|
+
customRow.className = 'fpp-custom-row';
|
|
469
|
+
this._customColorRow = customRow;
|
|
470
|
+
|
|
471
|
+
const addBtn = document.createElement('button');
|
|
472
|
+
addBtn.className = 'fpp-add-color-btn';
|
|
473
|
+
addBtn.title = 'Добавить цвет';
|
|
474
|
+
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
|
+
addBtn.addEventListener('click', (e) => {
|
|
476
|
+
e.stopPropagation();
|
|
477
|
+
this._openColorPicker();
|
|
478
|
+
});
|
|
479
|
+
this._addColorBtn = addBtn;
|
|
480
|
+
|
|
481
|
+
customRow.appendChild(addBtn);
|
|
482
|
+
customSection.appendChild(customLabel);
|
|
483
|
+
customSection.appendChild(customRow);
|
|
484
|
+
popup.appendChild(customSection);
|
|
485
|
+
|
|
486
|
+
return popup;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_toggleColorPopup(button) {
|
|
490
|
+
if (!this._colorPopup) { return; }
|
|
491
|
+
const isOpen = this._colorPopup.classList.contains('is-open');
|
|
492
|
+
if (isOpen) {
|
|
493
|
+
this._hideColorPopup();
|
|
494
|
+
} else {
|
|
495
|
+
this._closeMenus();
|
|
496
|
+
this._showColorPopup(button);
|
|
192
497
|
}
|
|
498
|
+
}
|
|
193
499
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
500
|
+
_showColorPopup(button) {
|
|
501
|
+
if (!this._colorPopup) { return; }
|
|
502
|
+
const buttonRect = button.getBoundingClientRect();
|
|
503
|
+
const panelRect = this.panel.getBoundingClientRect();
|
|
504
|
+
this._colorPopup.style.left = (buttonRect.left - panelRect.left) + 'px';
|
|
505
|
+
this._colorPopup.style.top = (buttonRect.bottom - panelRect.top + 6) + 'px';
|
|
506
|
+
this._colorPopup.classList.add('is-open');
|
|
507
|
+
this._syncColorPopupFromObject();
|
|
508
|
+
this._attachOutside();
|
|
509
|
+
}
|
|
199
510
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const objectHeight = sizeData.size.height * scale;
|
|
511
|
+
_hideColorPopup() {
|
|
512
|
+
if (this._colorPopup) { this._colorPopup.classList.remove('is-open'); }
|
|
513
|
+
this._closeColorPicker();
|
|
514
|
+
}
|
|
205
515
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
516
|
+
_selectBgMode(mode) {
|
|
517
|
+
if (!this.currentId) { return; }
|
|
518
|
+
|
|
519
|
+
this._bgTypeBtns.forEach((btn) => {
|
|
520
|
+
btn.classList.toggle('is-active', btn.dataset.bgMode === mode);
|
|
521
|
+
});
|
|
211
522
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
523
|
+
this._setColorSectionDisabled(false);
|
|
524
|
+
|
|
525
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
526
|
+
objectId: this.currentId,
|
|
527
|
+
updates: { properties: { bgMode: mode } },
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_applyTransparent() {
|
|
532
|
+
if (!this.currentId) { return; }
|
|
533
|
+
this._setColorSectionDisabled(true);
|
|
534
|
+
this._markNoneSelected();
|
|
535
|
+
|
|
536
|
+
const outlineBtn = this._bgTypeBtns.find((b) => b.dataset.bgMode === 'outline');
|
|
537
|
+
if (outlineBtn) {
|
|
538
|
+
this._bgTypeBtns.forEach((b) => b.classList.remove('is-active'));
|
|
539
|
+
outlineBtn.classList.add('is-active');
|
|
215
540
|
}
|
|
216
541
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
542
|
+
if (this.colorButton) {
|
|
543
|
+
this.colorButton.style.backgroundColor = 'transparent';
|
|
544
|
+
this.colorButton.classList.add('fpp-color-button--none');
|
|
545
|
+
}
|
|
221
546
|
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
_createFrameControls(panel) {
|
|
227
|
-
// Контейнер для названия
|
|
228
|
-
const titleContainer = document.createElement('div');
|
|
229
|
-
titleContainer.className = 'fpp-section';
|
|
230
|
-
|
|
231
|
-
// Лейбл
|
|
232
|
-
const titleLabel = document.createElement('span');
|
|
233
|
-
titleLabel.textContent = 'Название:';
|
|
234
|
-
titleLabel.className = 'fpp-label';
|
|
235
|
-
|
|
236
|
-
// Поле ввода для названия
|
|
237
|
-
const titleInput = document.createElement('input');
|
|
238
|
-
titleInput.type = 'text';
|
|
239
|
-
titleInput.placeholder = 'Название фрейма';
|
|
240
|
-
titleInput.className = 'fpp-input';
|
|
241
|
-
Object.assign(titleInput.style, {
|
|
242
|
-
height: '22px',
|
|
243
|
-
flex: '1',
|
|
244
|
-
outline: 'none'
|
|
547
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
548
|
+
objectId: this.currentId,
|
|
549
|
+
updates: { properties: { bgMode: 'outline' } },
|
|
245
550
|
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
_selectColor(hex) {
|
|
554
|
+
if (!this.currentId) { return; }
|
|
555
|
+
const pixi = hexToPixi(hex);
|
|
246
556
|
|
|
247
|
-
|
|
248
|
-
this.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
557
|
+
this._setColorSectionDisabled(false);
|
|
558
|
+
this._syncSwatchSelection(hex.toUpperCase());
|
|
559
|
+
|
|
560
|
+
if (this.colorButton) {
|
|
561
|
+
this.colorButton.style.backgroundColor = hex;
|
|
562
|
+
this.colorButton.classList.remove('fpp-color-button--none');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
566
|
+
objectId: this.currentId,
|
|
567
|
+
updates: { backgroundColor: pixi },
|
|
257
568
|
});
|
|
569
|
+
}
|
|
258
570
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
571
|
+
_getCurrentBgMode() {
|
|
572
|
+
const obj = this._getObjectData();
|
|
573
|
+
return (obj?.properties?.bgMode) || (obj?.bgMode) || 'solid';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
_setColorSectionDisabled(disabled) {
|
|
577
|
+
if (this._colorSection) {
|
|
578
|
+
this._colorSection.classList.toggle('is-disabled', disabled);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
_markNoneSelected() {
|
|
583
|
+
this._colorSwatches.forEach((s) => s.classList.remove('is-selected'));
|
|
584
|
+
if (this._noneSwatch) { this._noneSwatch.classList.add('is-selected'); }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_syncSwatchSelection(hexUpper) {
|
|
588
|
+
this._colorSwatches.forEach((s) => {
|
|
589
|
+
s.classList.toggle('is-selected', (s.dataset.colorHex || '').toUpperCase() === hexUpper);
|
|
265
590
|
});
|
|
591
|
+
}
|
|
266
592
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
593
|
+
_syncColorPopupFromObject() {
|
|
594
|
+
const obj = this._getObjectData();
|
|
595
|
+
if (!obj) { return; }
|
|
596
|
+
|
|
597
|
+
const bgMode = (obj.properties?.bgMode) || (obj.bgMode) || 'solid';
|
|
598
|
+
const bgColor = obj.backgroundColor ?? (obj.properties?.backgroundColor);
|
|
599
|
+
const isTransparent = bgColor === null || bgColor === undefined || bgMode === 'outline';
|
|
600
|
+
|
|
601
|
+
this._bgTypeBtns.forEach((b) => b.classList.toggle('is-active', b.dataset.bgMode === bgMode));
|
|
602
|
+
|
|
603
|
+
if (isTransparent) {
|
|
604
|
+
this._setColorSectionDisabled(true);
|
|
605
|
+
this._markNoneSelected();
|
|
606
|
+
} else {
|
|
607
|
+
this._setColorSectionDisabled(false);
|
|
608
|
+
const hex = (typeof bgColor === 'number') ? pixiToHex(bgColor) : '#FFFFFF';
|
|
609
|
+
this._syncSwatchSelection(hex.toUpperCase());
|
|
610
|
+
|
|
611
|
+
const customMatch = this._sessionCustomColors.find((c) => c.toUpperCase() === hex.toUpperCase());
|
|
612
|
+
if (!customMatch) {
|
|
613
|
+
const presetMatch = FRAME_COLORS.find((c) => c.hex.toUpperCase() === hex.toUpperCase());
|
|
614
|
+
if (!presetMatch) {
|
|
615
|
+
this._syncSwatchSelection(''); // custom colour not in list — deselect all presets
|
|
616
|
+
}
|
|
271
617
|
}
|
|
272
|
-
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
273
620
|
|
|
274
|
-
|
|
275
|
-
this.titleInput = titleInput;
|
|
621
|
+
// ─── HSV colour picker ─────────────────────────────────────────────────────
|
|
276
622
|
|
|
277
|
-
|
|
278
|
-
|
|
623
|
+
_makeColorPickerModal() {
|
|
624
|
+
const modal = document.createElement('div');
|
|
625
|
+
modal.className = 'fpp-color-picker';
|
|
626
|
+
this._colorPickerModal = modal;
|
|
279
627
|
|
|
280
|
-
//
|
|
281
|
-
const
|
|
282
|
-
|
|
628
|
+
// Gradient canvas
|
|
629
|
+
const canvasWrap = document.createElement('div');
|
|
630
|
+
canvasWrap.className = 'fpp-color-picker__canvas-wrap';
|
|
283
631
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
632
|
+
const canvas = document.createElement('canvas');
|
|
633
|
+
canvas.width = 212;
|
|
634
|
+
canvas.height = 148;
|
|
635
|
+
canvas.className = 'fpp-color-picker__canvas';
|
|
636
|
+
this._hsvCanvas = canvas;
|
|
288
637
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
638
|
+
const cursor = document.createElement('div');
|
|
639
|
+
cursor.className = 'fpp-color-picker__cursor';
|
|
640
|
+
this._hsvCursor = cursor;
|
|
292
641
|
|
|
293
|
-
|
|
294
|
-
|
|
642
|
+
canvasWrap.appendChild(canvas);
|
|
643
|
+
canvasWrap.appendChild(cursor);
|
|
644
|
+
modal.appendChild(canvasWrap);
|
|
645
|
+
|
|
646
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
295
647
|
e.stopPropagation();
|
|
296
|
-
this.
|
|
648
|
+
this._pickerDragging = true;
|
|
649
|
+
this._updatePickerSV(e);
|
|
650
|
+
document.addEventListener('mousemove', this._boundPickerMouseMove);
|
|
651
|
+
document.addEventListener('mouseup', this._boundPickerMouseUp);
|
|
297
652
|
});
|
|
298
653
|
|
|
299
|
-
//
|
|
300
|
-
|
|
654
|
+
// Hue slider
|
|
655
|
+
const hueWrap = document.createElement('div');
|
|
656
|
+
hueWrap.className = 'fpp-color-picker__hue-wrap';
|
|
301
657
|
|
|
302
|
-
|
|
303
|
-
|
|
658
|
+
const hueSlider = document.createElement('input');
|
|
659
|
+
hueSlider.type = 'range';
|
|
660
|
+
hueSlider.min = '0';
|
|
661
|
+
hueSlider.max = '360';
|
|
662
|
+
hueSlider.value = '0';
|
|
663
|
+
hueSlider.className = 'fpp-color-picker__hue-slider';
|
|
664
|
+
this._hueSlider = hueSlider;
|
|
304
665
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
666
|
+
hueSlider.addEventListener('input', (e) => {
|
|
667
|
+
e.stopPropagation();
|
|
668
|
+
this._pickerHue = parseInt(e.target.value);
|
|
669
|
+
this._drawHsvCanvas();
|
|
670
|
+
this._syncPickerHex();
|
|
671
|
+
});
|
|
308
672
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
typeLabel.className = 'fpp-label';
|
|
673
|
+
hueWrap.appendChild(hueSlider);
|
|
674
|
+
modal.appendChild(hueWrap);
|
|
312
675
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
676
|
+
// Bottom row: eyedropper + hex input
|
|
677
|
+
const bottomRow = document.createElement('div');
|
|
678
|
+
bottomRow.className = 'fpp-color-picker__bottom';
|
|
679
|
+
|
|
680
|
+
const eyedropper = document.createElement('button');
|
|
681
|
+
eyedropper.className = 'fpp-color-picker__eyedropper';
|
|
682
|
+
eyedropper.title = 'Пипетка';
|
|
683
|
+
eyedropper.innerHTML = ICONS.eyedropper;
|
|
684
|
+
eyedropper.addEventListener('click', (e) => {
|
|
685
|
+
e.stopPropagation();
|
|
686
|
+
if (typeof EyeDropper !== 'undefined') {
|
|
687
|
+
new EyeDropper().open().then(({ sRGBHex }) => {
|
|
688
|
+
this._setPickerFromHex(sRGBHex);
|
|
689
|
+
this._syncPickerHex();
|
|
690
|
+
}).catch(() => {});
|
|
691
|
+
}
|
|
319
692
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
693
|
+
|
|
694
|
+
const hexInput = document.createElement('input');
|
|
695
|
+
hexInput.type = 'text';
|
|
696
|
+
hexInput.className = 'fpp-color-picker__hex-input';
|
|
697
|
+
hexInput.value = '#ffffff';
|
|
698
|
+
hexInput.maxLength = 7;
|
|
699
|
+
hexInput.spellcheck = false;
|
|
700
|
+
this._hexInput = hexInput;
|
|
701
|
+
|
|
702
|
+
hexInput.addEventListener('click', (e) => e.stopPropagation());
|
|
703
|
+
hexInput.addEventListener('keydown', (e) => {
|
|
704
|
+
e.stopPropagation();
|
|
705
|
+
if (e.key === 'Enter') {
|
|
706
|
+
const hex = hexInput.value.trim();
|
|
707
|
+
if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
|
708
|
+
this._confirmCustomColor(hex);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
332
711
|
});
|
|
333
|
-
|
|
712
|
+
hexInput.addEventListener('change', (e) => {
|
|
713
|
+
e.stopPropagation();
|
|
714
|
+
const hex = hexInput.value.trim();
|
|
715
|
+
if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
|
716
|
+
this._setPickerFromHex(hex);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
bottomRow.appendChild(eyedropper);
|
|
721
|
+
bottomRow.appendChild(hexInput);
|
|
722
|
+
modal.appendChild(bottomRow);
|
|
334
723
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
724
|
+
// Apply button
|
|
725
|
+
const applyBtn = document.createElement('button');
|
|
726
|
+
applyBtn.className = 'fpp-color-picker__apply';
|
|
727
|
+
applyBtn.textContent = 'Добавить';
|
|
728
|
+
applyBtn.addEventListener('click', (e) => {
|
|
729
|
+
e.stopPropagation();
|
|
730
|
+
const hex = this._hexInput?.value?.trim();
|
|
731
|
+
if (hex && /^#[0-9a-fA-F]{6}$/.test(hex)) {
|
|
732
|
+
this._confirmCustomColor(hex);
|
|
733
|
+
}
|
|
339
734
|
});
|
|
735
|
+
modal.appendChild(applyBtn);
|
|
736
|
+
|
|
737
|
+
return modal;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
_openColorPicker() {
|
|
741
|
+
if (!this._colorPickerModal || !this._colorPopup || !this._addColorBtn) { return; }
|
|
742
|
+
|
|
743
|
+
const popupRect = this._colorPopup.getBoundingClientRect();
|
|
744
|
+
const panelRect = this.panel.getBoundingClientRect();
|
|
340
745
|
|
|
341
|
-
|
|
342
|
-
|
|
746
|
+
this._colorPickerModal.style.left = (popupRect.right - panelRect.left + 6) + 'px';
|
|
747
|
+
this._colorPickerModal.style.top = (popupRect.top - panelRect.top) + 'px';
|
|
748
|
+
this._colorPickerModal.classList.add('is-open');
|
|
343
749
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
750
|
+
this._pickerHue = 0;
|
|
751
|
+
this._pickerSat = 0;
|
|
752
|
+
this._pickerVal = 1;
|
|
753
|
+
if (this._hueSlider) { this._hueSlider.value = '0'; }
|
|
754
|
+
this._drawHsvCanvas();
|
|
755
|
+
this._syncPickerHex();
|
|
756
|
+
}
|
|
347
757
|
|
|
348
|
-
|
|
349
|
-
this.
|
|
758
|
+
_closeColorPicker() {
|
|
759
|
+
if (this._colorPickerModal) { this._colorPickerModal.classList.remove('is-open'); }
|
|
760
|
+
document.removeEventListener('mousemove', this._boundPickerMouseMove);
|
|
761
|
+
document.removeEventListener('mouseup', this._boundPickerMouseUp);
|
|
762
|
+
this._pickerDragging = false;
|
|
350
763
|
}
|
|
351
764
|
|
|
352
|
-
|
|
353
|
-
|
|
765
|
+
_drawHsvCanvas() {
|
|
766
|
+
const canvas = this._hsvCanvas;
|
|
767
|
+
if (!canvas) { return; }
|
|
768
|
+
const ctx = canvas.getContext('2d');
|
|
769
|
+
const w = canvas.width;
|
|
770
|
+
const h = canvas.height;
|
|
771
|
+
|
|
772
|
+
const hueColor = `hsl(${this._pickerHue}, 100%, 50%)`;
|
|
773
|
+
|
|
774
|
+
const satGrad = ctx.createLinearGradient(0, 0, w, 0);
|
|
775
|
+
satGrad.addColorStop(0, '#ffffff');
|
|
776
|
+
satGrad.addColorStop(1, hueColor);
|
|
777
|
+
ctx.fillStyle = satGrad;
|
|
778
|
+
ctx.fillRect(0, 0, w, h);
|
|
779
|
+
|
|
780
|
+
const darkGrad = ctx.createLinearGradient(0, 0, 0, h);
|
|
781
|
+
darkGrad.addColorStop(0, 'rgba(0,0,0,0)');
|
|
782
|
+
darkGrad.addColorStop(1, 'rgba(0,0,0,1)');
|
|
783
|
+
ctx.fillStyle = darkGrad;
|
|
784
|
+
ctx.fillRect(0, 0, w, h);
|
|
785
|
+
|
|
786
|
+
if (this._hsvCursor) {
|
|
787
|
+
const cx = Math.round(this._pickerSat * w);
|
|
788
|
+
const cy = Math.round((1 - this._pickerVal) * h);
|
|
789
|
+
this._hsvCursor.style.left = cx + 'px';
|
|
790
|
+
this._hsvCursor.style.top = cy + 'px';
|
|
791
|
+
}
|
|
792
|
+
}
|
|
354
793
|
|
|
794
|
+
_updatePickerSV(e) {
|
|
795
|
+
const canvas = this._hsvCanvas;
|
|
796
|
+
if (!canvas) { return; }
|
|
797
|
+
const rect = canvas.getBoundingClientRect();
|
|
798
|
+
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
799
|
+
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
800
|
+
this._pickerSat = x / rect.width;
|
|
801
|
+
this._pickerVal = 1 - y / rect.height;
|
|
802
|
+
this._drawHsvCanvas();
|
|
803
|
+
this._syncPickerHex();
|
|
804
|
+
}
|
|
355
805
|
|
|
356
|
-
|
|
357
|
-
this.
|
|
358
|
-
|
|
359
|
-
|
|
806
|
+
_pickerMouseMove(e) {
|
|
807
|
+
if (!this._pickerDragging) { return; }
|
|
808
|
+
this._updatePickerSV(e);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
_pickerMouseUp() {
|
|
812
|
+
this._pickerDragging = false;
|
|
813
|
+
document.removeEventListener('mousemove', this._boundPickerMouseMove);
|
|
814
|
+
document.removeEventListener('mouseup', this._boundPickerMouseUp);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
_syncPickerHex() {
|
|
818
|
+
if (!this._hexInput) { return; }
|
|
819
|
+
const hex = hsvToHex(this._pickerHue, this._pickerSat, this._pickerVal);
|
|
820
|
+
this._hexInput.value = hex.toUpperCase();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
_setPickerFromHex(hex) {
|
|
824
|
+
const { h, s, v } = hexToHsv(hex);
|
|
825
|
+
this._pickerHue = h;
|
|
826
|
+
this._pickerSat = s;
|
|
827
|
+
this._pickerVal = v;
|
|
828
|
+
if (this._hueSlider) { this._hueSlider.value = String(h); }
|
|
829
|
+
this._drawHsvCanvas();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_confirmCustomColor(hex) {
|
|
833
|
+
const normalised = hex.toUpperCase();
|
|
834
|
+
if (!this._sessionCustomColors.includes(normalised)) {
|
|
835
|
+
this._sessionCustomColors.push(normalised);
|
|
836
|
+
this._addCustomSwatch(normalised);
|
|
837
|
+
}
|
|
838
|
+
this._selectColor(normalised);
|
|
839
|
+
this._closeColorPicker();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
_addCustomSwatch(hex) {
|
|
843
|
+
const btn = document.createElement('button');
|
|
844
|
+
btn.className = 'fpp-color-swatch';
|
|
845
|
+
btn.title = hex;
|
|
846
|
+
btn.dataset.colorHex = hex;
|
|
847
|
+
btn.style.setProperty('--swatch-color', hex);
|
|
848
|
+
btn.addEventListener('click', (e) => {
|
|
849
|
+
e.stopPropagation();
|
|
850
|
+
this._selectColor(hex);
|
|
360
851
|
});
|
|
852
|
+
|
|
853
|
+
if (this._customColorRow && this._addColorBtn) {
|
|
854
|
+
this._customColorRow.insertBefore(btn, this._addColorBtn);
|
|
855
|
+
}
|
|
856
|
+
this._colorSwatches.push(btn);
|
|
361
857
|
}
|
|
362
858
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
859
|
+
// ─── shared helpers ────────────────────────────────────────────────────────
|
|
860
|
+
|
|
861
|
+
_makeMoreButton() {
|
|
862
|
+
const wrapper = document.createElement('div');
|
|
863
|
+
wrapper.className = 'ipp-btn-wrapper';
|
|
864
|
+
wrapper.style.position = 'relative';
|
|
865
|
+
wrapper.style.display = 'inline-flex';
|
|
866
|
+
|
|
867
|
+
const mainBtn = document.createElement('button');
|
|
868
|
+
mainBtn.className = 'ipp-btn';
|
|
869
|
+
mainBtn.title = 'Ещё';
|
|
870
|
+
mainBtn.dataset.id = 'fpp-btn-more';
|
|
871
|
+
mainBtn.innerHTML = ICONS.more;
|
|
872
|
+
|
|
873
|
+
const dropdown = document.createElement('div');
|
|
874
|
+
dropdown.className = 'ipp-more-dropdown';
|
|
875
|
+
this._moreDropdown = dropdown;
|
|
876
|
+
|
|
877
|
+
const infoPopover = document.createElement('div');
|
|
878
|
+
infoPopover.className = 'ipp-info-popover';
|
|
879
|
+
dropdown.appendChild(infoPopover);
|
|
880
|
+
this._infoPopover = infoPopover;
|
|
881
|
+
|
|
882
|
+
const items = [
|
|
883
|
+
{ id: 'copy', label: 'Копировать', shortcut: 'Ctrl+C' },
|
|
884
|
+
{ id: 'export-pdf', label: 'Экспорт в PDF' },
|
|
885
|
+
{ divider: true },
|
|
886
|
+
{ id: 'copy-link', label: 'Копировать ссылку на объект' },
|
|
887
|
+
{ id: 'bring-front', label: 'На передний план', shortcut: ']' },
|
|
888
|
+
{ id: 'bring-forward', label: 'Переместить вперёд', shortcut: 'Ctrl+]' },
|
|
889
|
+
{ id: 'send-backward', label: 'Переместить назад', shortcut: 'Ctrl+[' },
|
|
890
|
+
{ id: 'send-back', label: 'На задний план', shortcut: '[' },
|
|
891
|
+
{ id: 'info', label: 'Информация', shortcut: '>' },
|
|
892
|
+
{ id: 'lock', label: 'Заблокировать', shortcut: 'Ctrl+Shift+L' },
|
|
893
|
+
{ id: 'duplicate', label: 'Дублировать', shortcut: 'Ctrl+D' },
|
|
894
|
+
{ divider: true },
|
|
895
|
+
{ id: 'add-comment', label: 'Добавить комментарий', icon: ICONS.comment },
|
|
896
|
+
{ divider: true },
|
|
897
|
+
{ id: 'copy-png', label: 'Копировать как PNG', shortcut: 'Ctrl+Shift+C' },
|
|
898
|
+
{ id: 'save-template', label: 'Сохранить как шаблон' },
|
|
899
|
+
{ id: 'save-selection', label: 'Сохранить выделение как...' },
|
|
900
|
+
{ divider: true },
|
|
901
|
+
{ id: 'delete', label: 'Удалить', shortcut: 'Delete' },
|
|
372
902
|
];
|
|
373
903
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
backgroundColor: 'white',
|
|
382
|
-
border: '1px solid #ddd',
|
|
383
|
-
borderRadius: '6px',
|
|
384
|
-
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
385
|
-
padding: '8px',
|
|
386
|
-
zIndex: '10001',
|
|
387
|
-
minWidth: '200px'
|
|
388
|
-
});
|
|
904
|
+
items.forEach((item) => {
|
|
905
|
+
if (item.divider) {
|
|
906
|
+
const div = document.createElement('div');
|
|
907
|
+
div.className = 'ipp-dropdown-divider';
|
|
908
|
+
dropdown.appendChild(div);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
389
911
|
|
|
390
|
-
// Сетка цветов, как в TextPropertiesPanel
|
|
391
|
-
const grid = document.createElement('div');
|
|
392
|
-
grid.style.cssText = `
|
|
393
|
-
display: grid;
|
|
394
|
-
grid-template-columns: repeat(6, 28px);
|
|
395
|
-
gap: 6px;
|
|
396
|
-
margin: 0;
|
|
397
|
-
align-items: center;
|
|
398
|
-
justify-items: center;
|
|
399
|
-
`;
|
|
400
|
-
|
|
401
|
-
this._paletteButtons = [];
|
|
402
|
-
|
|
403
|
-
this._frameColors.forEach(color => {
|
|
404
912
|
const btn = document.createElement('button');
|
|
405
|
-
btn.
|
|
406
|
-
btn.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
border-right: 2px solid #111;
|
|
432
|
-
border-bottom: 2px solid #111;
|
|
433
|
-
display: none;
|
|
434
|
-
pointer-events: none;
|
|
435
|
-
`;
|
|
436
|
-
btn.appendChild(tick);
|
|
437
|
-
|
|
438
|
-
btn.addEventListener('click', () => {
|
|
439
|
-
// Снимаем активность со всех
|
|
440
|
-
this._paletteButtons.forEach(b => {
|
|
441
|
-
const i = b.querySelector('i');
|
|
442
|
-
if (i) i.style.display = 'none';
|
|
443
|
-
});
|
|
444
|
-
tick.style.display = 'block';
|
|
445
|
-
this._selectColor(color);
|
|
446
|
-
this._hideColorPalette();
|
|
913
|
+
btn.className = 'ipp-dropdown-item';
|
|
914
|
+
btn.dataset.id = 'fpp-more-' + item.id;
|
|
915
|
+
|
|
916
|
+
if (item.icon) {
|
|
917
|
+
const iconSpan = document.createElement('span');
|
|
918
|
+
iconSpan.className = 'ipp-dropdown-icon';
|
|
919
|
+
iconSpan.innerHTML = item.icon;
|
|
920
|
+
btn.appendChild(iconSpan);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const labelSpan = document.createElement('span');
|
|
924
|
+
labelSpan.textContent = item.label;
|
|
925
|
+
btn.appendChild(labelSpan);
|
|
926
|
+
|
|
927
|
+
if (item.shortcut) {
|
|
928
|
+
const sc = document.createElement('span');
|
|
929
|
+
sc.className = 'ipp-dropdown-item-shortcut';
|
|
930
|
+
sc.textContent = item.shortcut;
|
|
931
|
+
btn.appendChild(sc);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
btn.addEventListener('click', (e) => {
|
|
935
|
+
e.stopPropagation();
|
|
936
|
+
if (item.id === 'info') { this._showInfoModal(); return; }
|
|
937
|
+
this._handleMoreAction(item.id);
|
|
938
|
+
this._closeMenus();
|
|
447
939
|
});
|
|
448
940
|
|
|
449
|
-
|
|
450
|
-
this.
|
|
941
|
+
if (item.id === 'lock') { this._moreLockLabel = labelSpan; }
|
|
942
|
+
if (item.id === 'info') { this._infoBtnEl = btn; }
|
|
943
|
+
|
|
944
|
+
dropdown.appendChild(btn);
|
|
451
945
|
});
|
|
452
946
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
947
|
+
mainBtn.addEventListener('click', (e) => {
|
|
948
|
+
e.stopPropagation();
|
|
949
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
950
|
+
this._closeMenus();
|
|
951
|
+
if (!isOpen) {
|
|
952
|
+
dropdown.classList.add('is-open');
|
|
953
|
+
mainBtn.classList.add('is-active');
|
|
954
|
+
this._attachOutside();
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
this._moreMainBtn = mainBtn;
|
|
457
958
|
|
|
458
|
-
|
|
459
|
-
|
|
959
|
+
wrapper.appendChild(mainBtn);
|
|
960
|
+
wrapper.appendChild(dropdown);
|
|
961
|
+
return wrapper;
|
|
962
|
+
}
|
|
460
963
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
this.
|
|
465
|
-
} else {
|
|
466
|
-
this.
|
|
964
|
+
_handleMoreAction(id) {
|
|
965
|
+
if (!this.currentId) { return; }
|
|
966
|
+
if (id === 'copy') {
|
|
967
|
+
this.eventBus.emit(Events.Keyboard.Copy);
|
|
968
|
+
} else if (id === 'bring-front') {
|
|
969
|
+
this.eventBus.emit(Events.UI.LayerBringToFront, { objectId: this.currentId });
|
|
970
|
+
} else if (id === 'bring-forward') {
|
|
971
|
+
this.eventBus.emit(Events.UI.LayerBringForward, { objectId: this.currentId });
|
|
972
|
+
} else if (id === 'send-backward') {
|
|
973
|
+
this.eventBus.emit(Events.UI.LayerSendBackward, { objectId: this.currentId });
|
|
974
|
+
} else if (id === 'send-back') {
|
|
975
|
+
this.eventBus.emit(Events.UI.LayerSendToBack, { objectId: this.currentId });
|
|
976
|
+
} else if (id === 'lock') {
|
|
977
|
+
this._toggleLocked();
|
|
978
|
+
} else if (id === 'duplicate') {
|
|
979
|
+
this._duplicateFrame();
|
|
980
|
+
} else if (id === 'add-comment') {
|
|
981
|
+
this.eventBus.emit(Events.Comment.OpenImageDraft, { objectId: this.currentId });
|
|
982
|
+
} else if (id === 'delete') {
|
|
983
|
+
this.eventBus.emit(Events.Tool.ObjectsDelete, { objects: [this.currentId] });
|
|
467
984
|
}
|
|
468
985
|
}
|
|
469
986
|
|
|
470
|
-
|
|
471
|
-
if (!this.
|
|
987
|
+
_showInfoModal() {
|
|
988
|
+
if (!this.currentId || !this._infoPopover) { return; }
|
|
989
|
+
if (this._infoPopover.classList.contains('is-open')) {
|
|
990
|
+
this._infoPopover.classList.remove('is-open');
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const obj = this._getObjectData() || {};
|
|
995
|
+
const props = obj.properties || {};
|
|
472
996
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
997
|
+
const formatDate = (iso) => {
|
|
998
|
+
if (!iso) { return null; }
|
|
999
|
+
try {
|
|
1000
|
+
const d = new Date(iso);
|
|
1001
|
+
if (isNaN(d.getTime())) { return null; }
|
|
1002
|
+
return d.toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
|
|
1003
|
+
} catch (_) { return null; }
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const makeSection = (label, name, date) => {
|
|
1007
|
+
const section = document.createElement('div');
|
|
1008
|
+
section.className = 'ipp-info-section';
|
|
1009
|
+
const labelEl = document.createElement('div');
|
|
1010
|
+
labelEl.className = 'ipp-info-section__label';
|
|
1011
|
+
labelEl.textContent = label;
|
|
1012
|
+
const nameEl = document.createElement('div');
|
|
1013
|
+
nameEl.className = 'ipp-info-section__name';
|
|
1014
|
+
nameEl.textContent = name || '—';
|
|
1015
|
+
section.appendChild(labelEl);
|
|
1016
|
+
section.appendChild(nameEl);
|
|
1017
|
+
if (date) {
|
|
1018
|
+
const dateEl = document.createElement('div');
|
|
1019
|
+
dateEl.className = 'ipp-info-section__date';
|
|
1020
|
+
dateEl.textContent = date;
|
|
1021
|
+
section.appendChild(dateEl);
|
|
1022
|
+
}
|
|
1023
|
+
return section;
|
|
1024
|
+
};
|
|
480
1025
|
|
|
481
|
-
|
|
482
|
-
this.
|
|
1026
|
+
this._infoPopover.innerHTML = '';
|
|
1027
|
+
this._infoPopover.appendChild(makeSection('Кем создан:', null, formatDate(obj.created || props.createdAt)));
|
|
1028
|
+
this._infoPopover.appendChild(makeSection('Последний раз изменён:', null, formatDate(obj.updated || props.updatedAt)));
|
|
483
1029
|
|
|
484
|
-
|
|
1030
|
+
if (this._infoBtnEl) { this._infoPopover.style.top = this._infoBtnEl.offsetTop + 'px'; }
|
|
1031
|
+
this._infoPopover.classList.add('is-open');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
_attachOutside() {
|
|
485
1035
|
setTimeout(() => {
|
|
486
|
-
|
|
1036
|
+
if (this._boundDocumentClickHandler) {
|
|
1037
|
+
document.addEventListener('click', this._boundDocumentClickHandler);
|
|
1038
|
+
}
|
|
487
1039
|
}, 0);
|
|
488
1040
|
}
|
|
489
1041
|
|
|
490
|
-
|
|
491
|
-
if (this.colorPalette) {
|
|
492
|
-
this.colorPalette.style.display = 'none';
|
|
493
|
-
}
|
|
1042
|
+
_detachOutside() {
|
|
494
1043
|
if (this._boundDocumentClickHandler) {
|
|
495
1044
|
document.removeEventListener('click', this._boundDocumentClickHandler);
|
|
496
1045
|
}
|
|
497
1046
|
}
|
|
498
1047
|
|
|
1048
|
+
_closeMenus() {
|
|
1049
|
+
this._hideColorPopup();
|
|
1050
|
+
if (this._ratioDropdown) { this._ratioDropdown.classList.remove('is-open'); }
|
|
1051
|
+
if (this._ratioBtn) { this._ratioBtn.classList.remove('is-active'); }
|
|
1052
|
+
if (this._moreDropdown) { this._moreDropdown.classList.remove('is-open'); }
|
|
1053
|
+
if (this._moreMainBtn) { this._moreMainBtn.classList.remove('is-active'); }
|
|
1054
|
+
if (this._infoPopover) { this._infoPopover.classList.remove('is-open'); }
|
|
1055
|
+
this._detachOutside();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
499
1058
|
_documentClickHandler(e) {
|
|
500
|
-
|
|
501
|
-
if (this.
|
|
502
|
-
|
|
503
|
-
this._hideColorPalette();
|
|
504
|
-
}
|
|
1059
|
+
const t = e.target;
|
|
1060
|
+
if (this.panel && this.panel.contains(t)) { return; }
|
|
1061
|
+
this._closeMenus();
|
|
505
1062
|
}
|
|
506
1063
|
|
|
507
|
-
|
|
508
|
-
if (!this.currentId) return;
|
|
1064
|
+
_updateControlsFromObject() {
|
|
1065
|
+
if (!this.currentId) { return; }
|
|
1066
|
+
const obj = this._getObjectData();
|
|
1067
|
+
if (!obj) { return; }
|
|
1068
|
+
|
|
1069
|
+
const bgColor = obj.backgroundColor ?? (obj.properties?.backgroundColor);
|
|
1070
|
+
const bgMode = (obj.properties?.bgMode) || (obj.bgMode) || 'solid';
|
|
1071
|
+
const isTransparent = bgColor === null || bgColor === undefined || bgMode === 'outline';
|
|
1072
|
+
|
|
1073
|
+
if (this.colorButton) {
|
|
1074
|
+
if (isTransparent) {
|
|
1075
|
+
this.colorButton.style.backgroundColor = 'transparent';
|
|
1076
|
+
this.colorButton.classList.add('fpp-color-button--none');
|
|
1077
|
+
} else {
|
|
1078
|
+
const hex = (typeof bgColor === 'number') ? pixiToHex(bgColor) : '#FFFFFF';
|
|
1079
|
+
this.colorButton.style.backgroundColor = hex;
|
|
1080
|
+
this.colorButton.classList.remove('fpp-color-button--none');
|
|
1081
|
+
this.colorButton.title = 'Цвет фона: ' + hex;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
509
1085
|
|
|
1086
|
+
_syncTypeFromObject() {
|
|
1087
|
+
if (!this._ratioValueEl || !this.currentId) { return; }
|
|
1088
|
+
const objectData = this._getObjectData();
|
|
1089
|
+
const t = (objectData && objectData.properties && objectData.properties.type) || 'custom';
|
|
1090
|
+
this._ratioValueEl.textContent = RATIO_LABELS[t] || RATIO_LABELS.custom;
|
|
1091
|
+
}
|
|
510
1092
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1093
|
+
_isLocked() {
|
|
1094
|
+
const data = this._getObjectData();
|
|
1095
|
+
return !!(data && data.properties && data.properties.locked);
|
|
1096
|
+
}
|
|
514
1097
|
|
|
515
|
-
|
|
1098
|
+
_toggleLocked() {
|
|
1099
|
+
if (!this.currentId) { return; }
|
|
1100
|
+
const newLocked = !this._isLocked();
|
|
516
1101
|
this.eventBus.emit(Events.Object.StateChanged, {
|
|
517
1102
|
objectId: this.currentId,
|
|
518
|
-
updates: {
|
|
1103
|
+
updates: { properties: { locked: newLocked } },
|
|
519
1104
|
});
|
|
1105
|
+
this._updateLockUI();
|
|
1106
|
+
this.reposition();
|
|
520
1107
|
}
|
|
521
1108
|
|
|
522
|
-
|
|
523
|
-
if (!this.
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
1109
|
+
_updateLockUI() {
|
|
1110
|
+
if (!this._btn_lock) { return; }
|
|
1111
|
+
const locked = this._isLocked();
|
|
1112
|
+
|
|
1113
|
+
const data = this._getObjectData();
|
|
1114
|
+
const hidden = !!(data && data.properties && data.properties.hidden);
|
|
1115
|
+
|
|
1116
|
+
this._btn_lock.innerHTML = locked ? ICONS.lock : ICONS.unlock;
|
|
1117
|
+
this._btn_lock.title = locked ? 'Разблокировать' : 'Заблокировать';
|
|
1118
|
+
|
|
1119
|
+
if (this._eyeBtn) {
|
|
1120
|
+
this._eyeBtn.dataset.hidden = hidden ? '1' : '0';
|
|
1121
|
+
this._eyeBtn.innerHTML = hidden ? ICONS.eyeOff : ICONS.eye;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (Array.isArray(this._lockableEls)) {
|
|
1125
|
+
this._lockableEls.forEach((el) => {
|
|
1126
|
+
if (!el) { return; }
|
|
1127
|
+
if (locked) {
|
|
1128
|
+
el.style.display = 'none';
|
|
1129
|
+
} else if (hidden && this._leftEls && this._leftEls.includes(el)) {
|
|
1130
|
+
el.style.display = 'none';
|
|
1131
|
+
} else {
|
|
1132
|
+
el.style.display = '';
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (this._moreLockLabel) {
|
|
1137
|
+
this._moreLockLabel.textContent = locked ? 'Разблокировать' : 'Заблокировать';
|
|
1138
|
+
}
|
|
528
1139
|
}
|
|
529
1140
|
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
1141
|
+
_toggleVisibility(btn) {
|
|
1142
|
+
if (!this.currentId) { return; }
|
|
1143
|
+
const objectData = this._getObjectData();
|
|
1144
|
+
const currentlyHidden = !!(objectData && objectData.properties && objectData.properties.hidden);
|
|
1145
|
+
const newHidden = !currentlyHidden;
|
|
1146
|
+
|
|
1147
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
1148
|
+
objectId: this.currentId,
|
|
1149
|
+
updates: { properties: { hidden: newHidden } },
|
|
539
1150
|
});
|
|
1151
|
+
|
|
1152
|
+
if (this._eyeBtn) {
|
|
1153
|
+
this._eyeBtn.dataset.hidden = newHidden ? '1' : '0';
|
|
1154
|
+
this._eyeBtn.innerHTML = newHidden ? ICONS.eyeOff : ICONS.eye;
|
|
1155
|
+
}
|
|
1156
|
+
this._updateLockUI();
|
|
1157
|
+
this.reposition();
|
|
540
1158
|
}
|
|
541
1159
|
|
|
542
|
-
|
|
543
|
-
if (!this.currentId) return;
|
|
1160
|
+
_openRename() {
|
|
1161
|
+
if (!this.currentId) { return; }
|
|
1162
|
+
this._closeMenus();
|
|
1163
|
+
const data = this._getObjectData();
|
|
1164
|
+
const selectTool = this.core?.selectTool;
|
|
1165
|
+
if (selectTool && typeof selectTool._openFrameTitleEditor === 'function') {
|
|
1166
|
+
selectTool._openFrameTitleEditor({ id: this.currentId, properties: data?.properties || {} });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
544
1169
|
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1170
|
+
_duplicateFrame() {
|
|
1171
|
+
if (!this.currentId) { return; }
|
|
1172
|
+
const posData = { objectId: this.currentId, position: null };
|
|
1173
|
+
const sizeData = { objectId: this.currentId, size: null };
|
|
1174
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
1175
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
1176
|
+
if (!posData.position || !sizeData.size) { return; }
|
|
551
1177
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
1178
|
+
const originalId = this.currentId;
|
|
1179
|
+
const newPos = { x: posData.position.x + sizeData.size.width + 14, y: posData.position.y };
|
|
1180
|
+
|
|
1181
|
+
const onReady = (data) => {
|
|
1182
|
+
if (!data || data.originalId !== originalId) { return; }
|
|
1183
|
+
this.eventBus.off(Events.Tool.DuplicateReady, onReady);
|
|
1184
|
+
this._selectObject(data.newId);
|
|
1185
|
+
};
|
|
1186
|
+
this.eventBus.on(Events.Tool.DuplicateReady, onReady);
|
|
1187
|
+
this.eventBus.emit(Events.Tool.DuplicateRequest, { originalId, position: newPos });
|
|
562
1188
|
}
|
|
563
1189
|
|
|
564
|
-
|
|
565
|
-
if (!
|
|
1190
|
+
_selectObject(objectId) {
|
|
1191
|
+
if (!objectId) { return; }
|
|
1192
|
+
const selectTool = this.core?.selectTool;
|
|
1193
|
+
if (!selectTool || typeof selectTool.setSelection !== 'function') { return; }
|
|
1194
|
+
selectTool.setSelection([objectId]);
|
|
1195
|
+
if (typeof selectTool.updateResizeHandles === 'function') { selectTool.updateResizeHandles(); }
|
|
1196
|
+
}
|
|
566
1197
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
|
|
1198
|
+
_repositionThrottled() {
|
|
1199
|
+
if (this._repositionScheduled) { return; }
|
|
1200
|
+
this._repositionScheduled = true;
|
|
1201
|
+
const rafId = requestAnimationFrame(() => {
|
|
1202
|
+
this._repositionScheduled = false;
|
|
1203
|
+
this._repositionRafId = null;
|
|
1204
|
+
if (!this.panel) { return; }
|
|
1205
|
+
this.reposition();
|
|
1206
|
+
});
|
|
1207
|
+
this._repositionRafId = rafId;
|
|
571
1208
|
}
|
|
572
1209
|
|
|
573
|
-
|
|
574
|
-
if (!this.
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
this.
|
|
1210
|
+
reposition() {
|
|
1211
|
+
if (!this.panel || !this.currentId || this.panel.style.display === 'none') { return; }
|
|
1212
|
+
|
|
1213
|
+
const ids = this.core?.selectTool ? Array.from(this.core.selectTool.selectedObjects || []) : [];
|
|
1214
|
+
if (!ids.includes(this.currentId)) { this.hide(); return; }
|
|
1215
|
+
|
|
1216
|
+
const posData = { objectId: this.currentId, position: null };
|
|
1217
|
+
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
1218
|
+
const sizeData = { objectId: this.currentId, size: null };
|
|
1219
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
1220
|
+
if (!posData.position || !sizeData.size) { return; }
|
|
1221
|
+
|
|
1222
|
+
const worldLayer = this.core?.pixi?.worldLayer;
|
|
1223
|
+
const scale = worldLayer?.scale?.x || 1;
|
|
1224
|
+
const worldX = worldLayer?.x || 0;
|
|
1225
|
+
const worldY = worldLayer?.y || 0;
|
|
1226
|
+
|
|
1227
|
+
const screenX = posData.position.x * scale + worldX;
|
|
1228
|
+
const screenY = posData.position.y * scale + worldY;
|
|
1229
|
+
const objectWidth = sizeData.size.width * scale;
|
|
1230
|
+
const objectHeight = sizeData.size.height * scale;
|
|
1231
|
+
|
|
1232
|
+
const panelW = this.panel.offsetWidth || 300;
|
|
1233
|
+
const panelH = this.panel.offsetHeight || 44;
|
|
1234
|
+
let panelX = screenX + (objectWidth / 2) - (panelW / 2);
|
|
1235
|
+
let panelY = screenY - panelH - 40;
|
|
1236
|
+
if (panelY < 0) { panelY = screenY + objectHeight + 40; }
|
|
1237
|
+
|
|
1238
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1239
|
+
const finalX = Math.max(10, Math.min(panelX, containerRect.width - panelW - 10));
|
|
1240
|
+
const finalY = Math.max(10, panelY);
|
|
1241
|
+
|
|
1242
|
+
this.panel.style.left = Math.round(finalX) + 'px';
|
|
1243
|
+
this.panel.style.top = Math.round(finalY) + 'px';
|
|
578
1244
|
}
|
|
579
1245
|
|
|
580
1246
|
_applyFrameType(typeValue) {
|
|
581
|
-
if (!this.currentId || !this.core?.history) return;
|
|
582
|
-
|
|
583
|
-
const objectData = this.core.getObjectData(this.currentId);
|
|
1247
|
+
if (!this.currentId || !this.core?.history) { return; }
|
|
1248
|
+
const objectData = this._getObjectData();
|
|
584
1249
|
const oldType = (objectData?.properties?.type) || 'custom';
|
|
585
|
-
|
|
586
|
-
if (oldType === typeValue) return;
|
|
1250
|
+
if (oldType === typeValue) { this._syncTypeFromObject(); return; }
|
|
587
1251
|
|
|
588
1252
|
const willLockAfter = typeValue !== 'custom';
|
|
589
1253
|
|
|
590
1254
|
if (!willLockAfter) {
|
|
591
|
-
|
|
592
|
-
const command = new UpdateFrameTypeCommand(
|
|
593
|
-
this.core,
|
|
594
|
-
this.currentId,
|
|
595
|
-
oldType,
|
|
596
|
-
typeValue,
|
|
597
|
-
null,
|
|
598
|
-
null,
|
|
599
|
-
null,
|
|
600
|
-
null
|
|
601
|
-
);
|
|
1255
|
+
const command = new UpdateFrameTypeCommand(this.core, this.currentId, oldType, typeValue, null, null, null, null);
|
|
602
1256
|
command.setEventBus(this.core.eventBus);
|
|
603
1257
|
this.core.history.executeCommand(command);
|
|
604
1258
|
} else {
|
|
605
|
-
|
|
606
|
-
const aspectMap = { 'a4': 210 / 297, '1x1': 1, '4x3': 4 / 3, '16x9': 16 / 9 };
|
|
1259
|
+
const aspectMap = { a4: 210 / 297, '1x1': 1, '4x3': 4 / 3, '16x9': 16 / 9 };
|
|
607
1260
|
const aspect = aspectMap[typeValue] || 1;
|
|
608
1261
|
|
|
609
1262
|
const posData = { objectId: this.currentId, position: null };
|
|
610
1263
|
const sizeData = { objectId: this.currentId, size: null };
|
|
611
1264
|
this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
|
|
612
1265
|
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
613
|
-
if (!posData.position || !sizeData.size) return;
|
|
1266
|
+
if (!posData.position || !sizeData.size) { return; }
|
|
614
1267
|
|
|
615
1268
|
const oldX = posData.position.x;
|
|
616
1269
|
const oldY = posData.position.y;
|
|
@@ -620,20 +1273,15 @@ export class FramePropertiesPanel {
|
|
|
620
1273
|
const cy = oldY + oldH / 2;
|
|
621
1274
|
|
|
622
1275
|
const area = oldW * oldH;
|
|
623
|
-
|
|
624
|
-
|
|
1276
|
+
const newW = Math.max(1, Math.round(Math.sqrt(area * aspect)));
|
|
1277
|
+
const newH = Math.max(1, Math.round(newW / aspect));
|
|
625
1278
|
const newX = Math.round(cx - newW / 2);
|
|
626
1279
|
const newY = Math.round(cy - newH / 2);
|
|
627
1280
|
|
|
628
1281
|
const command = new UpdateFrameTypeCommand(
|
|
629
|
-
this.core,
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
typeValue,
|
|
633
|
-
{ width: oldW, height: oldH },
|
|
634
|
-
{ width: newW, height: newH },
|
|
635
|
-
{ x: oldX, y: oldY },
|
|
636
|
-
{ x: newX, y: newY }
|
|
1282
|
+
this.core, this.currentId, oldType, typeValue,
|
|
1283
|
+
{ width: oldW, height: oldH }, { width: newW, height: newH },
|
|
1284
|
+
{ x: oldX, y: oldY }, { x: newX, y: newY }
|
|
637
1285
|
);
|
|
638
1286
|
command.setEventBus(this.core.eventBus);
|
|
639
1287
|
this.core.history.executeCommand(command);
|
|
@@ -643,7 +1291,9 @@ export class FramePropertiesPanel {
|
|
|
643
1291
|
}
|
|
644
1292
|
|
|
645
1293
|
destroy() {
|
|
646
|
-
this.
|
|
1294
|
+
this._closeColorPicker();
|
|
1295
|
+
this._detachOutside();
|
|
1296
|
+
|
|
647
1297
|
if (this._repositionRafId != null) {
|
|
648
1298
|
cancelAnimationFrame(this._repositionRafId);
|
|
649
1299
|
this._repositionRafId = null;
|
|
@@ -659,7 +1309,9 @@ export class FramePropertiesPanel {
|
|
|
659
1309
|
this.eventBus.off(Events.Tool.GroupDragUpdate, this._handlers.onGroupDragUpdate);
|
|
660
1310
|
this.eventBus.off(Events.Tool.GroupDragStart, this._handlers.onGroupDragStart);
|
|
661
1311
|
this.eventBus.off(Events.Tool.GroupDragEnd, this._handlers.onGroupDragEnd);
|
|
1312
|
+
this.eventBus.off(Events.Tool.ResizeStart, this._handlers.onResizeStart);
|
|
662
1313
|
this.eventBus.off(Events.Tool.ResizeUpdate, this._handlers.onResizeUpdate);
|
|
1314
|
+
this.eventBus.off(Events.Tool.ResizeEnd, this._handlers.onResizeEnd);
|
|
663
1315
|
this.eventBus.off(Events.Tool.RotateUpdate, this._handlers.onRotateUpdate);
|
|
664
1316
|
this.eventBus.off(Events.UI.ZoomPercent, this._handlers.onZoomPercent);
|
|
665
1317
|
this.eventBus.off(Events.Tool.PanUpdate, this._handlers.onPanUpdate);
|
|
@@ -674,17 +1326,19 @@ export class FramePropertiesPanel {
|
|
|
674
1326
|
document.removeEventListener('click', this._boundDocumentClickHandler);
|
|
675
1327
|
this._boundDocumentClickHandler = null;
|
|
676
1328
|
}
|
|
677
|
-
if (this.
|
|
678
|
-
|
|
679
|
-
|
|
1329
|
+
if (this._boundPickerMouseMove) {
|
|
1330
|
+
document.removeEventListener('mousemove', this._boundPickerMouseMove);
|
|
1331
|
+
}
|
|
1332
|
+
if (this._boundPickerMouseUp) {
|
|
1333
|
+
document.removeEventListener('mouseup', this._boundPickerMouseUp);
|
|
680
1334
|
}
|
|
681
1335
|
if (this.panel && this.panel.parentNode) {
|
|
682
1336
|
this.panel.parentNode.removeChild(this.panel);
|
|
683
1337
|
}
|
|
684
1338
|
this.panel = null;
|
|
685
|
-
this.
|
|
1339
|
+
this._colorPopup = null;
|
|
1340
|
+
this._colorPickerModal = null;
|
|
686
1341
|
this.colorButton = null;
|
|
687
|
-
this.titleInput = null;
|
|
688
1342
|
this.currentId = null;
|
|
689
1343
|
}
|
|
690
1344
|
}
|