@mhmttosun/dnd 0.0.0-stage → 1.1.1
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/LICENSE +21 -0
- package/README.md +327 -2
- package/css/answer-key.css +200 -0
- package/css/editor.css +1797 -0
- package/css/player.css +782 -0
- package/css/stage.css +490 -0
- package/dist/dnd-player-element.js +1758 -0
- package/dist/dnd.js +2041 -0
- package/dist/editor.css +3278 -0
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +8506 -0
- package/dist/index.d.ts +102 -0
- package/dist/player.css +782 -0
- package/dist/player.d.ts +1 -0
- package/dist/player.js +2041 -0
- package/dist/player.umd.cjs +1758 -0
- package/index.html +203 -0
- package/js/app.js +188 -0
- package/js/core/AnswerKeyManager.js +494 -0
- package/js/core/EditorCore.js +220 -0
- package/js/core/HistoryManager.js +194 -0
- package/js/core/LayerManager.js +86 -0
- package/js/core/ObjectManager.js +468 -0
- package/js/core/SelectionManager.js +459 -0
- package/js/core/StageManager.js +295 -0
- package/js/data/SampleProjects.js +883 -0
- package/js/engine/TransformEngine.js +632 -0
- package/js/engine/ValidationEngine.js +147 -0
- package/js/player/ActivityEngine.js +1272 -0
- package/js/player/ExportManager.js +820 -0
- package/js/ui/MiniToolbar.js +1546 -0
- package/js/ui/PropertyPanel.js +776 -0
- package/js/ui/UIManager.js +596 -0
- package/js/utils/AudioSynthesizer.js +111 -0
- package/package.json +69 -4
- package/src/ActivityPlayer.js +309 -0
- package/src/DNDEditor.js +217 -0
- package/src/components/DNDPlayerElement.js +191 -0
- package/src/index.js +12 -0
- package/src/player.js +13 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyPanel - Sağ tarafta yer alan dinamik özellikler paneli.
|
|
3
|
+
* Seçili nesnenin özelliklerini düzenleme, katmanlar ve nesne listesini yönetme,
|
|
4
|
+
* kilit açma/kapama ve genel proje/oyun ayarlarını yapılandırma imkanı sunar.
|
|
5
|
+
*/
|
|
6
|
+
class PropertyPanel {
|
|
7
|
+
constructor(core, objectManager, selectionManager, layerManager, panelEl) {
|
|
8
|
+
this.core = core;
|
|
9
|
+
this.objectManager = objectManager;
|
|
10
|
+
this.selectionManager = selectionManager;
|
|
11
|
+
this.layerManager = layerManager;
|
|
12
|
+
this.panel = panelEl;
|
|
13
|
+
|
|
14
|
+
this.activeTab = 'properties'; // 'properties' | 'layers' | 'answer-key' | 'settings'
|
|
15
|
+
this.init();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init() {
|
|
19
|
+
this.core.on('selectionChanged', () => this.render());
|
|
20
|
+
this.core.on('objectUpdated', () => this.updateFieldsIfSelected());
|
|
21
|
+
this.core.on('projectLoaded', () => this.render());
|
|
22
|
+
this.core.on('activityTypeChanged', () => this.render());
|
|
23
|
+
this.core.on('layerChanged', () => {
|
|
24
|
+
if (this.activeTab === 'layers') this.render();
|
|
25
|
+
});
|
|
26
|
+
this.core.on('answerKeyChanged', () => {
|
|
27
|
+
if (this.activeTab === 'answer-key') this.render();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setTab(tabName) {
|
|
34
|
+
this.activeTab = tabName;
|
|
35
|
+
this.render();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
const selectedObj = this.selectionManager.getPrimarySelected();
|
|
40
|
+
const selectedCount = this.selectionManager.selectedIds.size;
|
|
41
|
+
|
|
42
|
+
this.panel.innerHTML = `
|
|
43
|
+
<div class="panel-tabs">
|
|
44
|
+
<button class="panel-tab-btn ${this.activeTab === 'properties' ? 'active' : ''}" data-tab="properties">
|
|
45
|
+
${selectedCount > 0 ? '🎨 Nesne' : '⚙️ Proje'}
|
|
46
|
+
</button>
|
|
47
|
+
<button class="panel-tab-btn ${this.activeTab === 'layers' ? 'active' : ''}" data-tab="layers">
|
|
48
|
+
📑 Katmanlar
|
|
49
|
+
</button>
|
|
50
|
+
<button class="panel-tab-btn ${this.activeTab === 'answer-key' ? 'active' : ''}" data-tab="answer-key">
|
|
51
|
+
🔑 Cevaplar
|
|
52
|
+
</button>
|
|
53
|
+
<button class="panel-tab-btn ${this.activeTab === 'settings' ? 'active' : ''}" data-tab="settings">
|
|
54
|
+
🎮 Ayarlar
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="panel-scroll-content">
|
|
58
|
+
${this.renderTabContent(selectedObj, selectedCount)}
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
// Tab buton dinleyicileri
|
|
63
|
+
this.panel.querySelectorAll('.panel-tab-btn').forEach(btn => {
|
|
64
|
+
btn.onclick = () => this.setTab(btn.getAttribute('data-tab'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.bindEvents(selectedObj);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
renderTabContent(selectedObj, selectedCount) {
|
|
71
|
+
if (this.activeTab === 'settings') {
|
|
72
|
+
return this.renderProjectSettings();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.activeTab === 'layers') {
|
|
76
|
+
return this.renderLayersTab();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.activeTab === 'answer-key') {
|
|
80
|
+
return this.renderAnswerKeySettings(selectedObj);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Properties Tab
|
|
84
|
+
if (selectedCount > 1) {
|
|
85
|
+
return this.renderMultiSelectProperties(selectedCount);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (selectedObj) {
|
|
89
|
+
return this.renderSingleObjectProperties(selectedObj);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.renderStageProperties();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
renderSingleObjectProperties(obj) {
|
|
96
|
+
const isText = obj.type === 'text';
|
|
97
|
+
const isImage = obj.type === 'image';
|
|
98
|
+
const isLine = obj.type === 'line';
|
|
99
|
+
const isZone = ['target', 'category', 'sequence_slot'].includes(obj.role);
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<!-- Kilit Durumu ve Hızlı Kilit Açma/Kilitleme Bannerı -->
|
|
103
|
+
${obj.locked ? `
|
|
104
|
+
<div class="locked-banner">
|
|
105
|
+
<div class="locked-banner-info">
|
|
106
|
+
<span class="locked-icon">🔒</span>
|
|
107
|
+
<div>
|
|
108
|
+
<strong style="color: #b45309;">Bu Nesne Kilitli</strong>
|
|
109
|
+
<div class="locked-desc">Taşıma ve boyutlandırma korumalıdır.</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<button type="button" class="btn-unlock-action" id="btn-unlock-prop" title="Kilidi Aç">🔓 Kilidi Aç</button>
|
|
113
|
+
</div>
|
|
114
|
+
` : `
|
|
115
|
+
<div class="lock-quick-bar">
|
|
116
|
+
<button type="button" class="btn-lock-action" id="btn-lock-prop" title="Yanlışlıkla taşınmasını önlemek için kilitle">
|
|
117
|
+
🔒 Nesneyi Kilitle
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
`}
|
|
121
|
+
|
|
122
|
+
<!-- Genel Bilgiler -->
|
|
123
|
+
<div class="prop-group">
|
|
124
|
+
<div class="prop-group-title">Genel Özellikler</div>
|
|
125
|
+
<div class="prop-row">
|
|
126
|
+
<label class="prop-label">Nesne Adı</label>
|
|
127
|
+
<input type="text" class="prop-input" id="prop-name" value="${window.DND.ValidationEngine.sanitizeText(obj.name)}">
|
|
128
|
+
</div>
|
|
129
|
+
<div class="prop-row">
|
|
130
|
+
<label class="prop-label">Nesne ID</label>
|
|
131
|
+
<input type="text" class="prop-input readonly" value="${obj.id}" readonly>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="prop-row">
|
|
134
|
+
<label class="prop-label">Rol</label>
|
|
135
|
+
<select class="prop-select" id="prop-role">
|
|
136
|
+
<option value="decorative" ${obj.role === 'decorative' ? 'selected' : ''}>Dekoratif (Sabit)</option>
|
|
137
|
+
<option value="draggable" ${obj.role === 'draggable' ? 'selected' : ''}>🎯 Sürüklenebilir Öğe</option>
|
|
138
|
+
<option value="target" ${obj.role === 'target' ? 'selected' : ''}>📥 Hedef Alanı</option>
|
|
139
|
+
<option value="category" ${obj.role === 'category' ? 'selected' : ''}>📦 Kategori Alanı</option>
|
|
140
|
+
<option value="sequence_slot" ${obj.role === 'sequence_slot' ? 'selected' : ''}>🔢 Sıralama Yuvası</option>
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Konum ve Boyut -->
|
|
146
|
+
<div class="prop-group">
|
|
147
|
+
<div class="prop-group-title">Konum ve Boyut (px)</div>
|
|
148
|
+
<div class="prop-grid-2">
|
|
149
|
+
<div class="prop-col">
|
|
150
|
+
<label class="prop-sublabel">X</label>
|
|
151
|
+
<input type="number" class="prop-input" id="prop-x" value="${Math.round(obj.x)}" ${obj.locked ? 'disabled' : ''}>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="prop-col">
|
|
154
|
+
<label class="prop-sublabel">Y</label>
|
|
155
|
+
<input type="number" class="prop-input" id="prop-y" value="${Math.round(obj.y)}" ${obj.locked ? 'disabled' : ''}>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="prop-grid-2">
|
|
159
|
+
<div class="prop-col">
|
|
160
|
+
<label class="prop-sublabel">Genişlik</label>
|
|
161
|
+
<input type="number" class="prop-input" id="prop-width" value="${Math.round(obj.width)}" ${obj.locked ? 'disabled' : ''}>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="prop-col">
|
|
164
|
+
<label class="prop-sublabel">Yükseklik</label>
|
|
165
|
+
<input type="number" class="prop-input" id="prop-height" value="${Math.round(obj.height)}" ${obj.locked ? 'disabled' : ''}>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="prop-grid-2">
|
|
169
|
+
<div class="prop-col">
|
|
170
|
+
<label class="prop-sublabel">Döndürme (°)</label>
|
|
171
|
+
<input type="number" class="prop-input" id="prop-rotation" value="${Math.round(obj.rotation || 0)}" ${obj.locked ? 'disabled' : ''}>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="prop-col">
|
|
174
|
+
<label class="prop-sublabel">Opaklık (0-1)</label>
|
|
175
|
+
<input type="number" class="prop-input" id="prop-opacity" step="0.1" min="0" max="1" value="${obj.opacity !== undefined ? obj.opacity : 1}">
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Katman ve Durum -->
|
|
181
|
+
<div class="prop-group">
|
|
182
|
+
<div class="prop-group-title">Katman ve Kilit</div>
|
|
183
|
+
<div class="prop-btn-row">
|
|
184
|
+
<button class="prop-tool-btn" id="btn-bring-front" title="En Öne Getir">🔝 En Öne</button>
|
|
185
|
+
<button class="prop-tool-btn" id="btn-bring-forward" title="Bir Katman Öne">⬆️ Öne</button>
|
|
186
|
+
<button class="prop-tool-btn" id="btn-send-backward" title="Bir Katman Arkaya">⬇️ Arkaya</button>
|
|
187
|
+
<button class="prop-tool-btn" id="btn-send-back" title="En Arkaya Gönder">🔚 En Arkaya</button>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="prop-row checkbox-row" style="margin-top: 6px;">
|
|
190
|
+
<label><input type="checkbox" id="prop-locked" ${obj.locked ? 'checked' : ''}> 🔒 Nesneyi Kilitle (Taşınamaz)</label>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
${isLine ? `
|
|
195
|
+
<!-- Çizgi Görünümü -->
|
|
196
|
+
<div class="prop-group">
|
|
197
|
+
<div class="prop-group-title">Çizgi Görünümü</div>
|
|
198
|
+
<div class="prop-grid-2">
|
|
199
|
+
<div class="prop-col">
|
|
200
|
+
<label class="prop-sublabel">Çizgi Rengi</label>
|
|
201
|
+
<input type="color" class="prop-color-picker" id="prop-bordercolor" value="${this.sanitizeColorHex(obj.borderColor, '#1e293b')}">
|
|
202
|
+
</div>
|
|
203
|
+
<div class="prop-col">
|
|
204
|
+
<label class="prop-sublabel">Çizgi Kalınlığı (px)</label>
|
|
205
|
+
<input type="number" class="prop-input" id="prop-borderwidth" min="1" max="40" value="${obj.borderWidth !== undefined ? obj.borderWidth : 3}">
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="prop-row">
|
|
209
|
+
<label class="prop-label">Çizgi Stili</label>
|
|
210
|
+
<select class="prop-select" id="prop-borderstyle">
|
|
211
|
+
<option value="solid" ${obj.borderStyle === 'solid' ? 'selected' : ''}>Düz (Solid)</option>
|
|
212
|
+
<option value="dashed" ${obj.borderStyle === 'dashed' ? 'selected' : ''}>Kesikli (Dashed)</option>
|
|
213
|
+
<option value="dotted" ${obj.borderStyle === 'dotted' ? 'selected' : ''}>Noktalı (Dotted)</option>
|
|
214
|
+
</select>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
` : `
|
|
218
|
+
<!-- Metin ve Tipografi -->
|
|
219
|
+
<div class="prop-group">
|
|
220
|
+
<div class="prop-group-title">İçerik ve Tipografi</div>
|
|
221
|
+
<div class="prop-row">
|
|
222
|
+
<label class="prop-label">Metin</label>
|
|
223
|
+
<textarea class="prop-textarea" id="prop-text" rows="2" placeholder="${['rectangle', 'circle', 'image'].includes(obj.type) ? 'Metin eklemek için yazın...' : ''}">${window.DND.ValidationEngine.sanitizeText(obj.text || '')}</textarea>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="prop-grid-2">
|
|
226
|
+
<div class="prop-col">
|
|
227
|
+
<label class="prop-sublabel">Boyut (px)</label>
|
|
228
|
+
<input type="number" class="prop-input" id="prop-fontsize" value="${obj.fontSize || 16}">
|
|
229
|
+
</div>
|
|
230
|
+
<div class="prop-col">
|
|
231
|
+
<label class="prop-sublabel">Yazı Rengi</label>
|
|
232
|
+
<input type="color" class="prop-color-picker" id="prop-textcolor" value="${obj.textColor || '#1e293b'}">
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="prop-grid-2">
|
|
236
|
+
<div class="prop-col">
|
|
237
|
+
<label class="prop-sublabel">Kalınlık</label>
|
|
238
|
+
<select class="prop-select" id="prop-fontweight">
|
|
239
|
+
<option value="normal" ${obj.fontWeight === 'normal' ? 'selected' : ''}>Normal</option>
|
|
240
|
+
<option value="600" ${obj.fontWeight === '600' ? 'selected' : ''}>Yarı Kalın</option>
|
|
241
|
+
<option value="bold" ${obj.fontWeight === 'bold' || obj.fontWeight === '700' || obj.fontWeight === '800' ? 'selected' : ''}>Kalın</option>
|
|
242
|
+
</select>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="prop-col">
|
|
245
|
+
<label class="prop-sublabel">Hizalama</label>
|
|
246
|
+
<select class="prop-select" id="prop-textalign">
|
|
247
|
+
<option value="left" ${obj.textAlign === 'left' ? 'selected' : ''}>Sol</option>
|
|
248
|
+
<option value="center" ${obj.textAlign === 'center' ? 'selected' : ''}>Orta</option>
|
|
249
|
+
<option value="right" ${obj.textAlign === 'right' ? 'selected' : ''}>Sağ</option>
|
|
250
|
+
</select>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Görünüm ve Kenarlık -->
|
|
256
|
+
<div class="prop-group">
|
|
257
|
+
<div class="prop-group-title">Renk ve Kenarlık</div>
|
|
258
|
+
<div class="prop-grid-2">
|
|
259
|
+
<div class="prop-col">
|
|
260
|
+
<label class="prop-sublabel">Arka Plan</label>
|
|
261
|
+
<input type="color" class="prop-color-picker" id="prop-bgcolor" value="${this.sanitizeColorHex(obj.backgroundColor, '#ffffff')}">
|
|
262
|
+
</div>
|
|
263
|
+
<div class="prop-col">
|
|
264
|
+
<label class="prop-sublabel">Kenarlık Rengi</label>
|
|
265
|
+
<input type="color" class="prop-color-picker" id="prop-bordercolor" value="${this.sanitizeColorHex(obj.borderColor, '#94a3b8')}">
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="prop-grid-2">
|
|
269
|
+
<div class="prop-col">
|
|
270
|
+
<label class="prop-sublabel">Kenar Kalınlığı</label>
|
|
271
|
+
<input type="number" class="prop-input" id="prop-borderwidth" min="0" max="20" value="${obj.borderWidth !== undefined ? obj.borderWidth : 2}">
|
|
272
|
+
</div>
|
|
273
|
+
<div class="prop-col">
|
|
274
|
+
<label class="prop-sublabel">Köşe Yuvarlaklığı</label>
|
|
275
|
+
<input type="number" class="prop-input" id="prop-borderradius" min="0" max="100" value="${obj.borderRadius !== undefined ? obj.borderRadius : 8}">
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="prop-row">
|
|
279
|
+
<label class="prop-label">Kenar Stili</label>
|
|
280
|
+
<select class="prop-select" id="prop-borderstyle">
|
|
281
|
+
<option value="solid" ${obj.borderStyle === 'solid' ? 'selected' : ''}>Düz (Solid)</option>
|
|
282
|
+
<option value="dashed" ${obj.borderStyle === 'dashed' ? 'selected' : ''}>Kesikli (Dashed)</option>
|
|
283
|
+
<option value="dotted" ${obj.borderStyle === 'dotted' ? 'selected' : ''}>Noktalı (Dotted)</option>
|
|
284
|
+
<option value="none" ${obj.borderStyle === 'none' ? 'selected' : ''}>Yok</option>
|
|
285
|
+
</select>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
`}
|
|
289
|
+
|
|
290
|
+
<!-- Hedef / Kategori Özel Alanları -->
|
|
291
|
+
${isZone ? `
|
|
292
|
+
<div class="prop-group">
|
|
293
|
+
<div class="prop-group-title">Hedef / Kategori Ayarları</div>
|
|
294
|
+
${obj.role === 'category' ? `
|
|
295
|
+
<div class="prop-row">
|
|
296
|
+
<label class="prop-label">Kapasite (0 = Sınırsız)</label>
|
|
297
|
+
<input type="number" class="prop-input" id="prop-acceptlimit" min="0" value="${obj.acceptLimit || 0}">
|
|
298
|
+
</div>
|
|
299
|
+
` : ''}
|
|
300
|
+
${obj.role === 'sequence_slot' ? `
|
|
301
|
+
<div class="prop-row">
|
|
302
|
+
<label class="prop-label">Sıra Numarası (Adım)</label>
|
|
303
|
+
<input type="number" class="prop-input" id="prop-slotindex" min="0" value="${obj.slotIndex !== undefined ? obj.slotIndex + 1 : 1}">
|
|
304
|
+
</div>
|
|
305
|
+
` : ''}
|
|
306
|
+
</div>
|
|
307
|
+
` : ''}
|
|
308
|
+
|
|
309
|
+
<!-- Silme Butonu -->
|
|
310
|
+
<div class="prop-group">
|
|
311
|
+
<button class="prop-danger-btn" id="btn-delete-selected">🗑️ Nesneyi Sil</button>
|
|
312
|
+
</div>
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
renderLayersTab() {
|
|
317
|
+
const objects = [...this.objectManager.getObjects()].sort((a, b) => (b.zIndex || 1) - (a.zIndex || 1));
|
|
318
|
+
const selectedIds = this.selectionManager.selectedIds;
|
|
319
|
+
|
|
320
|
+
return `
|
|
321
|
+
<div class="prop-group">
|
|
322
|
+
<div class="prop-group-title">📑 Sahne Nesneleri ve Katmanlar</div>
|
|
323
|
+
<p class="prop-info-text">
|
|
324
|
+
Sahnedeki tüm nesneleri listeleyebilir, tıklayarak seçebilir, kilitlerini açıp kapatabilir veya gizleyebilirsiniz.
|
|
325
|
+
</p>
|
|
326
|
+
|
|
327
|
+
<div class="layers-list">
|
|
328
|
+
${objects.length === 0 ? '<p class="prop-empty-text">Henüz nesne eklenmedi.</p>' : ''}
|
|
329
|
+
${objects.map(obj => {
|
|
330
|
+
const isSelected = selectedIds.has(obj.id);
|
|
331
|
+
let roleIcon = '🎨';
|
|
332
|
+
if (obj.role === 'draggable') roleIcon = '🎯';
|
|
333
|
+
else if (obj.role === 'target') roleIcon = '📥';
|
|
334
|
+
else if (obj.role === 'category') roleIcon = '📦';
|
|
335
|
+
else if (obj.role === 'sequence_slot') roleIcon = '🔢';
|
|
336
|
+
else if (obj.type === 'image') roleIcon = '🖼️';
|
|
337
|
+
else if (obj.type === 'line') roleIcon = '━';
|
|
338
|
+
|
|
339
|
+
return `
|
|
340
|
+
<div class="layer-item-row ${isSelected ? 'is-active' : ''} ${obj.locked ? 'is-locked-row' : ''}" data-layer-id="${obj.id}">
|
|
341
|
+
<div class="layer-item-left" data-layer-select="${obj.id}" title="Tuvalde seçmek için tıklayın">
|
|
342
|
+
<span class="layer-role-icon">${roleIcon}</span>
|
|
343
|
+
<span class="layer-name">${window.DND.ValidationEngine.sanitizeText(obj.name || obj.text || obj.id)}</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="layer-item-actions">
|
|
346
|
+
<button type="button" class="layer-action-btn btn-toggle-lock ${obj.locked ? 'active-locked' : ''}" data-lock-id="${obj.id}" title="${obj.locked ? 'Kilidi Aç' : 'Kilitle'}">
|
|
347
|
+
${obj.locked ? '🔒' : '🔓'}
|
|
348
|
+
</button>
|
|
349
|
+
<button type="button" class="layer-action-btn btn-toggle-visibility ${obj.visible === false ? 'is-hidden' : ''}" data-vis-id="${obj.id}" title="${obj.visible === false ? 'Görünür Yap' : 'Gizle'}">
|
|
350
|
+
${obj.visible === false ? '🙈' : '👁️'}
|
|
351
|
+
</button>
|
|
352
|
+
<button type="button" class="layer-action-btn btn-delete-layer" data-del-id="${obj.id}" title="Sil">
|
|
353
|
+
🗑️
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
}).join('')}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
renderMultiSelectProperties(count) {
|
|
365
|
+
return `
|
|
366
|
+
<div class="prop-group">
|
|
367
|
+
<div class="prop-group-title">Çoklu Seçim</div>
|
|
368
|
+
<p class="prop-info-text">Şu anda <strong>${count}</strong> nesne seçili durumda.</p>
|
|
369
|
+
<div class="prop-btn-row">
|
|
370
|
+
<button class="prop-tool-btn" id="btn-bring-front">🔝 En Öne</button>
|
|
371
|
+
<button class="prop-tool-btn" id="btn-send-back">🔚 En Arkaya</button>
|
|
372
|
+
</div>
|
|
373
|
+
<button class="prop-danger-btn" id="btn-delete-selected" style="margin-top:12px;">🗑️ Seçilenleri Sil</button>
|
|
374
|
+
</div>
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
renderStageProperties() {
|
|
379
|
+
const stage = this.core.data.stage;
|
|
380
|
+
const project = this.core.data.project;
|
|
381
|
+
|
|
382
|
+
return `
|
|
383
|
+
<div class="prop-group">
|
|
384
|
+
<div class="prop-group-title">Sahne ve Tuval Özellikleri</div>
|
|
385
|
+
<p class="prop-info-text">Hiçbir nesne seçili değil. Sahne ayarlarını düzenleyebilirsiniz.</p>
|
|
386
|
+
<div class="prop-row">
|
|
387
|
+
<label class="prop-label">Etkinlik Türü</label>
|
|
388
|
+
<select class="prop-select" id="prop-activity-type">
|
|
389
|
+
<option value="image-labeling" ${project.activityType === 'image-labeling' ? 'selected' : ''}>Görsel Üzerinde Etiketleme</option>
|
|
390
|
+
<option value="matching" ${project.activityType === 'matching' ? 'selected' : ''}>Sürükle-Bırak Eşleştirme</option>
|
|
391
|
+
<option value="categorization" ${project.activityType === 'categorization' ? 'selected' : ''}>Kategorilere Ayırma</option>
|
|
392
|
+
<option value="sequencing" ${project.activityType === 'sequencing' ? 'selected' : ''}>Sürükleyerek Sıralama</option>
|
|
393
|
+
</select>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="prop-grid-2">
|
|
396
|
+
<div class="prop-col">
|
|
397
|
+
<label class="prop-sublabel">Genişlik</label>
|
|
398
|
+
<input type="number" class="prop-input readonly" value="${stage.width}" readonly>
|
|
399
|
+
</div>
|
|
400
|
+
<div class="prop-col">
|
|
401
|
+
<label class="prop-sublabel">Yükseklik</label>
|
|
402
|
+
<input type="number" class="prop-input readonly" value="${stage.height}" readonly>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="prop-row">
|
|
406
|
+
<label class="prop-label">Sahne Arka Plan Rengi</label>
|
|
407
|
+
<input type="color" class="prop-color-picker" id="prop-stage-bg" value="${this.sanitizeColorHex(stage.backgroundColor, '#f8fafc')}">
|
|
408
|
+
</div>
|
|
409
|
+
<div class="prop-row">
|
|
410
|
+
<label class="prop-label">Arka Plan Görseli</label>
|
|
411
|
+
<input type="file" id="prop-stage-bg-file" accept="image/*" class="prop-file-input">
|
|
412
|
+
${stage.backgroundImage ? `
|
|
413
|
+
<button class="prop-tool-btn" id="btn-remove-stage-bg" style="margin-top:6px;">Görseli Kaldır</button>
|
|
414
|
+
` : ''}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
renderAnswerKeySettings(selectedObj) {
|
|
421
|
+
const activityType = this.core.data.project.activityType;
|
|
422
|
+
const answerKey = this.core.data.answerKey || {};
|
|
423
|
+
const draggables = this.core.data.objects.filter(o => o.role === 'draggable');
|
|
424
|
+
const targets = this.core.data.objects.filter(o =>
|
|
425
|
+
['target', 'category', 'sequence_slot'].includes(o.role) ||
|
|
426
|
+
(activityType === 'matching' && o.role !== 'draggable')
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return `
|
|
430
|
+
<div class="prop-group">
|
|
431
|
+
<div class="prop-group-title">🔑 Cevap Anahtarı Yönetimi</div>
|
|
432
|
+
<p class="prop-info-text">
|
|
433
|
+
Sürüklenebilir nesnelerin doğru hedeflerini aşağıdaki listeden belirleyebilir
|
|
434
|
+
veya üst araç çubuğundaki <strong>"Cevap Anahtarı"</strong> moduna geçip
|
|
435
|
+
sahne üzerinde nesnelere tıklayarak görsel olarak bağlayabilirsiniz.
|
|
436
|
+
</p>
|
|
437
|
+
|
|
438
|
+
<div class="ak-mappings-list">
|
|
439
|
+
${draggables.length === 0 ? '<p class="prop-empty-text">Henüz sürüklenebilir nesne eklenmedi.</p>' : ''}
|
|
440
|
+
${draggables.map(drag => {
|
|
441
|
+
const currentTargetId = answerKey[drag.id] || '';
|
|
442
|
+
const isAssigned = !!currentTargetId;
|
|
443
|
+
|
|
444
|
+
return `
|
|
445
|
+
<div class="ak-item-row ${drag.id === selectedObj?.id ? 'highlight-row' : ''}">
|
|
446
|
+
<div class="ak-item-header">
|
|
447
|
+
<span class="ak-drag-name">🎯 ${window.DND.ValidationEngine.sanitizeText(drag.name || drag.text || drag.id)}</span>
|
|
448
|
+
<span class="ak-status-pill ${isAssigned ? 'assigned' : 'unassigned'}">
|
|
449
|
+
${isAssigned ? '✓ Eşleşti' : '✕ Hedef Yok'}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="ak-select-wrap">
|
|
453
|
+
<select class="prop-select ak-target-select" data-drag-id="${drag.id}">
|
|
454
|
+
<option value="">-- Doğru Hedefi Seçin --</option>
|
|
455
|
+
${targets.map(t => `
|
|
456
|
+
<option value="${t.id}" ${currentTargetId === t.id ? 'selected' : ''}>
|
|
457
|
+
${t.role === 'category' ? '📦 ' : (t.role === 'sequence_slot' ? '🔢 ' : '📥 ')}
|
|
458
|
+
${window.DND.ValidationEngine.sanitizeText(t.name || t.text || t.id)}
|
|
459
|
+
</option>
|
|
460
|
+
`).join('')}
|
|
461
|
+
</select>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
`;
|
|
465
|
+
}).join('')}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
renderProjectSettings() {
|
|
472
|
+
const settings = this.core.data.settings;
|
|
473
|
+
|
|
474
|
+
return `
|
|
475
|
+
<div class="prop-group">
|
|
476
|
+
<div class="prop-group-title">🎮 Oyun ve Değerlendirme Ayarları</div>
|
|
477
|
+
|
|
478
|
+
<div class="prop-row">
|
|
479
|
+
<label class="prop-label">Değerlendirme Modu</label>
|
|
480
|
+
<select class="prop-select" id="setting-evaluation-mode">
|
|
481
|
+
<option value="instant" ${settings.evaluationMode === 'instant' ? 'selected' : ''}>Anında Kontrol (Bırakıldığı Anda)</option>
|
|
482
|
+
<option value="submit" ${settings.evaluationMode === 'submit' ? 'selected' : ''}>"Kontrol Et" Butonuyla (Toplu)</option>
|
|
483
|
+
</select>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div class="prop-row">
|
|
487
|
+
<label class="prop-label">Yanlış Cevap Davranışı</label>
|
|
488
|
+
<select class="prop-select" id="setting-wrong-behavior">
|
|
489
|
+
<option value="return" ${settings.wrongAnswerBehavior === 'return' ? 'selected' : ''}>Başlangıç Konumuna Döndür</option>
|
|
490
|
+
<option value="stay" ${settings.wrongAnswerBehavior === 'stay' ? 'selected' : ''}>Olduğu Yerde Bırak (Kırmızı Göster)</option>
|
|
491
|
+
</select>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<div class="prop-row checkbox-row">
|
|
495
|
+
<label><input type="checkbox" id="setting-shuffle" ${settings.shuffleDraggables ? 'checked' : ''}> Sürüklenebilir Nesneleri Karıştır</label>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<div class="prop-row checkbox-row">
|
|
499
|
+
<label><input type="checkbox" id="setting-sound" ${settings.soundEnabled ? 'checked' : ''}> Ses Efektlerini Aç</label>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<div class="prop-row checkbox-row">
|
|
503
|
+
<label><input type="checkbox" id="setting-retry" ${settings.allowRetry ? 'checked' : ''}> Tekrar Denemeye İzin Ver</label>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="prop-row">
|
|
507
|
+
<label class="prop-label">Tamamlanma Kutlama Mesajı</label>
|
|
508
|
+
<textarea class="prop-textarea" id="setting-completion-msg" rows="3">${window.DND.ValidationEngine.sanitizeText(settings.completionMessage || '')}</textarea>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
bindEvents(selectedObj) {
|
|
515
|
+
// 1. Katmanlar Tabı Dinleyicileri
|
|
516
|
+
if (this.activeTab === 'layers') {
|
|
517
|
+
this.panel.querySelectorAll('[data-layer-select]').forEach(el => {
|
|
518
|
+
el.onclick = () => {
|
|
519
|
+
const id = el.getAttribute('data-layer-select');
|
|
520
|
+
this.selectionManager.select(id, false);
|
|
521
|
+
this.render();
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
this.panel.querySelectorAll('[data-lock-id]').forEach(btn => {
|
|
526
|
+
btn.onclick = (e) => {
|
|
527
|
+
e.stopPropagation();
|
|
528
|
+
const id = btn.getAttribute('data-lock-id');
|
|
529
|
+
const obj = this.objectManager.getObject(id);
|
|
530
|
+
if (obj) {
|
|
531
|
+
this.objectManager.updateObject(id, { locked: !obj.locked });
|
|
532
|
+
this.selectionManager.updateSelectionBox();
|
|
533
|
+
this.render();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
this.panel.querySelectorAll('[data-vis-id]').forEach(btn => {
|
|
539
|
+
btn.onclick = (e) => {
|
|
540
|
+
e.stopPropagation();
|
|
541
|
+
const id = btn.getAttribute('data-vis-id');
|
|
542
|
+
const obj = this.objectManager.getObject(id);
|
|
543
|
+
if (obj) {
|
|
544
|
+
this.objectManager.updateObject(id, { visible: obj.visible === false });
|
|
545
|
+
this.selectionManager.updateSelectionBox();
|
|
546
|
+
this.render();
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
this.panel.querySelectorAll('[data-del-id]').forEach(btn => {
|
|
552
|
+
btn.onclick = (e) => {
|
|
553
|
+
e.stopPropagation();
|
|
554
|
+
const id = btn.getAttribute('data-del-id');
|
|
555
|
+
this.objectManager.deleteObject(id);
|
|
556
|
+
this.selectionManager.deselect(id);
|
|
557
|
+
this.render();
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!selectedObj) {
|
|
564
|
+
// Sahne ve proje ayarları dinleyicileri
|
|
565
|
+
const actTypeSelect = this.panel.querySelector('#prop-activity-type');
|
|
566
|
+
if (actTypeSelect) {
|
|
567
|
+
actTypeSelect.onchange = (e) => this.core.setActivityType(e.target.value);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const stageBg = this.panel.querySelector('#prop-stage-bg');
|
|
571
|
+
if (stageBg) {
|
|
572
|
+
stageBg.oninput = (e) => this.core.updateStage({ backgroundColor: e.target.value });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const stageBgFile = this.panel.querySelector('#prop-stage-bg-file');
|
|
576
|
+
if (stageBgFile) {
|
|
577
|
+
stageBgFile.onchange = (e) => {
|
|
578
|
+
const file = e.target.files[0];
|
|
579
|
+
if (file) {
|
|
580
|
+
const reader = new FileReader();
|
|
581
|
+
reader.onload = (re) => this.core.updateStage({ backgroundImage: re.target.result });
|
|
582
|
+
reader.readAsDataURL(file);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const removeBgBtn = this.panel.querySelector('#btn-remove-stage-bg');
|
|
588
|
+
if (removeBgBtn) {
|
|
589
|
+
removeBgBtn.onclick = () => this.core.updateStage({ backgroundImage: null });
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
// Seçili nesne özellikleri dinleyicileri
|
|
593
|
+
const id = selectedObj.id;
|
|
594
|
+
|
|
595
|
+
const update = (props) => {
|
|
596
|
+
const targets = this.selectionManager.getSelectedObjects();
|
|
597
|
+
|
|
598
|
+
if (targets.length > 1) {
|
|
599
|
+
targets.forEach(object => {
|
|
600
|
+
this.objectManager.updateObject(object.id, props, false);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
this.core.pushHistoryState('Seçili Nesneleri Güncelle');
|
|
604
|
+
} else {
|
|
605
|
+
this.objectManager.updateObject(id, props, true);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
this.selectionManager.updateSelectionBox();
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Kilit açma/kapama butonları
|
|
612
|
+
const unlockPropBtn = this.panel.querySelector('#btn-unlock-prop');
|
|
613
|
+
if (unlockPropBtn) {
|
|
614
|
+
unlockPropBtn.onclick = () => {
|
|
615
|
+
update({ locked: false });
|
|
616
|
+
this.render();
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const lockPropBtn = this.panel.querySelector('#btn-lock-prop');
|
|
621
|
+
if (lockPropBtn) {
|
|
622
|
+
lockPropBtn.onclick = () => {
|
|
623
|
+
update({ locked: true });
|
|
624
|
+
this.render();
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const bindInput = (selector, prop, isNumber = false) => {
|
|
629
|
+
const el = this.panel.querySelector(selector);
|
|
630
|
+
if (el) {
|
|
631
|
+
el.oninput = (e) => {
|
|
632
|
+
const val = isNumber ? parseFloat(e.target.value) || 0 : e.target.value;
|
|
633
|
+
update({ [prop]: val });
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
bindInput('#prop-name', 'name');
|
|
639
|
+
bindInput('#prop-x', 'x', true);
|
|
640
|
+
bindInput('#prop-y', 'y', true);
|
|
641
|
+
bindInput('#prop-width', 'width', true);
|
|
642
|
+
bindInput('#prop-height', 'height', true);
|
|
643
|
+
bindInput('#prop-rotation', 'rotation', true);
|
|
644
|
+
bindInput('#prop-opacity', 'opacity', true);
|
|
645
|
+
|
|
646
|
+
bindInput('#prop-text', 'text');
|
|
647
|
+
bindInput('#prop-fontsize', 'fontSize', true);
|
|
648
|
+
bindInput('#prop-textcolor', 'textColor');
|
|
649
|
+
bindInput('#prop-fontweight', 'fontWeight');
|
|
650
|
+
bindInput('#prop-textalign', 'textAlign');
|
|
651
|
+
|
|
652
|
+
bindInput('#prop-bgcolor', 'backgroundColor');
|
|
653
|
+
bindInput('#prop-bordercolor', 'borderColor');
|
|
654
|
+
bindInput('#prop-borderwidth', 'borderWidth', true);
|
|
655
|
+
bindInput('#prop-borderradius', 'borderRadius', true);
|
|
656
|
+
bindInput('#prop-borderstyle', 'borderStyle');
|
|
657
|
+
|
|
658
|
+
const roleSelect = this.panel.querySelector('#prop-role');
|
|
659
|
+
if (roleSelect) {
|
|
660
|
+
roleSelect.onchange = (e) => {
|
|
661
|
+
update({ role: e.target.value });
|
|
662
|
+
this.render();
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const lockedCb = this.panel.querySelector('#prop-locked');
|
|
667
|
+
if (lockedCb) {
|
|
668
|
+
lockedCb.onchange = (e) => {
|
|
669
|
+
update({ locked: e.target.checked });
|
|
670
|
+
this.render();
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const acceptLimitInput = this.panel.querySelector('#prop-acceptlimit');
|
|
675
|
+
if (acceptLimitInput) {
|
|
676
|
+
acceptLimitInput.oninput = (e) => update({ acceptLimit: parseInt(e.target.value, 10) || 0 });
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const slotIndexInput = this.panel.querySelector('#prop-slotindex');
|
|
680
|
+
if (slotIndexInput) {
|
|
681
|
+
slotIndexInput.oninput = (e) => update({ slotIndex: (parseInt(e.target.value, 10) || 1) - 1 });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Katman butonları
|
|
685
|
+
const bringFront = this.panel.querySelector('#btn-bring-front');
|
|
686
|
+
if (bringFront) bringFront.onclick = () => this.layerManager.bringToFront(id);
|
|
687
|
+
|
|
688
|
+
const bringForward = this.panel.querySelector('#btn-bring-forward');
|
|
689
|
+
if (bringForward) bringForward.onclick = () => this.layerManager.bringForward(id);
|
|
690
|
+
|
|
691
|
+
const sendBackward = this.panel.querySelector('#btn-send-backward');
|
|
692
|
+
if (sendBackward) sendBackward.onclick = () => this.layerManager.sendBackward(id);
|
|
693
|
+
|
|
694
|
+
const sendBack = this.panel.querySelector('#btn-send-back');
|
|
695
|
+
if (sendBack) sendBack.onclick = () => this.layerManager.sendToBack(id);
|
|
696
|
+
|
|
697
|
+
const deleteBtn = this.panel.querySelector('#btn-delete-selected');
|
|
698
|
+
if (deleteBtn) deleteBtn.onclick = () => this.selectionManager.deleteSelected();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Cevap Anahtarı Açılır Kutuları Dinleyicisi
|
|
702
|
+
this.panel.querySelectorAll('.ak-target-select').forEach(sel => {
|
|
703
|
+
sel.onchange = (e) => {
|
|
704
|
+
const dragId = sel.getAttribute('data-drag-id');
|
|
705
|
+
const targetId = e.target.value;
|
|
706
|
+
if (targetId) {
|
|
707
|
+
this.core.data.answerKey[dragId] = targetId;
|
|
708
|
+
} else {
|
|
709
|
+
delete this.core.data.answerKey[dragId];
|
|
710
|
+
}
|
|
711
|
+
this.core.emit('answerKeyChanged', { answerKey: this.core.data.answerKey });
|
|
712
|
+
this.core.autoSave();
|
|
713
|
+
this.render();
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Oyun Ayarları Dinleyicileri
|
|
718
|
+
const evalMode = this.panel.querySelector('#setting-evaluation-mode');
|
|
719
|
+
if (evalMode) {
|
|
720
|
+
evalMode.onchange = (e) => this.core.updateSettings({ evaluationMode: e.target.value });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const wrongBeh = this.panel.querySelector('#setting-wrong-behavior');
|
|
724
|
+
if (wrongBeh) {
|
|
725
|
+
wrongBeh.onchange = (e) => this.core.updateSettings({ wrongAnswerBehavior: e.target.value });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const shuffleCb = this.panel.querySelector('#setting-shuffle');
|
|
729
|
+
if (shuffleCb) {
|
|
730
|
+
shuffleCb.onchange = (e) => this.core.updateSettings({ shuffleDraggables: e.target.checked });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const soundCb = this.panel.querySelector('#setting-sound');
|
|
734
|
+
if (soundCb) {
|
|
735
|
+
soundCb.onchange = (e) => this.core.updateSettings({ soundEnabled: e.target.checked });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const retryCb = this.panel.querySelector('#setting-retry');
|
|
739
|
+
if (retryCb) {
|
|
740
|
+
retryCb.onchange = (e) => this.core.updateSettings({ allowRetry: e.target.checked });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const completionMsg = this.panel.querySelector('#setting-completion-msg');
|
|
744
|
+
if (completionMsg) {
|
|
745
|
+
completionMsg.oninput = (e) => this.core.updateSettings({ completionMessage: e.target.value });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
updateFieldsIfSelected() {
|
|
750
|
+
const selectedObj = this.selectionManager.getPrimarySelected();
|
|
751
|
+
if (!selectedObj) return;
|
|
752
|
+
|
|
753
|
+
const setVal = (id, val) => {
|
|
754
|
+
const el = this.panel.querySelector(id);
|
|
755
|
+
if (el && document.activeElement !== el) el.value = val;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
setVal('#prop-x', Math.round(selectedObj.x));
|
|
759
|
+
setVal('#prop-y', Math.round(selectedObj.y));
|
|
760
|
+
setVal('#prop-width', Math.round(selectedObj.width));
|
|
761
|
+
setVal('#prop-height', Math.round(selectedObj.height));
|
|
762
|
+
setVal('#prop-rotation', Math.round(selectedObj.rotation || 0));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
sanitizeColorHex(val, fallback = '#000000') {
|
|
766
|
+
if (!val || typeof val !== 'string') return fallback;
|
|
767
|
+
if (val.startsWith('#') && (val.length === 7 || val.length === 4)) {
|
|
768
|
+
return val;
|
|
769
|
+
}
|
|
770
|
+
return fallback;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Global namespace
|
|
775
|
+
window.DND = window.DND || {};
|
|
776
|
+
window.DND.PropertyPanel = PropertyPanel;
|